snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
This commit is contained in:
254
.claude/hooks/render-status.mjs
Executable file
254
.claude/hooks/render-status.mjs
Executable file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env node
|
||||
// SubagentStop - render a portable harness board from the session's cheap state
|
||||
// (tickets, review flags, progress log, live worktrees) into
|
||||
// <REPO>/.harness/<SESSION_SHORT_ID>/{STATUS.md,TICKETS.md,status.json}.
|
||||
//
|
||||
// Why: on the VS Code extension the /tmp worktrees are invisible, and on any
|
||||
// surface the raw `harness-progress.log` tail drowns the chat. `.harness/` sits
|
||||
// in the project dir, gitignored, so it shows in the VS Code Explorer AND the
|
||||
// desktop file browser (which renders Markdown as a formatted preview). The
|
||||
// developer sees who is doing what without the log tail polluting the chat.
|
||||
//
|
||||
// Gated to #technical-harness runs: renders ONLY when the technical-only
|
||||
// harness-progress.log exists (orchestrator.md writes it under PERSONA: technical),
|
||||
// so a normal / web-chat run never drops a board into the repo. Per-session
|
||||
// subdir (SESSION_SHORT_ID) keeps concurrent sessions from colliding. No-op under
|
||||
// a managed launcher (owns its own UI). Never throws: a view must not break a run.
|
||||
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { createHookContext } from "./lib/context.mjs";
|
||||
import { REPO } from "./lib/paths.mjs";
|
||||
import { reviewsDir } from "./lib/reviews.mjs";
|
||||
import { getBaseBranch, git } from "./lib/git.mjs";
|
||||
import { sessionBaseBranch, sessionBranch } from "./lib/topology.mjs";
|
||||
|
||||
if (process.env.CHAT_SESSION_DIR) process.exit(0);
|
||||
|
||||
let input = {};
|
||||
try {
|
||||
input = JSON.parse(readFileSync(0, "utf8"));
|
||||
} catch {
|
||||
process.exit(0);
|
||||
}
|
||||
const ctx = createHookContext(input, "render-status");
|
||||
|
||||
const progressLog = join(ctx.sessionDir, "harness-progress.log");
|
||||
// No technical progress log → not a #technical-harness run → stay inert.
|
||||
if (!existsSync(progressLog)) process.exit(0);
|
||||
|
||||
const WT_RE = /^(TASK-\d+|simple|_session)$/;
|
||||
const TICKET_RE = /^TASK-\d+\.json$/;
|
||||
|
||||
// Tickets live in the session dir itself or a `tickets/` subdir - try both.
|
||||
function readTickets() {
|
||||
for (const dir of [ctx.ticketsDir, ctx.sessionDir]) {
|
||||
if (!existsSync(dir)) continue;
|
||||
const files = readdirSync(dir).filter((f) => TICKET_RE.test(f));
|
||||
if (!files.length) continue;
|
||||
return files.sort().map((f) => {
|
||||
try {
|
||||
const t = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
||||
return {
|
||||
id: t.id || f.replace(/\.json$/, ""),
|
||||
title: t.title || t.description || "",
|
||||
status: t.status || "planned",
|
||||
acceptanceCriteria: t.acceptance_criteria || [],
|
||||
files: t.files_to_modify || [],
|
||||
dependencies: t.dependencies || [],
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
id: f.replace(/\.json$/, ""),
|
||||
title: "",
|
||||
status: "unreadable",
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// A live worktree dir under the session base means work still in flight (the
|
||||
// merger's cleanup-worktree removes it on merge).
|
||||
function liveWorktrees() {
|
||||
if (!existsSync(ctx.worktreeBase)) return [];
|
||||
return readdirSync(ctx.worktreeBase, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && WT_RE.test(d.name))
|
||||
.map((d) => d.name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
const approved = (id) =>
|
||||
existsSync(join(reviewsDir(ctx), `${id}-quality-reviewer`));
|
||||
|
||||
function recentActivity(max = 25) {
|
||||
const lines = readFileSync(progressLog, "utf8")
|
||||
.split("\n")
|
||||
.filter((l) => l.trim());
|
||||
return lines.slice(-max);
|
||||
}
|
||||
|
||||
const cell = (s) => String(s ?? "").replace(/\|/g, "\\|");
|
||||
|
||||
// The cumulative diff of everything the session has changed so far: work already
|
||||
// merged into `session/<short>` PLUS in-flight work still on live dev branches
|
||||
// (`<short>/TASK-XXX`, `<short>/simple`) not yet merged. Written as a `.diff` file
|
||||
// so the developer can open it and SEE the change on any surface (the VS Code
|
||||
// extension does not show the /tmp worktree diffs). Updated on every agent STOP
|
||||
// (not continuously while an agent works), so a developer's work appears once THAT
|
||||
// developer stops - before the merge, not only after it. Null when nothing has
|
||||
// changed vs the fork anchor yet.
|
||||
const verifyRef = (ref) =>
|
||||
git(["rev-parse", "--verify", "--quiet", ref]).status === 0;
|
||||
|
||||
function diffSection(label, range) {
|
||||
const patch = git(["diff", range]).stdout;
|
||||
if (!patch.trim()) return null;
|
||||
return {
|
||||
label,
|
||||
range,
|
||||
patch,
|
||||
stat: git(["diff", "--stat", range]).stdout.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function sessionDiff() {
|
||||
const anchor = sessionBaseBranch(ctx);
|
||||
const baseRef = verifyRef(anchor) ? anchor : getBaseBranch();
|
||||
const sessRef = sessionBranch(ctx);
|
||||
const sections = [];
|
||||
|
||||
// Merged work: fork anchor -> session branch.
|
||||
if (verifyRef(sessRef)) {
|
||||
const s = diffSection(`merged into ${sessRef}`, `${baseRef}...${sessRef}`);
|
||||
if (s) sections.push(s);
|
||||
}
|
||||
|
||||
// In-flight work: each <short>/* dev branch with commits not yet on the session
|
||||
// branch (diffed against it, so the already-merged part is not counted twice).
|
||||
const inflightBase = verifyRef(sessRef) ? sessRef : baseRef;
|
||||
const devBranches = git([
|
||||
"for-each-ref",
|
||||
"--format=%(refname:short)",
|
||||
`refs/heads/${ctx.sessionShort}/`,
|
||||
])
|
||||
.stdout.split("\n")
|
||||
.map((b) => b.trim())
|
||||
.filter(Boolean);
|
||||
for (const br of devBranches) {
|
||||
const s = diffSection(`in flight: ${br}`, `${inflightBase}...${br}`);
|
||||
if (s) sections.push(s);
|
||||
}
|
||||
|
||||
if (!sections.length) return null;
|
||||
return {
|
||||
patch: sections
|
||||
.map((s) => `### ${s.label} (${s.range})\n\n${s.patch}`)
|
||||
.join("\n\n"),
|
||||
stat: sections.map((s) => `${s.label}:\n${s.stat}`).join("\n\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function build() {
|
||||
const tickets = readTickets();
|
||||
const worktrees = liveWorktrees();
|
||||
const activeTasks = worktrees.filter((w) => /^TASK-\d+$/.test(w));
|
||||
const merged = tickets.filter((t) => t.status === "merged").length;
|
||||
const updated = new Date(statSync(progressLog).mtime).toISOString();
|
||||
const activity = recentActivity();
|
||||
const diff = sessionDiff();
|
||||
|
||||
// STATUS.md - the live board.
|
||||
const statusMd = [
|
||||
`# ⚙ Harness session \`${ctx.sessionShort}\``,
|
||||
`_updated ${updated}_`,
|
||||
"",
|
||||
`**${merged}/${tickets.length} merged** · ${activeTasks.length} in flight`,
|
||||
"",
|
||||
"## Tickets",
|
||||
tickets.length
|
||||
? "| Ticket | Status | Review | Title |"
|
||||
: "_no tickets yet_",
|
||||
tickets.length ? "|---|---|---|---|" : "",
|
||||
...tickets.map(
|
||||
(t) =>
|
||||
`| ${t.id} | ${t.status} | ${approved(t.id) ? "✅" : ""} | ${cell(t.title)} |`,
|
||||
),
|
||||
"",
|
||||
"## Live worktrees",
|
||||
worktrees.length
|
||||
? worktrees.map((w) => `- \`${w}\``).join("\n")
|
||||
: "_none (nothing in flight)_",
|
||||
"",
|
||||
"## Changes",
|
||||
diff
|
||||
? `Full patch in \`session.diff\` (same folder).\n\n\`\`\`\n${diff.stat}\n\`\`\``
|
||||
: "_no changes yet_",
|
||||
"",
|
||||
"## Recent activity",
|
||||
"```",
|
||||
...activity,
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
.filter((l) => l !== "")
|
||||
.join("\n");
|
||||
|
||||
// TICKETS.md - full ticket detail for the plan gate.
|
||||
const ticketsMd = [
|
||||
`# Tickets: session \`${ctx.sessionShort}\``,
|
||||
"",
|
||||
...tickets.flatMap((t) => [
|
||||
`## ${t.id} · ${t.title}`,
|
||||
`- **Status:** ${t.status}${approved(t.id) ? " (reviewed ✅)" : ""}`,
|
||||
`- **Depends on:** ${t.dependencies?.length ? t.dependencies.join(", ") : "(none)"}`,
|
||||
`- **Files:** ${t.files?.length ? t.files.map((f) => `\`${f}\``).join(", ") : "(none)"}`,
|
||||
"- **Acceptance criteria:**",
|
||||
...(t.acceptanceCriteria?.length
|
||||
? t.acceptanceCriteria.map((c) => ` - ${c}`)
|
||||
: [" - (none)"]),
|
||||
"",
|
||||
]),
|
||||
].join("\n");
|
||||
|
||||
// status.json - compact, for the opt-in statusline.
|
||||
const statusJson = {
|
||||
short: ctx.sessionShort,
|
||||
updated,
|
||||
tickets: {
|
||||
total: tickets.length,
|
||||
merged,
|
||||
inProgress: activeTasks.length,
|
||||
},
|
||||
active: activeTasks.map((task) => ({ task })),
|
||||
last: activity[activity.length - 1] || "",
|
||||
};
|
||||
|
||||
return { statusMd, ticketsMd, statusJson, diffPatch: diff?.patch };
|
||||
}
|
||||
|
||||
try {
|
||||
const { statusMd, ticketsMd, statusJson, diffPatch } = build();
|
||||
const outDir = join(REPO, ".harness", ctx.sessionShort);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
writeFileSync(join(outDir, "STATUS.md"), statusMd);
|
||||
writeFileSync(join(outDir, "TICKETS.md"), ticketsMd);
|
||||
writeFileSync(
|
||||
join(outDir, "status.json"),
|
||||
JSON.stringify(statusJson, null, 2),
|
||||
);
|
||||
if (diffPatch) writeFileSync(join(outDir, "session.diff"), diffPatch);
|
||||
ctx.log(`rendered board -> ${outDir}`);
|
||||
} catch (e) {
|
||||
ctx.log(`skipped: ${e?.message ?? e}`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user