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