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:
1 parent
968e0a1045
commit
86f6e391b7
14 files changed
+410
-187
No files matched your search
@@ -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>
|
||||
```
|
||||
@@ -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
@@ -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 ``, 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 === "<" ? "<" : "&")),
|
||||
TITLE: `${data.key}: ${data.title}`.replace(/[<&]/g, c => (c === "<" ? "<" : "&")),
|
||||
// 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>
|
||||
|
||||
Reference in new issue
Block a user