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

This commit is contained in:
upstream-mirror
2026-09-18 01:48:50 +05:00
commit 93ecc1a6f8
787 changed files with 75240 additions and 0 deletions

View File

@@ -0,0 +1,682 @@
#!/usr/bin/env node
//
// Harness monitor — a terminal view of an agent-workflow session.
//
// Gather informations from three places, all keyed by the session id:
//
// 1. hooks.log <TMP_ROOT>/<sanitized-repo>/<id>/hooks.log
// 2. subagent transcripts ~/.claude/projects/<slug>/<id>/subagents/*.jsonl
// 3. tickets <CHAT_SESSION_DIR or TICKETS_DIR>/TASK-*.json
//
// Usage:
// .claude/scripts/harness-monitor.mjs # latest session, one-shot summary
// .claude/scripts/harness-monitor.mjs --watch # latest session, live (re-renders)
// .claude/scripts/harness-monitor.mjs --session <id> # a specific session id
// .claude/scripts/harness-monitor.mjs --list # list known sessions, newest first
import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
import { join, basename } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process";
// ─── paths & session discovery ──────────────────────────────────────────────
const REPO = (() => {
if (process.env.CLAUDE_PROJECT_DIR) return process.env.CLAUDE_PROJECT_DIR;
try {
return execSync("git rev-parse --show-toplevel", {
encoding: "utf8",
}).trim();
} catch {
return process.cwd();
}
})();
const TMP_ROOT = process.env.CRM_TMP_ROOT || "/tmp";
const sanitize = (p) => String(p).replace(/\//g, "_");
const WORKTREE_ROOT = join(TMP_ROOT, sanitize(REPO)); // /tmp/_workspaces_atomic-crm
const CLAUDE_PROJ = join(
homedir(),
".claude",
"projects",
// Claude Code names the project dir by replacing EVERY non-alphanumeric char
// (slashes AND dots, e.g. ".code-workspace") with "-". Replacing only "/" broke
// discovery for any repo path containing a dot → "no subagent transcripts found".
REPO.replace(/[^a-zA-Z0-9]/g, "-"),
);
// A session id is a worktree-base subdir that has a hooks.log. Test fixtures
// (td-1, td-2, …) are skipped — they are not real sessions.
function listSessions() {
if (!existsSync(WORKTREE_ROOT)) return [];
return readdirSync(WORKTREE_ROOT)
.filter((name) => !/^td-/.test(name))
.map((id) => ({ id, log: join(WORKTREE_ROOT, id, "hooks.log") }))
.filter((s) => existsSync(s.log))
.map((s) => ({ ...s, mtime: statSync(s.log).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime);
}
function sessionPaths(id) {
const base = join(WORKTREE_ROOT, id);
return {
id,
base,
hooksLog: join(base, "hooks.log"),
mainTranscript: join(CLAUDE_PROJ, `${id}.jsonl`),
subagentsDir: join(CLAUDE_PROJ, id, "subagents"),
// Tickets land in the session dir itself (TICKETS_DIR = session_dir = base);
// <base>/tickets is the legacy fallback layout.
ticketDirs: [base, join(base, "tickets")],
};
}
// A worktree path → a short, human label. `simple` is the simple-developer's
// worktree; `_session` is the shared integration worktree the merger merges
// into before promoting to the base branch.
function wtLabel(path) {
if (!path) return "";
const b = basename(path);
if (b === "_session") return "_session (integration)";
if (b === "simple") return "simple (dev worktree)";
return b; // TASK-XXX
}
// ─── colour helpers ─────────────────────────────────────────────────────────
const COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
const c = (code, s) => (COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
const dim = (s) => c("2", s);
const bold = (s) => c("1", s);
const red = (s) => c("31", s);
const green = (s) => c("32", s);
const yellow = (s) => c("33", s);
const blue = (s) => c("36", s);
const mag = (s) => c("35", s);
const hhmmss = (iso) => {
const d = new Date(iso);
return Number.isNaN(d.getTime()) ? iso : d.toISOString().slice(11, 19);
};
// ─── hooks.log parsing ──────────────────────────────────────────────────────
// Line shape: `[<iso-ts>] [<hook>] <rest...>`. Lines without that prefix are
// continuation output (e.g. a multi-line validation failure) and attach to the
// previous event as detail.
const LINE_RE = /^\[([^\]]+)\]\s+\[([^\]]+)\]\s+(.*)$/;
function parseHooksLog(path) {
if (!existsSync(path)) return [];
const events = [];
for (const line of readFileSync(path, "utf8").split("\n")) {
if (!line) continue;
const m = line.match(LINE_RE);
if (!m) {
if (events.length) events[events.length - 1].detail.push(line);
continue;
}
const [, ts, hook, rest] = m;
const fields = {};
for (const f of rest.matchAll(/(\w+)=(\S+)/g)) fields[f[1]] = f[2];
const state = rest.split(/\s+/)[0];
events.push({ ts, hook, state, rest, fields, detail: [] });
}
return events;
}
// ─── subagent transcripts ───────────────────────────────────────────────────
function readJsonl(path) {
if (!existsSync(path)) return [];
const out = [];
for (const line of readFileSync(path, "utf8").split("\n")) {
if (!line.trim()) continue;
try {
out.push(JSON.parse(line));
} catch {
/* skip malformed line */
}
}
return out;
}
// Claude Code records a failed hook command as an `attachment` whose inner type
// is hook_(non_)blocking_error.
function hookErrorsFrom(records) {
const errs = [];
for (const rec of records) {
const a = rec.attachment;
if (!a || !/hook_(non_)?blocking_error/.test(a.type || "")) continue;
errs.push({
ts: rec.timestamp,
hookName: a.hookName || a.hookEvent || "hook",
stderr: (a.stderr || "").split("\n")[0].slice(0, 120),
command: (a.command || "").slice(0, 80),
exitCode: a.exitCode,
blocking: !/non_blocking/.test(a.type || ""),
});
}
return errs;
}
function readAgents(dir) {
if (!existsSync(dir)) return [];
const agents = [];
for (const file of readdirSync(dir)) {
if (!file.endsWith(".jsonl")) continue;
const agentId = basename(file, ".jsonl");
const meta = (() => {
const p = join(dir, `${agentId}.meta.json`);
try {
return JSON.parse(readFileSync(p, "utf8"));
} catch {
return {};
}
})();
const records = readJsonl(join(dir, file));
const toolCounts = {};
const messages = []; // SendMessage payloads, timestamped
let lastText = "";
let firstTs = null;
let lastTs = null;
for (const rec of records) {
if (rec.timestamp) {
if (!firstTs) firstTs = rec.timestamp;
lastTs = rec.timestamp;
}
const content = rec.message?.content;
if (!Array.isArray(content)) continue;
for (const block of content) {
if (block.type === "tool_use") {
toolCounts[block.name] = (toolCounts[block.name] || 0) + 1;
if (block.name === "SendMessage") {
const to = block.input?.to ?? "?";
const msg = block.input?.message;
messages.push({
to,
text: typeof msg === "string" ? msg : JSON.stringify(msg),
ts: rec.timestamp,
});
}
} else if (block.type === "text" && block.text.trim()) {
lastText = block.text.trim();
}
}
}
agents.push({
agentId,
type: meta.agentType || "?",
description: meta.description || "",
toolCounts,
messages,
lastText,
firstTs,
lastTs,
hookErrors: hookErrorsFrom(records),
verdict: classifyVerdict(lastText),
});
}
// Chronological by last activity so repeated dispatches of the same agent
// (retries / resumes) read top-to-bottom in the order they ran.
return agents.sort((a, b) =>
String(a.lastTs || "").localeCompare(String(b.lastTs || "")),
);
}
// The orchestrator is the main session transcript, NOT a subagent.
function readOrchestrator(path) {
const records = readJsonl(path);
if (!records.length) return null;
let lastText = "";
let lastTs = null;
const dispatches = []; // { ts, type } — when the orchestrator spawned each agent
for (const rec of records) {
if (rec.timestamp) lastTs = rec.timestamp;
const content = rec.message?.content;
if (!Array.isArray(content)) continue;
for (const block of content) {
if (
block.type === "tool_use" &&
(block.name === "Agent" || block.name === "Task")
)
dispatches.push({
ts: rec.timestamp,
type:
block.input?.subagent_type || block.input?.description || "agent",
});
else if (block.type === "text" && block.text.trim())
lastText = block.text.trim();
}
}
return { lastText, lastTs, dispatches, hookErrors: hookErrorsFrom(records) };
}
// Merge every timestamped signal — orchestrator spawns, agent SendMessages,
// agent completions, and the key hook milestones — into one chronological
// stream. This is the "what happened, in order" view: dev → merger, etc.
function buildEventTimeline(orchestrator, agents, events) {
const items = [];
const push = (ts, color, icon, text) =>
ts && items.push({ ts, color, icon, text });
for (const d of orchestrator?.dispatches ?? [])
push(d.ts, blue, "▶", `orchestrator spawned ${bold(d.type)}`);
for (const a of agents) {
for (const m of a.messages)
push(
m.ts,
mag,
"→",
`${a.type} → ${bold(m.to)}: ${m.text.split("\n")[0].slice(0, 80)}`,
);
const mark =
a.verdict.kind === "done"
? green("✓")
: a.verdict.kind === "fail"
? red("✗")
: dim("■");
const vcolor =
a.verdict.kind === "fail" ? red : a.verdict.kind === "done" ? green : dim;
push(
a.lastTs,
vcolor,
mark,
`${a.type} finished — ${a.verdict.label.slice(0, 80)}`,
);
}
for (const e of events) {
if (e.hook === "setup-worktree" && e.state === "CREATED")
push(e.ts, blue, "⊕", `worktree created ${dim(wtLabel(e.fields.path))}`);
else if (e.hook === "validate" && e.state === "OK")
push(e.ts, green, "✓", `validation OK ${dim(wtLabel(e.fields.wt))}`);
else if (e.hook === "validate" && e.state === "FAIL")
push(
e.ts,
red,
"✗",
`validation FAIL ${e.fields.step || ""}${e.detail.length ? dim(" — " + e.detail.join(" ").slice(0, 60)) : ""}`,
);
else if (e.hook === "circuit-breaker" && /^BLOCK/.test(e.rest))
push(e.ts, red, "⛔", `circuit-breaker BLOCK (count=${e.fields.count})`);
}
if (orchestrator?.lastTs)
push(
orchestrator.lastTs,
dim,
"▣",
`orchestrator last said: ${orchestrator.lastText.split("\n").pop().slice(0, 80)}`,
);
return items.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
}
// Synthesize a single completion verdict for the whole session from the
// strongest available signal: the merger's DONE line, then a clean
// no-dirty-worktree finish, else still-running / unknown.
function sessionStatus(agents, events) {
const merger = agents.find((a) => a.type === "merger");
if (merger) {
const m = merger.lastText.match(/DONE:\s*commit=(\S+)/i);
if (m)
return green(
`✓ COMPLETE — merger promoted to main (commit ${m[1].replace(/[.,]$/, "")})`,
);
if (/promotion merged|merged .*into main/i.test(merger.lastText))
return green("✓ COMPLETE — merger reported the session merged into main");
if (merger.verdict.kind === "fail")
return red(`✗ merger FAILED — ${merger.verdict.label.slice(0, 120)}`);
}
if (agents.some((a) => a.verdict.kind === "fail"))
return red("✗ FAILED — an agent ended in failure (see DIAGNOSIS)");
const lastValidate = [...events]
.reverse()
.find((e) => e.hook === "validate" && e.state === "ACCEPT");
if (lastValidate && /no_dirty_worktree/.test(lastValidate.rest))
return yellow(
"… likely done — last validation found nothing left to do, but no merger DONE was seen",
);
return yellow("… in progress / outcome unclear");
}
// The structured-text contract: agents end with DONE / FAILED / BLOCKED, or a
// reviewer verdict.
function classifyVerdict(text) {
if (!text) return { kind: "running", label: "running / no final message" };
const head =
text
.split("\n")
.find((l) => l.trim())
?.slice(0, 200) ?? "";
if (/^FAILED\b|permanently deadlocked|\bBLOCKED\b/im.test(text))
return { kind: "fail", label: head };
if (
/^DONE:|merged\s+TASK|merged successfully|promotion merged|completed successfully|^Verdict:\s*(APPROVED|GREEN)/im.test(
text,
)
)
return { kind: "done", label: head };
return { kind: "info", label: head };
}
// ─── tickets ────────────────────────────────────────────────────────────────
function readTickets(dirs) {
for (const dir of dirs) {
if (!existsSync(dir)) continue;
const files = readdirSync(dir).filter((f) => /^TASK-\d+\.json$/.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 || basename(f, ".json"),
status: t.status || "?",
title: t.title || t.description || "",
};
} catch {
return { id: basename(f, ".json"), status: "unreadable", title: "" };
}
});
}
return [];
}
// ─── diagnosis ──────────────────────────────────────────────────────────────
// Turn the raw event stream into named pathologies.
function diagnose(events, agents, hookErrors = []) {
const findings = [];
// 0. Failed hook commands (from the transcript, not hooks.log). Aggregate by
// hook + first error line so a hook that fires on every tool use collapses
// into one finding with a count.
const byErr = new Map();
for (const h of hookErrors) {
const key = `${h.hookName} → ${h.stderr}`;
const row = byErr.get(key) ?? { ...h, count: 0 };
row.count++;
byErr.set(key, row);
}
for (const h of byErr.values()) {
const sev = h.blocking ? red("✗") : yellow("⚠");
const cmd = h.command ? dim(` [${h.command}]`) : "";
findings.push(
`${sev} ${h.blocking ? "BLOCKING " : ""}hook error ×${h.count}: ${h.hookName} — ${h.stderr} (exit ${h.exitCode})${cmd}`,
);
}
// 1. A validation step that keeps failing on the same worktree.
const validFails = events.filter(
(e) => e.hook === "validate" && e.state === "FAIL" && e.fields.step,
);
const byStep = {};
for (const e of validFails)
byStep[e.fields.step] = (byStep[e.fields.step] || 0) + 1;
for (const [step, n] of Object.entries(byStep)) {
if (n >= 2)
findings.push(
red(
`✗ validation never passes: '${step}' failed ${n}× — the flow cannot complete until this is green.`,
),
);
}
// 2. Circuit-breaker blocks → an agent is re-submitting an identical action.
const blocks = events.filter(
(e) => e.hook === "circuit-breaker" && /^BLOCK/.test(e.rest),
);
if (blocks.length) {
const maxCount = Math.max(
...blocks.map((b) => Number(b.fields.count) || 0),
);
findings.push(
red(
`✗ retry loop: circuit-breaker BLOCKED ${blocks.length}× (up to ${maxCount} identical attempts) — the agent is stuck repeating the same call.`,
),
);
}
// 3. bash-guard blocks → agent fighting the validation/permission rules.
const guard = events.filter(
(e) => e.hook === "bash-guard" && /BLOCK/.test(e.rest),
);
if (guard.length >= 3)
findings.push(
yellow(
`⚠ ${guard.length} bash-guard blocks — the agent tried to run forbidden commands (often a sign it's flailing).`,
),
);
// 4. Agents that reported failure
const seen = new Set();
for (const a of agents) {
const signals = [
a.verdict.kind === "fail" ? a.verdict.label : "",
...a.messages.map((m) => m.text),
];
for (const s of signals) {
const head = s.split("\n")[0];
if (
!/FAILED|BLOCKED|worktree not found|cannot create worktree/i.test(head)
)
continue;
const key = `${a.type}:${head.slice(0, 60)}`;
if (seen.has(key)) continue;
seen.add(key);
findings.push(red(`✗ ${a.type}: ${head.slice(0, 140)}`));
}
}
// 4b. A TASK developer ran but its worktree was never created.
const createdTasks = new Set(
events
.filter(
(e) =>
e.hook === "setup-worktree" &&
/CREATED/.test(e.rest) &&
e.fields.branch,
)
.map((e) => (e.fields.branch.match(/TASK-\d+/) || [])[0])
.filter(Boolean),
);
for (const a of agents) {
const task = (a.type.match(/TASK-\d+/) || [])[0];
if (task && !createdTasks.has(task) && /developer/.test(a.type))
findings.push(
red(
`✗ ${task}: worktree never created (no setup-worktree CREATED entry) — the developer has nowhere to work.`,
),
);
}
// 5. Worktree setup failure.
if (
events.some(
(e) => e.hook === "setup-worktree" && /FAILED|EXIT=2/.test(e.rest),
)
)
findings.push(
red("✗ setup-worktree failed — no usable worktree was created."),
);
if (!findings.length) findings.push(green("✓ no known pathology detected."));
return findings;
}
// ─── rendering ──────────────────────────────────────────────────────────────
function render(id, limit = 0) {
const p = sessionPaths(id);
const events = parseHooksLog(p.hooksLog);
const agents = readAgents(p.subagentsDir);
const orchestrator = readOrchestrator(p.mainTranscript);
const tickets = readTickets(p.ticketDirs);
const out = [];
const line = (s = "") => out.push(s);
line(bold(blue(`━━━ harness session ${id} ━━━`)));
line(dim(`repo: ${REPO}`));
line(dim(`base: ${p.base}`));
if (existsSync(p.hooksLog))
line(
dim(
`hooks.log: ${events.length} events, updated ${hhmmss(new Date(statSync(p.hooksLog).mtime).toISOString())}`,
),
);
else line(red("hooks.log: missing"));
line();
// Headline: did the session complete?
line(bold("STATUS ") + sessionStatus(agents, events));
if (orchestrator)
line(
` ${dim("orchestrator last said:")} ${orchestrator.lastText.split("\n").pop().slice(0, 140) || dim("(none)")}`,
);
else line(` ${dim("orchestrator: main transcript not found")}`);
line();
// Agents
line(bold("AGENTS"));
if (!agents.length) line(dim(" (no subagent transcripts found)"));
for (const a of agents) {
const mark =
a.verdict.kind === "done"
? green("✓")
: a.verdict.kind === "fail"
? red("✗")
: yellow("…");
const tools = Object.entries(a.toolCounts)
.map(([n, k]) => `${n}:${k}`)
.join(" ");
line(` ${mark} ${bold(a.type)} ${dim(a.description)}`);
if (tools) line(` ${dim("tools")} ${tools}`);
for (const m of a.messages)
line(` ${mag("→ " + m.to)} ${m.text.split("\n")[0].slice(0, 100)}`);
if (a.verdict.label)
line(
` ${a.verdict.kind === "fail" ? red(a.verdict.label) : dim(a.verdict.label)}`,
);
}
line();
// Tickets
if (tickets.length) {
line(bold("TICKETS"));
for (const t of tickets) {
const cl =
t.status === "merged"
? green
: t.status === "in_progress"
? yellow
: dim;
line(` ${cl(t.status.padEnd(12))} ${t.id} ${dim(t.title.slice(0, 80))}`);
}
line();
}
// Unified event timeline: orchestrator spawns + agent messages + completions
// + key hook milestones, in chronological order. Consecutive identical rows
// (e.g. the same validation failing 40×) are run-length collapsed to `×N`.
let timeline = buildEventTimeline(orchestrator, agents, events);
const total = timeline.length;
if (limit && total > limit) timeline = timeline.slice(total - limit);
line(
bold("EVENT TIMELINE") +
dim(
` (chronological${limit && total > limit ? `, last ${limit} of ${total}` : ""})`,
),
);
let i = 0;
while (i < timeline.length) {
let j = i;
while (j + 1 < timeline.length && timeline[j + 1].text === timeline[i].text)
j++;
const n = j - i + 1;
const ev = timeline[i];
const span = n > 1 ? dim(` ×${n} (until ${hhmmss(timeline[j].ts)})`) : "";
line(` ${dim(hhmmss(ev.ts))} ${ev.color(ev.icon)} ${ev.text}${span}`);
i = j + 1;
}
line();
// Diagnosis — include hook-command failures gathered from every transcript
// (orchestrator + each subagent), which never appear in hooks.log.
const hookErrors = [
...(orchestrator?.hookErrors ?? []),
...agents.flatMap((a) => a.hookErrors ?? []),
];
line(bold("DIAGNOSIS"));
for (const f of diagnose(events, agents, hookErrors)) line(` ${f}`);
return out.join("\n");
}
// ─── main ───────────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
const has = (f) => args.includes(f);
const valueOf = (f) => {
const i = args.indexOf(f);
return i >= 0 ? args[i + 1] : null;
};
// This is a CLI tool: its whole job is to write a report to stdout. ESLint's
// no-console rule only permits warn/error, so write to stdout directly.
const print = (s = "") => process.stdout.write(s + "\n");
if (has("--list")) {
const sessions = listSessions();
if (!sessions.length) {
print("No sessions found under " + WORKTREE_ROOT);
process.exit(0);
}
print(bold("Sessions (newest first):"));
for (const s of sessions)
print(
` ${s.id} ${dim(new Date(s.mtime).toISOString().slice(0, 19).replace("T", " "))}`,
);
process.exit(0);
}
const sessionId = valueOf("--session") || listSessions()[0]?.id;
if (!sessionId) {
console.error(
red(
`No session found under ${WORKTREE_ROOT}. Start a harness session first (ask Claude Code for a change).`,
),
);
process.exit(1);
}
if (has("--watch")) {
// Use the terminal's alternate screen buffer (like top/htop/less): the view
// redraws in place and never pollutes the scrollback. We restore the normal
// screen on exit. The timeline is capped to the visible height so the newest
// events are always on screen instead of scrolling off the top.
const enter = () => process.stdout.write("\x1b[?1049h\x1b[?25l");
const leave = () => process.stdout.write("\x1b[?1049l\x1b[?25h");
const tick = () => {
const limit = Math.max(8, (process.stdout.rows || 40) - 18); // leave room for header/agents
process.stdout.write("\x1b[H\x1b[J"); // home + clear to end
process.stdout.write(render(sessionId, limit));
process.stdout.write(
"\n" + dim(`watching ${sessionId} — refresh 1.5s — Ctrl-C to quit`),
);
};
enter();
tick();
const iv = setInterval(tick, 1500);
const quit = () => {
clearInterval(iv);
leave();
process.exit(0);
};
process.on("SIGINT", quit);
process.on("SIGTERM", quit);
} else {
print(render(sessionId));
}