muse-crew 0.13.2 → 0.13.3

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.
@@ -26,25 +26,10 @@ const startStepIndex = inputs.start_step_index || 0;
26
26
  // resolution back via updatetask in the self-claim below.
27
27
  const RESOLVED_WORKFLOW = inputs.resolved_workflow || null;
28
28
  const WORKFLOW_WAS_NULL = inputs.workflow_was_null === true;
29
- // One-shot recovery routing: the dispatcher sets inputs.next_phase when it
30
- // routes this run via an explicit recover-task redirect. The value is
31
- // consumed (cleared) atomically by the successful self-claim below:
32
- // claim-task takes expected_next_phase and clears the matching next_phase in
33
- // the same transaction as the winning session insert, so no platform death
34
- // can slip between claim and consumption and replay the routing. A stale or
35
- // superseded routing survives — only an exact match clears.
36
- // what the dispatcher routed on.
29
+ // See docs/decisions/workflow-core.md#oneshot-recovery: the dispatcher sets input for one-shot recovery routing.
37
30
  const NEXT_PHASE_ROUTED = (typeof inputs.next_phase === "string" && inputs.next_phase.length > 0) ? inputs.next_phase : null;
38
31
 
39
- // Visual verdict protocol availability — the workflow parks for parent-run
40
- // baseline capture ONLY when the protocol is fully shipped. The protocol
41
- // requires docs/visual-verdict.md in the release AND the parent-side
42
- // capture tooling (task b309a97d, "QA owns the visual verdict"). Until both
43
- // exist, the park would deadlock waiting for a parent who cannot fulfill
44
- // it.
45
- // Effective value for this run, resolved by the dispatcher from the
46
- // project's visual_protocol setting (null=inherits crew default=off).
47
- // Manual launches without the arg default to off (previous behavior).
32
+ // See docs/decisions/qa-reproduce.md#visual-protocol-avail: the workflow parks if the visual protocol is unavailable.
48
33
  var VISUAL_PROTOCOL_AVAILABLE = inputs.visual_protocol === true;
49
34
 
50
35
  // crewHome is required — the dispatcher always passes it (crew-dispatch.js
@@ -142,15 +127,7 @@ const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
142
127
  const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
143
128
  const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
144
129
  const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
145
- // The seven basenames the pin step must materialize — asserted mechanically
146
- // by workflow code from the verbatim listing, never from agent prose.
147
- // COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
148
- // 2026-09-17): the diff is computed by this script, never ferried as an
149
- // agent JSON string. Pinned like the other publish-critical modules so a
150
- // mid-run release swap cannot change it under the workflow.
151
- // CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
152
- // crew-api.js statically imports it, so the pin must carry it — a pin
153
- // without it kills every claim with ERR_MODULE_NOT_FOUND.
130
+ // See docs/decisions/qa-reproduce.md#pin-basenames: the pin step materializes the required scripts.
154
131
  const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
155
132
 
156
133
  // Project config — passed by dispatcher, falls back to dashboard defaults
@@ -200,12 +177,7 @@ const SURFACE_TRIAGE_DESC = SURFACE_ARTIFACT
200
177
  : SURFACE_TERMINAL
201
178
  ? "This project's user-facing surface is terminal: a command-line interface."
202
179
  : "This project's user-facing surface is unclassified (environment_type not set): judge by what a user would directly observe.";
203
- // UX doctrine page: the shared UX bar for this run's surface, resolved
204
- // mechanically — every phase prompt reads UX_DOCTRINE_PATH, never a
205
- // hardcoded filename. Canonical map: lib/ux-doctrine.js (mirrored here as a
206
- // one-liner because the workflow runtime's relative-import support is
207
- // unverified; tests pin the mirror). Null on unclassified surfaces: no
208
- // shared page, and prompts say so instead of naming the wrong one.
180
+ // See docs/decisions/qa-reproduce.md#ux-doctrine-page: the shared UX bar for this run's surface.
209
181
  const UX_DOCTRINE_PAGE = SURFACE_TERMINAL ? "terminal-ux.md" : (SURFACE_ARTIFACT ? "artifact-ux.md" : null);
210
182
  const UX_DOCTRINE_PATH = UX_DOCTRINE_PAGE ? crewHome + "/current/docs/" + UX_DOCTRINE_PAGE : null;
211
183
  const PUBLISH_SLUG = projectConfig.deploy_slug || "";
@@ -216,21 +188,7 @@ if (!taskId) {
216
188
  throw new Error("task_id is required in args");
217
189
  }
218
190
 
219
- // Closeout is deterministic: the work agent returns the runtime's native
220
- // transport envelope {"status": "ok", "result": "<prose report>"} with no
221
- // schema, so the workflow receives the report as a plain string. There is no
222
- // {"report"} wrapper: that invented shape invited agents to improvise sibling
223
- // keys (notably "status"), which the runtime duck-types as its own envelope
224
- // and fatally misparses. The envelope is the runtime's own documented shape
225
- // — not a demand for machine-structured reasoning.
226
- // The verdict is extracted mechanically by extractVerdict below — never by an
227
- // agent. The summary is the worker's report truncated. The release decision
228
- // comes from extractReleaseDecision. No formatter agent: it added a failure
229
- // mode while contributing nothing the workflow doesn't compute itself.
230
- // Steps whose passed=false drives a control-flow branch (rework bounce,
231
- // block) declare their verdict explicitly on a VERDICT: line. The verdict is
232
- // extracted DETERMINISTICALLY by workflow code (extractVerdict) — never by
233
- // an agent. Missing, malformed, or contradictory lines fail the phase (never silently pass).
191
+ // See docs/decisions/workflow-core.md#closeout-envelope: the work agent returns the runtime's native envelope; verdict extracted mechanically.
234
192
  const VERDICT_STEPS = ["Build", "Review", "Integrate", "Publish"];
235
193
  function extractVerdict(workerText) {
236
194
  // The verdict is the LAST VERDICT: PASS/FAIL in the report (contract: end
@@ -254,16 +212,7 @@ function extractVerdict(workerText) {
254
212
  if (uniq.length !== 1) return { ok: false, count: matches.length };
255
213
  return { ok: true, passed: last.value === "PASS" };
256
214
  }
257
- // Verdict re-ask (bug cd18ccc2): a verdict-step report that fails
258
- // extractVerdict is not failed immediately. Stochastic verdict-line
259
- // non-compliance (the agent did the work but omitted or garbled the VERDICT
260
- // line) gets up to two bounded follow-up agent() calls whose only job is to
261
- // read the preserved report and emit exactly one VERDICT line. The verdict
262
- // is still extracted mechanically by extractVerdict — the re-ask agent
263
- // transcribes, never decides the phase outcome. Each attempt uses a fresh
264
- // stable-key suffix so a cached failure can never replay deterministically.
265
- // Exhaustion keeps the existing fail-closed behavior. This is structure, not
266
- // prompt hardening: no instruction text was stern-ified to get here.
215
+ // See docs/decisions/workflow-core.md#verdict-reask: a report that fails extractVerdict gets bounded re-ask calls.
267
216
  function verdictReaskKey(stepName, reworkSuffix, attempt) {
268
217
  return "verdict-reask-" + stepName + reworkSuffix + "-a" + attempt;
269
218
  }
@@ -321,12 +270,7 @@ function workRetryKey(stepName, reworkSuffix, attempt) {
321
270
  function attemptKey(base, reworkCount) {
322
271
  return base + (reworkCount > 0 ? "-r" + reworkCount : "");
323
272
  }
324
- // pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
325
- // the verbatim `ls -1` listing so WORKFLOW CODE asserts the six pinned
326
- // basenames; the agent cannot self-certify. (The pin step was the one place
327
- // the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
328
- // empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
329
- // tests/pin-location.test.js.
273
+ // See docs/decisions/qa-reproduce.md#pin-lifecycle: snapshot the lifecycle scripts in the pin.
330
274
  function pinLifecycle(key) {
331
275
  return agent(
332
276
  "Snapshot lifecycle scripts for version pinning.\n" +
@@ -530,24 +474,7 @@ function hydrateReleaseDecision(rec) {
530
474
  // read-back cannot be
531
475
  // fabricated from the diff; it must match the artifact's real content.
532
476
 
533
- // Durable publish-attempt ledger (2026-09-12): every artifact publish
534
- // attempt is recorded append-only at $CREW_HOME/.publish-ledger/<slug>.jsonl
535
- // on persistent disk (NOT /tmp). The ledger is the correlation record for
536
- // publish attempts whose outcome is UNKNOWN. When the rebuild trigger's
537
- // child returns prose instead of JSON (structured-output failure), the edit
538
- // may already have been accepted as pending_init — and artifact_status
539
- // cannot see pending_init (diagnostic canary 2026-09-12: an edit accepted
540
- // as pending_init was immediately followed by an all-false status check,
541
- // and the old retry issued a DUPLICATE edit). "No build visible" is NOT
542
- // evidence the edit did not go through, so the workflow never blind-retries
543
- // on an unknown outcome: it records the attempt and parks fail-closed. A
544
- // human or a later run correlates the accepted edit via the ledger (commit
545
- // hash + attempt key + the artifact build's agent_id when one was observed)
546
- // instead of guessing from a blind status poll.
547
- // Best-effort observability: a failed write is logged loudly but never
548
- // throws — the caller's park/proceed decision never depends on the ledger.
549
- // Byte-identical across standard/bugfix/chore — pinned by
550
- // tests/publish-ledger.test.js.
477
+ // See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
551
478
  async function recordPublishLedger(entry, rework) {
552
479
  try {
553
480
  var ledgerDir = crewHome + "/.publish-ledger";
@@ -597,45 +524,19 @@ function extractMarkerLines(workerText) {
597
524
  return markers.join("\n");
598
525
  }
599
526
 
600
- // Already-merged idempotency (canary 2026-09-15, task 1d692d91): when the
601
- // builder correctly makes no commit because the deliverable is already on
602
- // main (a prior merge or hand-repair landed it), it declares
603
- // `repo_diff: none (already-merged: <sha>)` naming the main commit that
604
- // carries the work. Room #16 blocker 11 (2026-09-18): the line anchor
605
- // missed Wren's mid-paragraph declaration, and the persisted notes truncated
606
- // the tail — so the anchor is gone and a sha followed by `)`, whitespace, or
607
- // end-of-string (truncation) is accepted. The sha is hex-only (7-40 chars)
608
- // so the workflow can interpolate it into the mechanical ancestor check
609
- // without injection risk; a over-long hex run never matches (the lookahead
610
- // fails on the extra hex char). Pure — pinned byte-identical across
611
- // standard/bugfix/chore.
527
+ // See docs/decisions/publish-path.md#already-merged-idem2: idempotency for already-merged tasks.
612
528
  function extractAlreadyMerged(workerText) {
613
529
  var m = /repo_diff:\s*none\s*\(already-merged:\s*([0-9a-f]{7,40})(?=[\s)]|$)/i.exec(workerText || "");
614
530
  return m ? { sha: m[1].toLowerCase() } : { sha: null };
615
531
  }
616
532
 
617
- // Explicit artifact refusal (room #16 blocker 10, 2026-09-18): the rebuild
618
- // trigger child ends its turn with `ARTIFACT_EDIT_REFUSED: <text>` when
619
- // artifact_edit explicitly refuses the edit (e.g. the artifact does not
620
- // exist). A refusal is conclusive negative evidence — the edit provably did
621
- // NOT go through — distinct from an unconsumed trigger return (unknown).
622
- // Pure — pinned byte-identical across standard/bugfix/chore.
623
- // The signal must be the ENTIRE trimmed turn output (not a line within prose):
624
- // the trigger child is instructed to end its turn with exactly this line and
625
- // nothing else. A confused child quoting the instructions back in prose must
626
- // NOT produce a conclusive negative — that degrades to unknown (fail-closed).
533
+ // See docs/decisions/publish-path.md#refusal-signal: the refusal signal must be the ENTIRE trimmed turn output.
627
534
  function extractRefusal(workerText) {
628
535
  var m = /^ARTIFACT_EDIT_REFUSED:\s*(.+?)\s*$/.exec(String(workerText || "").trim());
629
536
  return m ? m[1].slice(0, 300) : null;
630
537
  }
631
538
 
632
- // Worktree confinement: the Build agent must declare the exact worktree
633
- // path it built in on a `worktree:` marker line. The workflow compares it
634
- // against WORKTREE_HINT mechanically (exact string match) — never by
635
- // reading agent prose. This closes the hole where a builder whose prepare
636
- // failed freelanced into a different checkout (canary, 2026-09-11): the
637
- // honest-but-confused case fails here, and a fabricated path is caught one
638
- // phase later when Review's inspect finds no commits in the configured repo.
539
+ // See docs/decisions/qa-reproduce.md#worktree-confinement: the Build agent must declare its worktree.
639
540
  function extractWorktree(workerText) {
640
541
  var lines = (workerText || "").split("\n");
641
542
  var found = null;
@@ -665,26 +566,7 @@ function extractExperiential(workerText) {
665
566
  if (!r) return null;
666
567
  return r[1].toLowerCase() === "yes";
667
568
  }
668
- function buildVisualCapturePlan(taskTitle, taskDescription, kind, captureTargets) {
669
- // Deterministic visual-capture frame. kind: "baseline" | "postchange".
670
- // This string IS the capture script: fixed viewport matrix, scroll
671
- // positions, and interaction states — the inspection agent executes it
672
- // verbatim, nothing is improvised. Task-specific targets fill the slots.
673
- var title = String(taskTitle || "").replace(/"/g, "'").slice(0, 120);
674
- var targets = String(captureTargets || "").trim() ||
675
- String(taskDescription || "").replace(/"/g, "'").slice(0, 300);
676
- return "VISUAL CAPTURE — " + kind.toUpperCase() + " — task: " + title + ". " +
677
- "Target views/controls: " + targets + ". " +
678
- "For EACH target, capture exactly: " +
679
- "(1) desktop 1440x900, full view, scrolled to top; " +
680
- "(2) desktop 1440x900, scrolled so the target is vertically centered; " +
681
- "(3) mobile 390x844, scrolled so the target is vertically centered; " +
682
- "(4) desktop 1440x900, hover state on the target control; " +
683
- "(5) desktop 1440x900, keyboard-focus state on the target control; " +
684
- "(6) desktop 1440x900, active/pressed state if the target is a button or control. " +
685
- "Also record: console error count, the ARIA tree of the target region, any horizontal overflow. " +
686
- "Name captures " + kind + "-<n>-<viewport>-<state>. Return the captures, not a summary.";
687
- }
569
+
688
570
  // Experiential flag resolution: the task is experiential when Sage's Triage
689
571
  // report ends with the machine-read marker "experiential: yes". The flag is
690
572
  // opt-in — a missing or garbled line degrades to "unknown", which callers
@@ -825,23 +707,9 @@ function releaseDecisionText() {
825
707
  if (!releaseDecision) return "no machine-readable release decision from the Build report";
826
708
  return "release: " + releaseDecision.release + (releaseDecision.version_bump ? ", version_bump: " + releaseDecision.version_bump : " (no version_bump line)");
827
709
  }
828
- // Park the task for human attention and end the run. "blocked" is never
829
- // manually authored — the dashboard derives it mechanically from unmet
830
- // dependencies — so a workflow outcome that needs a human parks the task
831
- // instead. Parking is one atomic dashboard action (parktask): the parked
832
- // state and the explanatory note land in one transaction, never half.
833
- // The dispatcher skips parked tasks; a human moving parked→todo
834
- // mechanically resets the retry counters. Returns the workflow result
835
- // envelope the launcher sees. If the park call itself fails, the run
836
- // reports "failed" (retryable) so the next tick re-attempts the park —
837
- // a lost park is never reported as parked.
838
- // Terminal cleanup: the run's last act at every park/fail boundary. A run
839
- // that parks or fails must not leak its worktree, branch, or merge lock.
840
- // The lifecycle's terminal-cleanup releases the lock unconditionally and
841
- // reclaims the worktree+branch ONLY when the task branch is fully merged
842
- // into main (then it is redundant); unmerged work is preserved for the
843
- // human by design. Fire-and-forget with one bounded retry — the merge-lock
844
- // lease expiry and the orphan sweep are the backstop for a dead transport.
710
+ // Terminal cleanup: the run's last act — release the lock unconditionally;
711
+ // reclaim worktree+branch only when fully merged (unmerged work is preserved
712
+ // for the human by design). See `docs/decisions/workflow-core.md#park-contract`.
845
713
  async function terminalCleanup() {
846
714
  for (var attempt = 1; attempt <= 2; attempt++) {
847
715
  try {
@@ -1104,14 +972,7 @@ while (i < STEPS.length) {
1104
972
  lockHolder = taskId + "/" + activeSessionId;
1105
973
  }
1106
974
 
1107
- // ── Capture: baseline evidence for experiential tasks ─────────────
1108
- // Hazel's QA capture pass runs right after Triage, before Map, for tasks
1109
- // Sage flagged experiential. The capture itself is parent-driven (the
1110
- // inspection handoff arrives at the root agent, outside this script), so
1111
- // when no baseline evidence is recorded yet the script logs a note event
1112
- // and parks with the exact parent protocol + resume path. Never fails the
1113
- // task over missing evidence: after two requests, baseline:none is
1114
- // recorded and the task continues without baseline comparison.
975
+ // See docs/decisions/qa-reproduce.md#capture-baseline: baseline evidence for experiential tasks is captured before work begins.
1115
976
  if (step.name === "Capture") {
1116
977
  var capExp = await resolveExperiential();
1117
978
  var bounceSuffix = (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : "");
@@ -1133,12 +994,7 @@ while (i < STEPS.length) {
1133
994
  continue;
1134
995
  }
1135
996
  var capStatus = await baselineStatus();
1136
- // Stale-decision guard: a "baseline: none (visual protocol unavailable)"
1137
- // note is only durable while the protocol is unavailable. When
1138
- // VISUAL_PROTOCOL_AVAILABLE is true, that old decision no longer
1139
- // stands — fall through to the request path for a fresh capture
1140
- // attempt. Exact-string trim comparison against the workflow's own
1141
- // written message (explicit state, never English matching).
997
+ // See docs/decisions/qa-reproduce.md#stale-decision-guard: a stale baseline decision parks fail-closed.
1142
998
  var baselineLatestMessage = ("baseline: " + capStatus.baseline_kind + capStatus.baseline_refs).trim();
1143
999
  var baselineStale = VISUAL_PROTOCOL_AVAILABLE && baselineLatestMessage === "baseline: none (visual protocol unavailable)";
1144
1000
  if (capStatus.baseline_found && !baselineStale) {
@@ -1283,17 +1139,7 @@ while (i < STEPS.length) {
1283
1139
  (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.");
1284
1140
 
1285
1141
  } else if (step.name === "Review") {
1286
- // Already-merged hydration: when this run did not execute Build itself
1287
- // (dispatcher resume at Review after a platform death between phases),
1288
- // recover the workflow-verified sha. Room #16 blocker 11: the structured
1289
- // session field is read FIRST — the `already_merged_verified:` notes line
1290
- // is only a fallback, because session notes are hard-capped at 3000
1291
- // chars and a truthful declaration at the report's tail was silently
1292
- // truncated. The structured value was written by the workflow after a
1293
- // mechanical ancestor check — it is trusted; the builder's bare
1294
- // declaration never is. Absent both, the mechanical fact below reads
1295
- // "none declared" and Cass fails closed. The hydration read is best-effort:
1296
- // a transport throw degrades to "none declared" rather than crashing Review.
1142
+ // See docs/decisions/publish-path.md#already-merged-hydra: when this run did not execute, hydration uses the existing merge.
1297
1143
  if (!alreadyMergedSha) {
1298
1144
  var hydResult = null;
1299
1145
  try {
@@ -1401,25 +1247,7 @@ while (i < STEPS.length) {
1401
1247
  "published: muse-crew@" + publishTarget.target + "\n" +
1402
1248
  "VERDICT: PASS\n\n";
1403
1249
  } else if (PUBLISH_TYPE === "artifact") {
1404
- // Deterministic artifact publish (canary b5efd1b1, 2026-09-10): the work
1405
- // agent claimed "Rebuilt and deployed" while no build ran and no
1406
- // provenance was stamped — prose-trusted side effects, the same failure
1407
- // class as the npm double-skip (bb739316). The npm path already runs one
1408
- // deterministic script; the artifact path now has the same shape. Lock
1409
- // refresh, rebuild trigger, build-completion poll, and post-deploy are
1410
- // narrow schema'd bookkeeping calls owned by the workflow — the work
1411
- // agent reports on the mechanical outcome and cannot skip what it never
1412
- // owned. Any step failing parks with an honest, step-specific reason
1413
- // (fail-closed). There is deliberately NO workflow-side provenance
1414
- // stamp: the builder's applied-report is circular (canary run 8,
1415
- // 2026-09-11), so the stamp moved to the parent — after the build
1416
- // lands, the workflow records the session completed and parks with
1417
- // "publish: verification-requested". The parent owns verification
1418
- // (docs/publish-verification.md); the primary sensor is the
1419
- // deterministic lib/readback-disk.js (the agent-callable read-back
1420
- // tool is unavailable — artifact_inspect was removed by the platform
1421
- // 2026-09-14 — so the LLM-inspector path is manual-fallback only).
1422
- // Chore has no QA: the parent's verification is the final gate.
1250
+ // See docs/decisions/publish-path.md#deterministic-artifact-publish: the work agent never publishes; the parent runs the deterministic publish script.
1423
1251
  var artifactPublish = null;
1424
1252
  var publishLockRefreshed = false;
1425
1253
  var publishSkippedNoLock = false;
@@ -1446,20 +1274,7 @@ while (i < STEPS.length) {
1446
1274
  }
1447
1275
  publishLockRefreshed = true;
1448
1276
  }
1449
- // STEP 1 (mechanical): carry the merged change to the artifact
1450
- // builder. The builder's source tree is NOT the crew's repo —
1451
- // canary run 4 (2026-09-11) proved it: Publish asked for "rebuild
1452
- // from current source. Do not modify any source files" and the
1453
- // builder rebuilt a stale copy predating the canary's changes, then
1454
- // the workflow stamped the new commit hash on the stale build.
1455
- // Provenance fiction; all eight phases passed. The merge diff is
1456
- // embedded in the edit request; the builder applies it to its own
1457
- // tree and reports the applied changes; the workflow verifies the
1458
- // report matches the diff BEFORE stamping provenance. A mismatch
1459
- // parks without stamping — the stamp must never certify a build
1460
- // whose content was not verified.
1461
- // Skipped entirely when no lock was held — nothing merged, nothing
1462
- // to ship.
1277
+ // See docs/decisions/publish-path.md#step1-builder-source: the builder's source tree is NOT the crew's repo; verify report before stamping.
1463
1278
  if (!publishSkippedNoLock) {
1464
1279
  // The trigger key of the attempt that last ran, for the publish ledger.
1465
1280
  // Minted once here (not re-minted per use site) so the ledger always
@@ -1540,20 +1355,7 @@ while (i < STEPS.length) {
1540
1355
  return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact target preflight was inconclusive (no parsable signal). Target existence is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
1541
1356
  }
1542
1357
  log("Publish artifact preflight for task " + taskId + ": target " + artifactTargetDir + " present");
1543
- // (below) the diff computation, rebuild trigger, application
1544
- // verification, bounded poll, and provenance stamp. The builder
1545
- // only makes the artifact_edit call and reports the applied
1546
- // changes — no prose claim to trust. If the artifact tool namespace
1547
- // is missing from this child it reports honestly and the workflow
1548
- // retries once with a fresh key (bounded); anything else parks.
1549
- // Publish diff base (2026-09-14, task 0c53af4e): the carried diff is
1550
- // BASE..HEAD where BASE is the previously-stamped provenance
1551
- // source_commit — NOT HEAD^1. A push-time reconcile merge puts the
1552
- // task's own changes behind an intermediate merge, so HEAD^1..HEAD
1553
- // silently drops the task's fix while the artifact builds without
1554
- // it. The stamped base is the artifact's actual content; BASE..HEAD
1555
- // is the complete unpublished delta. Empty tree only for a genuine
1556
- // first publish (no provenance stamped yet).
1358
+ // See docs/decisions/publish-path.md#diff-computation: the diff is computed, the rebuild is triggered, and the report is verified.
1557
1359
  var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1558
1360
  var provResult = await agent(
1559
1361
  crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
@@ -1569,16 +1371,7 @@ while (i < STEPS.length) {
1569
1371
  } else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
1570
1372
  return await parkTask("Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
1571
1373
  }
1572
- // Publish diff transport (room #14, 2026-09-17): the diff used to be
1573
- // ferried as a JSON string field in the agent's response — the agent
1574
- // produced a valid 700-line diff on disk but the JSON ferry dropped
1575
- // it, and the parse saw zero files ("Publish diff parsed to zero
1576
- // files"). The diff now travels git -> file -> deterministic script
1577
- // summary; the LLM never carries diff bytes. The agent is pure hands:
1578
- // it runs exactly one command (the PINNED compute-publish-diff.js —
1579
- // a mid-run release swap cannot change it under the workflow) and
1580
- // returns the small JSON summary verbatim. All fail-closed parks
1581
- // below are unchanged in meaning.
1374
+ // See docs/decisions/publish-path.md#diff-transport: the diff travels via file, not the LLM.
1582
1375
  var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (reworkCount > 0 ? "-r" + reworkCount : "") + ".diff";
1583
1376
  var diffResult = await agent(
1584
1377
  "Run exactly one command and nothing else:\n" +
@@ -1603,6 +1396,7 @@ while (i < STEPS.length) {
1603
1396
  return await parkTask("Publish diff base mismatch: script reported '" + String(diffSummary.base || "").slice(0, 12) + "' but the stamped base is '" + publishBase.slice(0, 12) + "'. Human attention needed.");
1604
1397
  }
1605
1398
  var mergeCommitForPublish = String(diffSummary.commit || "").trim();
1399
+ var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
1606
1400
  var publishDiffSha256 = String(diffSummary.sha256 || "");
1607
1401
  if (!diffSummary.bytes) {
1608
1402
  return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
@@ -1654,37 +1448,11 @@ while (i < STEPS.length) {
1654
1448
  "- If artifact_edit is not available after the load, do NOT improvise — end your turn.\n" +
1655
1449
  "- You do NOT call setprovenance, artifact_inspect, or post-deploy yourself.\n" +
1656
1450
  "No report is needed: do not return JSON, do not summarize what you did, do not echo the diff. End your turn after the artifact_edit call.\n";
1657
- // The artifact build's agent_id, attributed to this edit by the
1658
- // workflow-owned observation below. The agent_id is the artifact
1659
- // system's in-flight correlation ID (research 2026-09-12):
1660
- // artifact.edit returns pending_init with NO agent_id, but
1661
- // artifact_status exposes build.agent_id immediately after
1662
- // acceptance, stable across polls. Recorded in the ledger so an
1663
- // attempt correlates to the exact builder run; null when no build
1664
- // was ever observed.
1451
+ // See docs/decisions/publish-path.md#agent-id-attribution: the artifact build's agent_id is attributed to the edit call.
1665
1452
  var rebuildAgentId = null;
1666
- // The builder's applied report is gone (2026-09-16): it rode on the
1667
- // trigger's JSON closeout contract, which is removed below. The
1668
- // parent's independent read-back (docs/publish-verification.md) is
1669
- // the verification — this field stays "missing-report" on ledger
1670
- // lines for issued triggers; pre-trigger parks (toolcheck
1671
- // rejected/inconclusive) and unattributed-unknown parks write null
1672
- // (no trigger was observed, so there is nothing to report).
1453
+ // See docs/decisions/publish-path.md#applied-report-gone: the builder's applied report is gone; the workflow verifies differently.
1673
1454
  var publishAppliedObservation = "missing-report";
1674
- // Durable-evidence snapshot (2026-09-14): the observation below only
1675
- // detects IN-FLIGHT builds. A build that finished before the
1676
- // observation leaves no in-flight trace — but the platform's audit
1677
- // harness leaves a durable one:
1678
- // ~/workspace/ts-spaces/<slug>/audits/<timestamp>-<id>/ per
1679
- // completed build. Snapshot the listing BEFORE the trigger so the
1680
- // fallback can diff before/after: a directory appearing during the
1681
- // trigger window is positive evidence the edit went through and
1682
- // the build completed. Best-effort and non-gating: if the snapshot
1683
- // fails, auditBeforeOk stays false and BOTH fallback comparisons
1684
- // are disabled (2026-09-16, critic finding 4) — without a baseline,
1685
- // an empty before-list would make every historical audit dir look
1686
- // "new". No wall-clock in-script (deterministic replay) — the
1687
- // comparison is a pure before/after set diff.
1455
+ // See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
1688
1456
  var auditDirsBeforeTrigger = [];
1689
1457
  var auditBeforeOk = false;
1690
1458
  try {
@@ -1701,36 +1469,7 @@ while (i < STEPS.length) {
1701
1469
  } catch (auditBeforeErr) {
1702
1470
  log("Publish audit-dir snapshot before trigger failed for task " + taskId + " (non-fatal): audit fallback DISABLED for this attempt — without a baseline, historical dirs would look new: " + (auditBeforeErr && auditBeforeErr.message ? auditBeforeErr.message : auditBeforeErr));
1703
1471
  }
1704
- // Fire-and-forget trigger + workflow-owned observation (2026-09-16,
1705
- // clean-room task e2a8d9f8): the trigger's JSON closeout contract
1706
- // traveled over the stochastic text channel, and the runtime's
1707
- // JSON-candidate heuristic misfired on it ("workflow agent output
1708
- // was not JSON: no JSON object or array found in final response"),
1709
- // parking a task whose edit may have gone through. The contract's
1710
- // content was already observation-only (the applied report never
1711
- // gated; the pre_hashes were diagnostic-only), so the contract is
1712
- // removed: the trigger carries NO schema and its return value is
1713
- // never consumed, which takes the extraction heuristic out of this
1714
- // call entirely. The workflow attributes the edit itself through
1715
- // the tiny schema'd reads below — no prose is parsed for the
1716
- // trigger outcome.
1717
- // (Probe, 2026-09-16: the workflow scope exposes only agent() —
1718
- // tool_search, artifact_edit and artifact_status are undefined
1719
- // there — so the workflow cannot call the artifact tools directly;
1720
- // observation still goes through minimal child calls with tiny
1721
- // schemas, never a broad JSON contract.)
1722
- //
1723
- // Pre-trigger toolcheck (tiny, schema'd): the artifact namespace is
1724
- // deferred for workflow children — the child self-loads it and emits
1725
- // one exact signal line, read mechanically (never English prose).
1726
- // Only a parsed ARTIFACT_TOOLS: missing signal is explicit negative
1727
- // evidence: it gets one bounded retry with a fresh key, then parks
1728
- // rejected — without the tools the edit provably did NOT go through,
1729
- // so this is the one safe retry on the publish path. A throw (or an
1730
- // unparseable signal) is INCONCLUSIVE transport noise, never
1731
- // evidence of missing tools (2026-09-16, critic finding 3): it is
1732
- // recorded, it retries once in case the flake clears, but it can
1733
- // never take the rejected path.
1472
+ // See docs/decisions/publish-path.md#fire-and-forget-trigger: the trigger child returns immediately; the workflow owns observation and verdict.
1734
1473
  var publishToolsOk = false;
1735
1474
  var publishToolsMissing = false;
1736
1475
  for (var toolcheckAttempt = 1; toolcheckAttempt <= 2 && !publishToolsOk; toolcheckAttempt++) {
@@ -1778,14 +1517,7 @@ while (i < STEPS.length) {
1778
1517
  }, reworkCount);
1779
1518
  return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact tool namespace was explicitly missing (parsed signal — the edit provably did not go through, so no trigger was issued and nothing was retried blindly). Human attention needed.");
1780
1519
  }
1781
- // Pre-trigger build-state baseline (tiny, schema'd): one read of
1782
- // artifact_status. The post-trigger observation diffs against this
1783
- // baseline — a build whose agent_id was absent from (or differs
1784
- // from) the baseline is attributed to our edit; a build already in
1785
- // flight at baseline predates the trigger and is never attributed
1786
- // to it. If the baseline read itself fails, receipt attribution is
1787
- // skipped and the durable audit-dir evidence below is the only
1788
- // positive signal.
1520
+ // See docs/decisions/publish-path.md#pretrigger-baseline: a tiny schema'd baseline is captured before the trigger.
1789
1521
  var baselineAgentId = null;
1790
1522
  var baselineFailed = false;
1791
1523
  try {
@@ -1802,32 +1534,13 @@ while (i < STEPS.length) {
1802
1534
  baselineFailed = true;
1803
1535
  log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
1804
1536
  }
1805
- // The trigger itself: the artifact_edit call is AWAITED (the workflow
1806
- // waits for it to complete) but its return value is intentionally
1807
- // UNCONSUMED — NO schema, so no schema validation can fail this
1808
- // call: a schema-less call resolves to the child's raw response as
1809
- // a plain string (probed live 2026-09-16 — never parsed, never
1810
- // throws on content). One caveat, also probed: the runtime still
1811
- // scans the response for a JSON candidate, and an unparseable
1812
- // {...}-looking substring in the child's prose throws ("response
1813
- // JSON candidate", probe P6). The prompt tells the child to end its
1814
- // turn with no prose at all, which keeps the common case clean —
1815
- // but the channel is stochastic, so any throw is possible and
1816
- // inconclusive: the edit may still have gone through, so the
1817
- // outcome stays unknown until the observation below confirms it —
1818
- // never inferred from the throw, and never blind-retried (a blind
1819
- // re-trigger duplicated the edit on 2026-09-12).
1537
+ // See docs/decisions/publish-path.md#trigger-await: the artifact_edit call is awaited.
1820
1538
  var rebuildTrigger = null;
1821
1539
  try {
1822
1540
  var triggerText = String(await agent(rebuildPrompt,
1823
1541
  { key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
1824
1542
  log("Publish rebuild trigger for task " + taskId + " returned (" + triggerText.length + " chars; awaited; scanned only for the explicit refusal signal)");
1825
- // Explicit refusal (room #16 blocker 10): the child ends its turn
1826
- // with ARTIFACT_EDIT_REFUSED when artifact_edit explicitly refused.
1827
- // Conclusive negative evidence — the edit provably did NOT go
1828
- // through — so this parks rejected and skips observation polling.
1829
- // A missing/unparseable signal is NOT a refusal: it stays unknown
1830
- // and fail-closed below.
1543
+ // See docs/decisions/publish-path.md#explicit-refusal-16: the child must explicitly refuse artifact work.
1831
1544
  var refusalText = extractRefusal(triggerText);
1832
1545
  if (refusalText) {
1833
1546
  await recordPublishLedger({
@@ -1874,17 +1587,79 @@ while (i < STEPS.length) {
1874
1587
  log("Publish post-trigger build-state check failed for task " + taskId + " (" + (buildCheckErr && buildCheckErr.message ? buildCheckErr.message : buildCheckErr) + ") — this signal is unknown, not negative");
1875
1588
  }
1876
1589
  var observedAgentId = (buildState && buildState.build && typeof buildState.build.agent_id === "string" && buildState.build.agent_id) || null;
1877
- // Known limitation (failure-mode audit 2026-09-16): attribution
1878
- // is timing-based — any agent_id new relative to the baseline is
1879
- // treated as this edit's receipt. A stranger's build starting inside
1880
- // the trigger window is indistinguishable by timing and would be
1881
- // misattributed here. The consequence is bounded: the completion
1882
- // poll below tracks the recorded id, and the parent's mechanical
1883
- // content read-back (docs/publish-verification.md) certifies the
1884
- // exact commit's content — a wrong build's content fails closed as
1885
- // verification-failed, never stamped. Timing narrows the candidate;
1886
- // content decides.
1590
+ // See docs/decisions/publish-path.md#attribution-limitation: attribution is timing-based; content verification bounds the risk.
1887
1591
  var receiptAgentId = (!buildStateFailed && !baselineFailed && observedAgentId && observedAgentId !== baselineAgentId) ? observedAgentId : null;
1592
+ // auditReportOk: pure tri-state read of a report.json body —
1593
+ // true (build ok), false (build failed), null (missing or
1594
+ // unreadable — not evidence either way). The child returns the
1595
+ // raw body verbatim; interpretation lives here, never in prose.
1596
+ // Hoisted to Publish-step scope (before the receipt branch) so both
1597
+ // the immediate and post-poll audit fallbacks share it on every path —
1598
+ // the receipt path skips the else below, which must not leave these
1599
+ // undefined.
1600
+ var auditReportOk = function (raw) {
1601
+ if (typeof raw !== "string") return null;
1602
+ var trimmed = raw.trim();
1603
+ if (trimmed === "" || trimmed === "MISSING") return null;
1604
+ var parsed;
1605
+ try { parsed = JSON.parse(trimmed); } catch (e) { return null; }
1606
+ if (parsed && typeof parsed.ok === "boolean") return parsed.ok;
1607
+ return null;
1608
+ };
1609
+ // (2026-09-18, H2 verdict-first) Publish-verdict vocabulary. The
1610
+ // verdict is one of "landed" | "unknown". Decided once,
1611
+ // before the receipt poll, and dispatched on — never re-derived.
1612
+ // Pure and self-contained: unit-tested by
1613
+ // tests/publish-verdict-first.test.js.
1614
+ // See docs/decisions/publish-path.md#h2-verdict-dispatch.
1615
+ var decidePublishVerdict = function (opts) {
1616
+ var immediateReport = (opts && "immediateReport" in opts) ? opts.immediateReport : null;
1617
+ var newDirCount = (opts && typeof opts.newDirCount === "number") ? opts.newDirCount : 0;
1618
+ if (newDirCount === 1 && immediateReport === true) return { verdict: "landed", unattributableReason: null };
1619
+ if (newDirCount === 1 && immediateReport === false) return { verdict: "unknown", unattributableReason: "audit-report-ok-false" };
1620
+ if (newDirCount === 1) return { verdict: "unknown", unattributableReason: "audit-report-unreadable-or-missing" };
1621
+ if (newDirCount === 0) return { verdict: "unknown", unattributableReason: "no-new-audit-dir-in-window" };
1622
+ return { verdict: "unknown", unattributableReason: "audit-dir-ambiguity", ambiguousDirCount: newDirCount };
1623
+ };
1624
+ // (2026-09-18, H2 message contract, design §1.5) The human line is
1625
+ // the output of a mechanical field→template mapping: the trigger
1626
+ // commit short-sha, one plain clause per unattributable reason, the
1627
+ // no-republish warning, the ledger path with (outcome: unknown), and
1628
+ // the unknown-recovery clause. The appendix carries every machine
1629
+ // field. Pure and self-contained: unit-tested by
1630
+ // tests/publish-verdict-first.test.js.
1631
+ var composeUnattributedParkReason = function (fields) {
1632
+ var f = fields || {};
1633
+ var task = f.taskId || taskId;
1634
+ var reason = f.unattributableReason || "unknown-outcome";
1635
+ var shortSha = String(f.commitShortSha || "").substring(0, 7) || "unknown";
1636
+ var reasonClauses = {
1637
+ "no-new-audit-dir-in-window": "no build could be tied to this attempt",
1638
+ "audit-report-unreadable-or-missing": "the build's report is unreadable",
1639
+ "audit-dir-ambiguity": "more than one build appeared in the check window",
1640
+ "stranger-build-observed-during-poll": "a different build was running during the check",
1641
+ "status-read-errors-during-poll": "status reads kept failing"
1642
+ };
1643
+ var clause = reasonClauses[reason] || "no build could be tied to this attempt";
1644
+ return "Publish outcome unknown for task " + task +
1645
+ ": the trigger was sent for commit " + shortSha + " but the outcome could not be confirmed — " + clause + ". " +
1646
+ "Do NOT republish: if the trigger was accepted, a retry duplicates the build (2026-09-12). " +
1647
+ "The attempt is in the ledger at " + crewHome + "/.publish-ledger/" + PUBLISH_SLUG + ".jsonl (outcome: unknown) " +
1648
+ "and the crew's unknown-recovery will re-examine it. " +
1649
+ "Appendix: unattributable_reason=" + reason +
1650
+ "; poll_end_state=" + (f.pollEndState || "not-polled") +
1651
+ "; saw_our_build=" + (f.sawOurBuild ? "true" : "false") +
1652
+ "; new_audit_dirs=" + (f.newAuditDirCount == null ? 0 : f.newAuditDirCount) +
1653
+ "; poll_chunks_failed=" + (f.chunkFailures == null ? 0 : f.chunkFailures) +
1654
+ "; poll_status_errors=" + (f.pollStatusErrors == null ? 0 : f.pollStatusErrors) + ".";
1655
+ };
1656
+ // publishFailure is declared here (per-Publish-pass scope) so the
1657
+ // post-poll explicit build failure survives to the final routing
1658
+ // below; publishUnknownFields carries the structured unknown fields
1659
+ // for the single composed fail-closed park. Both re-initialize on
1660
+ // every pass — a rework re-entry never leaks a stale verdict.
1661
+ var publishFailure = null;
1662
+ var publishUnknownFields = null;
1888
1663
  if (receiptAgentId) {
1889
1664
  // The edit went through — a build with a new agent_id appeared
1890
1665
  // after the trigger. The parent's independent read-back
@@ -1903,31 +1678,6 @@ while (i < STEPS.length) {
1903
1678
  }, reworkCount);
1904
1679
  } else {
1905
1680
  var newAuditDirs = [];
1906
- // auditReportOk: pure tri-state read of a report.json body —
1907
- // true (build ok), false (build failed), null (missing or
1908
- // unreadable — not evidence either way). The child returns the
1909
- // raw body verbatim; interpretation lives here, never in prose.
1910
- // Defined here so both the immediate and post-poll audit
1911
- // fallbacks share it.
1912
- var auditReportOk = function (raw) {
1913
- if (typeof raw !== "string") return null;
1914
- var trimmed = raw.trim();
1915
- if (trimmed === "" || trimmed === "MISSING") return null;
1916
- var parsed;
1917
- try { parsed = JSON.parse(trimmed); } catch (e) { return null; }
1918
- if (parsed && typeof parsed.ok === "boolean") return parsed.ok;
1919
- return null;
1920
- };
1921
- // (2026-09-16, critic finding 2) When durable audit evidence
1922
- // confirms (or refutes) the build, there is no receipt agent_id
1923
- // to chain the completion poll to — skipReceiptPoll bypasses the
1924
- // poll below, which with a null receipt could only observe
1925
- // strangers or nothing.
1926
- var skipReceiptPoll = false;
1927
- // publishFailure is declared here (moved up from below) so the
1928
- // immediate audit fallback can record an explicit build failure
1929
- // without the later declaration resetting it.
1930
- var publishFailure = null;
1931
1681
  try {
1932
1682
  var auditAfter = await agent(
1933
1683
  "List the artifact audit directories for slug \"" + PUBLISH_SLUG + "\" (best-effort, never a gate).\n" +
@@ -1947,88 +1697,109 @@ while (i < STEPS.length) {
1947
1697
  log("Publish audit-dir re-list after trigger failed for task " + taskId + " (non-fatal, durable-evidence check degraded): " + (auditAfterErr && auditAfterErr.message ? auditAfterErr.message : auditAfterErr));
1948
1698
  }
1949
1699
  if (newAuditDirs.length > 0) {
1950
- rebuildTrigger = { edit_started: true };
1951
1700
  rebuildAgentId = null;
1952
1701
  newAuditDirs.sort();
1953
1702
  var newestImmediateDir = newAuditDirs[newAuditDirs.length - 1];
1954
1703
  log("Publish rebuild trigger for task " + taskId + ": new audit dir(s) during the trigger window (" + newAuditDirs.join(", ") + ") — the edit went through and a build completed; no in-flight receipt was observed.");
1955
- // (2026-09-16, critic finding 2) Durable audit evidence exists,
1704
+ // (2026-09-18, H2 verdict-first) Durable audit evidence exists,
1956
1705
  // but there is no receipt agent_id to chain the completion poll
1957
1706
  // to — polling with a null receipt can only observe strangers
1958
1707
  // (any running build differs from "null") or nothing, burning
1959
- // 10.5 minutes to park unknown. Read the build report now
1960
- // instead of polling: ok=true confirms completion and routes
1961
- // directly to parent verification (the poll is skipped);
1962
- // ok=false is explicit failure; unreadable is unknown.
1708
+ // 10.5 minutes to park unknown. Read the build report now instead
1709
+ // of polling, then decide the verdict ONCE via
1710
+ // decidePublishVerdict: exactly one new dir with ok=true lands
1711
+ // (attribution by window, not identity — never poll blind);
1712
+ // ok=false is UNKNOWN with the failure evidence preserved in the
1713
+ // ledger detail (the evidence is explicit, the attribution is
1714
+ // not); unreadable / zero / ambiguous dirs are UNKNOWN.
1715
+ // Verdict-first: landed bypasses the poll below, unknown falls
1716
+ // through to post-deploy. STEP 2 runs on every path.
1717
+ // See docs/decisions/publish-path.md#h2-verdict-dispatch.
1963
1718
  var immediateReportOk = null;
1964
- try {
1965
- var immediateOkRead = await agent(
1966
- "Read the artifact build report for slug \"" + PUBLISH_SLUG + "\".\n" +
1967
- "Run: cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestImmediateDir + "/report.json 2>/dev/null || echo MISSING\n" +
1968
- "Return JSON { \"raw\": \"<verbatim file contents, or the literal string MISSING when the file does not exist>\" } and nothing else.",
1969
- { key: attemptKey("publish-audit-ok-immediate-" + taskId, reworkCount), label: "Reading build report for audit-confirmed build",
1970
- schema: { type: "object", properties: { raw: { type: "string" } }, required: ["raw"] } }
1971
- );
1972
- immediateReportOk = auditReportOk(immediateOkRead && immediateOkRead.raw);
1973
- } catch (immediateOkErr) {
1974
- log("Publish build-report read for audit-confirmed dir failed for task " + taskId + " (treated as unknown): " + (immediateOkErr && immediateOkErr.message ? immediateOkErr.message : immediateOkErr));
1975
- immediateReportOk = null;
1719
+ // (N1) Read the report only when exactly one new dir exists:
1720
+ // ambiguity (>1) forces UNKNOWN regardless — don't shell out to
1721
+ // read a report that will be discarded.
1722
+ if (newAuditDirs.length === 1) {
1723
+ try {
1724
+ var immediateOkRead = await agent(
1725
+ "Read the artifact build report for slug \"" + PUBLISH_SLUG + "\".\n" +
1726
+ "Run: cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestImmediateDir + "/report.json 2>/dev/null || echo MISSING\n" +
1727
+ "Return JSON { \"raw\": \"<verbatim file contents, or the literal string MISSING when the file does not exist>\" } and nothing else.",
1728
+ { key: attemptKey("publish-audit-ok-immediate-" + taskId, reworkCount), label: "Reading build report for audit-confirmed build",
1729
+ schema: { type: "object", properties: { raw: { type: "string" } }, required: ["raw"] } }
1730
+ );
1731
+ immediateReportOk = auditReportOk(immediateOkRead && immediateOkRead.raw);
1732
+ } catch (immediateOkErr) {
1733
+ log("Publish build-report read for audit-confirmed dir failed for task " + taskId + " (treated as unknown): " + (immediateOkErr && immediateOkErr.message ? immediateOkErr.message : immediateOkErr));
1734
+ immediateReportOk = null;
1735
+ }
1976
1736
  }
1977
- if (immediateReportOk === true) {
1737
+ var immediateVerdict = decidePublishVerdict({ editStarted: true, immediateReport: immediateReportOk, newDirCount: newAuditDirs.length });
1738
+ if (immediateVerdict.verdict === "landed") {
1978
1739
  publishBuildLanded = true;
1979
1740
  artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
1980
- skipReceiptPoll = true;
1981
- log("Publish build landed for task " + taskId + " via immediate durable audit evidence (audit dir " + newestImmediateDir + ", report ok=true) — receipt poll skipped (no receipt to chain to), routing directly to parent verification");
1982
1741
  await recordPublishLedger({
1983
1742
  commit: mergeCommitForPublish,
1984
1743
  attempt: rebuildAttemptKey,
1985
1744
  agent_id: null,
1986
1745
  applied_report: publishAppliedObservation,
1987
1746
  outcome: "submitted",
1988
- detail: "durable audit evidence shows a build completed during the attempt window (audit dir " + newestImmediateDir + ", report ok=true); receipt poll skipped (no receipt agent_id), routed to parent verification"
1989
- }, reworkCount);
1990
- } else if (immediateReportOk === false) {
1991
- skipReceiptPoll = true;
1992
- publishFailure = "Artifact build FAILED for slug " + PUBLISH_SLUG + " (audit dir " + newestImmediateDir + ", report ok=false — immediate audit evidence, no receipt observed). Explicit negative evidence: a build ran and failed. The publish did not land — provenance was not stamped. Fail-closed.";
1993
- await recordPublishLedger({
1994
- commit: mergeCommitForPublish,
1995
- attempt: rebuildAttemptKey,
1996
- agent_id: null,
1997
- applied_report: publishAppliedObservation,
1998
- outcome: "failed",
1999
- detail: "a build ran and failed: audit dir " + newestImmediateDir + " report ok=false (immediate audit evidence, no receipt)"
1747
+ detail: "durable audit evidence shows a build completed during the attempt window (no receipt agent_id — attribution by window, not identity; receipt poll bypassed (verdict decided), routed to parent verification)"
2000
1748
  }, reworkCount);
1749
+ log("Publish verdict LANDED for task " + taskId + ": a completed build was observed during the attempt window — receipt poll bypassed (verdict decided, no receipt to chain to), routing directly to parent verification.");
2001
1750
  } else {
1751
+ // Verdict UNKNOWN on the immediate path. ok=false is explicit
1752
+ // failure evidence but not an attributable failure — the ledger
1753
+ // records unknown with the evidence preserved in the detail,
1754
+ // and the flow continues to post-deploy (never parks early).
1755
+ publishUnknownFields = {
1756
+ taskId: taskId,
1757
+ unattributableReason: immediateVerdict.unattributableReason,
1758
+ pollEndState: "not-polled",
1759
+ sawOurBuild: false,
1760
+ newAuditDirCount: newAuditDirs.length,
1761
+ chunkFailures: 0,
1762
+ pollStatusErrors: 0
1763
+ };
1764
+ var immediateDetail = "durable audit-dir fallback could not prove a completed build for this attempt (unattributable_reason=" + immediateVerdict.unattributableReason + ", no receipt agent_id)";
1765
+ if (immediateReportOk === false) {
1766
+ immediateDetail += "; explicit failure evidence preserved: report ok=false for audit dir " + newestImmediateDir;
1767
+ }
1768
+ if (immediateVerdict.unattributableReason === "audit-dir-ambiguity") {
1769
+ immediateDetail += "; audit-dir ambiguity: " + newAuditDirs.length + " new dirs in window";
1770
+ }
2002
1771
  await recordPublishLedger({
2003
1772
  commit: mergeCommitForPublish,
2004
1773
  attempt: rebuildAttemptKey,
2005
1774
  agent_id: null,
2006
- applied_report: null,
1775
+ applied_report: publishAppliedObservation,
2007
1776
  outcome: "unknown",
2008
- detail: "new audit dir " + newestImmediateDir + " appeared during the trigger window but its build report is unreadable/missing; no receipt agent_id to poll — outcome unknown, fail-closed with no blind retry"
1777
+ detail: immediateDetail
2009
1778
  }, reworkCount);
2010
- return await parkTask("Publish outcome unknown for task " + taskId + ": a new audit dir (" + newestImmediateDir + ") appeared during the trigger window but its build report is unreadable, and no in-flight receipt was observed to poll. The edit may have completed. Correlate the accepted edit via the publish ledger at " + crewHome + "/.publish-ledger/" + PUBLISH_SLUG + ".jsonl — do NOT reissue the edit blindly: if the trigger was accepted, a retry duplicates it (2026-09-12). Verify independently whether the build completed (audit dir + report, or the parent's content read-back) before deciding the next step. Fail-closed.");
1779
+ log("Publish verdict UNKNOWN for task " + taskId + ": " + immediateDetail + " — continuing to post-deploy; never polling blind and never parking early.");
2011
1780
  }
2012
1781
  } else {
2013
- // No attributable build and no durable evidence — but that
2014
- // proves nothing (a fast-completing build can finish between
2015
- // polls, or the checks themselves failed). The outcome is
2016
- // UNKNOWN. No retry: re-issuing the edit here duplicated it on
2017
- // 2026-09-12. Record the attempt durably and park fail-closed;
2018
- // correlate via the ledger, never by guessing from a blind poll.
2019
- log("Publish rebuild trigger for task " + taskId + ": no attributable build observed and no new audit dir — outcome UNKNOWN. Recording the attempt and parking fail-closed; no blind retry.");
1782
+ // See docs/decisions/publish-path.md#no-attributable-build: no attributable build and no durable evidence means no publish.
1783
+ publishUnknownFields = {
1784
+ taskId: taskId,
1785
+ unattributableReason: decidePublishVerdict({ editStarted: true, immediateReport: null, newDirCount: 0 }).unattributableReason,
1786
+ pollEndState: "not-polled",
1787
+ sawOurBuild: false,
1788
+ newAuditDirCount: 0,
1789
+ chunkFailures: 0,
1790
+ pollStatusErrors: 0
1791
+ };
2020
1792
  await recordPublishLedger({
2021
1793
  commit: mergeCommitForPublish,
2022
1794
  attempt: rebuildAttemptKey,
2023
1795
  agent_id: null,
2024
- applied_report: null,
1796
+ applied_report: publishAppliedObservation,
2025
1797
  outcome: "unknown",
2026
- detail: "fire-and-forget trigger; post-trigger build-state poll saw no attributable build (or the check failed) and the audit-dir diff found no new dir; the edit may have been accepted as pending_init"
1798
+ detail: "no new audit dir appeared in the trigger window and no receipt agent_id was observed — the edit was issued fire-and-forget, so completion is unproven; never poll blind on a null receipt"
2027
1799
  }, reworkCount);
2028
- return await parkTask("Publish outcome unknown for task " + taskId + ": the rebuild trigger was issued fire-and-forget (no schema, so no validation failure mode; a candidate-parse throw stays possible and is inconclusive), and the follow-up observation could not attribute a build to the edit for slug " + PUBLISH_SLUG + " — no in-flight build with a new agent_id appeared in the poll window and no new audit dir landed. The edit may have been accepted as pending_init, so no retry was issued: a blind retry duplicated the edit on 2026-09-12. The attempt is recorded in the publish ledger at " + crewHome + "/.publish-ledger/" + PUBLISH_SLUG + ".jsonl (commit " + String(mergeCommitForPublish || "unknown").slice(0, 12) + "). Correlate the accepted edit via the ledger and the builder's eventual completion — do NOT reissue the edit blindly. Verify independently whether the build completed before deciding the next step. Fail-closed.");
1800
+ log("Publish verdict UNKNOWN for task " + taskId + ": no new audit dir in window and no receipt — continuing to post-deploy; never polling blind and never parking early.");
2029
1801
  }
2030
1802
  }
2031
-
2032
1803
  // Durable publish-attempt ledger: record the trigger outcome while the
2033
1804
  // attempt key and commit are in scope. Every attempt lands here with
2034
1805
  // its outcome — submitted, rejected, or unknown (unknown is recorded
@@ -2038,9 +1809,45 @@ while (i < STEPS.length) {
2038
1809
  // already recorded the ledger's submitted line on both positive paths
2039
1810
  // and parked on unknown — there is no applied report to observe and
2040
1811
  // no rejection signal to record.
2041
- // (publishFailure is declared with the immediate audit fallback
2042
- // above so an explicit build failure there survives to here.)
2043
- if (rebuildTrigger.edit_started && !skipReceiptPoll) {
1812
+ // (2026-09-18, H2 verdict-first) The verdict was decided exactly
1813
+ // once above; dispatch on it. landed bypasses the receipt poll
1814
+ // (the audit evidence already proved completion); an explicit
1815
+ // publishFailure is preserved verbatim through post-deploy.
1816
+ // Otherwise the verdict is open — but the poll below is only
1817
+ // legitimate against a real receipt: the null-safe assertion records
1818
+ // UNKNOWN and continues to STEP 2 instead of polling blind.
1819
+ // See docs/decisions/publish-path.md#h2-verdict-dispatch.
1820
+ if (publishBuildLanded) {
1821
+ log("Publish verdict already LANDED for task " + taskId + " — bypassing receipt poll.");
1822
+ } else if (publishFailure) {
1823
+ // design §1.2 pre-poll branch — currently unassigned; kept for the converged dispatch shape.
1824
+ log("Publish verdict already FAILED for task " + taskId + " — preserved verbatim through post-deploy.");
1825
+ } else if (!rebuildTrigger || !rebuildTrigger.edit_started) {
1826
+ // Loud defensive assertion (replaces the old lying "Unreachable"
1827
+ // else): with no receipt state the poll would observe strangers or
1828
+ // nothing — record UNKNOWN and continue to STEP 2. Never park
1829
+ // early here; never poll blind.
1830
+ if (!publishUnknownFields) {
1831
+ publishUnknownFields = {
1832
+ taskId: taskId,
1833
+ unattributableReason: "no-receipt-state",
1834
+ pollEndState: "not-polled",
1835
+ sawOurBuild: false,
1836
+ newAuditDirCount: 0,
1837
+ chunkFailures: 0,
1838
+ pollStatusErrors: 0
1839
+ };
1840
+ await recordPublishLedger({
1841
+ commit: mergeCommitForPublish,
1842
+ attempt: rebuildAttemptKey,
1843
+ agent_id: null,
1844
+ applied_report: publishAppliedObservation,
1845
+ outcome: "unknown",
1846
+ detail: "no receipt state was recorded for this attempt — never polling blind; continuing to post-deploy"
1847
+ }, reworkCount);
1848
+ }
1849
+ log("Publish verdict UNKNOWN for task " + taskId + ": no receipt state recorded — never polling blind, continuing to post-deploy.");
1850
+ } else {
2044
1851
  // (2026-09-16) There is no builder report: the fire-and-forget
2045
1852
  // trigger carries no JSON contract, so there is nothing to
2046
1853
  // compare and no pre-hash diagnostic. The builder's old
@@ -2113,13 +1920,7 @@ while (i < STEPS.length) {
2113
1920
  timeoutMs: 270000 }
2114
1921
  );
2115
1922
  } catch (chunkErr) {
2116
- // A hung or failed chunk is inconclusive, never terminal:
2117
- // record it and continue to the next chunk. (2026-09-16,
2118
- // clean-room task 1febe8eb: the platform's 270s agent
2119
- // timeout killed chunk 2, which threw out of this loop —
2120
- // skipping chunk 3 AND the STEP 1b audit-dir fallback and
2121
- // parking on the exception path.) Fail-closed still applies
2122
- // after chunk 3 and the fallback are exhausted.
1923
+ // See docs/decisions/publish-path.md#hung-chunk: a hung or failed chunk is inconclusive, never terminal.
2123
1924
  chunkFailures.push("chunk " + chunk + ": " + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr));
2124
1925
  log("Artifact build poll chunk " + chunk + " of 3 failed (" + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr) + ") \u2014 continuing to the next chunk; build completion still unproven.");
2125
1926
  }
@@ -2135,20 +1936,7 @@ while (i < STEPS.length) {
2135
1936
  buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
2136
1937
  }
2137
1938
  if (buildPoll.build_done && pollSawOurBuild) {
2138
- // STEP 1c (mechanical): NO provenance stamp here. Canary run 8
2139
- // (2026-09-11) proved the stamp cannot certify content: the
2140
- // builder's applied-report was derived from the carried diff, so
2141
- // the old report check was circular — a fabricated report
2142
- // passed by construction, and every phase went green on a hollow
2143
- // build. The stamp moves to the parent (docs/publish-verification.md);
2144
- // the deterministic lib/readback-disk.js is the primary sensor
2145
- // (the agent-callable read-back tool is unavailable —
2146
- // artifact_inspect was removed by the platform 2026-09-14 — so
2147
- // the LLM-inspector path is manual-fallback only), and the task
2148
- // parks for parent verification.
2149
- // Chore has no QA: the parent's verification is the final gate.
2150
- // An unverified publish fails loudly in QA instead of passing
2151
- // silently here.
1939
+ // See docs/decisions/publish-path.md#step-1c-no-stamp: no provenance stamp in STEP 1c.
2152
1940
  publishBuildLanded = true;
2153
1941
  artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
2154
1942
  log("Publish build landed for task " + taskId + " — provenance stamp deferred to parent content verification");
@@ -2231,7 +2019,11 @@ while (i < STEPS.length) {
2231
2019
  detail: "durable audit evidence shows a build completed during the attempt window (audit dir " + newestAuditDirAfterPoll + ", report ok=true); routed to parent verification"
2232
2020
  }, reworkCount);
2233
2021
  } else if (auditOkAfterPoll === false) {
2234
- publishFailure = "Artifact build FAILED for slug " + PUBLISH_SLUG + " (audit dir " + newestAuditDirAfterPoll + ", report ok=false). Explicit negative evidence: a build ran and failed (attribution by window, not by build identity — no stranger build was observed in flight during the poll). The publish did not land — provenance was not stamped. Fail-closed.";
2022
+ // Explicit negative evidence under the stranger guard: a build
2023
+ // ran and failed during the poll window with no stranger in
2024
+ // flight. This is an attributable failure — preserved verbatim
2025
+ // through post-deploy. Message contract unchanged.
2026
+ publishFailure = "Artifact build FAILED for slug " + PUBLISH_SLUG + " (audit dir " + newestAuditDirAfterPoll + ", report ok=false). Explicit negative evidence: a build ran and failed (attribution by window, not by build identity — no stranger build was observed in flight during the poll). The publish did not land — provenance was not stamped — your change was NOT published. Build log: ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestAuditDirAfterPoll + "/. This explicit failure is not auto-retried. Appendix: attribution=window; report=ok=false; provenance=unstamped.";
2235
2027
  await recordPublishLedger({
2236
2028
  commit: mergeCommitForPublish,
2237
2029
  attempt: rebuildAttemptKey,
@@ -2245,7 +2037,19 @@ while (i < STEPS.length) {
2245
2037
  : ((pollStatusErrors > 0 && !pollSawOurBuild) ? "status-read-errors-during-poll"
2246
2038
  : (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
2247
2039
  : (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing")));
2248
- publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + "; poll_status_errors=" + pollStatusErrors + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
2040
+ // Verdict UNKNOWN (poll path): the build cannot be attributed
2041
+ // to this attempt. The fields feed the single composed
2042
+ // fail-closed park reason after post-deploy — never a
2043
+ // fabricated verdict, never a silent pass.
2044
+ publishUnknownFields = {
2045
+ taskId: taskId,
2046
+ unattributableReason: unattributableReason,
2047
+ pollEndState: pollEndState,
2048
+ sawOurBuild: pollSawOurBuild,
2049
+ newAuditDirCount: newAuditDirsAfterPoll.length,
2050
+ chunkFailures: chunkFailures.length,
2051
+ pollStatusErrors: pollStatusErrors
2052
+ };
2249
2053
  await recordPublishLedger({
2250
2054
  commit: mergeCommitForPublish,
2251
2055
  attempt: rebuildAttemptKey,
@@ -2256,11 +2060,8 @@ while (i < STEPS.length) {
2256
2060
  }, reworkCount);
2257
2061
  }
2258
2062
  }
2259
- } else {
2260
- // Unreachable: the observation above either attributes the edit
2261
- // (edit_started) or parks. Defensive only — never a silent pass.
2262
- publishFailure = "Artifact rebuild trigger failed: the edit was not attributed to any observed build. The publish is unattributed (not proven landed, not proven failed) — provenance was not stamped. Fail-closed.";
2263
2063
  }
2064
+
2264
2065
  } // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
2265
2066
  // STEP 2 (mechanical, always — skip path included): post-deploy
2266
2067
  // commits builder leftovers if any, removes the worktree, and releases
@@ -2284,6 +2085,19 @@ while (i < STEPS.length) {
2284
2085
  ? " Post-deploy finalized cleanup."
2285
2086
  : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
2286
2087
  }
2088
+ if (!publishBuildLanded) {
2089
+ // Verdict UNKNOWN (single fail-closed park — post-deploy always
2090
+ // runs first; no early parks anywhere above). Machine contract:
2091
+ // "unattributable_reason=" and "poll_end_state=" are always
2092
+ // present; attribution is by window, not identity.
2093
+ // (2026-09-18, H2 message contract) Thread the commit short-sha
2094
+ // through the unknown fields so the human line names the trigger
2095
+ // commit (design §1.5: "the trigger was sent for commit <short-sha>").
2096
+ if (publishUnknownFields) publishUnknownFields.commitShortSha = mergeCommitShortForPublish;
2097
+ return await parkTask(composeUnattributedParkReason(publishUnknownFields) + (postDeploy.deployed
2098
+ ? " Post-deploy finalized cleanup."
2099
+ : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
2100
+ }
2287
2101
  if (!postDeploy.deployed) {
2288
2102
  return await parkTask("Post-deploy failed after the artifact build landed: " + (postDeploy.output || "no output") + ". The build may have landed but worktree cleanup and lock release are unknown — human attention needed.");
2289
2103
  }
@@ -2508,18 +2322,7 @@ while (i < STEPS.length) {
2508
2322
  }
2509
2323
  log("Build worktree confinement passed: " + wt.path);
2510
2324
 
2511
- // Already-merged idempotency: a `repo_diff: none (already-merged:
2512
- // <sha>)` declaration is verified mechanically — <sha> must resolve
2513
- // and be an ancestor of main in the configured repo. A fabricated or
2514
- // mistaken declaration fails the phase here (the dispatcher retries
2515
- // Build under its consecutive-failure cap); a verified declaration is
2516
- // recorded in alreadyMergedSha for Review's no-diff branch. Without
2517
- // this guard, Build correctly doing nothing left Review with no
2518
- // mechanical way to accept an empty diff, and Cass rejected for "no
2519
- // commits ahead of main — the builder likely forgot to commit" while
2520
- // the deliverable sat on main (canary 2026-09-15, task 1d692d91).
2521
- // The sha is hex-only by construction (extractAlreadyMerged), so
2522
- // interpolating it into the shell command cannot inject.
2325
+ // See docs/decisions/publish-path.md#already-merged-idem: an already-merged repo_diff is idempotent; no rebuild.
2523
2326
  var am = extractAlreadyMerged(workerText);
2524
2327
  if (am.sha) {
2525
2328
  var amCheck = await agent(
@@ -2565,20 +2368,7 @@ while (i < STEPS.length) {
2565
2368
  };
2566
2369
  let passed = stepResult.passed === true;
2567
2370
 
2568
- // Deterministic integrate verification: the agent cannot self-certify a
2569
- // merge. After the Integrate agent claims success, the workflow confirms
2570
- // mechanically that the task branch tip is an ancestor of main via the
2571
- // lifecycle script's verify-merge command (which resolves the branch
2572
- // through the crew registry — never by reconstructing "task/"+taskId — so
2573
- // the check cannot verify the wrong branch). The VERIFIED marker is matched
2574
- // by regex on the script's own stdout; agent prose is never read. This
2575
- // closes the hole where an agent reported "merged empty" while approved
2576
- // commits were still stranded on the task branch (bug b1b1f919). A genuine
2577
- // empty-diff Integrate (MERGED_EMPTY: no commits ahead of main) verifies
2578
- // vacuously — the tip is then an ancestor of main. Verification failure is
2579
- // an operational step failure, not a park: the dispatcher retries Integrate
2580
- // under its consecutive-failure cap, and the retry finds the commits still
2581
- // on the branch and performs the real merge — self-healing.
2371
+ // See docs/decisions/publish-path.md#integrate-verify: the agent cannot verify integrate mechanically; the workflow checks the diff.
2582
2372
  if (step.name === "Integrate" && passed) {
2583
2373
  var integrateVerifyOut = "";
2584
2374
  try {
@@ -2609,17 +2399,7 @@ while (i < STEPS.length) {
2609
2399
  // retryable under the dispatcher's consecutive-failure cap.
2610
2400
  const status = passed ? "completed" : (step.name === "Integrate" || step.name === "Publish" ? "failed" : "rejected");
2611
2401
 
2612
- // Deterministic publish verification: the agent cannot self-certify a publish.
2613
- // Skip-aware (park 2026-09-11): when the deterministic publish script found
2614
- // no merge lock held (empty-diff Integrate), it skips the publish path
2615
- // gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
2616
- // marker. The preflight (bugfix 2026-09-17) emits
2617
- // PUBLISH_SKIPPED=no-npm-publish when npm publish is not configured on
2618
- // this machine (helper or credential absent) — also before any mutation.
2619
- // Verification is then vacuous — nothing was shipped, and the
2620
- // registry must NOT have moved. The marker is script-emitted explicit state
2621
- // (pasted verbatim per the Publish agent instructions), not agent prose; a
2622
- // report without the marker still runs the full verification fail-closed.
2402
+ // See docs/decisions/publish-path.md#publish-verify: the agent cannot verify publish; the parent does.
2623
2403
  var publishVerified = false;
2624
2404
  var npmPublishSkipped = false;
2625
2405
  if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
@@ -2645,15 +2425,7 @@ while (i < STEPS.length) {
2645
2425
  publishTarget.target + " (" + publishTarget.base + " + " + publishTarget.scope + "). The publish did not land.");
2646
2426
  }
2647
2427
  publishVerified = true;
2648
- // Provenance refresh for self-publishes (task 7946d2a4): the npm path
2649
- // installs and activates a new immutable release (crew-release.sh deploy
2650
- // swaps the `current` symlink inside publish-npm.sh) but never stamped
2651
- // the dashboard's provenance record — every crew release left
2652
- // crew_release pointing at a pruned release. After a verified landed
2653
- // publish, refresh the record's crew_release to the now-live release
2654
- // identity, preserving the existing source_commit (the dashboard
2655
- // artifact's build source — a crew-repo commit here would fail the
2656
- // dashboard QA source check).
2428
+ // See docs/decisions/publish-path.md#provenance-refresh: refresh crew_release after landed publish, preserving source_commit.
2657
2429
  try {
2658
2430
  var provRefresh = await agent(
2659
2431
  "Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
@@ -2683,31 +2455,10 @@ while (i < STEPS.length) {
2683
2455
  } // end: !npmPublishSkipped — a skipped publish has nothing to verify
2684
2456
  }
2685
2457
 
2686
- // Publish content verification — parent-owned (docs/publish-verification.md).
2687
- // The old block read back the workflow's OWN provenance stamp and compared
2688
- // it to HEAD: that verifies the stamp, not the content. Canary run 8
2689
- // (2026-09-11) passed it with a hollow build — the stamp was honest, the
2690
- // artifact was stale, all eight phases green. The stamp now moves to the
2691
- // parent (docs/publish-verification.md); the independent read-back step
2692
- // is currently unavailable (no agent-callable read-back tool exists —
2693
- // artifact_inspect was removed by the platform 2026-09-14), so the parent
2694
- // cannot confirm content and the task parks for verification.
2695
- // Skip-aware (park 2026-09-11): an empty-diff Integrate takes no merge
2696
- // lock, and the deterministic publish path skips rebuild/stamp entirely —
2697
- // there is no new content to verify, so verification is vacuous.
2698
- // publishSkippedNoLock is workflow-computed state from the explicit
2699
- // lock-status read in STEP 0, not agent prose.
2700
- // The parent (tick worker) triggers the ONE read-back inspection it can
2701
- // actually receive (async results go to the root agent, never into a
2702
- // workflow run — a workflow-side trigger would be an orphan). The workflow
2703
- // only parks; the parent's scan builds the request deterministically via
2704
- // lib/build-readback-request.js and ferries the inspection.
2705
- // publishBuildLanded and publishSkippedNoLock are workflow-computed state;
2706
- // a skipped or failed publish has nothing to verify.
2707
- // Session notes. Machine-readable marker lines are extracted from the full
2708
- // worker report and appended AFTER the slice so a long report can never
2709
- // amputate them; later phases (Review reading repo_diff:, QA backstop
2710
- // reading TARGET_VERSION=) depend on them.
2458
+ // Parent-owned verification (docs/publish-verification.md): the parent's
2459
+ // scan builds the request via lib/build-readback-request.js and ferries
2460
+ // the inspection; a skipped or failed publish has nothing to verify.
2461
+ // See docs/decisions/publish-path.md#parent-owned-verification for history.
2711
2462
  let summary;
2712
2463
  var workerMarkers = extractMarkerLines(workerText);
2713
2464
  if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
@@ -2721,12 +2472,7 @@ while (i < STEPS.length) {
2721
2472
  } else {
2722
2473
  summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
2723
2474
  }
2724
- // Already-merged attestation: when the Build gate verified the builder's
2725
- // already-merged declaration, the workflow records its own marker line in
2726
- // the session notes (like the builder markers above, it is appended after
2727
- // the slice so it can never be amputated). A later run resumed at Review
2728
- // hydrates alreadyMergedSha from this workflow-attested line — never from
2729
- // the builder's declaration alone.
2475
+ // See docs/decisions/publish-path.md#already-merged: when the Build gate verifies already-merged, attestation is recorded.
2730
2476
  if (step.name === "Build" && alreadyMergedSha) {
2731
2477
  summary += "\nalready_merged_verified: " + alreadyMergedSha;
2732
2478
  }
@@ -2798,18 +2544,17 @@ while (i < STEPS.length) {
2798
2544
  return { status: "failed", task_id: taskId, reason: "Publish failed: " + summary };
2799
2545
  }
2800
2546
 
2801
- // Publish verification park: the build landed and post-deploy finalized,
2802
- // but provenance is UNSTAMPED until the parent's independent read-back
2803
- // (docs/publish-verification.md) confirms the artifact's actual content
2804
- // matches the merged diff. The parent stamps provenance, then re-queues;
2805
- // the dispatcher resumes at QA, whose provenance check enforces the stamp
2806
- // mechanically. A failed Publish never reaches this park — it returned
2807
- // failed above and retries under the dispatcher's cap. The merge lock is
2808
- // already released (post-deploy), so the parked task holds no resources.
2547
+ // See docs/decisions/publish-path.md#verification-park: the build landed but provenance is unstamped until parent verification.
2809
2548
  if (passed && step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG && !publishSkippedNoLock && publishBuildLanded) {
2549
+ // Success contract (2026-09-18, H2): the parent asked for exactly
2550
+ // one build for this publish. The build landed — do NOT republish: a
2551
+ // duplicate build would re-publish the same change. content_check=pending
2552
+ // means the parent's independent read-back has not happened yet; this
2553
+ // park is NOT proof the content is correct.
2810
2554
  return await parkTask("publish: verification-requested " + mergeCommitForPublish +
2811
- " (build " + (rebuildAgentId || "agent_id unobserved") + ")" +
2812
- " — artifact build landed, post-deploy finalized, provenance NOT stamped. Parent: run docs/publish-verification.md.");
2555
+ " Do NOT republish: a duplicate build would re-publish the same change. " +
2556
+ "Artifact build landed, post-deploy finalized, provenance not stamped — the crew has not yet independently confirmed the live artifact contains exactly the change; waiting on the manual read-back in docs/publish-verification.md. " +
2557
+ "Appendix: build=" + (rebuildAgentId || "agent_id unobserved") + "; provenance=unstamped; content_check=pending.");
2813
2558
  }
2814
2559
 
2815
2560
  i++;