Run the ticket skills through T3 Code, with a review council before pre-mr-review

Agents now start pre-mr-review on the work account themselves, through a Klaude
delegate, and continue from its verdict. A council of models on my own accounts
replaces the single reviewer before it, and adding a seat is one table row.

Proof comes from T3's browser, with mp4 recordings, and capture-proof stays as
the fallback. Ticket pages carry their own data, so index.html renders alone in
T3's file viewer, and worktree threads share the main checkout's docs.

A PreToolUse hook sends subagents in T3 threads to delegate_task. Blocker, proof,
review and MR summary rules come from ideas in t3dotgg/tstack.
This commit is contained in:
Gabriel Brown committed 2026-11-04 18:04:18 -05:00
1 parent 968e0a1045
commit 86f6e391b7
14 files changed
+410 -187

No files matched your search

+4 -1
View File
@@ -67,7 +67,10 @@ I run my agents in T3 Code, and I want to stay in it rather than juggle terminal
- Show visuals in the thread: charts, tables, mocks, plans, to-do lists. `html_preview`, then `html_render`.
- Link files by absolute path. HTML, PDF, images and Markdown open rendered in T3's file viewer, beside the thread. Do not start an http server just so I can look at a file.
- Reach my other machines through T3's linked environments (`t3_environment_links`, then that environment's id), not through ssh, unless I ask for ssh.
- Claude in Chrome is still the browser for my own signed-in sessions.
- Every subagent goes through `delegate_task`, whatever the provider. That includes same-provider work, which T3's own prompt says can use native tools: my rule wins. Native `Agent`, `Workflow`, Codex's `spawn_agent`, `claude -p`, `codex exec`, and `opencode run` are only for when I ask for one by name. A hook blocks the Claude Code routes and tells you how to pass when I have asked.
- `delegate_task` children know only their `task` text, and `/skill` lines do not expand there, so tell a child to call the Skill tool. A local child always runs in this thread's project.
- Klaude (`claudeAgent_klaude`) is my work Claude account, and it runs `pre-mr-review` and nothing else. Omit `model` to get its default. Everything else, reviews included, runs on my own accounts.
- T3's preview browser is the default for checking an app or capturing proof, and I can sign in there. Claude in Chrome is still the browser for my own signed-in sessions.
---
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# PreToolUse hook: inside a T3 Code thread, subagents go through delegate_task, so T3 can
# show them, track them, and wake the parent. Blocks the native Agent and Workflow tools and
# shelling out to another agent CLI. Does nothing outside T3 Code.
#
# The escape is for when Gib asks for a native route by name: start the Agent or Workflow
# description with "native:", or prefix the command with T3_NATIVE_OK=1.
#
# web-fetch stays allowed because it stands in for a WebFetch tool, not a worker.
set -euo pipefail
[[ -n "${T3CODE_THREAD_ID:-}" ]] || exit 0
input="$(cat)"
tool="$(jq -r '.tool_name' <<<"$input")"
deny() {
jq -n --arg reason "$1" '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: $reason}}'
exit 0
}
advice="This is a T3 Code thread, so use the delegate_task tool (load it with ToolSearch: select:mcp__t3-code__delegate_task). This overrides T3's note that native subagents are fine for the same provider. If you are a delegated child, do the work yourself."
case "$tool" in
Agent | Task | Workflow)
[[ "$(jq -r '.tool_input.subagent_type // ""' <<<"$input")" == "web-fetch" ]] && exit 0
[[ "$(jq -r '.tool_input.description // ""' <<<"$input")" == native:* ]] && exit 0
deny "$tool is blocked. $advice Only if Gib asked for a native agent by name: start the description with \"native:\"."
;;
Bash)
# Only a command position counts, so grep patterns and prose that mention these pass.
if jq -r '.tool_input.command // ""' <<<"$input" |
grep -qE '(^|[;&|(]|\$\()[[:space:]]*(timeout[[:space:]]+[^[:space:]]+[[:space:]]+)?(claude|clauded|claudedr|klaude|codex|opencode2?)[[:space:]]+([^;&|]*[[:space:]])?(-p|--print|exec|run)([[:space:]]|$)'; then
deny "Starting an agent CLI from the shell is blocked. $advice Only if Gib asked for this by name: prefix the command with T3_NATIVE_OK=1."
fi
;;
esac
+2 -3
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env bash
# Playwright's MCP server, for driving a real browser: pages, screenshots,
# forms, end-to-end runs. This is what the ticket skill's proof screenshots are
# captured with, so it belongs on every machine rather than only where a Claude
# Code plugin happened to be installed.
# forms, end-to-end runs. It belongs on every machine rather than only where a
# Claude Code plugin happened to be installed.
#
# link-mcp registers this file as a stdio server. It takes no token, so its
# row in user/agents/mcp/servers names no variable and nothing arrives in
@@ -90,6 +90,9 @@ Same conventions as the `/ticket` skill:
`.claude/docs/epics/<EPIC-KEY>/<TICKET-KEY>/story.md` (or `spike.md` /
`task.md`), creating directories as needed. These are personal docs, never
committed.
9. **Report.** Give the Jira link, the title, and any numbered Notes that still name an
open owner, and link the local copy by absolute path so it opens rendered in T3's
file viewer.
## House rules for all content
+4 -3
View File
@@ -12,8 +12,8 @@ Invoked as `/review-ticket KACP-XXXXX`. Two phases with a hard stop between them
This is a *dev review*, not a code review. It is part of writing a ticket, before any
code exists: it fills in implementation guidance, an estimate, a risk level, and the
testing tables so a developer can pick the story up. Reviewing code that has already
been written is a separate thing entirely, and lives in the `ticket` skill at Phase 2
step 5.
been written is a separate thing entirely, and lives in the `ticket` skill's
`COUNCIL.md`.
## Files this skill uses
@@ -63,7 +63,8 @@ step 5.
- Prose sweep: call the Skill tool with "unslop" and apply its pattern detection to `review.md`, meaning puffery, superficial -ing phrases, "not just X but Y", vague attributions, and rule of three padding, alongside the punctuation rules above. Skip its "Adding soul" section, the voice here is the Lead writing to the implementing developer, not a blog. Henry reads every one of these, and a review that reads as machine generated loses its credibility before its content is weighed.
- Anatomy check against the section list below.
- Confirm no Jira write has happened.
12. Stop. Hand the user the draft with the estimate, risk level, dependency list, and any split recommendation surfaced in the summary, and wait for their review. Do not apply in the same run unless the user has already told you to.
12. If the Story Risk Level is MEDIUM or above, or the story changes the datamodel, get the draft checked before Gib reads it. Read **Seats** and **Checking a dev review** in `~/.agents/skills/ticket/COUNCIL.md`, start one `delegate_task` per seat, and end your turn. T3 wakes you as each finishes. Verify each finding against the code yourself, fix what is real in `review.md`, and rerun the path check and prose sweep on what changed. A seat that cannot run is named in the summary, never replaced with another model. Outside T3 Code, skip this step and say so.
13. Stop. Link `review.md` by absolute path so it opens rendered in T3's file viewer. Hand the user the draft with the estimate, risk level, dependency list, any split recommendation, and what the seats changed surfaced in the summary, and wait for their review. Do not apply in the same run unless the user has already told you to.
## Phase 2, apply
+137
View File
@@ -0,0 +1,137 @@
# The review council
Several fresh agents from different model families review the same change, each knowing nothing
of how it was built. You judge what they return. This runs before `pre-mr-review`, because
that audit may not run anything and so cannot catch a claim that is plausible on the page and
false in reality. A reviewer that can run things can. It has caught a mitigation proven by
construction to be wrong, on a branch whose own audit had blessed it.
Order matters. `pre-mr-review`'s verdict gets pasted verbatim into `mr.md`, and anything that
lands after it invalidates that verdict. Council first, fix, commit, then the audit over the
final tree.
## Seats
| Seat | `providerInstanceId` | `model` |
|---|---|---|
| Claude | `claudeAgent` | omit it, so the seat runs that provider's default |
| MiMo | `opencode` | `opencode-go/mimo-v2.6-pro` |
Klaude never takes a seat. It is the work account, and its only job is the `pre-mr-review` run
in Phase 2 step 7, which stays a separate required step after the council.
Adding a seat is adding a row. `orchestrator_capabilities` has the IDs, with `search` for a
model it does not list. Nothing else here changes with the seat count. T3 can leave out a model
the provider really has, as it does with `opencode-go/*`. List such a model under that provider's
`customModels` in `~/.t3/userdata/settings.json`, which T3 picks up without a restart.
## When it runs
Run it whenever the change adds or modifies a procedure, touches schema or migrations, touches
permissions or auth, involves concurrency, or has a risk table in its ticket. When unsure, run
it. A copy tweak, a styling fix, or a one line correction with no logic skips it.
## Convene
1. Confirm the suite is green, and note the base and head of the commit range. A seat should
never spend its review on code the suite already rejects.
2. In one turn, call `delegate_task` once per seat with the brief below: `role: "review"`,
`mode: "async"`, a title of `Council <seat> <KEY>`, and the seat's `target`. Leave `isolation`
unset, so each seat reads this checkout, and leave `runtimeMode` unset.
3. End your turn. T3 wakes you as each seat finishes. Leave the checkout alone until every seat
has returned, because they are reading it.
4. A seat refused with `model_unavailable`, or one that fails: record it with `ticket-page event`,
carry on with the rest, and name it at hand-off. Keep the seat empty rather than filling it with
another model. If no seat can run, ask Gib with AskUserQuestion. A timeout or silence is not
an approval.
Outside T3 Code (no `T3CODE_THREAD_ID`), run one fresh subagent from the harness with the same
brief. Never review the change in your own context: the value here is the reviewer not being
the author.
### The brief
A delegate gets none of this thread, so the brief is all it knows. Fill the placeholders.
```
Review only. Do not edit, commit, push, run anything destructive, or start subagents.
Repo: <absolute path> Range: <base>..<head> Ticket: <KEY>
Read in this order, or you will report conventions this project already settled:
1. .claude/docs/epics/<EPIC-KEY>/summary.md, if it exists
2. <scratch path of `ticket-page md <KEY>`>
3. <target-dir>/resources/
4. AGENTS.md
5. git diff <base>..<head>, then the code around it
Verify rather than suspect. You may run tests and read-only checks. Try to refute each finding
before you report it. A plausible worry is not a finding.
Re-derive every "proven by X" already written into the ticket's risk or test tables. A false
proof claim in Jira is worse than an unproven one, because it stops anyone else from checking.
Answer these against the ticket and plan, not the diff alone, quoting the AC ID or plan step:
- What did the ticket ask for that is missing or only partly done?
- What behavior is in the diff that nothing asked for?
- What looks implemented but is implemented wrongly?
Hold the change to its smallest correct form. Flag code, tests, or guards the ticket does not need.
blocker = the intended case is wrong. should = a real bug callers will hit. nit = everything else,
including rare paths, small performance costs, and style.
Reply with:
VERDICT: APPROVE or CHANGES
S1 [blocker|should|nit] [CONFIRMED|PLAUSIBLE] <file>:<line> <claim>. Failure: <concrete scenario>. Fix: <smallest fix>
```
## Judge the findings
Findings are not automatically true, and reviewers produce confident wrong answers. Check each
one against the code yourself, and regrade it: a rare path is a nit whatever label it came with.
Two seats reporting the same thing is one finding with more weight behind it. A finding only one
seat raised is still worth checking, since catching what the others miss is why the seats differ.
Put each on the page as you judge it, with its grade and its seats in the text:
`finding <KEY> add "[should] <text> (Claude, MiMo)" --severity CONFIRMED --where <file:line>`, then
`fixed --commit <sha>`, or `rejected --reason "<why>"`.
- **Fix every blocker and should you confirm,** each meaningful fix as its own commit. These do
not defer to a follow-up ticket unless Gib decides that.
- **Take the smallest fix.** Prefer deleting or narrowing code over a new lock, cache, helper, or
option. Compare `git diff --shortstat <base>` before and after: if review grew the diff, cut
first. Review loops grow a change one cheap-looking fix at a time, and it is your job to hold
the scope.
- **Decline nits in one line** with `rejected --reason`, and list them at hand-off as follow-ups.
Agreeing just to end the review is how a change bloats.
- **When a fix changes a mechanism rather than a line,** write a test that fails against the old
behavior and passes against the new one, and confirm it fails by reverting the fix. A test that
passes either way proves nothing.
- **If a finding invalidates something already in Jira or a deliverable,** correct it there too.
Fixing the code does not fix the false claim.
## Stop
One round is the default. `pre-mr-review` reviews the whole diff, your fixes included, and serves
as the next pass. A second round runs only if a fix changed a mechanism: fresh `delegate_task`
calls, the same brief, the ledger of what you fixed and rejected with your evidence, and an
instruction to check only the open ids and the changed code. Two rounds is the limit. A finding
still disputed then goes to Gib with AskUserQuestion, both sides in two or three lines each.
## Checking a dev review
`review-ticket` borrows the seats for a draft instead of a diff. Same `delegate_task` settings, and
no code gets changed. Give each seat this:
```
Review only. Do not edit anything, write to Jira, or start subagents.
Repo: <absolute path> Draft: <dir>/review.md Story: <scratch path of the fetched story>
Check the draft against the code. Report each Current State claim that is false, each "does not
exist" that does, each named pattern that is a poor fit or has a better neighbour, each seam the
Relationships section gets wrong, and any story AC the implementation guidance would not satisfy.
Say whether the estimate misses work the code paths table leaves out.
Reply with:
VERDICT: APPROVE or CHANGES
S1 [blocker|should|nit] <section> <claim>. Evidence: <file:line>. Fix: <suggestion>
```
+11 -9
View File
@@ -15,17 +15,19 @@ sees. It is a personal working file, exempt from the prose bar like the rest of
tickets/<TICKET-KEY>/ # same shape, for a ticket with no epic, and no index
```
Each `index.html` carries the renderer from `templates/site/` inline, rewritten on every
write, and loads only its own data file beside it. That is what lets T3 Code's file viewer
show it: an HTML file there can load files from its own folder and below, nothing above.
Each `index.html` carries the renderer from `templates/site/` and its own data inline, rewritten
on every write, so it renders alone with no server and no other file. That is what T3 Code's
file viewer needs: it sandboxes the page, with no `fetch` or `localStorage`, and serves files
only from the page's own folder and below. The `.data.js` beside it is the store `ticket-page`
reads and writes. An open page polls it to stay live, and shows its last write if it cannot.
Screenshots and attachments stay as relative files under the ticket folder.
## In T3 Code
Gib follows the work in a T3 Code thread, so the page and the plan come to him there.
- **The live page.** `ticket-page open <KEY>` refreshes the page and prints its absolute path.
Link that path in your reply, and it opens in T3's file viewer beside the thread, still
reloading as you write. Outside T3 (no `T3CODE_THREAD_ID`) it also opens the browser.
Link that path in your reply, and it opens in T3's file viewer beside the thread. Outside T3 (no `T3CODE_THREAD_ID`) it also opens the browser.
Never start an http server for the page.
- **Inline snapshots.** `ticket-page view <KEY> <view>` prints a self-contained page styled
with T3's theme. Pass its output, verbatim, to `html_preview`, check the screenshot, then
@@ -36,7 +38,7 @@ Gib follows the work in a T3 Code thread, so the page and the plan come to him t
| Moment | View |
|---|---|
| Plan ready for approval (Phase 1, last step) | `plan`, then `mocks --mock <id>` once per artboard |
| Hand-off for pre-mr-review, and on resume | `progress` |
| Starting pre-mr-review, final hand-off, and on resume | `progress` |
- One artboard per render, so each can be expanded full screen on its own and options sit
one after another for comparison. `view` refuses a page over html_render's 512 KB and
@@ -68,8 +70,8 @@ the repo.
| Starting a step | `step <KEY> <n> running` |
| A step's commit exists | `step <KEY> <n> done --commit <sha>`, one `--commit` per commit |
| A criterion is met | `criterion <KEY> <ID> done --evidence "<what shows it>"` |
| Proof captured, or dropped from `proof/` | `proof <KEY> --file proof/<name> --proves AC1,AC3 --caption "<test case>" [--mock <id>]`, `proof <KEY> --test "<name>" ...`, `unproof <KEY> <file>` |
| Reviewer finding, and its resolution | `finding <KEY> add "<text>" --severity CONFIRMED --where <file:line>`, then `finding <KEY> F1 fixed --commit <sha>` or `rejected --reason "<why>"` |
| Proof captured (image, mp4, or text), or dropped from `proof/` | `proof <KEY> --file proof/<name> --proves AC1,AC3 --caption "<test case>" [--mock <id>]`, `proof <KEY> --test "<name>" ...`, `unproof <KEY> <file>` |
| Council finding, and its resolution | `finding <KEY> add "[should] <text> (<seats>)" --severity CONFIRMED --where <file:line>`, then `finding <KEY> F1 fixed --commit <sha>` or `rejected --reason "<why>"` |
| pre-mr-review audit read | `verdict <KEY> <verdict text> --recommendation <R> --head <sha>` |
| A Jira field PUT and verified | `jira <KEY> "<field name>"` |
| `mr.md` written | `mr <KEY>` |
@@ -163,7 +165,7 @@ The rest of the index is yours to seed and keep true.
## Reading it as an agent
`ticket-page md <KEY>` prints the ticket and the plan as markdown. Use it when you resume a
ticket, and hand it to the fresh reviewer as the ticket and plan in its reading order.
ticket, and write it to a scratch file for the council and the audit, which read it there.
## Tickets from before the page
+138 -140
View File
@@ -1,6 +1,6 @@
---
name: ticket
description: End-to-end Jira ticket workflow — fetch a ticket into a live ticket page under .claude/docs/epics/, plan it, implement it with clean commits, drive the pre-mr-review audit to convergence (the user runs it themselves from their work Claude account), and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket.
description: End-to-end Jira ticket workflow — fetch a ticket into a live ticket page under .claude/docs/epics/, plan it, implement it with clean commits, review it with a council of models, drive the pre-mr-review audit to convergence on the work Claude account, and write the MR doc. Use when the user gives you a Jira ticket key (e.g. KACP-11111) to work, or asks to plan/implement/wrap up a ticket.
---
# Ticket Workflow
@@ -16,7 +16,7 @@ again later with the same key, it resumes wherever it left off — see **Phase 0
Not to be confused with `/review-ticket`. That skill writes the *developer review* on a
Jira story, the estimate, risk level, and testing tables, as part of authoring a ticket
before anyone builds it. It has nothing to do with reviewing code. The code review in
this skill is Phase 2 step 5 and is performed by a dispatched agent, not by a skill.
this skill is Phase 2 step 5 and is performed by the review council in [COUNCIL.md](COUNCIL.md).
## The ticket page
@@ -35,6 +35,23 @@ moment the phases below name, the plan's JSON shape, how to draw mocks, and how
is seeded. Record each moment when it happens, not in a batch at the end, because Gib is
watching the page change.
## Working in T3 Code
Gib runs this skill in a T3 Code thread (`T3CODE_THREAD_ID` is set) and stays there, so the work
comes to him through T3's own tools.
- **Agents are T3 delegates.** Every agent this skill starts is a `delegate_task`, and a local one
always runs in this thread's project. A delegate gets only its `task` text, so write the brief
as though it knows nothing. `/skill` lines do not expand there: tell it to call the Skill tool.
Start one with `mode: "async"` and end your turn, because T3 wakes you when it finishes.
- **Proof comes from T3's browser,** the `preview_*` tools. `/ticket` is the request for browser
work that T3's rules ask for, so use them at the proof step. Phase 2 step 9 has the recipe.
Claude in Chrome stays for reading Gib's signed-in sites, such as a Jira page.
- **Questions go through AskUserQuestion,** with the context, what blocks you, and your
recommendation first, because a question in a normal message leaves the thread looking done.
Without `T3CODE_THREAD_ID`, the fallbacks are named where each one applies.
## Directory layout this skill maintains
```
@@ -64,9 +81,10 @@ watching the page change.
- `~/.agents/skills/ticket/scripts/ticket-page <command> <KEY> ...` — the only writer of the
ticket page and epic index. See [SITE.md](SITE.md), or `ticket-page help`. Runs under bun.
- `~/.agents/skills/ticket/scripts/capture-proof` — sharp Playwright screenshots of the
running app for proof and spike screenshots. `capture-proof help` lists its options. The
first run installs a pinned Playwright into `~/.cache/ticket-page/`.
- `~/.agents/skills/ticket/scripts/capture-proof` — the fallback for proof screenshots: 2x
Playwright captures, full page or cropped, for what T3's browser cannot do (Phase 2 step 9).
`capture-proof help` lists its options. The first run installs a pinned Playwright into
`~/.cache/ticket-page/`.
- `~/.agents/skills/ticket/scripts/jira-fetch-issue.sh <KEY> <OUT.json>` — fetches one
issue (`fields=*all`, rendered HTML description, field-name map) via `JIRA_BASE_URL` /
`JIRA_CREDENTIALS`.
@@ -356,11 +374,10 @@ what was quick to wire up. Ship both, and don't let either stand in for the othe
- **Visual mocks alongside the code, one or two of them.** The built version is
deliberately rough, so on its own it under-sells the idea to anyone reviewing it.
Produce one or two polished mocks that aren't constrained by what was cheap to build:
the layout, spacing, and density as they should actually be. In Claude Code the
`design` skill produces a canvas of artboards for exactly this; in a harness without
it, a self-contained HTML page does the same job. These are design intent, not
implementation promises, so call out anything a mock shows that the code doesn't do
yet.
the layout, spacing, and density as they should actually be. A self-contained HTML
page does this job, shown through `html_preview` and `html_render`. These are design
intent, not implementation promises, so call out anything a mock shows that the code
doesn't do yet.
- **Scope discipline still applies.** This is a proof of concept, not a production
feature. Don't chase every edge case and don't fix unrelated pre-existing bugs you
happen to notice along the way, just implement enough of the real thing to be
@@ -368,8 +385,8 @@ what was quick to wire up. Ship both, and don't let either stand in for the othe
than a static mock would, say so in the plan's approach rather than silently scoping it down
without mentioning the tradeoff.
- **Screenshot the real thing.** Once it's built and running, use the `run` skill to
get the app up and capture the actual screens with `capture-proof`, as the proof step
in Phase 2 describes, instead of drawing a wireframe. Save screenshots under
get the app up and capture the actual screens as the proof step in Phase 2 describes,
instead of drawing a wireframe. Save screenshots under
`deliverables/<slug>/screenshots/` and the visual mocks under
`deliverables/<slug>/mocks/`, and embed both in `<slug>.typ` via `image()` when
building the mockups deliverable. Label which is which. A reader who can't tell a
@@ -427,6 +444,11 @@ through, and the branch needs to already exist when that happens.
5. **More than one match** — don't guess which one is "the" branch. List them (last
commit date and subject each) and ask the user which to use.
In a T3 worktree thread (`git rev-parse --git-dir` differs from `--git-common-dir`) the
default branch is usually checked out elsewhere, so cut from the remote instead:
`git fetch origin` and `git checkout -b <name> origin/<default>`. `ticket-page` finds the
main checkout's `.claude/docs/` from a worktree on its own.
Once this resolves you're on the correct branch, and Phase 1/Phase 2 work happens
there. `.claude/docs/` itself is gitignored in this repo, so which branch you're on
doesn't affect the ticket page or `mr.md` directly — this is about making sure any
@@ -453,7 +475,7 @@ place from the start.
- Proceed to implementing the existing plan as-is (they reviewed and approved it)
- Resume implementation (some steps are already done on the page / some commits
already exist on the ticket branch — pick up from the first open step)
- Move to the pre-mr-review stage (implementation looks done, just need the audit + MR doc; the user runs the audit from their work account, see Phase 2 step 7)
- Move to the pre-mr-review stage (implementation looks done, just need the audit + MR doc, see Phase 2 step 7)
Route to **Phase 1** or **Phase 2** accordingly.
- **A plan and `mr.md`** — this ticket looks finished. Tell the user
`mr.md` already exists at its path and ask whether they want you to refresh it
@@ -516,8 +538,9 @@ place from the start.
ticket's real requirements, write what it says as a note (`ticket-page note`), the
way "What the video says" summarizes a recording point by point.
10. Now actually understand the ticket in the context of this codebase: read whatever
source files, tests, or docs are relevant to what's being asked. Use Explore/grep
as needed — this is normal engineering research, not scripted. Use the
source files, tests, or docs are relevant to what's being asked. Read them yourself:
this is normal engineering research, not scripted. Only a sweep too broad for one
pass earns a `delegate_task` with `role: "research"`. Use the
**Verification tools available** above to check the ticket's own claims against
reality (codebase, Jira, staging, and prod/CI env with permission) rather than
taking the ticket's description at face value — this is what makes the plan
@@ -595,7 +618,9 @@ place from the start.
## Phase 2 — implement, verify, and hand off
Entered only when the user has confirmed (per Phase 0) that the plan is approved.
Entered only when the user has confirmed (per Phase 0) that the plan is approved. From
there this phase runs through to the step 10 hand-off without waiting on Gib: the council
and the audit both run as delegates. Stop only for a real blocker, asked with AskUserQuestion.
1. Do **Branch setup** above first, unconditionally — even if Phase 1 already did this
for the same ticket earlier, confirm you're still actually on that branch now (the
@@ -637,90 +662,51 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
wouldn't actually catch, e.g. confirming behavior against real staging data via
Infisical, or a specific prod/CI config question, when that's genuinely what it
takes to be confident the change works as intended rather than just "looks right."
5. Get an independent code review, from a FRESH agent, before running `pre-mr-review`.
This step exists because `pre-mr-review` cannot do it. That skill is constitutionally
read-only: it may not run tests, execute code, probe a database, or run any
experiment, and its own rules tell it to mark anything unprovable as "Needs
verification" rather than proving it. So it cannot catch a claim that is plausible on
the page and false in reality. A reviewer that can actually run things can. It has
already caught a mitigation that was proven by construction to be wrong, on a branch
whose own audit had blessed it.
Order matters and is not just convenience. `pre-mr-review`'s output is a readiness
verdict that gets pasted verbatim into `mr.md`. Anything that runs after it
invalidates that verdict by construction, and you end up rerunning it and rewriting
the handoff. Review first, fix, commit, and only then have the user
run `pre-mr-review` over the final tree.
- **Skip this step for genuinely trivial changes**: a copy tweak, a styling fix, a
one line correction with no logic in it. Run it whenever the change adds or
modifies a procedure, touches schema or migrations, touches permissions or
auth, involves concurrency, or has a risk table in its ticket. When unsure, run it.
- **Always dispatch a NEW agent.** Never reuse an agent from earlier in this ticket
and never review your own work in your own context. The value here is entirely in
the reviewer not being the author, so an agent that helped implement or plan the
change cannot provide it.
- **Give the reviewer its reading order explicitly**, or it produces noise about
conventions the project already settled: the epic summary if there is one
(`.claude/docs/epics/<EPIC-KEY>/summary.md`), then the ticket and plan as
`ticket-page md <KEY>` prints them, then this ticket's `resources/`, then
`AGENTS.md`, and only then the diff. Give it the exact commit range.
- **Tell it to verify rather than suspect**, to try to refute its own findings before
reporting them, to mark each finding CONFIRMED or PLAUSIBLE, and to give exact
`file:line` plus a concrete failure scenario. Tell it explicitly not to write,
commit, push, or run anything destructive.
- **Point it at the claims already made.** Every "proven by X" already written into
the ticket's risk or test tables must be re-derived, not taken on trust. A false
proof claim in Jira is worse than an unproven one, because it stops anyone else
from checking.
- **Ask the spec questions explicitly**, or the review only finds bugs in what you
did write and never notices what you didn't. Three questions, each answered against
the ticket and the plan rather than the diff alone: what the ticket asked for that
is missing or only partly done; what behavior is in the diff that nothing asked for;
and what looks implemented but is implemented wrongly. Have it quote the AC ID or
the plan step each finding traces to.
Then respond to the review, in writing, and put each finding on the page as you judge
it: added, then fixed with its commit or rejected with the reason.
- **Findings are not automatically true.** Verify each one against the codebase
before acting on it. Reviewers do produce confident wrong answers. Say plainly
which findings you rejected and why, rather than quietly accepting all of them.
- **Fix everything real**, each meaningful fix as its own commit. Do not defer a
known hole to a follow-up ticket unless the user decides that.
- **When a fix changes a mechanism rather than a line**, write a test that fails
against the old behavior and passes against the new one, and actually confirm it
fails by reverting the fix. A test that passes either way proves nothing.
- **If a finding invalidates something already written into Jira or a deliverable,
correct it there too.** The code being fixed does not fix the false claim.
- **Terminate deliberately.** Run the reviewer ONCE on the branch. `pre-mr-review`
then reviews the whole diff including your fixes and serves as the second pass.
Only dispatch a second reviewer if a fix changed a mechanism, not for line level
fixes, and never loop "review until clean."
5. Convene the review council before `pre-mr-review`. Read [COUNCIL.md](COUNCIL.md) and follow
it: it holds the seats, when the step applies, the brief, how to judge what comes back,
and when to stop. This step is done when every seat has returned or been recorded as
unavailable, and every finding on the page is fixed or rejected with its reason.
6. Confirm this project actually has a `pre-mr-review` skill (`.claude/skills/pre-mr-review/`
in the current repo). If it doesn't, stop here, tell the user implementation and
local verification are done but this repo has no `pre-mr-review` skill to run, and
let them decide how to proceed.
7. **Do NOT invoke the `pre-mr-review` skill yourself.** The org asks that this
review run from the developer's separate work Claude account, so the audit on
record must come from there, not from this session. Instead:
7. **Run `pre-mr-review` on the work account, from a Klaude delegate.** The org asks
that this review run from the developer's work Claude account, so the audit on record must
come from there, never from this session and never from another provider. Klaude is that
account (`~/.klaude`). The audit is one agent, not a council.
1. Show where the build stands with `ticket-page view <KEY> progress`, through
`html_preview` and then `html_render`. Then tell the user the branch is ready for
its pre-mr-review and ask them to run `/pre-mr-review` from their work account,
then let you know when it has finished. Stop and wait, this is a hard gate.
2. When they say it ran, read the audit and handoff files it wrote under
`.claude/audits/pre-mr/` (the context script from the repo's `pre-mr-review`
skill prints the exact paths). Check the `last_reviewed_head` in the audit
header matches the current HEAD; if the branch moved after their run, say so
and ask them to rerun before acting on a stale audit.
3. Read the verdict, and record it on the page with `ticket-page verdict`.
`html_preview` and then `html_render`. Write `ticket-page md <KEY>` to a scratch file.
2. Start it, then end your turn. T3 wakes you when it finishes:
```
delegate_task
title: pre-mr-review <KEY>
role: review
mode: async
target: { providerInstanceId: "claudeAgent_klaude" }
task: Call the Skill tool with "pre-mr-review" and run the audit on this branch.
Follow that skill's own rules, and start no subagents. The ticket and its
plan are in <scratch file>. <decisions from earlier runs, if any>
Reply with the verdict, the recommendation, the audited HEAD, and the audit
and handoff paths.
```
Leave `model` out so it runs Klaude's default, and leave `isolation` unset so the audit
lands in this checkout. A rerun is always a new `delegate_task`, one at a time.
Without `T3CODE_THREAD_ID`, or if Klaude is refused, ask Gib to run `/pre-mr-review`
from his work account and wait for him. Never fall back to another provider.
3. Read the audit and handoff files it wrote under `.claude/audits/pre-mr/` (the context
script from the repo's `pre-mr-review` skill prints the exact paths), not just the
delegate's summary. Check the `last_reviewed_head` in the audit header matches the
current HEAD. If the branch moved after the run, start a new run before acting on a
stale audit.
4. Read the verdict, and record it on the page with `ticket-page verdict`.
- **Ready to Open MR**: continue to step 8.
- **Almost Ready / Not Ready Yet**: fix what it flagged (each meaningful fix
as its own commit), then ask the user to rerun the review from the work
account and wait again. Repeat until the verdict is Ready to Open MR. Don't
write `mr.md` before that verdict is reached.
- **Almost Ready / Not Ready Yet**: fix what it flagged (each meaningful fix as its own
commit), then start a new run. Repeat until the verdict is Ready to Open MR. Don't
write `mr.md` before that verdict is reached. After three runs with no Ready verdict,
stop and ask Gib how to proceed.
Findings that need no code change (mentions, disclosures) get addressed in
`mr.md`'s Additional Notes or the Jira fields as usual.
@@ -736,22 +722,18 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
normal and expected at a Ready verdict. Never treat the recommendation as a
blocker or report it as one.
**Reruns converge by giving the audit more, not by repeating the command.**
When a rerun is needed, the user is running it, so every rerun costs them.
Make each one count:
- **Feed it context on the first run.** It reviews the diff, and left alone it
re-derives intent from the branch name and re-raises decisions that are
already settled. Ask the user to point it at the ticket's `ticket.data.js`, or give
them the output of `ticket-page md <KEY>` to paste in. This is the cheapest thing that reduces
repeat findings.
- **Answer its decisions explicitly in the rerun.** When it parks a finding
under "Decide before opening", write the decision and its reasoning into the
prompt for the next run, so it can reclassify rather than re-ask. A silent
rerun on an unchanged diff re-asks the same question.
- **Say so when it finds new things in unchanged code.** That means the earlier
run was an incomplete pass, not that the branch got worse. Tell the user
plainly, because it is the audit's problem to fix and it changes how much
weight the next run deserves.
**Reruns converge by giving the audit more, not by repeating the command.** Each run
spends the work account's quota, so make each one count:
- **Feed it context on the first run.** It reviews the diff, and left alone it re-derives
intent from the branch name and re-raises decisions that are already settled. The
scratch file in the task is the cheapest thing that reduces repeat findings.
- **Answer its decisions explicitly in the rerun.** When it parks a finding under "Decide
before opening", write the decision and its reasoning into the next task, so it can
reclassify rather than re-ask. A silent rerun on an unchanged diff re-asks the same
question. A decision that is genuinely Gib's goes to him first.
- **Say so when it finds new things in unchanged code.** That means the earlier run was an
incomplete pass, not that the branch got worse, and it changes how much weight the next
run deserves. Tell Gib at hand-off.
8. Read `~/.agents/skills/ticket/templates/mr.md` — this is the org's MR template,
copied into this skill so it still works even though the original
`.claude/docs/mr/template.md` no longer exists in the command-center repo. `mr.md`
@@ -760,8 +742,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
Fill it in for real:
- H1: from the ticket and the actual work done, not the ticket title verbatim if the
real change ended up narrower/broader.
- Summary: one short paragraph max. High level only, what changed, not how. This
is not the place for a list of every file touched.
- Summary: one short paragraph max. Open with the problem in plain words, then say
what changed, not how. Lead with why this MR exists rather than an inventory of what
it touches.
- Issue Link: `<JIRA_BASE_URL>/browse/<KEY>`.
- New ENVs: list any env vars actually introduced; "None" if none.
- Additional Notes: a bulleted checklist of the changes you actually made, not a
@@ -785,9 +768,9 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
the audit's own words, not a retelling of them. Concretely:
- Never hand-write a handoff section, and never edit one in place. If it is
wrong, thin, or stale, ask the user to rerun `/pre-mr-review` from their work
account to regenerate the handoff file, then re-paste.
- On EVERY rerun of theirs, replace the whole existing handoff block with the whole
wrong, thin, or stale, start a new Klaude run (step 7) to regenerate the handoff
file, then re-paste.
- On EVERY rerun, replace the whole existing handoff block with the whole
regenerated one. Do not patch the copy sitting in `mr.md` to match the new
head — that is how the two silently diverge, and the version the reviewer
reads stops being the version the audit actually produced.
@@ -838,34 +821,46 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
it to ADF, proof and mitigation cells included. Everything here is read by the Lead
and the PM.
**Proof first.** Before filling any proof column, capture working feature proof
yourself wherever possible: run the app (`run` skill) and screenshot the real
feature with `capture-proof`, or capture test output for behavior with no UI.
**Proof first.** Proof is evidence you observed yourself in this session. Before filling
any proof column, capture it wherever possible, using the most direct form the change allows:
**Screenshots come from Playwright, never from a browser-extension screen grab.**
Extension captures come out soft and hard to read, and a proof screenshot exists to be
read. `capture-proof shot` renders at twice the pixel density with animations frozen,
fonts loaded, and dev overlays hidden:
- For a bug, reproduce the failure, apply the fix, and watch the same reproduction pass.
- For UI and runtime behavior, run the app (`run` skill) and look at the real feature.
Green mocked tests do not prove the user experience works.
- For behavior with no UI, capture the test or command output.
```
capture-proof shot http://localhost:3000/companies --wait-for "text=Canonical ID" --out <target-dir>/proof/companies-grid-canonical-ids.png
```
**Screenshots and recordings come from T3's browser.** It runs in the thread, so Gib can
watch and sign in without leaving T3. Use one tab and keep its `tabId`:
- **Sign-in.** The first `shot` against an app that needs a session stops with a sign-in
hint. Ask Gib to run `capture-proof login <origin>` himself: it opens a visible
browser, he signs in and closes the window, and every later `shot` against that
origin reuses the session. It is saved under `~/.cache/ticket-page/auth/`, never in a
repo. The hint only catches a redirect to a sign-in path, so look at every capture
before recording it.
- **Frame the evidence.** Pass `--wait-for` with the thing the proof is about, so the
capture never catches a loading spinner. Crop to the part that matters with
`--selector`, or take the whole page with `--full`.
- **Interaction first.** For state that takes clicks or typing (an open dialog, a
form showing its warning), write a small script,
`export default async (page) => { await page.getByLabel("Canonical ID").fill("NIKON"); }`,
and pass it with `--script`. Keep scripts in a scratch path, not in `proof/`.
- Read every capture with the Read tool before recording it. A crisp picture of the
wrong screen is still wrong.
1. `preview_open` with the app's URL, or `preview_navigate` with
`target: {kind: "environment-port", port}` for a dev server.
2. `preview_resize` to `{mode: "freeform", width: 1280, height: 800}`. Saved screenshots
top out at 1280x800, so a larger viewport comes out downscaled and soft.
3. `preview_wait_for` the thing the proof is about, so the capture never catches a spinner.
Drive any state with `preview_snapshot` refs and `preview_act`: open the dialog, fill the
form, trigger the warning. Use `preview_set_appearance` for dark mode.
4. `preview_snapshot` with `save: true`, then copy `screenshotPath` into `proof/`.
5. For a flow a still cannot show, `preview_recording_start`, drive it, then
`preview_recording_stop` and copy the mp4 from its `path` into `proof/`. Keep it short:
a recording caps at 50 MiB.
6. `t3_preview_close` when the proof is done.
- **Sign-in.** If the tab lands on a sign-in page, ask Gib with AskUserQuestion to sign in in
that tab, then continue. Do not drive a login yourself.
- **Look before you record it.** Read every capture with the Read tool. A crisp picture of
the wrong screen is still wrong.
- **The fallback.** Use `capture-proof` when the screen is too dense to read at 1280x800, when
the proof needs a full page or an element crop, or outside T3 Code. It renders at 2x with
animations frozen and dev overlays hidden:
```
capture-proof shot http://localhost:3000/companies --wait-for "text=Canonical ID" --out <target-dir>/proof/companies-grid-canonical-ids.png
```
Pass `--selector` to crop, `--full` for the whole page, and `--script` for clicks and
typing. It keeps its own session, so the first `shot` against an app that needs one stops
with a sign-in hint: ask Gib to run `capture-proof login <origin>`. That hint only catches a
redirect to a sign-in path.
Save artifacts under `<target-dir>/proof/` with names that match the test-case
rows they prove, and put each on the page with `ticket-page proof`, naming the
criteria it proves and the mock it answers. A proof cell references its artifact by filename plus "attached"
@@ -941,7 +936,10 @@ Entered only when the user has confirmed (per Phase 0) that the plan is approved
unanswered.
10. Tell the user: implementation is committed on `<branch>`, `pre-mr-review` verdict is
Ready to Open MR, the Jira fields are filled (name which), `mr.md` is ready at its
path, and the ticket page shows the finished work beside its mocks. Then the manual list, which should only ever be:
path, and the ticket page shows the finished work beside its mocks. Show
`ticket-page view <KEY> progress` in the thread, embed each proof screenshot by absolute
path as `![caption](path)`, and link `mr.md` and the page path. Name any council seat that
could not run, and list the nits you declined as follow-ups. Then the manual list, which should only ever be:
- push the branch, open the MR, and paste `mr.md` in as the description
- upload the named proof or deliverable files (from `proof/` or `deliverables/`)
to the ticket, plus anything flagged as proof only Gib can capture
@@ -1,6 +1,6 @@
// The ticket page's data model: one `ticket.data.js` per ticket and one `epic.data.js`
// per epic, each a single `window.X = <strict JSON>;` assignment so a page opened from
// file:// can load it with a <script> tag. Everything that reads or writes those files
// per epic, each a single `window.X = <strict JSON>;` assignment so an open page can poll
// it with a <script> tag, where file:// and T3 Code's sandboxed viewer cannot fetch. Everything that reads or writes those files
// goes through this module, so the shape is defined once and validated on every write.
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -70,7 +70,7 @@ export interface Plan {
}
export interface Proof {
kind: "image" | "text" | "test";
kind: "image" | "video" | "text" | "test";
/** Relative to the ticket directory, for image and text proof. */
file?: string;
/** Embedded content of a text proof, because file:// pages cannot fetch it. */
@@ -70,10 +70,32 @@ describe("the CLI, end to end", () => {
// Self-contained: T3 Code's file viewer serves nothing from above the page's own folder.
const page = readFileSync(join(dir, "index.html"), "utf8");
expect(page).toContain(readFileSync(join(import.meta.dir, "../templates/site/site.js"), "utf8"));
expect(page).toContain(`"key":"KACP-1"`);
expect(page).not.toMatch(/(src|href)="\.\./);
expect(epic().stories.map(s => [s.key, s.status])).toEqual([["KACP-1", "todo"]]);
});
test("inline data cannot close the page's script block, and follows every write", () => {
cli("event", "KACP-1", "pasted </script><img src=x>");
const page = readFileSync(join(dir, "index.html"), "utf8");
const inline = page.match(/window\.TICKET = (.*);\n/)![1];
expect(inline).not.toContain("</script>");
expect(JSON.parse(inline).events.at(-1).text).toBe("pasted </script><img src=x>");
});
test("a linked worktree shares the main checkout's docs", () => {
const repo = mkdtempSync(join(tmpdir(), "tp-repo-"));
const git = (...a: string[]) => spawnSync("git", a, { cwd: repo, encoding: "utf8" });
git("init", "-q");
git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "x");
const tree = join(mkdtempSync(join(tmpdir(), "tp-tree-")), "wt");
git("worktree", "add", "-q", tree);
const { TICKET_DOCS: _, ...env } = process.env;
expect(spawnSync("bun", [join(import.meta.dir, "ticket-page.ts"), "init", "KACP-1", "--issue", issuePath], { cwd: tree, encoding: "utf8", env }).status).toBe(0);
expect(existsSync(join(repo, ".claude/docs/epics/KACP-9/KACP-1/ticket.data.js"))).toBe(true);
expect(existsSync(join(tree, ".claude"))).toBe(false);
});
test("a step is done only once its commit exists, and the epic row follows", () => {
const plan = join(scratch, "plan.json");
writeFileSync(plan, JSON.stringify({ approach: "<p>a</p>", criteria: [{ id: "AC1", text: "c", steps: [1] }], steps: [{ n: 1, text: "s" }, { n: 2, text: "t" }] }));
@@ -103,6 +125,10 @@ describe("the CLI, end to end", () => {
expect(cli("proof", "KACP-1", "--file", "proof/counts.txt", "--proves", "AC1", "--caption", "Counts").status).toBe(0);
expect(ticket().proof[0]).toMatchObject({ kind: "text", text: "70 of 70 codes stored" });
expect(cli("proof", "KACP-1", "--file", "proof/counts.txt", "--proves", "AC7", "--caption", "x").status).toBe(1);
writeFileSync(join(dir, "proof", "flow.mp4"), "");
expect(cli("proof", "KACP-1", "--file", "proof/flow.mp4", "--proves", "AC1", "--caption", "Flow").status).toBe(0);
expect(ticket().proof[1]).toMatchObject({ kind: "video", file: "proof/flow.mp4" });
expect(cli("unproof", "KACP-1", "proof/flow.mp4").status).toBe(0);
});
test("--amend keeps progress for the steps that remain, a rewrite starts over", () => {
@@ -1,7 +1,7 @@
// ticket-page: the only writer of a ticket's page data. See ../SITE.md for when the
// ticket skill calls each command. Run `ticket-page help` for the command list.
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { htmlToText, importIssue, type JiraIssue, scanResources } from "./jira-import";
@@ -34,11 +34,13 @@ const list = (s?: string) => (s ? s.split(",").map(x => x.trim()).filter(Boolean
// ── Where things live ────────────────────────────────────────────────────────
/** The main checkout's docs, even from a linked worktree. `.claude/docs` is usually gitignored,
* so a T3 Code worktree thread would otherwise start a second, disconnected ticket page. */
export function docsRoot(): string {
if (process.env.TICKET_DOCS) return resolve(process.env.TICKET_DOCS);
const git = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
if (git.status !== 0) fail("not inside a git repository, and TICKET_DOCS is not set");
return join(git.stdout.trim(), ".claude/docs/epics");
const git = spawnSync("git", ["worktree", "list", "--porcelain"], { encoding: "utf8" });
const main = git.status === 0 ? git.stdout.match(/^worktree (.+)$/m)?.[1] : undefined;
return join(main ?? fail("not inside a git repository, and TICKET_DOCS is not set"), ".claude/docs/epics");
}
function ticketDir(key: string): string {
@@ -49,21 +51,28 @@ function ticketDir(key: string): string {
const ticketFile = (dir: string) => join(dir, "ticket.data.js");
const epicFile = (epicKey: string) => join(docsRoot(), epicKey, "epic.data.js");
/** Writes the page shell with the renderer's CSS and JS inlined, so the page carries
* everything but its own data file. T3 Code's file viewer serves an HTML file's assets only
* from its own folder and below, so a shell that pointed at a shared ../../_site/ renderer
* rendered blank there. Run on every write so an old ticket picks up the current renderer. */
function refreshSite(pageDir: string, kind: "ticket" | "epic", title: string) {
/** Writes the page shell with the renderer and the data inlined, so index.html renders with
* nothing else: no server and no other file. T3 Code's file viewer sandboxes the page and
* serves assets only from its own folder and below, so a shell that leaned on a shared
* ../../_site/ renderer rendered blank there. The sibling data file stays the store, and the
* open page polls it to stay live. Run on every write, so the page matches the data and an
* old ticket picks up the current renderer. */
function refreshSite(pageDir: string, kind: "ticket" | "epic", data: Ticket | Epic) {
const fill: Record<string, string> = {
CSS: readFileSync(join(SITE_TEMPLATES, "site.css"), "utf8"),
JS: readFileSync(join(SITE_TEMPLATES, "site.js"), "utf8"),
TITLE: title.replace(/[<&]/g, c => (c === "<" ? "&lt;" : "&amp;")),
TITLE: `${data.key}: ${data.title}`.replace(/[<&]/g, c => (c === "<" ? "&lt;" : "&amp;")),
// Jira HTML and mock artboards hold `</script>`, which would end the inline block early.
DATA: `window.${kind.toUpperCase()} = ${JSON.stringify(data).replace(/</g, "\\u003c")};`,
};
// A replacer function, because the renderer's own source is full of `$` sequences that
// a replacement string would read as patterns.
const shell = readFileSync(join(SITE_TEMPLATES, `${kind}.html`), "utf8").replace(/\{\{(CSS|JS|TITLE)\}\}/g, (_, k: string) => fill[k]);
const shell = readFileSync(join(SITE_TEMPLATES, `${kind}.html`), "utf8").replace(/\{\{(CSS|JS|TITLE|DATA)\}\}/g, (_, k: string) => fill[k]);
const page = join(pageDir, "index.html");
if (!existsSync(page) || readFileSync(page, "utf8") !== shell) writeFileSync(page, shell);
if (existsSync(page) && readFileSync(page, "utf8") === shell) return;
const tmp = `${page}.${process.pid}.tmp`;
writeFileSync(tmp, shell);
renameSync(tmp, page);
}
// ── Ticket reads and writes ──────────────────────────────────────────────────
@@ -75,7 +84,7 @@ function load(key: string) {
function save(dir: string, t: Ticket) {
writeData(ticketFile(dir), "ticket", t);
refreshSite(dir, "ticket", `${t.key}: ${t.title}`);
refreshSite(dir, "ticket", t);
if (t.epic) syncStory(t);
}
@@ -97,7 +106,7 @@ function syncStory(t: Ticket) {
s.progress = progress(t);
if (s.status !== "done") s.status = storyStatus(t);
writeData(file, "epic", e);
refreshSite(dirname(file), "epic", `${e.key}: ${e.title}`);
refreshSite(dirname(file), "epic", e);
}
// ── Commands ─────────────────────────────────────────────────────────────────
@@ -259,9 +268,10 @@ command("proof", "proof <KEY> (--file proof/<name> | --test NAME) --proves AC1,A
const caption = a.one("caption") ?? fail("proof needs --caption");
if (file && !existsSync(join(dir, file))) fail(`${file} does not exist in ${dir}`);
const isText = !!file && /\.(txt|log|md|json|csv|diff)$/i.test(file);
const isVideo = !!file && /\.(mp4|webm|mov)$/i.test(file);
t.proof = t.proof.filter(p => (file ? p.file !== file : p.test !== test));
t.proof.push({
kind: test ? "test" : isText ? "text" : "image", ...(file ? { file } : { test }),
kind: test ? "test" : isText ? "text" : isVideo ? "video" : "image", ...(file ? { file } : { test }),
...(isText ? { text: readFileSync(join(dir, file!), "utf8") } : {}),
proves, caption, ...(a.one("mock") ? { mock: a.one("mock") } : {}), at: now(),
});
@@ -341,9 +351,9 @@ command("open", "open <KEY> print the page path, and open it in the browser ou
const { dir, t } = load(a.pos[0]);
// Rewrites the shells first, so a page last written before the renderer was inlined
// opens in T3 too.
refreshSite(dir, "ticket", `${t.key}: ${t.title}`);
refreshSite(dir, "ticket", t);
const epic = t.epic && epicFile(t.epic.key);
if (epic && existsSync(epic)) { const e = readData(epic, "epic"); refreshSite(dirname(epic), "epic", `${e.key}: ${e.title}`); }
if (epic && existsSync(epic)) { const e = readData(epic, "epic"); refreshSite(dirname(epic), "epic", e); }
const page = join(dir, "index.html");
// Inside a T3 Code thread the agent links this path and the page opens in T3's file
// viewer, beside the conversation. A browser window would be one more thing to juggle.
@@ -388,7 +398,7 @@ function withEpic(key: string, fn: (e: Epic) => void) {
const e = existsSync(file) ? readData(file, "epic") : fail(`no epic page for ${key}. Run: ticket-page epic ${key} init --title T`);
fn(e);
writeData(file, "epic", e);
refreshSite(dirname(file), "epic", `${e.key}: ${e.title}`);
refreshSite(dirname(file), "epic", e);
}
command("epic", "epic <EPIC> init --title T [--jira URL] | story <KEY> [--title T --hours H --risk R --after K1,K2 --status S] | order <KEY>... | why --file <reasons.txt> | closed <KEY> --title T", a => {
@@ -399,7 +409,7 @@ command("epic", "epic <EPIC> init --title T [--jira URL] | story <KEY> [--title
e.title = a.one("title") ?? (e.title || fail("epic init needs --title"));
e.jira = a.one("jira") ?? (e.jira || `${process.env.JIRA_BASE_URL ?? "https://ksense-tech.atlassian.net"}/browse/${key}`);
writeData(file, "epic", e);
refreshSite(dirname(file), "epic", `${e.key}: ${e.title}`);
refreshSite(dirname(file), "epic", e);
return console.log(join(dirname(file), "index.html"));
}
withEpic(key, e => {
@@ -11,6 +11,7 @@
<body data-page="epic" data-src="epic.data.js">
<div id="app"><p class="loading">Loading epic.data.js…</p></div>
<script>
{{DATA}}
{{JS}}
</script>
</body>
@@ -1,8 +1,9 @@
// Renderer for ticket and epic pages. ticket-page inlines it into every page shell, so a
// page needs nothing outside its own folder (T3 Code's file viewer allows nothing else).
// A page shell names its data file in <body data-src>. This script loads that file with a
// <script> tag (file:// pages cannot fetch), re-loads it every two seconds, and redraws
// only when the data changed. The data shape is defined in scripts/ticket-data.ts.
// Renderer for ticket and epic pages. ticket-page inlines it and the page's data into every
// page shell, so index.html renders on its own with no server and no other file.
// A page shell also names its data file in <body data-src>. Re-loading that file every two
// seconds with a <script> tag (file:// and T3 Code's sandboxed viewer cannot fetch) is what
// keeps an open page live, and a page that cannot reach it still shows its last write.
// The data shape is defined in scripts/ticket-data.ts.
// @ts-check
/** @typedef {import('../../scripts/ticket-data').Ticket} Ticket */
/** @typedef {import('../../scripts/ticket-data').Epic} Epic */
@@ -40,12 +41,14 @@
const mark = st => h('span.mark' + (st === 'done' ? '.done' : st === 'running' ? '.run' : ''), { title: st });
const pill = (text, tone) => h('span.pill' + (tone ? '.' + tone : ''), text);
// T3 Code's sandboxed viewer has no localStorage, so ticks there last while the page stays open.
const ticks = new Map();
/** The viewer's own review tick, kept in this browser only and separate from the agent's done mark. */
function seen(scope, id) {
const key = `seen:${scope}:${id}`;
let on = false; try { on = localStorage.getItem(key) === '1'; } catch {}
let on = ticks.get(key) ?? false; try { on ||= localStorage.getItem(key) === '1'; } catch {}
return h('label.seen', { title: 'Your own review tick, stored in this browser' },
h('input', { type: 'checkbox', checked: on || null, onchange: e => { try { localStorage.setItem(key, e.target.checked ? '1' : '0'); } catch {} } }), 'reviewed');
h('input', { type: 'checkbox', checked: on || null, onchange: e => { ticks.set(key, e.target.checked); try { localStorage.setItem(key, e.target.checked ? '1' : '0'); } catch {} } }), 'reviewed');
}
// ── Loading and live reload ──────────────────────────────────────────────
@@ -66,7 +69,7 @@
const el = document.querySelector('.live');
if (!el) return;
el.classList.toggle('stale', failed);
el.textContent = failed ? 'Live: last reload failed, showing the previous version' : `Live, checked ${clock(new Date(loadedAt).toISOString())}`;
el.textContent = failed ? 'Snapshot from the last write' : `Live, checked ${clock(new Date(loadedAt).toISOString())}`;
}
function draw() {
@@ -151,7 +154,8 @@
/** @param {Ticket} t @param {import('../../scripts/ticket-data').Proof} p */
function compare(t, p) {
const m = p.mock && t.plan?.mocks.find(x => x.id === p.mock);
const evidence = p.kind === 'text' ? h('pre.textproof', p.text || '') : p.kind === 'test' ? h('pre.textproof', 'Passing test: ' + p.test) : h('a', { href: p.file, target: '_blank' }, h('img.shot', { src: p.file, alt: p.file }));
const evidence = p.kind === 'text' ? h('pre.textproof', p.text || '') : p.kind === 'test' ? h('pre.textproof', 'Passing test: ' + p.test)
: p.kind === 'video' ? keep('video:' + p.file, () => h('video.shot', { src: p.file, controls: true, preload: 'metadata' })) : h('a', { href: p.file, target: '_blank' }, h('img.shot', { src: p.file, alt: p.file }));
return h('div.compare',
h('figure', h('figcaption', 'Mock, drawn at plan time'), m ? artboard(m) : h('div.placeholder', 'No mock for this one.')),
h('figure', h('figcaption', 'Proof, captured from the build'), evidence));
@@ -304,6 +308,7 @@
e.closed.length ? h('p.muted', { style: 'margin-top:18px;font-size:.9em' }, 'Closed: ', e.closed.map(c => `${c.key} ${c.title}`).join(', '), '.') : null));
}
if (window[GLOBAL]) { last = JSON.stringify(window[GLOBAL]); loadedAt = Date.now(); draw(); }
load();
setInterval(load, 2000);
})();
@@ -11,6 +11,7 @@
<body data-page="ticket" data-src="ticket.data.js">
<div id="app"><p class="loading">Loading ticket.data.js…</p></div>
<script>
{{DATA}}
{{JS}}
</script>
</body>