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:
111
.claude/hooks/validate-on-stop.mjs
Executable file
111
.claude/hooks/validate-on-stop.mjs
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
// SubagentStop — full validation chain (prettier auto-fix, typecheck, unit app/functions, e2e) on active worktrees with changes; exit 2 keeps the subagent alive to fix and commit. VALIDATE_DRY_RUN=1 skips the chain; =fail simulates a failure.
|
||||
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { createHookContext } from "./lib/context.mjs";
|
||||
import { git } from "./lib/git.mjs";
|
||||
import { getFirstTaskId } from "./lib/teams.mjs";
|
||||
import { readAgentMeta } from "./lib/agent-meta.mjs";
|
||||
import {
|
||||
sessionBranch,
|
||||
simpleWorktreePath,
|
||||
taskWorktreePath,
|
||||
} from "./lib/topology.mjs";
|
||||
import { runValidationSteps } from "./lib/validation.mjs";
|
||||
|
||||
const raw = readFileSync(0, "utf8");
|
||||
const ctx = createHookContext(raw, "validate");
|
||||
let payload = {};
|
||||
try {
|
||||
payload = JSON.parse(raw);
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
|
||||
// Scope validation to the stopping subagent's OWN worktree: a per-ticket
|
||||
// developer-TASK-XXX validates <base>/TASK-XXX, a single-shot simple developer (on
|
||||
// the <short>/simple branch — rollback / migration) validates <base>/simple. Avoids the
|
||||
// "shared brakes" failure where one ticket's broken state blocks an unrelated
|
||||
// developer and N stops each re-validate every session worktree. Falls back to
|
||||
// all session worktrees when the identity can't be resolved.
|
||||
const ids = [ctx.agentName, ctx.agentType].filter(Boolean);
|
||||
let taskId = ids.map(getFirstTaskId).find(Boolean) || "";
|
||||
// A single-shot simple developer runs on the shared <base>/simple worktree; its agent
|
||||
// name carries no TASK suffix, so it's recovered from the dispatch prompt below.
|
||||
let isSimple = false;
|
||||
|
||||
// Reliable identity at SubagentStop: the sibling <agent>.meta.json (written at
|
||||
// spawn, so it exists even when the big transcript JSONL hasn't been flushed yet —
|
||||
// a real race here). Its `description` carries the ticket (e.g. "Implement
|
||||
// TASK-002: …"). Use it to scope validation to THIS dev's worktree instead of
|
||||
// re-validating every session worktree (wt=all) on every stop.
|
||||
if (!taskId) {
|
||||
const meta = readAgentMeta(payload);
|
||||
if (meta) {
|
||||
const m = meta.description.match(/TASK-\d+/);
|
||||
if (m) taskId = m[0];
|
||||
}
|
||||
}
|
||||
|
||||
// No suffixed agent name in this harness → recover the TASK_ID (or the single-shot
|
||||
// simple flow) from the dispatch prompt in the transcript, to scope validation to
|
||||
// this dev's worktree (not wt=all).
|
||||
if (!taskId) {
|
||||
const tp = payload.agent_transcript_path || payload.transcript_path;
|
||||
if (tp && existsSync(tp)) {
|
||||
try {
|
||||
const body = readFileSync(tp, "utf8");
|
||||
for (const line of body.split("\n")) {
|
||||
const m =
|
||||
line.match(/TASK_ID[:=\s]+(TASK-\d+)/) ||
|
||||
line.match(/TICKET_FILE[=:\s]+\S*(TASK-\d+)/);
|
||||
if (m) {
|
||||
taskId = m[1];
|
||||
break;
|
||||
}
|
||||
if (/BRANCH_NAME[:=\s]+\S+\/simple\b/.test(line)) {
|
||||
isSimple = true;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// best-effort — fall back to all-worktree validation below
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ownWorktree = taskId
|
||||
? taskWorktreePath(ctx, taskId)
|
||||
: isSimple
|
||||
? simpleWorktreePath(ctx)
|
||||
: "";
|
||||
|
||||
// Diff each worktree against session/<short>, not the repo's checked-out base
|
||||
// branch, so validation sees a per-ticket developer's OWN change set. (A
|
||||
// single-shot simple developer is the exception: resolving-rollback-conflicts does
|
||||
// `git reset --hard <BASE_BRANCH>`, re-forking <short>/simple onto the default
|
||||
// branch, so its diff against session/<short> can span unrelated files — accepted
|
||||
// noise, since the rollback's whole point is to diverge from the session.)
|
||||
// Empty base → validation.mjs falls back to the repo base branch (e.g. before the
|
||||
// session branch exists).
|
||||
const sessionRef = sessionBranch(ctx);
|
||||
const base =
|
||||
git(["show-ref", "--verify", "--quiet", `refs/heads/${sessionRef}`])
|
||||
.status === 0
|
||||
? sessionRef
|
||||
: "";
|
||||
|
||||
ctx.log(
|
||||
`START REPO=${ctx.repo} MODE=${process.env.MODE || ""} wt=${ownWorktree || "all"} base=${base || "repo-default"}`,
|
||||
);
|
||||
|
||||
const result = runValidationSteps(ctx, { worktree: ownWorktree, base });
|
||||
|
||||
if (!result.ok) {
|
||||
ctx.fail(
|
||||
`Validation failed at step '${result.step}' — fix the errors and commit before completing:\n` +
|
||||
result.output,
|
||||
{ log: `step=${result.step}` },
|
||||
);
|
||||
}
|
||||
|
||||
ctx.accept(result.skipReason || `OK (${ownWorktree || "all worktrees"})`);
|
||||
Reference in New Issue
Block a user