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

@@ -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 => {