muse-crew 0.14.6 → 0.14.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -101,6 +101,13 @@ async function telemetryFlush() {
101
101
  }
102
102
  async function telemetryEnd(status) {
103
103
  if (!telemetryRunId) return;
104
+ // Room #26 blocker 37: record-run-end rejects any status outside the
105
+ // schema CHECK — and this function swallowed that throw as "non-fatal",
106
+ // leaving the run row 'running' forever. An invalid status is a
107
+ // programmer error: fail loud. Transport errors stay non-fatal.
108
+ if (!["completed", "failed", "parked", "timed_out"].includes(status)) {
109
+ throw new Error("telemetryEnd: invalid status " + JSON.stringify(status) + " — must be completed|failed|parked|timed_out");
110
+ }
104
111
  // Flush any buffered events before recording the end.
105
112
  await telemetryFlush();
106
113
  try {
@@ -315,6 +322,142 @@ function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason)
315
322
  "if the " + stepName + " work is already complete, report on what was done rather than duplicating side effects. " +
316
323
  "Then return your report as JSON in exactly the shape specified above.";
317
324
  }
325
+ // parseClassifyFerry — room #26 blocker 42 (2026-09-21). Shape-constrained
326
+ // ferry parser for the classify-branch agent() call. The schema buys SHAPE,
327
+ // not provenance: the fields are LLM-authored, so the parser stays
328
+ // defensive. Byte-identical across standard.js, bugfix.js, chore.js
329
+ // (pinned by tests/already-merged.test.js). Triplication is structural:
330
+ // each workflow ships as a standalone launch payload (240 KiB budget,
331
+ // individually git-archived) — there is no shared module to hold it.
332
+ // The schema is the guard; the prompt is instruction, never load-bearing.
333
+ // Returns { state, diagField, diagText, failReason }:
334
+ // state: "has-work" | "already-merged:<40-hex>" | "empty-no-work" | null
335
+ // (null = unparseable/ambiguous/channel violation — UNKNOWN)
336
+ // diagField: "stderr" | "stdout" | null (which field sourced the DIAG pin)
337
+ // diagText: the matched DIAG line, or ""
338
+ // failReason: computed reason, for the retry trailer and the park note
339
+ function parseClassifyFerry(bsResult) {
340
+ var bsStdout = (bsResult && typeof bsResult.stdout === "string") ? bsResult.stdout : null;
341
+ var bsStderr = (bsResult && typeof bsResult.stderr === "string") ? bsResult.stderr : null;
342
+ if (bsStdout === null || bsStderr === null) {
343
+ return { state: null, diagField: null, diagText: "",
344
+ failReason: "classifier return fields missing or non-string (stdout and stderr must both be strings)" };
345
+ }
346
+ // State channel: stdout ONLY. A BRANCH_STATE match on stderr is never
347
+ // trusted — workers merge streams constantly, and trusting a channel
348
+ // swap as truth is the wrong shape. Search, never anchor (blocker 11):
349
+ // the ferry labels output ("stdout: BRANCH_STATE: has-work"). Collect
350
+ // ALL matches and dedupe normalized values: exactly one distinct value
351
+ // is truth; zero or two-or-more is UNKNOWN — alternation order never
352
+ // adjudicates.
353
+ var bsRe = /\bBRANCH_STATE:\s*(has-work|already-merged:[0-9a-f]{40}|empty-no-work)(?![\w-])/gi;
354
+ var bsSeen = {};
355
+ var bsM;
356
+ while ((bsM = bsRe.exec(bsStdout)) !== null) {
357
+ bsSeen[bsM[1].toLowerCase()] = true;
358
+ }
359
+ var bsStates = Object.keys(bsSeen);
360
+ if (bsStates.length !== 1) {
361
+ return { state: null, diagField: null, diagText: "",
362
+ failReason: bsStates.length === 0
363
+ ? "no BRANCH_STATE match in the stdout field"
364
+ : "ambiguous BRANCH_STATE values in stdout (" + bsStates.length + " distinct)" };
365
+ }
366
+ var bsState = bsStates[0];
367
+ // DIAG pin: search both fields, stderr first. Keep the DIAG: prefix — a
368
+ // prose mention without it must not satisfy the pin.
369
+ var bsDiagRe = /DIAG:\s*classify-branch\s+records=\d+\s+resolvable=\d+/;
370
+ var bsDiagField = bsDiagRe.test(bsStderr) ? "stderr" : (bsDiagRe.test(bsStdout) ? "stdout" : null);
371
+ var bsDiagText = "";
372
+ if (bsDiagField !== null) {
373
+ bsDiagText = bsDiagRe.exec(bsDiagField === "stderr" ? bsStderr : bsStdout)[0];
374
+ }
375
+ if (bsState.indexOf("already-merged:") === 0 && bsDiagField === null) {
376
+ // already-merged's sha flows into Cass's `git diff <sha>^1 <sha>` — a
377
+ // hallucinated-but-well-formed sha is a false-PASS vector. The script
378
+ // emits exactly one DIAG on every path that emits a BRANCH_STATE, so a
379
+ // missing pin means stderr wasn't faithfully ferried: UNKNOWN.
380
+ // Residual risk, named (blocker-42 fib 4): a worker could fabricate
381
+ // BOTH fields wholesale — the pin is a tripwire, not proof of
382
+ // execution. The workflow has no exec channel for a deterministic
383
+ // cross-check; if the platform ever offers one, verify the sha
384
+ // against the task's durable merge records directly.
385
+ return { state: null, diagField: null, diagText: "",
386
+ failReason: "already-merged without the DIAG pin — stderr not faithfully ferried" };
387
+ }
388
+ return { state: bsState, diagField: bsDiagField, diagText: bsDiagText, failReason: "" };
389
+ }
390
+
391
+ // classifyRetryTrailer — room #26 blocker 42 (2026-09-21). Corrective (not
392
+ // flat) trailer for the in-place classify retries. The failure mode was
393
+ // systematic labeling, not a coin flip: the trailer carries the COMPUTED
394
+ // parse-failure reason, the failed return truncated as a negative example,
395
+ // and the explicit field mapping — feedback from the actual failure,
396
+ // following the buildTransportRetryTrailer idiom. Bound: 2 retries
397
+ // (unmeasured house convention, matches the shared rework bound of 2 —
398
+ // against room retry data). Byte-identical across the three workflows
399
+ // (pinned by tests/already-merged.test.js).
400
+ function classifyRetryTrailer(reason, failedReturn) {
401
+ // Pass-2 simplification: the reason string already carries the truth
402
+ // (throw vs parse failure), so one neutral sentence replaces the
403
+ // throw/parse branch — the stringly-typed prefix contract between the
404
+ // call site and this function is deleted, not moved. Prompt accuracy,
405
+ // not prompt hardening.
406
+ return "\n\nCLASSIFY RETRY: the previous classifier attempt failed (" + reason + "). " +
407
+ "Return ONLY the two named fields: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }. " +
408
+ "Put each stream in its named field — do not add labels like \"stdout:\" in front of the content. " +
409
+ "Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
410
+ }
411
+
412
+ // parseVersionFerry — review pass 2, blocker-42 class (2026-09-21). The npm
413
+ // version-check ferry had the full pre-42 shape: schema-less agent(),
414
+ // line-anchored parser, and an unparseable fallback asserting a POSITIVE
415
+ // touched claim that spent the shared rework budget. Same treatment as
416
+ // parseClassifyFerry: shape-constrained {stdout} schema, search-and-dedupe
417
+ // parse, UNKNOWN on unparseable with in-place retries, honest park.
418
+ // Byte-identical across standard.js, bugfix.js, chore.js
419
+ // (pinned by tests/already-merged.test.js).
420
+ // Returns { versionState, failReason }:
421
+ // versionState: "touched" | "clean" | null (null = UNKNOWN)
422
+ // failReason: computed reason, for the retry trailer and the park note
423
+ function parseVersionFerry(vtResult) {
424
+ var vtStdout = (vtResult && typeof vtResult.stdout === "string") ? vtResult.stdout : null;
425
+ if (vtStdout === null) {
426
+ return { versionState: null,
427
+ failReason: "version-check return field missing or non-string (stdout must be a string)" };
428
+ }
429
+ // Search, never anchor (blocker 11): the ferry labels output
430
+ // ("stdout: VERSION_CLEAN"). Collect ALL matches and dedupe normalized
431
+ // values: exactly one distinct value is truth; zero or two-or-more is
432
+ // UNKNOWN — alternation order never adjudicates.
433
+ var vtRe = /\bVERSION_(TOUCHED|CLEAN)(?![\w-])/gi;
434
+ var vtSeen = {};
435
+ var vtM;
436
+ while ((vtM = vtRe.exec(vtStdout)) !== null) {
437
+ vtSeen[vtM[1].toUpperCase()] = true;
438
+ }
439
+ var vtStates = Object.keys(vtSeen);
440
+ if (vtStates.length !== 1) {
441
+ return { versionState: null,
442
+ failReason: vtStates.length === 0
443
+ ? "no VERSION_(TOUCHED|CLEAN) match in the stdout field"
444
+ : "ambiguous VERSION values in stdout (" + vtStates.length + " distinct)" };
445
+ }
446
+ return { versionState: vtStates[0] === "CLEAN" ? "clean" : "touched", failReason: "" };
447
+ }
448
+
449
+ // versionRetryTrailer — review pass 2, blocker-42 class (2026-09-21).
450
+ // Neutral corrective trailer for the in-place version-check retries: the
451
+ // computed parse-failure reason plus the failed return as a negative
452
+ // example. Byte-identical across the three workflows
453
+ // (pinned by tests/already-merged.test.js).
454
+ function versionRetryTrailer(reason, failedReturn) {
455
+ return "\n\nVERSION-CHECK RETRY: the previous version-check attempt failed (" + reason + "). " +
456
+ "Return ONLY the named field: { \"stdout\": \"<the command's exact stdout>\" }. " +
457
+ "Put the command's stdout in the named field — do not add labels like \"stdout:\" in front of the content. " +
458
+ "Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
459
+ }
460
+
318
461
  // parseToolSignals - bug 3472bf36. The work agent's TOOL CHECK emits two
319
462
  // exact signal lines: artifact_tools: ok|missing and
320
463
  // shell_transport: ok|unavailable. The workflow reads ONLY these lines.
@@ -517,12 +660,6 @@ function extractMarkerLines(workerText) {
517
660
  return markers.join("\n");
518
661
  }
519
662
 
520
- // See docs/decisions/publish-path.md#already-merged-idem2: idempotency for already-merged tasks.
521
- function extractAlreadyMerged(workerText) {
522
- var m = /repo_diff:\s*none\s*\(already-merged:\s*([0-9a-f]{7,40})(?=[\s)]|$)/i.exec(workerText || "");
523
- return m ? { sha: m[1].toLowerCase() } : { sha: null };
524
- }
525
-
526
663
 
527
664
  // See docs/decisions/qa-reproduce.md#worktree-confinement: the Build agent must declare its worktree.
528
665
  function extractWorktree(workerText) {
@@ -676,12 +813,24 @@ let mapGateBounceCount = 0;
676
813
  // rationalized a skip against explicit instruction text — text alone did not
677
814
  // hold, so the decision now lives in workflow code, not agent judgment.
678
815
  let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
679
- // Already-merged idempotency: the verified sha from the builder's
680
- // `repo_diff: none (already-merged: <sha>)` declaration (null when the
681
- // builder made commits or declared a runtime-state deliverable). The
682
- // workflow verifies the sha is an ancestor of the integration target at Build closeout;
683
- // Review's no-diff branch reads this, never the builder's prose.
816
+ // Already-merged attribution (blocker 34, 2026-09-21): the sha of the
817
+ // task-attributed merge when classify-branch reports already-merged.
818
+ // Set ONLY from the classifier's mechanical output — never from agent
819
+ // prose. The agent-authored sha-bearing repo_diff declaration path is
820
+ // deleted; attribution is identity-first from git, then the task's own
821
+ // durable merge records.
684
822
  let alreadyMergedSha = null;
823
+ // Room #26 blocker 34 (redesign, 2026-09-21): mechanical branch-state
824
+ // classification at Review entry. Git facts are never adjudicated by a
825
+ // reviewer — the workflow classifies the branch with `classify-branch`
826
+ // (pure git) before Cass is dispatched: "has-work",
827
+ // "already-merged:<sha>", or "empty-no-work". An empty branch with no
828
+ // verified sha and no `repo_diff: none` claim fails mechanically without
829
+ // dispatching Cass; reviewer prose is never parsed for git identity.
830
+ let branchState = null; // "has-work" | "already-merged:<sha>" | "empty-no-work"
831
+ let buildClaimedNoDiff = false; // Build declared plain `repo_diff: none` (no repo change — runtime-state deliverable, or believed already-merged). Same-process Build report only; there is no cross-process hydration — an empty branch on a fresh dispatch with no task-attributed merge fails mechanically.
832
+ let versionTouched = false; // npm: branch changed package.json's `version` (mechanical git fact, blocker-34 follow-up)
833
+ let branchStateErr = ""; // classifier failure detail (retry trailer + park note grounds)
685
834
  // Deterministic publish target — computed by the workflow (registry base +
686
835
  // bumpVersion), never by the Publish agent.
687
836
  let publishTarget = null; // { base, scope, target }
@@ -927,7 +1076,12 @@ while (i < STEPS.length) {
927
1076
  );
928
1077
  if (!claimResult.claimed) {
929
1078
  log("Standing down — task " + taskId + " was already claimed by another run");
930
- await telemetryEnd("duplicate");
1079
+ // Room #26 blocker 37: telemetryEnd only accepts schema-valid
1080
+ // statuses ("duplicate" violated the CHECK and silently left the run
1081
+ // row 'running' forever). The stand-down is a clean exit — the run
1082
+ // completed without doing work; the duplicate verdict itself stays in
1083
+ // the workflow's returned status and this log line.
1084
+ await telemetryEnd("completed");
931
1085
  return { status: "duplicate", task_id: taskId, reason: "task already claimed by another run" };
932
1086
  }
933
1087
  activeSessionId = claimResult.session_id;
@@ -1121,55 +1275,202 @@ while (i < STEPS.length) {
1121
1275
  "cd " + WORKTREE_HINT + "\n" +
1122
1276
  "git add -A\n" +
1123
1277
  "git commit -m \"chore: " + safeTitle + "\"\n\n" +
1124
- "If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of the integration target and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on the integration target (a prior merge or hand-repair landed it — do NOT re-implement working code), make no commit and declare `repo_diff: none (already-merged: <sha>)` naming the integration-target commit that carries the work; the workflow verifies the sha is an ancestor of the integration target, and a false declaration fails the phase. Otherwise commit your changes normally.\n\n" +
1278
+ "If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of the integration target and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on the integration target (a prior merge landed it — do NOT re-implement working code), make no commit and declare `repo_diff: none` in your report. The workflow verifies the claim mechanically from git — the task's own merge records, never your declaration, are the proof of delivery. Otherwise commit your changes normally.\n\n" +
1125
1279
  (rejectionNotes ? "REWORK after rejection. Address:\n" + rejectionNotes + "\n\n" : "") +
1126
1280
  "Report back in plain prose: what you built and the outcome." +
1127
1281
  (PUBLISH_TYPE === "npm" ? " End your report with the release: and version_bump: lines exactly as specified above — keep them on their own lines, lowercase, unrephrased — then a line `worktree: ` followed by the exact working directory path from above (copy it verbatim \u2014 it must match character-for-character), then a final line with exactly: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not." : " End your report with a line `worktree: ` followed by the exact working directory path from above (copy it verbatim \u2014 it must match character-for-character), then exactly one line: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not.");
1128
1282
 
1129
1283
  } else if (step.name === "Review") {
1130
- // See docs/decisions/publish-path.md#already-merged-hydra: when this run did not execute, hydration uses the existing merge.
1131
- if (!alreadyMergedSha) {
1132
- var hydResult = null;
1284
+ // Room #26 blocker 34 (redesign, 2026-09-21): classify the branch
1285
+ // mechanically BEFORE Cass is dispatched. The classifier is pure git
1286
+ // (classify-branch in the lifecycle script): has-work |
1287
+ // already-merged:<sha> | empty-no-work. Attribution is identity-first
1288
+ // from git, then the task's own durable merge records — there is no
1289
+ // agent-authored declared-sha input.
1290
+ // Blocker 42 (2026-09-21): the classifier output crosses an agent()
1291
+ // ferry — the old comment's "no cross-process hydration ferry" was a
1292
+ // fib. Machine facts travel in structured fields ({stdout, stderr}
1293
+ // schema) now, never in prose to be "returned verbatim". The schema
1294
+ // is the guard; the prompt is instruction, never load-bearing.
1295
+ // Unparseable is UNKNOWN, never a positive empty-no-work claim (room
1296
+ // #27: a labeled ferry — "stdout: BRANCH_STATE: has-work" — defeated
1297
+ // the ^-anchored parsers, and the empty-no-work fallback burned the
1298
+ // whole shared rework budget on a branch with real work). UNKNOWN
1299
+ // retries the classify call IN PLACE (up to 2, fresh -u1/-u2 keys — a
1300
+ // full Build round cannot fix an instrument failure, and the only
1301
+ // in-process bounce path spends the shared budget unconditionally).
1302
+ // Instrumentation failures NEVER touch reworkCount: an instrument
1303
+ // failure is not a build-quality failure. Exhaustion parks honestly
1304
+ // as unclassifiable — "unknown keeps the task alive (bounded)"; the
1305
+ // park on exhaustion is the fail-closed part. The UNKNOWN path never
1306
+ // asserts empty-no-work: no log line or park note presents it as the
1307
+ // classification. The string can still appear inside quoted evidence
1308
+ // (the verbatim failed return travels in the trailer and the note).
1309
+ // INVARIANT: branchState leaves this block holding a known value, or
1310
+ // the step has parked — unknown never reaches any consumer below.
1311
+ // Re-classify on EVERY Review entry: rework rewinds this step
1312
+ // in-process, and the branch changed between rounds. The loop's
1313
+ // null-gate governs only the in-place instrument retries within
1314
+ // this pass — without the reset, a second Review pass would trust
1315
+ // the first pass's stale classification and a real fix commit
1316
+ // would never "become has-work".
1317
+ branchState = null;
1318
+ branchStateErr = "";
1319
+ alreadyMergedSha = null;
1320
+ var classifyFailedReturn = "";
1321
+ for (var classifyAttempt = 0; classifyAttempt <= 2 && branchState === null; classifyAttempt++) {
1322
+ var classifyPrompt =
1323
+ "Classify the task branch state. This is a mechanical git check, not a judgment call.\n" +
1324
+ "Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " classify-branch " + taskId + "\n" +
1325
+ "Return the two streams as the two named fields and nothing else: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }." +
1326
+ (classifyAttempt === 0 ? "" : classifyRetryTrailer(branchStateErr, classifyFailedReturn));
1133
1327
  try {
1134
- hydResult = await agent(
1135
- "Read the latest completed Build session for task " + taskId + ".\n" +
1136
- "Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
1137
- "In the returned sessions array, find the most recent session (by started_at) with task_id \"" + taskId + "\", step \"Build\", and status \"completed\". Return exactly two sections, verbatim, with no commentary:\n" +
1138
- "SHA: <the session's already_merged_sha field value, or the word null when it is null>\n" +
1139
- "NOTES:\n<the session's notes field, verbatim>",
1140
- { key: "hydrate-already-merged" + (reworkCount > 0 ? "-r" + reworkCount : ""), label: "Hydrating already-merged verification" }
1328
+ var bsResult = await agent(
1329
+ classifyPrompt,
1330
+ { key: "classify-branch" + (classifyAttempt === 0 ? "" : "-u" + classifyAttempt) + (reworkCount > 0 ? "-r" + reworkCount : ""),
1331
+ label: "Classifying branch state (mechanical)" + (classifyAttempt === 0 ? "" : " (instrument retry " + classifyAttempt + " of 2)"),
1332
+ schema: { type: "object", properties: { stdout: { type: "string" }, stderr: { type: "string" } }, required: ["stdout", "stderr"] } }
1141
1333
  );
1142
- } catch (hydErr) {
1143
- log("Hydration read failed (" + String(hydErr && hydErr.message || hydErr) + "); treating as none declared.");
1334
+ classifyFailedReturn = (bsResult && typeof bsResult === "object" ? JSON.stringify(bsResult) : String(bsResult)).slice(0, 500);
1335
+ var classifyParsed = parseClassifyFerry(bsResult);
1336
+ if (classifyParsed.state !== null) {
1337
+ branchState = classifyParsed.state;
1338
+ branchStateErr = "";
1339
+ // DIAG pin: advisory for has-work/empty-no-work (blocker-34 v2.1
1340
+ // semantics — logged with its source field so silent record loss
1341
+ // surfaces in the evidence); already-merged is gated on the pin
1342
+ // inside parseClassifyFerry.
1343
+ if (classifyParsed.diagField !== null) {
1344
+ log("classify-branch DIAG pin (" + classifyParsed.diagField + " field): " + classifyParsed.diagText);
1345
+ } else {
1346
+ log("classify-branch: no DIAG: record-scan counter returned (scan unverifiable)");
1347
+ }
1348
+ } else {
1349
+ branchStateErr = classifyParsed.failReason;
1350
+ if (classifyAttempt < 2) {
1351
+ log("classify-branch unparseable (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
1352
+ }
1353
+ }
1354
+ } catch (bsErr) {
1355
+ classifyFailedReturn = String(bsErr && bsErr.message || bsErr).slice(0, 500);
1356
+ branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
1357
+ if (classifyAttempt < 2) {
1358
+ log("classify-branch instrument failure (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
1359
+ }
1144
1360
  }
1145
- var hydStr = hydResult ? ((typeof hydResult === "string") ? hydResult : JSON.stringify(hydResult)) : "";
1146
- var hydSha = /^SHA:\s*([0-9a-f]{7,40})\s*$/im.exec(hydStr);
1147
- if (hydSha) {
1148
- alreadyMergedSha = hydSha[1].toLowerCase();
1149
- log("Hydrated already-merged verification from structured session field: " + alreadyMergedSha);
1150
- } else {
1151
- var hvm = /already_merged_verified:\s*([0-9a-f]{7,40})/i.exec(hydStr);
1152
- if (hvm) {
1153
- alreadyMergedSha = hvm[1].toLowerCase();
1154
- log("Hydrated already-merged verification from Build session notes (fallback): " + alreadyMergedSha);
1361
+ }
1362
+ if (branchState === null) {
1363
+ // Exhaustion: park honestly. An instrument failure is not a
1364
+ // measurement of emptiness — never "empty-no-work".
1365
+ return await parkTask("Branch state unclassifiable after 2 instrument retries (" + branchStateErr + "; last failure: " + classifyFailedReturn + ") — the classifier instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy branch classification was produced.");
1366
+ }
1367
+ if (branchState.indexOf("already-merged:") === 0) {
1368
+ alreadyMergedSha = branchState.slice("already-merged:".length);
1369
+ log("Branch state already-merged: " + alreadyMergedSha + " (DIAG pin present — tripwire satisfied) — Cass reviews the frozen merge diff");
1370
+ } else {
1371
+ log("Branch state: " + branchState);
1372
+ }
1373
+ // buildClaimedNoDiff was set at the Build gate from the full Build
1374
+ // report (same process only). Only an empty branch WITH this claim
1375
+ // goes to Cass for a plausibility judgment; without it, empty-no-work
1376
+ // fails mechanically and Cass is never dispatched.
1377
+ // Blocker-34 follow-up: the package.json `version` check is a mechanical
1378
+ // git fact, not a reviewer judgment. For npm projects the workflow checks
1379
+ // it here, before Cass is dispatched — a touched `version` fails Review
1380
+ // mechanically (versions are assigned at publish time, never in
1381
+ // branches). Review pass 2 (blocker-42 class): this ferry had the full
1382
+ // pre-42 shape — schema-less agent(), line-anchored parser, and an
1383
+ // unparseable fallback asserting a POSITIVE touched claim that spent the
1384
+ // shared rework budget. Same treatment as the classifier:
1385
+ // shape-constrained {stdout} schema, search-and-dedupe parse, UNKNOWN
1386
+ // with in-place retries, honest park. An instrument failure is not a
1387
+ // measurement of touched — never assert touched from unparseable output.
1388
+ if (PUBLISH_TYPE === "npm") {
1389
+ var versionState = null; // "touched" | "clean" | null (null = UNKNOWN)
1390
+ var versionStateErr = "";
1391
+ var versionFailedReturn = "";
1392
+ for (var versionAttempt = 0; versionAttempt <= 2 && versionState === null; versionAttempt++) {
1393
+ var versionPrompt =
1394
+ "Check whether the task branch changed package.json's `version` field. This is a mechanical git check, not a judgment call.\n" +
1395
+ "Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " version-check " + taskId + "\n" +
1396
+ "Return the command's exact stdout as the single named field and nothing else: { \"stdout\": \"<the command's exact stdout>\" }." +
1397
+ (versionAttempt === 0 ? "" : versionRetryTrailer(versionStateErr, versionFailedReturn));
1398
+ try {
1399
+ var vtResult = await agent(
1400
+ versionPrompt,
1401
+ { key: "version-check" + (versionAttempt === 0 ? "" : "-u" + versionAttempt) + (reworkCount > 0 ? "-r" + reworkCount : ""),
1402
+ label: "Checking package.json version (mechanical)" + (versionAttempt === 0 ? "" : " (instrument retry " + versionAttempt + " of 2)"),
1403
+ schema: { type: "object", properties: { stdout: { type: "string" } }, required: ["stdout"] } }
1404
+ );
1405
+ versionFailedReturn = (vtResult && typeof vtResult === "object" ? JSON.stringify(vtResult) : String(vtResult)).slice(0, 500);
1406
+ var vtParsed = parseVersionFerry(vtResult);
1407
+ if (vtParsed.versionState !== null) {
1408
+ versionState = vtParsed.versionState;
1409
+ versionStateErr = "";
1410
+ } else {
1411
+ versionStateErr = vtParsed.failReason;
1412
+ if (versionAttempt < 2) {
1413
+ log("version-check unparseable (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
1414
+ }
1415
+ }
1416
+ } catch (vtErr) {
1417
+ versionFailedReturn = String(vtErr && vtErr.message || vtErr).slice(0, 500);
1418
+ versionStateErr = "version-check call failed: " + String(vtErr && vtErr.message || vtErr).slice(0, 120);
1419
+ if (versionAttempt < 2) {
1420
+ log("version-check instrument failure (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
1421
+ }
1155
1422
  }
1156
1423
  }
1424
+ if (versionState === null) {
1425
+ // Exhaustion: park honestly. An instrument failure is not a
1426
+ // measurement of touched — never assert touched.
1427
+ return await parkTask("package.json `version` state unclassifiable after 2 instrument retries (" + versionStateErr + "; last failure: " + versionFailedReturn + ") — the version-check instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy version classification was produced.");
1428
+ }
1429
+ versionTouched = (versionState === "touched");
1430
+ log("Review: package.json `version` " + (versionTouched ? "touched by the branch — mechanical FAIL, Cass not dispatched" : "untouched — mechanical check clean"));
1431
+ }
1432
+ // The empty-branch rule Cass used to adjudicate is gone. What she
1433
+ // examines and what rule she applies depend on the classified state —
1434
+ // never on her own judgment of git identity.
1435
+ var reviewChangeExam, reviewBranchRule;
1436
+ if (branchState === "has-work") {
1437
+ reviewChangeExam =
1438
+ "Examine the code changes by running:\n" +
1439
+ LIFECYCLE_ENV + LIFECYCLE + " inspect " + taskId + "\n\n" +
1440
+ "The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of the integration target (inspect prints the target name). Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n";
1441
+ reviewBranchRule =
1442
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): branch state = has-work. The branch has commits ahead of the integration target. Review their content; branch emptiness is settled and is not yours to judge.\n";
1443
+ } else if (branchState.indexOf("already-merged:") === 0) {
1444
+ reviewChangeExam =
1445
+ "The change under review is the frozen merge " + alreadyMergedSha + " — it is already on the integration target (see MECHANICAL FACT below). Examine it by running:\n" +
1446
+ "cd " + REPO_PATH + " && git diff " + alreadyMergedSha + "^1 " + alreadyMergedSha + "\n\n" +
1447
+ "That first-parent diff is the frozen, attributable change. Base your review ONLY on this diff — do NOT run git log to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n";
1448
+ reviewBranchRule =
1449
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): branch state = already-merged:" + alreadyMergedSha + ". The deliverable landed on the integration target via this task's own prior merge " + alreadyMergedSha + " (verified ancestor of the integration target). The branch is empty by design — emptiness is settled fact, not a finding.\n";
1450
+ } else if (buildClaimedNoDiff) {
1451
+ reviewChangeExam =
1452
+ "The branch has no commits ahead of the integration target (see MECHANICAL FACT below) — there is no diff to examine. Judge the Build report's `repo_diff: none` claim on its plausibility.\n\n";
1453
+ reviewBranchRule =
1454
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the builder likely forgot to commit', then end your report with exactly this line: VERDICT: FAIL.\n";
1455
+ } else {
1456
+ // Mechanical FAIL — Cass is never dispatched (see the dispatch gate
1457
+ // below). Instructions are unused; keep the shape.
1458
+ reviewChangeExam = "";
1459
+ reviewBranchRule = "";
1157
1460
  }
1158
1461
  instructions = "Review independently and cold. No prior context from the builder.\nDo NOT access the task dashboard, event log, or any comments. Your review is based solely on the spec and the code.\n\n" +
1159
1462
  (mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec from the task description.\n\n") +
1160
- "Examine the code changes by running:\n" +
1161
- LIFECYCLE_ENV + LIFECYCLE + " inspect " + taskId + "\n\n" +
1162
- "The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of the integration target (inspect prints the target name). Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n" +
1463
+ reviewChangeExam +
1163
1464
  "You can also read specific files in the worktree at:\n" +
1164
1465
  WORKTREE_HINT + "/\n\n" +
1165
1466
  "Check quality, correctness, spec compliance.\n" +
1166
1467
  (SURFACE_TERMINAL ? "TERMINAL UX REVIEW: judge the CLI surface against " + UX_DOCTRINE_PATH + " — help accuracy, error quality, exit codes, output clarity. Reject when the bar is not met.\n" : "") +
1167
1468
  (SURFACE_ARTIFACT ? "ARTIFACT UX REVIEW: judge the rendered surface against " + UX_DOCTRINE_PATH + " — alignment, spacing, hierarchy, composition, balance, finish, correctness. Reject when the bar is not met.\n" : "") +
1168
1469
  "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, report what is stale, then end your report with exactly this line: VERDICT: FAIL.\n" +
1169
- "If the branch has no commits ahead of the integration target (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with (a) a plausible runtime-state deliverable (e.g. a cron created via the cron tool), or (b) an already-merged declaration `repo_diff: none (already-merged: <sha>)` AND the mechanical fact below confirms the sha verified. MECHANICAL FACT (computed by the workflow, never by the builder): already_merged sha = " + (alreadyMergedSha ? alreadyMergedSha + " (verified ancestor of the integration target: YES)" : "none declared") + ". Otherwise report 'no commits ahead of the integration target and no valid repo_diff: none declaration — the builder likely forgot to commit', then end your report with exactly this line: VERDICT: FAIL.\n" +
1170
- (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches. Two checks:\n" +
1171
- "(a) The task branch must NOT have changed package.json's `version` field. First resolve the integration target: " + LIFECYCLE_ENV + LIFECYCLE + " integration-target. Then check: cd " + REPO_PATH + " && git diff <target>..." + TASK_BRANCH + " -- package.json (substitute the exact token integration-target printed — when it prints HEAD, use the literal word HEAD: `git diff HEAD...<branch>` diffs the detached checkout against the branch). If the branch touched `version` in any way, report 'versions are assigned at publish time, never in branches — remove the version change' in your notes, then end your report with exactly this line: VERDICT: FAIL.\n" +
1172
- "(b) The accepted Build report declares: " + releaseDecisionText() + ". " +
1470
+ reviewBranchRule +
1471
+ (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches.\n" +
1472
+ "MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the task branch did not change package.json's `version` field. A branch that touches `version` fails Review mechanically before any reviewer is dispatched, so this is settled — do not re-check it.\n" +
1473
+ "The accepted Build report declares: " + releaseDecisionText() + ". " +
1173
1474
  (releaseDecision
1174
1475
  ? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, report your notes, then end with exactly this line: VERDICT: FAIL."
1175
1476
  : "The decision is missing or malformed — report 'Build report must end with release: yes|no and (when release is yes) version_bump: patch|minor|major lines', then end with exactly this line: VERDICT: FAIL.") + "\n" : "") +
@@ -1183,8 +1484,9 @@ while (i < STEPS.length) {
1183
1484
  "Then run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " CREW_STAGED_BASE=<provenance.source_commit, empty when null> integrate " + taskId + " \"merge: chore: " + safeTitle + "\\\"\\n" +
1184
1485
  "The script merges, reconciles, records, and pushes under the merge lock. Informational only: RECONCILED, NO_REMOTE, NO_REMOTE_RECONCILE. Read the output:\n" +
1185
1486
  "- STAGED_BASE_MISMATCH: the line left the staged base (stray checkout). VERDICT: FAIL.\n" +
1186
- "- MERGED_EMPTY — check FIRST (it contains the word MERGED): either a runtime-state deliverable or an already-merged sha the workflow verified (Build declared repo_diff: none). No lock taken, no new commit. Report 'merged empty: no repo changes'. VERDICT: PASS.\n" +
1487
+ "- MERGED_EMPTY — check FIRST (it contains the word MERGED): either a runtime-state deliverable or a task-attributed merge the workflow verified from the task's own merge records. No lock taken, no new commit. Report 'merged empty: no repo changes'. VERDICT: PASS.\n" +
1187
1488
  "- MERGED: the merge landed; the script pushed inline. Read the push line: PUSHED — report the hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; NO_REMOTE_PUSH — no remote configured, VERDICT: PASS; ERROR after MERGED — a retry recovers the push; report it, VERDICT: FAIL.\n" +
1489
+ "- STALE_MERGE: the recorded merge no longer matches the task branch (rework moved it after the merge) — the script refused to push stale state. Report the STALE_MERGE line, VERDICT: FAIL.\n" +
1188
1490
  "- LOCK_HELD: 10-minute backoff exhausted. VERDICT: FAIL.\n" +
1189
1491
  "- CONFLICT: the plain merge failed — aborted, the target is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
1190
1492
  "RESOLUTION:\n" +
@@ -1234,8 +1536,9 @@ while (i < STEPS.length) {
1234
1536
  // See docs/decisions/publish-path.md#deterministic-artifact-publish: the work agent never publishes; the parent runs the deterministic publish script.
1235
1537
  // Blocker 22 (one-party publish, room #24): verified re-entry guard.
1236
1538
  // The workflow parks at publish intent; the tick worker issues
1237
- // artifact_edit, verifies by read-back, stamps provenance, and
1238
- // re-queues. The dispatcher resumes this task at the Publish step
1539
+ // artifact_edit; the ack scan stamps provenance on exact per-attempt
1540
+ // version acknowledgement (sole positive completion criterion; no
1541
+ // read-back — docs/publish-verification.md) and re-queues. The dispatcher resumes this task at the Publish step
1239
1542
  // (the parked session's own step — the failed→retry path). If the
1240
1543
  // stamped provenance names THIS task and its source_commit is the
1241
1544
  // repo's current HEAD, the merge commit was already published and
@@ -1291,8 +1594,10 @@ while (i < STEPS.length) {
1291
1594
  }
1292
1595
  // See docs/decisions/publish-path.md#step1-builder-source: the builder's source tree is NOT the crew's repo; verify report before stamping.
1293
1596
  // (2026-09-20, one-party worker-owned publish) A verified re-entry
1294
- // skips the whole issuance tail: the edit already went out, was
1295
- // verified by the parent read-back, and was stamped. The empty-diff
1597
+ // skips the whole issuance tail: the edit already went out, the
1598
+ // ack scan saw the exact per-attempt version acknowledged (sole
1599
+ // positive completion criterion; no read-back) and it was stamped.
1600
+ // The empty-diff
1296
1601
  // park must not fire on this branch.
1297
1602
  if (!publishSkippedNoLock && !publishAlreadyVerified) {
1298
1603
  // The trigger key of the attempt that last ran, for the publish ledger.
@@ -1547,11 +1852,13 @@ while (i < STEPS.length) {
1547
1852
  // mechanical outcome above. It must not rebuild or re-stamp: a second
1548
1853
  // artifact_edit would trigger a duplicate build.
1549
1854
  // (2026-09-20, one-party worker-owned publish) On a verified re-entry
1550
- // the publish was issued by the crew worker, verified by the parent
1551
- // read-back, and stamped — the work agent reports that, VERDICT: PASS.
1855
+ // the publish was issued, the ack scan observed the artifact's exact
1856
+ // version acknowledgement, and provenance was stamped — already
1857
+ // verified, nothing to issue. The work agent reports that,
1858
+ // VERDICT: PASS.
1552
1859
  if (publishAlreadyVerified) {
1553
1860
  instructions = "Publish was already completed and verified for this task — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
1554
- "The crew's publish worker issued the artifact edit for the merged commit, the parent's independent content read-back confirmed the artifact contains the merged change, and provenance was stamped (docs/publish-verification.md). This Publish pass is a verified re-entry after the stamp: there is nothing to issue and nothing to re-verify.\n\n" +
1861
+ "Provenance stamps this task's merge commit as published (docs/publish-verification.md): the tick worker issued the edit, the ack scan observed the artifact's exact version acknowledgement — the sole positive completion criterion — and the publish was stamped. Already verified, nothing to issue and nothing to re-verify.\n\n" +
1555
1862
  "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
1556
1863
  "Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
1557
1864
  "The VERDICT line must be the last line of your report.";
@@ -1564,10 +1871,10 @@ while (i < STEPS.length) {
1564
1871
  instructions = "Publish the merged code to the live artifact.\n\n" +
1565
1872
  "The publish was performed deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would trigger a duplicate build or disturb the finalized state. You perform no publish actions.\n\n" +
1566
1873
  "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
1567
- "Mechanical outcome (the workflow's mechanical steps; content verification is the parent's, still pending):\n" +
1874
+ "Mechanical outcome (the workflow's mechanical steps; the ack scan's version-acknowledgement check is still pending):\n" +
1568
1875
  "- merge lock refreshed: yes\n" +
1569
1876
  "- artifact rebuild triggered and completed: yes\n" +
1570
- "- provenance stamped: NO — not yet, and you must NOT stamp it. The stamp moved to the parent, which stamps it only after an independent content read-back confirms the artifact actually contains the merged change (docs/publish-verification.md). A stamped-but-hollow build is exactly how canary run 8 (2026-09-11) went green on stale content.\n" +
1877
+ "- provenance stamped: NO — not yet, and you must NOT stamp it. The ack scan stamps it when it observes the artifact's exact on-disk acknowledgement of this attempt's version — version acknowledgement is the sole positive completion criterion (docs/publish-verification.md). A stamped-but-hollow build is exactly how canary run 8 (2026-09-11) went green on stale content.\n" +
1571
1878
  "- post-deploy finalized: yes (worktree removed, merge lock released)\n\n" +
1572
1879
  "POLICY: The live artifact is rebuilt only in this phase, from the repo. Never use artifact_edit to change the artifact directly — fixes go through the repo and the loop. A source fix is not done until the artifact is rebuilt from it here.\n\n" +
1573
1880
  "The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
@@ -1611,7 +1918,31 @@ while (i < STEPS.length) {
1611
1918
  var workKeyBase = "work-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : "");
1612
1919
  var workerResult = null;
1613
1920
  var workAttempts = [];
1614
- for (var workAttempt = 0; workAttempt <= 2; workAttempt++) {
1921
+ // Room #26 blocker 34 (redesign): an empty branch with no verified sha and
1922
+ // no repo_diff: none claim fails Review mechanically — Cass is never
1923
+ // dispatched, so no reviewer adjudicates the emptiness. The synthetic
1924
+ // report below is written by the workflow and flows through the same
1925
+ // verdict extraction, structured verdict recording, and rejection/bounce
1926
+ // path as a real FAIL.
1927
+ // Blocker-34 follow-up: the npm `version` check joins the mechanical gate.
1928
+ // Either mechanical fact fails Review without dispatching Cass.
1929
+ var mechanicalFailReason = (versionTouched ? "version-touched" : (branchState === "empty-no-work" && !buildClaimedNoDiff ? "empty-no-work" : null));
1930
+ var mechanicalReviewFail = (step.name === "Review" && mechanicalFailReason !== null);
1931
+ if (mechanicalReviewFail) {
1932
+ log("Review: " + mechanicalFailReason + " — mechanical FAIL, Cass not dispatched");
1933
+ workerResult =
1934
+ "MECHANICAL REVIEW VERDICT (written by the workflow — no reviewer was dispatched).\n" +
1935
+ (mechanicalFailReason === "version-touched"
1936
+ ? "Package.json `version` (checked by the workflow from git): the task branch changed the `version` field. Versions are assigned at publish time — never in branches. Remove the version change.\n"
1937
+ : "Branch state (classified by the workflow from git): empty-no-work — the task branch has no commits ahead of the integration target, " +
1938
+ "no task-attributed merge on the target was verified" + (branchStateErr ? " (branch classification itself failed: " + branchStateErr + ")" : "") + ", " +
1939
+ "and the accepted Build report declared no `repo_diff: none` deliverable. " +
1940
+ "No change was reviewed because there is no change to review. The builder likely forgot to commit.\n") +
1941
+ "Worktree: " + WORKTREE_HINT + "\n" +
1942
+ "Recovery: commit the deliverable on the task branch in the worktree above and re-run Review. If the deliverable is genuinely runtime state outside the repo, declare `repo_diff: none` in the Build report instead of leaving the branch empty without a declaration.\n" +
1943
+ "VERDICT: FAIL";
1944
+ }
1945
+ for (var workAttempt = 0; workAttempt <= 2 && !mechanicalReviewFail; workAttempt++) {
1615
1946
  var workKey = workAttempt === 0 ? workKeyBase : workRetryKey(step.name, (reworkCount > 0 ? "-r" + reworkCount : ""), workAttempt);
1616
1947
  var prevAttempt = workAttempt === 0 ? null : workAttempts[workAttempt - 1];
1617
1948
  var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? "discarded" : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
@@ -1709,7 +2040,18 @@ while (i < STEPS.length) {
1709
2040
  "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
1710
2041
  task_id: taskId,
1711
2042
  session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed", notes: "Worker report had no single unambiguous VERDICT: PASS/FAIL line (bounded re-ask exhausted)" },
1712
- event: { task_id: taskId, type: "failed", message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" }
2043
+ event: { task_id: taskId, type: "failed", message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" },
2044
+ // Room #26 blocker 33: INDETERMINATE verdict record — the report
2045
+ // could not be read at all, but its full text is still preserved
2046
+ // as grounds. Review-scoped; other verdict steps keep the
2047
+ // existing failed-session behavior with no verdict row.
2048
+ verdict: (step.name === "Review" ? {
2049
+ step: "Review",
2050
+ attempt: reworkCount,
2051
+ reviewer: step.identity,
2052
+ verdict: "INDETERMINATE",
2053
+ grounds: workerText
2054
+ } : null)
1713
2055
  }) + "\n",
1714
2056
  { key: "record-block-" + step.name, label: "Recording verdict failure" }
1715
2057
  );
@@ -1755,40 +2097,13 @@ while (i < STEPS.length) {
1755
2097
  }
1756
2098
  log("Build worktree confinement passed: " + wt.path);
1757
2099
 
1758
- // See docs/decisions/publish-path.md#already-merged-idem: an already-merged repo_diff is idempotent; no rebuild.
1759
- var am = extractAlreadyMerged(workerText);
1760
- if (am.sha) {
1761
- var amCheck = await agent(
1762
- "Verify the builder's already-merged declaration.\n" +
1763
- "Run in shell and return the stdout verbatim:\n" +
1764
- "TARGET=$(" + LIFECYCLE_ENV + LIFECYCLE + " integration-target) && cd " + REPO_PATH + " && git rev-parse --verify --quiet " + am.sha + " >/dev/null && git merge-base --is-ancestor " + am.sha + " $TARGET && echo ALREADY_MERGED_YES || echo ALREADY_MERGED_NO",
1765
- { key: "verify-already-merged" + (reworkCount > 0 ? "-r" + reworkCount : ""), label: "Verifying already-merged declaration" }
1766
- );
1767
- var amOut = (typeof amCheck === "string") ? amCheck : JSON.stringify(amCheck);
1768
- if (!/ALREADY_MERGED_YES/.test(amOut)) {
1769
- log("Build already-merged declaration failed verification — " + am.sha + " is not an ancestor of the integration target — marking failed for retry");
1770
- await agent(
1771
- "Record already-merged verification failure.\n" +
1772
- "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
1773
- task_id: taskId,
1774
- session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed",
1775
- notes: "Build declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of the integration target in the configured repo. The declaration is fabricated or mistaken; the work is not on the integration target. Phase failed for retry" },
1776
- event: { task_id: taskId, type: "failed", message: "Build already-merged declaration failed verification — " + am.sha + " not an ancestor of the integration target, phase failed, dispatcher will retry" }
1777
- }),
1778
- { key: "record-already-merged-fail-" + step.name, label: "Recording already-merged verification failure" }
1779
- );
1780
- return {
1781
- __hatchWorkflowControl: "blocked",
1782
- result: {
1783
- blocked_reason: "Build already-merged declaration failed verification",
1784
- message: "The builder declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of the integration target. The work is not on the integration target; the phase is marked failed and the dispatcher will retry Build.",
1785
- task_id: taskId
1786
- }
1787
- };
1788
- }
1789
- alreadyMergedSha = am.sha;
1790
- log("Build already-merged declaration verified: " + am.sha + " is an ancestor of the integration target");
1791
- }
2100
+ // Room #26 blocker 34 (redesign, 2026-09-21): capture the plain
2101
+ // `repo_diff: none` (no repo change) claim from the FULL Build report.
2102
+ // Each Build round sets this fresh from its own report; there is no
2103
+ // cross-process fallback. The agent-authored already-merged
2104
+ // declaration and its Build-closeout verification are deleted —
2105
+ // attribution is the classifier's job at Review entry.
2106
+ buildClaimedNoDiff = /repo_diff:\s*none/im.test(workerText || "");
1792
2107
  }
1793
2108
 
1794
2109
  // Deterministic closeout: no formatter agent. The verdict is mechanical
@@ -1901,9 +2216,23 @@ while (i < STEPS.length) {
1901
2216
  } else {
1902
2217
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
1903
2218
  }
1904
- // See docs/decisions/publish-path.md#already-merged: when the Build gate verifies already-merged, attestation is recorded.
1905
- if (step.name === "Build" && alreadyMergedSha) {
1906
- summary += "\nalready_merged_verified: " + alreadyMergedSha;
2219
+ // Room #26 blocker 34 (redesign, 2026-09-21): machine-readable review
2220
+ // basis, prepended to Review session notes for observability (proof
2221
+ // rooms read the summary to see what Review examined and why a
2222
+ // reviewer was or was not dispatched).
2223
+ if (step.name === "Review" && branchState) {
2224
+ var reviewBasisNote;
2225
+ if (mechanicalReviewFail) {
2226
+ reviewBasisNote = "REVIEW_BASIS: none — mechanical FAIL (" + mechanicalFailReason + "), no reviewer dispatched";
2227
+ } else if (branchState.indexOf("already-merged:") === 0) {
2228
+ var rbSha = branchState.slice("already-merged:".length);
2229
+ reviewBasisNote = "REVIEW_BASIS: frozen merge " + rbSha + " — reviewed via git diff " + rbSha + "^1 " + rbSha;
2230
+ } else if (branchState === "has-work") {
2231
+ reviewBasisNote = "REVIEW_BASIS: branch diff — reviewed via inspect output (task branch vs integration target)";
2232
+ } else {
2233
+ reviewBasisNote = "REVIEW_BASIS: runtime-state-none — no repo change; plausibility of Build's repo_diff: none claim";
2234
+ }
2235
+ summary = reviewBasisNote + "\n" + summary;
1907
2236
  }
1908
2237
 
1909
2238
  // Capture mapper's spec for Build and Review
@@ -1925,16 +2254,43 @@ while (i < STEPS.length) {
1925
2254
  releaseDecision = extractReleaseDecision(workerText);
1926
2255
  }
1927
2256
 
2257
+ // Room #26 blocker 34 (redesign, 2026-09-21): structured review basis
2258
+ // for the verdict row — what the review actually examined. A mechanical
2259
+ // FAIL has no reviewer and no basis beyond the mechanical fact. Only
2260
+ // computed for Review (branchState is null on other steps).
2261
+ var reviewBasis = null;
2262
+ if (step.name === "Review" && branchState) {
2263
+ reviewBasis =
2264
+ mechanicalReviewFail ? "mechanical-fail" :
2265
+ branchState.indexOf("already-merged:") === 0 ? "frozen-merge:" + branchState.slice("already-merged:".length) :
2266
+ branchState === "has-work" ? "branch-diff" : "runtime-state-none";
2267
+ }
2268
+
1928
2269
  await agent(
1929
2270
  "Update session and log event.\n" +
1930
2271
  "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
1931
2272
  task_id: taskId,
1932
- // Room #16 blocker 11: the workflow-verified already-merged sha as
1933
- // structured control state. Only the Build gate sets alreadyMergedSha
1934
- // (after the mechanical ancestor check); the API validates the shape
1935
- // and a later write without the field never clears it (COALESCE).
1936
- session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: status, notes: summary, already_merged_sha: (step.name === "Build" ? alreadyMergedSha : null) },
1937
- event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity }
2273
+ // Room #26 blocker 34 (2026-09-21): the already_merged_sha session
2274
+ // field is no longer written — the agent-authored declaration path
2275
+ // is deleted and nothing reads it. Removing the Crew API/database
2276
+ // column is a separate API-surface follow-up, not bundled here.
2277
+ session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: status, notes: summary },
2278
+ event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity },
2279
+ // Room #26 blocker 33: structured, non-lossy Review verdict record.
2280
+ // The session note above is truncated; the verdict row carries the
2281
+ // full worker report as grounds, written in the same transaction.
2282
+ // attempt is the rework round (0 = first Review).
2283
+ verdict: (step.name === "Review" && verdictPassed !== null ? {
2284
+ step: "Review",
2285
+ attempt: reworkCount,
2286
+ // Room #26 blocker 34 (redesign): a mechanical FAIL has no
2287
+ // reviewer — the workflow wrote the verdict. Identity rows still say
2288
+ // the Review step ran.
2289
+ reviewer: mechanicalReviewFail ? "workflow" : step.identity,
2290
+ verdict: verdictPassed ? "PASS" : "FAIL",
2291
+ review_basis: reviewBasis,
2292
+ grounds: workerText
2293
+ } : null)
1938
2294
  }) + "\n",
1939
2295
  {
1940
2296
  key: "record-" + step.name + (reworkCount > 0 ? "-r" + reworkCount : "") + (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : ""),
@@ -1943,21 +2299,20 @@ while (i < STEPS.length) {
1943
2299
  );
1944
2300
 
1945
2301
  if (!passed && step.name === "Review") {
2302
+ // Room #26 blocker 34 (redesign, 2026-09-21): the false-negative guard,
2303
+ // the Case B budget skip, and the already-merged corrective are all
2304
+ // gone. Emptiness is classified mechanically before Review, so a
2305
+ // rejected Review is never re-adjudicated here — the workflow owns the
2306
+ // git facts, the reviewer owns quality/spec compliance, and the budget
2307
+ // applies uniformly to every rejection. A rejected frozen merge
2308
+ // bounces with the reviewer's notes; a real fix commit becomes
2309
+ // has-work, and a do-nothing loop exhausts the normal budget.
1946
2310
  reworkCount++;
1947
2311
  if (reworkCount > MAX_REWORK) {
1948
2312
  log("Max rework attempts reached for task " + taskId + " — worktree preserved at " + WORKTREE_PRESERVED_HINT + " for manual inspection");
1949
2313
  return await parkTask("Exceeded " + MAX_REWORK + " rework attempts after Review rejection. Worktree preserved.");
1950
2314
  }
1951
2315
  rejectionNotes = summary;
1952
- // Already-merged corrective (room #16 blocker 11): when Review rejected
1953
- // an empty branch but the work is already on the integration target (the workflow verified
1954
- // the sha), Wren must declare it — not re-implement or re-commit
1955
- // already-landed work. Scoped to the empty-branch rejection; any other
1956
- // rejection already carries its own specific notes.
1957
- if (step.name === "Review" && alreadyMergedSha && /no commits ahead of (main|the integration target)/i.test(summary)) {
1958
- rejectionNotes += "\n\nCORRECTIVE (from the workflow, not the reviewer): the deliverable is already on the integration target — the workflow mechanically verified that " + alreadyMergedSha + " is an ancestor of the integration target. Do NOT re-implement the work and do NOT create a new commit for it. In your Build report, declare exactly: repo_diff: none (already-merged: " + alreadyMergedSha + ") — then end with VERDICT: PASS.";
1959
- log("Rework corrective appended for task " + taskId + ": already-merged " + alreadyMergedSha + " — Wren must declare, not rebuild");
1960
- }
1961
2316
  i = BUILD_INDEX;
1962
2317
  log("Review rejected — bouncing to Build (rework #" + reworkCount + ")");
1963
2318
  continue;