muse-crew 0.14.3 → 0.14.5

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.
Files changed (46) hide show
  1. package/AGENTS.md +2 -2
  2. package/docs/decisions/AGENTS.md +1 -0
  3. package/docs/decisions/publish-path.md +96 -3
  4. package/docs/guide.md +2 -2
  5. package/docs/publish-unknown-recovery.md +10 -3
  6. package/docs/publish-verification.md +126 -62
  7. package/docs/release-integrity.md +62 -0
  8. package/docs/reviews/critic-0144.md +83 -0
  9. package/docs/reviews/critic-0145.md +106 -0
  10. package/lib/AGENTS.md +12 -4
  11. package/lib/advance-publish-base.js +8 -0
  12. package/lib/append-ooda-step.js +12 -3
  13. package/lib/build-readback-request.js +10 -0
  14. package/lib/build-registry.js +8 -3
  15. package/lib/check-intent-freshness.js +101 -0
  16. package/lib/classify-publish-absence.js +11 -0
  17. package/lib/classify-surface.js +12 -2
  18. package/lib/commit-scaffold.js +22 -6
  19. package/lib/compose-evidence-caption.js +15 -4
  20. package/lib/compute-publish-diff.js +41 -11
  21. package/lib/crew-api.js +478 -26
  22. package/lib/crew-release.sh +233 -1
  23. package/lib/gitignore.js +23 -5
  24. package/lib/package.json +1 -0
  25. package/lib/publish-note-vocabulary.js +64 -0
  26. package/lib/read-ooda-verdict.js +11 -3
  27. package/lib/readback-disk.js +9 -0
  28. package/lib/render-html.js +17 -7
  29. package/lib/repo-orchestration.js +21 -5
  30. package/lib/retry-publish.js +116 -72
  31. package/lib/sample-project.js +22 -6
  32. package/lib/scaffold-crew.js +11 -2
  33. package/lib/see-act.js +17 -8
  34. package/lib/serve-artifact.js +12 -6
  35. package/lib/setup-project-repo.js +26 -7
  36. package/lib/update-watch.js +34 -17
  37. package/lib/ux-doctrine.js +31 -6
  38. package/lib/verify-publish.js +44 -6
  39. package/lib/write-ooda-verdict.js +12 -3
  40. package/package.json +1 -1
  41. package/seed/cron-body-template.md +50 -16
  42. package/workflows/bugfix.js +110 -643
  43. package/workflows/chore.js +110 -643
  44. package/workflows/crew-dispatch.js +36 -0
  45. package/workflows/standard.js +118 -633
  46. package/workflows/upgrade.js +4 -2
@@ -70,8 +70,10 @@ const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
70
70
  const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
71
71
  const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
72
72
  const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
73
+ const NOTE_VOCAB_SRC = crewHome + "/current/lib/publish-note-vocabulary.js";
74
+ const NOTE_VOCAB = RUN_LIB + "/publish-note-vocabulary.js";
73
75
  // See docs/decisions/qa-reproduce.md#pin-basenames: the pin step materializes the required scripts.
74
- 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(); });
76
+ const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE, NOTE_VOCAB].map(function (p) { return p.split("/").pop(); });
75
77
 
76
78
  // Project config — passed by dispatcher, falls back to dashboard defaults
77
79
  const projectConfig = inputs.project_config || {};
@@ -191,19 +193,12 @@ async function reaskVerdict(stepName, reworkSuffix, workerText) {
191
193
  return verdict;
192
194
  }
193
195
  // Transport retry: the work-agent agent() call can throw even when the agent
194
- // did the work. Stochastic envelope non-compliance (bare prose instead of
195
- // the native {"status":"ok","result":"..."} envelope) trips the runtime's
196
- // JSON-candidate heuristic when the prose contains a {...}-looking
197
- // substring — canary 39457ee9's QA report quoted the change's own
198
- // {/* ... */} JSX comment, the runtime tried to parse it as JSON, threw,
199
- // and the workflow discarded a complete VERDICT: PASS report as "no output".
200
- // The verdict re-ask covers an unreadable verdict inside a RECEIVED report;
201
- // this covers the report never arriving. The assignment is retried boundedly
202
- // with fresh keys (never a cached replay) before failing closed. Re-entry is
203
- // safe: lifecycle scripts answer REUSED for existing worktrees/branches, the
204
- // retry trailer tells the agent to check existing state first and report
205
- // rather than duplicate completed side effects, and the rework path already
206
- // re-runs Build after rejection — Build re-entry is an established pattern.
196
+ // did the work — the runtime's JSON-candidate heuristic trips on brace-shaped
197
+ // prose. Retry the assignment boundedly with fresh keys (never a cached
198
+ // replay) before failing closed. Re-entry is safe: lifecycle scripts answer
199
+ // REUSED for existing worktrees/branches, the retry trailer tells the agent
200
+ // to check existing state first, and Build re-entry after rejection is an
201
+ // established pattern. Full history: docs/decisions/workflow-core.md.
207
202
  function workRetryKey(stepName, reworkSuffix, attempt) {
208
203
  return "work-" + stepName + reworkSuffix + "-t" + attempt;
209
204
  }
@@ -215,7 +210,7 @@ function attemptKey(base, reworkCount) {
215
210
  function pinLifecycle(key) {
216
211
  return agent(
217
212
  "Snapshot lifecycle scripts for version pinning.\n" +
218
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
213
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && cp " + NOTE_VOCAB_SRC + " " + NOTE_VOCAB + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
219
214
  "Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
220
215
  { key: key, label: "Pinning lifecycle scripts",
221
216
  schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
@@ -396,30 +391,11 @@ function hydrateReleaseDecision(rec) {
396
391
  return null;
397
392
  }
398
393
 
399
- // Publish read-back request (currently unavailable): the verbatim_request
400
- // the parent protocol (docs/publish-verification.md) would hand to an
401
- // independent read-back tool after the artifact build lands. artifact_inspect
402
- // was removed by the platform (2026-09-14); artifact.inspect is malfunction
403
- // diagnosis, not a substitute — so no agent-callable read-back tool exists
404
- // and this request cannot currently be issued. Pure function — no I/O, no
405
- // clock. The request carries the merged diff as the expected change and asks
406
- // for an independent read of the artifact's actual source: for each file, the
407
- // exact current text of the changed regions plus a per-line present/absent
408
- // finding. Until a read-back path exists, the parent cannot independently
409
- // confirm content and verification parks at "publish: verification-requested"
410
- // (see docs/publish-verification.md). This preserves the circularity break
411
- // that hollowed canary run 8 (2026-09-11): the old verifyAppliedChanges
412
- // compared the builder's applied-report against the diff the report was
413
- // derived from — a fabricated report passed by construction. The report
414
- // itself is gone now (2026-09-16 fire-and-forget trigger). Independent
415
- // read-back cannot be
416
- // fabricated from the diff; it must match the artifact's real content.
417
-
418
394
  // See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
419
- // keyTag (optional): same-outcome ledger writes in one pass need distinct
420
- // agent-call keys — the runtime replays cached calls by key, so a shared key
421
- // silently drops the second write (2026-09-19, submitted-on-issuance).
422
- async function recordPublishLedger(entry, rework, keyTag) {
395
+ // (2026-09-20, one-party worker-owned publish) No keyTag: every ledger
396
+ // write in a Publish pass is followed by a return, so no pass can write two
397
+ // same-outcome entries — the runtime replay key can never collide in-pass.
398
+ async function recordPublishLedger(entry, rework) {
423
399
  try {
424
400
  var ledgerDir = crewHome + "/.publish-ledger";
425
401
  var line = JSON.stringify({
@@ -433,7 +409,19 @@ async function recordPublishLedger(entry, rework, keyTag) {
433
409
  applied_report: entry.applied_report || null,
434
410
  manifest_before: entry.manifest_before || null,
435
411
  outcome: entry.outcome,
436
- detail: entry.detail || ""
412
+ detail: entry.detail || "",
413
+ // D1 (2026-09-19): issued_at = upper bound on the trigger-issuance
414
+ // instant (null when no trigger); ts = ledger-write instant.
415
+ issued_at: entry.issued_at || null,
416
+ // (2026-09-20, one-party worker-owned publish) issuer = which party
417
+ // wrote the entry ("workflow" for the intent entry, "tick-worker" for
418
+ // the worker's own issuance); diff_path/diff_sha256 locate the
419
+ // checksummed diff the worker issues. The workflow never writes an
420
+ // issuance ("submitted") entry — it did not issue.
421
+ issuer: entry.issuer || null,
422
+ diff_path: entry.diff_path || null,
423
+ diff_sha256: entry.diff_sha256 || null,
424
+ base: entry.base || null,
437
425
  });
438
426
  var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
439
427
  var res = await agent(
@@ -441,7 +429,7 @@ async function recordPublishLedger(entry, rework, keyTag) {
441
429
  "Run: mkdir -p " + sq(ledgerDir) + " && printf '%s\n' " + sq(line) +
442
430
  " | sed \"s/@LEDGER_TS@/$(date -u +%Y-%m-%dT%H:%M:%SZ)/\" >> " + sq(ledgerDir + "/" + PUBLISH_SLUG + ".jsonl") + " && echo LEDGER_OK\n" +
443
431
  "Return JSON { \"result\": \"<verbatim output>\" } and nothing else.",
444
- { key: attemptKey("publish-ledger-" + taskId + "-" + entry.outcome + (keyTag ? "-" + keyTag : ""), rework),
432
+ { key: attemptKey("publish-ledger-" + taskId + "-" + entry.outcome, rework),
445
433
  label: "Recording publish attempt in ledger",
446
434
  schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
447
435
  );
@@ -473,11 +461,6 @@ function extractAlreadyMerged(workerText) {
473
461
  return m ? { sha: m[1].toLowerCase() } : { sha: null };
474
462
  }
475
463
 
476
- // See docs/decisions/publish-path.md#refusal-signal: the refusal signal must be the ENTIRE trimmed turn output.
477
- function extractRefusal(workerText) {
478
- var m = /^ARTIFACT_EDIT_REFUSED:\s*(.+?)\s*$/.exec(String(workerText || "").trim());
479
- return m ? m[1].slice(0, 300) : null;
480
- }
481
464
 
482
465
  // See docs/decisions/qa-reproduce.md#worktree-confinement: the Build agent must declare its worktree.
483
466
  function extractWorktree(workerText) {
@@ -1340,10 +1323,41 @@ while (i < STEPS.length) {
1340
1323
  "VERDICT: PASS\n\n";
1341
1324
  } else if (PUBLISH_TYPE === "artifact") {
1342
1325
  // See docs/decisions/publish-path.md#deterministic-artifact-publish: the work agent never publishes; the parent runs the deterministic publish script.
1343
- var artifactPublish = null;
1326
+ // Blocker 22 (one-party publish, room #24): verified re-entry guard.
1327
+ // The workflow parks at publish intent; the tick worker issues
1328
+ // artifact_edit, verifies by read-back, stamps provenance, and
1329
+ // re-queues. The dispatcher resumes this task at the Publish step
1330
+ // (the parked session's own step — the failed→retry path). If the
1331
+ // stamped provenance names THIS task and its source_commit is the
1332
+ // repo's current HEAD, the merge commit was already published and
1333
+ // verified by the parent: issuance is skipped entirely (the
1334
+ // empty-diff park below must NOT fire on this branch) and the
1335
+ // Publish work agent runs with the verified framing instead. A guard
1336
+ // failure never claims verification — it proceeds down the normal path.
1337
+ var publishAlreadyVerified = false;
1338
+ try {
1339
+ var reentryCheck = await agent(
1340
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
1341
+ "Then run in shell and return the stdout verbatim: cd " + REPO_PATH + " && git rev-parse HEAD\n" +
1342
+ "Return JSON { \"source_commit\": \"<provenance.source_commit, empty string when null>\", \"stamp_task_id\": \"<provenance.task_id, empty string when null>\", \"head\": \"<the rev-parse output, trimmed>\" } and nothing else.",
1343
+ { key: attemptKey("publish-reentry-guard-" + taskId, totalReworkCount), label: "Checking for a verified publish re-entry",
1344
+ schema: { type: "object", properties: { source_commit: { type: "string" }, stamp_task_id: { type: "string" }, head: { type: "string" } }, required: ["source_commit", "stamp_task_id", "head"] } }
1345
+ );
1346
+ var reentrySC = String((reentryCheck && reentryCheck.source_commit) || "").trim();
1347
+ var reentryTID = String((reentryCheck && reentryCheck.stamp_task_id) || "").trim();
1348
+ var reentryHEAD = String((reentryCheck && reentryCheck.head) || "").trim();
1349
+ if (reentrySC && reentryHEAD && reentrySC === reentryHEAD && reentryTID === taskId) {
1350
+ publishAlreadyVerified = true;
1351
+ log("Publish verified re-entry for task " + taskId + ": provenance stamps " + reentrySC + " == HEAD for this task — the parent already published and verified it; issuance skipped");
1352
+ }
1353
+ } catch (reentryErr) {
1354
+ log("Publish re-entry guard threw for task " + taskId + " (" + (reentryErr && reentryErr.message ? reentryErr.message : reentryErr) + ") — proceeding with the normal publish path; a guard failure never claims verification");
1355
+ }
1356
+ // publishAlreadyVerified is consumed by the intent-tail edit (next
1357
+ // chunk): on the verified branch the workflow skips to the Publish
1358
+ // work agent with the verified framing and proceeds to QA.
1344
1359
  var publishLockRefreshed = false;
1345
1360
  var publishSkippedNoLock = false;
1346
- var publishBuildLanded = false; // true once the artifact build poll completes: content exists to verify; the provenance stamp is deferred to the parent (docs/publish-verification.md)
1347
1361
  try {
1348
1362
  // STEP 0 (mechanical): read the merge-lock state explicitly — never
1349
1363
  // infer it from prose. An empty-diff Integrate (MERGED_EMPTY)
@@ -1367,7 +1381,11 @@ while (i < STEPS.length) {
1367
1381
  publishLockRefreshed = true;
1368
1382
  }
1369
1383
  // See docs/decisions/publish-path.md#step1-builder-source: the builder's source tree is NOT the crew's repo; verify report before stamping.
1370
- if (!publishSkippedNoLock) {
1384
+ // (2026-09-20, one-party worker-owned publish) A verified re-entry
1385
+ // skips the whole issuance tail: the edit already went out, was
1386
+ // verified by the parent read-back, and was stamped. The empty-diff
1387
+ // park must not fire on this branch.
1388
+ if (!publishSkippedNoLock && !publishAlreadyVerified) {
1371
1389
  // The trigger key of the attempt that last ran, for the publish ledger.
1372
1390
  // Minted once here (not re-minted per use site) so the ledger always
1373
1391
  // records the exact key that was issued — and so a re-minted duplicate
@@ -1511,39 +1529,12 @@ while (i < STEPS.length) {
1511
1529
  if (!diffSummary.file_count) {
1512
1530
  return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
1513
1531
  }
1514
- // The trigger agent reads the diff from the checksummed file — the
1532
+ // The tick worker reads the diff from the checksummed file — the
1515
1533
  // workflow never holds diff bytes (room #14, 2026-09-17). The file
1516
- // is small by construction: the 200-line budget above gates the
1517
- // trigger. The sha256 check is the agent's only verification step;
1518
- // a mismatch stops the trigger before any artifact_edit call.
1519
- var rebuildPrompt =
1520
- ARTIFACT_LOAD_PREAMBLE +
1521
- "The change to apply is the unified diff in the file \"" + publishDiffFile + "\" (sha256 " + publishDiffSha256 + ").\n" +
1522
- "1. Verify the file: run sha256sum on it. If the printed hash is not exactly " + publishDiffSha256 + ", STOP and end your turn — do not call artifact_edit.\n" +
1523
- "2. Read the file's full content.\n" +
1524
- "3. Call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
1525
- "'Apply the following change to your source tree, then rebuild and deploy.\n" +
1526
- "\n" +
1527
- "UNIFIED DIFF (relative to your source tree):\n" +
1528
- "```diff\n<the full content of the verified file, pasted verbatim>\n```\n" +
1529
- "\n" +
1530
- "Rules:\n" +
1531
- "- For each file in the diff, apply its hunks to the same path in your source tree (use git apply or equivalent).\n" +
1532
- "- For a new file (--- /dev/null), create it with the added (+) lines as its full content.\n" +
1533
- "- For a deleted file (+++ /dev/null), delete it.\n" +
1534
- "- If any hunk does not apply cleanly, STOP and report the failure — do not improvise or skip hunks.\n" +
1535
- "- Do not make any other source changes.\n" +
1536
- "- After applying, rebuild and deploy.'\n" +
1537
- "Edit-request contract (read carefully):\n" +
1538
- "- Call artifact_edit exactly once with the slug and verbatim_request above. Never retry the edit yourself: if the edit is not accepted, do NOT call artifact_edit again — end your turn.\n" +
1539
- "- If artifact_edit explicitly refuses the edit (the call is rejected — e.g. the artifact does not exist), do NOT call artifact_edit again: end your turn with exactly one line and nothing else: ARTIFACT_EDIT_REFUSED: <the refusal text, one line>.\n" +
1540
- "- If artifact_edit is not available after the load, do NOT improvise — end your turn.\n" +
1541
- "- You do NOT call setprovenance, artifact_inspect, or post-deploy yourself.\n" +
1542
- "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";
1543
- // See docs/decisions/publish-path.md#agent-id-attribution: the artifact build's agent_id is attributed to the edit call.
1544
- var rebuildAgentId = null;
1545
- // See docs/decisions/publish-path.md#applied-report-gone: the builder's applied report is gone; the workflow verifies differently.
1546
- var publishAppliedObservation = "missing-report";
1534
+ // is small by construction: the 200-line budget above gates it. The
1535
+ // worker verifies the sha256 from the intent entry before issuing;
1536
+ // a mismatch stops issuance. (2026-09-20, one-party worker-owned
1537
+ // publish: the workflow no longer issues the edit itself.)
1547
1538
  // See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
1548
1539
  var auditDirsBeforeTrigger = [];
1549
1540
  var auditBeforeOk = false;
@@ -1642,535 +1633,33 @@ while (i < STEPS.length) {
1642
1633
  baselineFailed = true;
1643
1634
  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");
1644
1635
  }
1645
- // See docs/decisions/publish-path.md#trigger-await: the artifact_edit call is awaited.
1646
- var rebuildTrigger = null, triggerThrew = false;
1647
- try {
1648
- var triggerText = String(await agent(rebuildPrompt,
1649
- { key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
1650
- log("Publish rebuild trigger for task " + taskId + " returned (" + triggerText.length + " chars; awaited; scanned only for the explicit refusal signal)");
1651
- // See docs/decisions/publish-path.md#explicit-refusal-16: the child must explicitly refuse artifact work.
1652
- var refusalText = extractRefusal(triggerText);
1653
- if (refusalText) {
1654
- await recordPublishLedger({
1655
- commit: mergeCommitForPublish,
1656
- attempt: rebuildAttemptKey,
1657
- agent_id: null,
1658
- applied_report: null,
1659
- outcome: "rejected",
1660
- detail: "artifact_edit explicitly refused the edit (parsed ARTIFACT_EDIT_REFUSED signal): " + refusalText + " — conclusive negative: the edit provably did not go through, no observation polling, no blind retry"
1661
- }, totalReworkCount);
1662
- return await parkTask("Publish cannot proceed for task " + taskId + ": artifact_edit explicitly refused the edit (" + refusalText + "). This is conclusive (rejected, not unknown): the edit did not go through. Repair or provision the artifact target, then re-run Publish. Human attention needed.");
1663
- }
1664
- } catch (triggerErr) {
1665
- log("Publish rebuild trigger for task " + taskId + " threw (" + (triggerErr && triggerErr.message ? triggerErr.message : triggerErr) + ") — outcome unknown until observation confirms it; the edit may have gone through");
1666
- triggerThrew = true;
1667
- }
1668
- // (2026-09-19, room #22) Submitted-on-issuance: see docs/decisions/publish-path.md#submitted-on-issuance.
1636
+ // (2026-09-20, one-party worker-owned publish) The workflow exits
1637
+ // Publish at intent. Everything above — preflight, provenance base,
1638
+ // checksummed diff, toolcheck, pre-trigger baselines — is read-only.
1639
+ // Issuance belongs to the session-carrying tick worker
1640
+ // (scan-publish-unknown's intent bucket), the only caller class that
1641
+ // can reach artifact_edit. This entry is issuer-written BY THE
1642
+ // WORKFLOW and names the intent: outcome "publish-intent", never
1643
+ // "submitted" — the workflow issued nothing. The worker claims it
1644
+ // via the park note below.
1669
1645
  await recordPublishLedger({
1670
1646
  commit: mergeCommitForPublish,
1671
1647
  attempt: rebuildAttemptKey,
1672
1648
  agent_id: null,
1673
- applied_report: publishAppliedObservation,
1649
+ applied_report: null,
1674
1650
  manifest_before: preTriggerManifest,
1675
- outcome: "submitted",
1676
- detail: "trigger issuance: rebuild-trigger agent invoked; receipt/observation pending. Invocation only - not evidence of artifact_edit or build acceptance." + (triggerThrew ? "; trigger-threw" : "; trigger-returned")
1677
- }, totalReworkCount, "issuance");
1678
- // Post-trigger observation (primary, not fallback): the workflow
1679
- // attributes the edit itself. First the in-flight build state — a
1680
- // build whose agent_id is new relative to the pre-trigger baseline
1681
- // is this edit's receipt. Then the durable audit-dir diff — a
1682
- // timestamped directory appearing during the trigger window proves
1683
- // the edit went through and the build completed even when no
1684
- // in-flight build was ever observed (the 2026-09-14 attempt-7 gap).
1685
- // Absence of both signals proves nothing: the outcome is UNKNOWN,
1686
- // never "did not go through". No blind retry — record the attempt
1687
- // and park fail-closed; correlate via the ledger, never by
1688
- // re-issuing.
1689
- log("Publish rebuild trigger issued for task " + taskId + " — observing build state to attribute the edit");
1690
- var buildState = null;
1691
- var buildStateFailed = false;
1692
- try {
1693
- buildState = await agent(
1694
- ARTIFACT_LOAD_PREAMBLE +
1695
- "Call artifact_status with slug \"" + PUBLISH_SLUG + "\".\n" +
1696
- "Poll up to 3 times, about 20 seconds apart, until the response shows a build (the \"build\" value is an object, not null). " +
1697
- "Return the raw \"build\" value verbatim as JSON — the build object exactly as returned, with its agent_id, operation, status, and any other fields untouched. " +
1698
- "Do not summarize, interpret, or derive booleans from it. " +
1699
- "If no build appears after 3 polls, return null. " +
1700
- "Return JSON { \"build\": <the raw build object or null> } and nothing else.",
1701
- { key: attemptKey("publish-artifact-buildcheck-" + taskId, totalReworkCount), label: "Reading artifact build state after trigger",
1702
- schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
1703
- );
1704
- } catch (buildCheckErr) {
1705
- buildStateFailed = true;
1706
- log("Publish post-trigger build-state check failed for task " + taskId + " (" + (buildCheckErr && buildCheckErr.message ? buildCheckErr.message : buildCheckErr) + ") — this signal is unknown, not negative");
1707
- }
1708
- var observedAgentId = (buildState && buildState.build && typeof buildState.build.agent_id === "string" && buildState.build.agent_id) || null;
1709
- // See docs/decisions/publish-path.md#attribution-limitation: attribution is timing-based; content verification bounds the risk.
1710
- var receiptAgentId = (!buildStateFailed && !baselineFailed && observedAgentId && observedAgentId !== baselineAgentId) ? observedAgentId : null;
1711
- // auditReportOk: pure tri-state read of a report.json body —
1712
- // true (build ok), false (build failed), null (missing or
1713
- // unreadable — not evidence either way). The child returns the
1714
- // raw body verbatim; interpretation lives here, never in prose.
1715
- // Hoisted to Publish-step scope (before the receipt branch) so both
1716
- // the immediate and post-poll audit fallbacks share it on every path —
1717
- // the receipt path skips the else below, which must not leave these
1718
- // undefined.
1719
- var auditReportOk = function (raw) {
1720
- if (typeof raw !== "string") return null;
1721
- var trimmed = raw.trim();
1722
- if (trimmed === "" || trimmed === "MISSING") return null;
1723
- var parsed;
1724
- try { parsed = JSON.parse(trimmed); } catch (e) { return null; }
1725
- if (parsed && typeof parsed.ok === "boolean") return parsed.ok;
1726
- return null;
1727
- };
1728
- // (2026-09-18, H2 verdict-first) Publish-verdict vocabulary. The
1729
- // verdict is one of "landed" | "unknown". Decided once,
1730
- // before the receipt poll, and dispatched on — never re-derived.
1731
- // Pure and self-contained: unit-tested by
1732
- // tests/publish-verdict-first.test.js.
1733
- // See docs/decisions/publish-path.md#h2-verdict-dispatch.
1734
- var decidePublishVerdict = function (opts) {
1735
- var immediateReport = (opts && "immediateReport" in opts) ? opts.immediateReport : null;
1736
- var newDirCount = (opts && typeof opts.newDirCount === "number") ? opts.newDirCount : 0;
1737
- if (newDirCount === 1 && immediateReport === true) return { verdict: "landed", unattributableReason: null };
1738
- if (newDirCount === 1 && immediateReport === false) return { verdict: "unknown", unattributableReason: "audit-report-ok-false" };
1739
- if (newDirCount === 1) return { verdict: "unknown", unattributableReason: "audit-report-unreadable-or-missing" };
1740
- if (newDirCount === 0) return { verdict: "unknown", unattributableReason: "no-new-audit-dir-in-window" };
1741
- return { verdict: "unknown", unattributableReason: "audit-dir-ambiguity", ambiguousDirCount: newDirCount };
1742
- };
1743
- // (2026-09-18, H2 message contract, design §1.5) The human line is
1744
- // the output of a mechanical field→template mapping: the trigger
1745
- // commit short-sha, one plain clause per unattributable reason, the
1746
- // no-republish warning, the ledger path with (outcome: unknown), and
1747
- // the unknown-recovery clause. The appendix carries every machine
1748
- // field. Pure and self-contained: unit-tested by
1749
- // tests/publish-verdict-first.test.js.
1750
- var composeUnattributedParkReason = function (fields) {
1751
- var f = fields || {};
1752
- var task = f.taskId || taskId;
1753
- var reason = f.unattributableReason || "unknown-outcome";
1754
- var shortSha = String(f.commitShortSha || "").substring(0, 7) || "unknown";
1755
- var reasonClauses = {
1756
- "no-new-audit-dir-in-window": "no build could be tied to this attempt",
1757
- "audit-report-unreadable-or-missing": "the build's report is unreadable",
1758
- "audit-dir-ambiguity": "more than one build appeared in the check window",
1759
- "stranger-build-observed-during-poll": "a different build was running during the check",
1760
- "status-read-errors-during-poll": "status reads kept failing"
1761
- };
1762
- var clause = reasonClauses[reason] || "no build could be tied to this attempt";
1763
- return "Publish outcome unknown for task " + task +
1764
- ": the trigger was sent for commit " + shortSha + " but the outcome could not be confirmed — " + clause + ". " +
1765
- "Do NOT republish: if the trigger was accepted, a retry duplicates the build (2026-09-12). " +
1766
- "The attempt is in the ledger at " + crewHome + "/.publish-ledger/" + PUBLISH_SLUG + ".jsonl (outcome: unknown) " +
1767
- "and the crew's unknown-recovery will re-examine it. " +
1768
- "Appendix: unattributable_reason=" + reason +
1769
- "; poll_end_state=" + (f.pollEndState || "not-polled") +
1770
- "; saw_our_build=" + (f.sawOurBuild ? "true" : "false") +
1771
- "; new_audit_dirs=" + (f.newAuditDirCount == null ? 0 : f.newAuditDirCount) +
1772
- "; poll_chunks_failed=" + (f.chunkFailures == null ? 0 : f.chunkFailures) +
1773
- "; poll_status_errors=" + (f.pollStatusErrors == null ? 0 : f.pollStatusErrors) + ".";
1774
- };
1775
- // publishFailure is declared here (per-Publish-pass scope) so the
1776
- // post-poll explicit build failure survives to the final routing
1777
- // below; publishUnknownFields carries the structured unknown fields
1778
- // for the single composed fail-closed park. Both re-initialize on
1779
- // every pass — a rework re-entry never leaks a stale verdict.
1780
- var publishFailure = null;
1781
- var publishUnknownFields = null;
1782
- if (receiptAgentId) {
1783
- // The edit went through — a build with a new agent_id appeared
1784
- // after the trigger. The parent's independent read-back
1785
- // (docs/publish-verification.md) is the verification, not any
1786
- // builder report.
1787
- rebuildTrigger = { edit_started: true };
1788
- rebuildAgentId = receiptAgentId;
1789
- log("Publish rebuild trigger for task " + taskId + ": artifact_status shows build " + receiptAgentId + " for slug " + PUBLISH_SLUG + " (new relative to the pre-trigger baseline) — the edit went through.");
1790
- await recordPublishLedger({
1791
- commit: mergeCommitForPublish,
1792
- attempt: rebuildAttemptKey,
1793
- agent_id: rebuildAgentId,
1794
- applied_report: publishAppliedObservation,
1795
- manifest_before: preTriggerManifest,
1796
- outcome: "submitted",
1797
- detail: "fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff); observation, not re-issuance"
1798
- }, totalReworkCount, "receipt");
1799
- } else {
1800
- var newAuditDirs = [];
1801
- try {
1802
- var auditAfter = await agent(
1803
- "List the artifact audit directories for slug \"" + PUBLISH_SLUG + "\" (best-effort, never a gate).\n" +
1804
- "Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null\n" +
1805
- "Return JSON { \"dirs\": \"<newline-separated names, empty string when the audits directory does not exist or is empty>\" } and nothing else.",
1806
- { key: attemptKey("publish-audit-after-" + taskId, totalReworkCount), label: "Re-listing audit dirs after trigger",
1807
- schema: { type: "object", properties: { dirs: { type: "string" } }, required: ["dirs"] } }
1808
- );
1809
- var auditDirsAfterTrigger = String((auditAfter && auditAfter.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
1810
- // Only timestamped build dirs count — the "latest" symlink
1811
- // and anything else are not builds. Gated on auditBeforeOk:
1812
- // without a baseline every historical dir would look new.
1813
- newAuditDirs = auditBeforeOk ? auditDirsAfterTrigger.filter(function (d) {
1814
- return auditDirsBeforeTrigger.indexOf(d) === -1 && /^20\d\d-\d\d-\d\dT\d\d-\d\d-\d\dZ-/.test(d);
1815
- }) : [];
1816
- } catch (auditAfterErr) {
1817
- log("Publish audit-dir re-list after trigger failed for task " + taskId + " (non-fatal, durable-evidence check degraded): " + (auditAfterErr && auditAfterErr.message ? auditAfterErr.message : auditAfterErr));
1818
- }
1819
- if (newAuditDirs.length > 0) {
1820
- rebuildAgentId = null;
1821
- newAuditDirs.sort();
1822
- var newestImmediateDir = newAuditDirs[newAuditDirs.length - 1];
1823
- 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.");
1824
- // (2026-09-18, H2 verdict-first) Durable audit evidence exists,
1825
- // but there is no receipt agent_id to chain the completion poll
1826
- // to — polling with a null receipt can only observe strangers
1827
- // (any running build differs from "null") or nothing, burning
1828
- // 10.5 minutes to park unknown. Read the build report now instead
1829
- // of polling, then decide the verdict ONCE via
1830
- // decidePublishVerdict: exactly one new dir with ok=true lands
1831
- // (attribution by window, not identity — never poll blind);
1832
- // ok=false is UNKNOWN with the failure evidence preserved in the
1833
- // ledger detail (the evidence is explicit, the attribution is
1834
- // not); unreadable / zero / ambiguous dirs are UNKNOWN.
1835
- // Verdict-first: landed bypasses the poll below, unknown falls
1836
- // through to post-deploy. STEP 2 runs on every path.
1837
- // See docs/decisions/publish-path.md#h2-verdict-dispatch.
1838
- var immediateReportOk = null;
1839
- // (N1) Read the report only when exactly one new dir exists:
1840
- // ambiguity (>1) forces UNKNOWN regardless — don't shell out to
1841
- // read a report that will be discarded.
1842
- if (newAuditDirs.length === 1) {
1843
- try {
1844
- var immediateOkRead = await agent(
1845
- "Read the artifact build report for slug \"" + PUBLISH_SLUG + "\".\n" +
1846
- "Run: cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestImmediateDir + "/report.json 2>/dev/null || echo MISSING\n" +
1847
- "Return JSON { \"raw\": \"<verbatim file contents, or the literal string MISSING when the file does not exist>\" } and nothing else.",
1848
- { key: attemptKey("publish-audit-ok-immediate-" + taskId, totalReworkCount), label: "Reading build report for audit-confirmed build",
1849
- schema: { type: "object", properties: { raw: { type: "string" } }, required: ["raw"] } }
1850
- );
1851
- immediateReportOk = auditReportOk(immediateOkRead && immediateOkRead.raw);
1852
- } catch (immediateOkErr) {
1853
- log("Publish build-report read for audit-confirmed dir failed for task " + taskId + " (treated as unknown): " + (immediateOkErr && immediateOkErr.message ? immediateOkErr.message : immediateOkErr));
1854
- immediateReportOk = null;
1855
- }
1856
- }
1857
- var immediateVerdict = decidePublishVerdict({ editStarted: true, immediateReport: immediateReportOk, newDirCount: newAuditDirs.length });
1858
- if (immediateVerdict.verdict === "landed") {
1859
- publishBuildLanded = true;
1860
- artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
1861
- await recordPublishLedger({
1862
- commit: mergeCommitForPublish,
1863
- attempt: rebuildAttemptKey,
1864
- agent_id: null,
1865
- applied_report: publishAppliedObservation,
1866
- manifest_before: preTriggerManifest,
1867
- outcome: "build-observed",
1868
- 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)"
1869
- }, totalReworkCount);
1870
- 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.");
1871
- } else {
1872
- // Verdict UNKNOWN on the immediate path. ok=false is explicit
1873
- // failure evidence but not an attributable failure — the ledger
1874
- // records unknown with the evidence preserved in the detail,
1875
- // and the flow continues to post-deploy (never parks early).
1876
- publishUnknownFields = {
1877
- taskId: taskId,
1878
- unattributableReason: immediateVerdict.unattributableReason,
1879
- pollEndState: "not-polled",
1880
- sawOurBuild: false,
1881
- newAuditDirCount: newAuditDirs.length,
1882
- chunkFailures: 0,
1883
- pollStatusErrors: 0
1884
- };
1885
- var immediateDetail = "durable audit-dir fallback could not prove a completed build for this attempt (unattributable_reason=" + immediateVerdict.unattributableReason + ", no receipt agent_id)";
1886
- if (immediateReportOk === false) {
1887
- immediateDetail += "; explicit failure evidence preserved: report ok=false for audit dir " + newestImmediateDir;
1888
- }
1889
- if (immediateVerdict.unattributableReason === "audit-dir-ambiguity") {
1890
- immediateDetail += "; audit-dir ambiguity: " + newAuditDirs.length + " new dirs in window";
1891
- }
1892
- await recordPublishLedger({
1893
- commit: mergeCommitForPublish,
1894
- attempt: rebuildAttemptKey,
1895
- agent_id: null,
1896
- applied_report: publishAppliedObservation,
1897
- outcome: "unknown",
1898
- detail: immediateDetail
1899
- }, totalReworkCount);
1900
- log("Publish verdict UNKNOWN for task " + taskId + ": " + immediateDetail + " — continuing to post-deploy; never polling blind and never parking early.");
1901
- }
1902
- } else {
1903
- // See docs/decisions/publish-path.md#no-attributable-build: no attributable build and no durable evidence means no publish.
1904
- publishUnknownFields = {
1905
- taskId: taskId,
1906
- unattributableReason: decidePublishVerdict({ editStarted: true, immediateReport: null, newDirCount: 0 }).unattributableReason,
1907
- pollEndState: "not-polled",
1908
- sawOurBuild: false,
1909
- newAuditDirCount: 0,
1910
- chunkFailures: 0,
1911
- pollStatusErrors: 0
1912
- };
1913
- await recordPublishLedger({
1914
- commit: mergeCommitForPublish,
1915
- attempt: rebuildAttemptKey,
1916
- agent_id: null,
1917
- applied_report: publishAppliedObservation,
1918
- outcome: "unknown",
1919
- 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"
1920
- }, totalReworkCount);
1921
- 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.");
1922
- }
1923
- }
1924
- // Durable publish-attempt ledger: record the trigger outcome while the
1925
- // attempt key and commit are in scope. Every attempt lands here with
1926
- // its outcome — submitted, rejected, or unknown (unknown is recorded
1927
- // at the park site above). A later run or human matches commit hash +
1928
- // attempt key against the builder's eventual completion.
1929
- // (2026-09-18, H2 verdict-first) The verdict was decided exactly
1930
- // once above; dispatch on it. landed bypasses the receipt poll
1931
- // (the audit evidence already proved completion); an explicit
1932
- // publishFailure is preserved verbatim through post-deploy.
1933
- // Otherwise the verdict is open — but the poll below is only
1934
- // legitimate against a real receipt: the null-safe assertion records
1935
- // UNKNOWN and continues to STEP 2 instead of polling blind.
1936
- // See docs/decisions/publish-path.md#h2-verdict-dispatch.
1937
- if (publishBuildLanded) {
1938
- log("Publish verdict already LANDED for task " + taskId + " — bypassing receipt poll.");
1939
- } else if (publishFailure) {
1940
- // design §1.2 pre-poll branch — currently unassigned; kept for the converged dispatch shape.
1941
- log("Publish verdict already FAILED for task " + taskId + " — preserved verbatim through post-deploy.");
1942
- } else if (!rebuildTrigger || !rebuildTrigger.edit_started) {
1943
- // Loud defensive assertion (replaces the old lying "Unreachable"
1944
- // else): with no receipt state the poll would observe strangers or
1945
- // nothing — record UNKNOWN and continue to STEP 2. Never park
1946
- // early here; never poll blind.
1947
- if (!publishUnknownFields) {
1948
- publishUnknownFields = {
1949
- taskId: taskId,
1950
- unattributableReason: "no-receipt-state",
1951
- pollEndState: "not-polled",
1952
- sawOurBuild: false,
1953
- newAuditDirCount: 0,
1954
- chunkFailures: 0,
1955
- pollStatusErrors: 0
1956
- };
1957
- await recordPublishLedger({
1958
- commit: mergeCommitForPublish,
1959
- attempt: rebuildAttemptKey,
1960
- agent_id: null,
1961
- applied_report: publishAppliedObservation,
1962
- outcome: "unknown",
1963
- detail: "no receipt state was recorded for this attempt — never polling blind; continuing to post-deploy"
1964
- }, totalReworkCount);
1965
- }
1966
- log("Publish verdict UNKNOWN for task " + taskId + ": no receipt state recorded — never polling blind, continuing to post-deploy.");
1967
- } else {
1968
- // (2026-09-16) There is no builder report: the fire-and-forget
1969
- // trigger carries no JSON contract, so there is nothing to
1970
- // compare and no pre-hash diagnostic. The builder's old
1971
- // self-report was circular by construction (canary run 8) with a
1972
- // demonstrated false-negative mode (task 23ca8f3f, 2026-09-12:
1973
- // applied:[] for a diff the builder had applied). The flow
1974
- // proceeds to the build poll regardless; real verification is the
1975
- // parent's independent read-back (docs/publish-verification.md)
1976
- // before the provenance stamp.
1977
- // STEP 1b (mechanical): bounded poll for build completion, chunked so
1978
- // the merge-lock lease is refreshed before it can expire. The 600s
1979
- // lease is shorter than the worst-case 10-minute build poll, so the
1980
- // poll runs in three chunks (7 checks x 30s ~= 3.5 min each) with a
1981
- // holder-only lease refresh between chunks. If a refresh fails, the
1982
- // lock was lost: stop the run and park the task — never continue to
1983
- // a provenance stamp or version assignment without holding the lock.
1984
- var buildPoll = null;
1985
- // STEP 1b poll-signal accumulators (2026-09-15, task aadeccc3):
1986
- // the durable audit-dir fallback below needs the poll's own
1987
- // observations, not just its final verdict — whether our build was
1988
- // ever seen, whether a stranger's build was ever in flight, and
1989
- // what the last check observed. OR-ed across all three chunks so
1990
- // a signal seen in any chunk survives the chunk boundary.
1991
- var pollSawOurBuild = false;
1992
- var pollSawStranger = false;
1993
- var lastObservedAgentId = null;
1994
- // Poll-chunk failure record (2026-09-16, task 1febe8eb): chunk
1995
- // agent calls that threw instead of returning a verdict —
1996
- // recorded for the post-poll diagnosis, never terminal alone.
1997
- var chunkFailures = [];
1998
- // Poll status-read error count (2026-09-16): a failed
1999
- // artifact_status read (rate limiting / TOO_MANY_REQUESTS /
2000
- // 429, network error, or any non-build response) is
2001
- // inconclusive — it is NOT evidence of absence and must never
2002
- // be folded into the no-build-observed signal. Counted for
2003
- // the post-poll diagnosis only; never terminal alone.
2004
- var pollStatusErrors = 0;
2005
- for (var chunk = 1; chunk <= 3; chunk++) {
2006
- if (chunk > 1) {
2007
- var refreshPoll = await agent(
2008
- "Refresh the merge lock for task " + taskId + ".\n" +
2009
- "Run: " + LIFECYCLE_ENV + " refresh-lock " + taskId + "\n" +
2010
- "If the output contains REFRESHED, return JSON { \"held\": true } and nothing else. Otherwise return JSON { \"held\": false, \"output\": \"<verbatim output>\" } and nothing else.",
2011
- { key: attemptKey("publish-lock-refresh-poll-" + taskId + "-c" + chunk, totalReworkCount), label: "Refreshing merge lock during build poll",
2012
- schema: { type: "object", properties: { held: { type: "boolean" }, output: { type: "string" } }, required: ["held"] },
2013
- timeoutMs: 60000 }
2014
- );
2015
- if (!refreshPoll || refreshPoll.held !== true) {
2016
- return await parkTask("Merge-lock lease lost during the artifact build poll (refresh before chunk " + chunk + " of 3 failed: " + ((refreshPoll && refreshPoll.output) || "no output") + "). Fail-closed: stopping before any provenance stamp or version assignment.");
2017
- }
2018
- }
2019
- // Chunk 1 keeps the original stable key; later chunks use -c<N>
2020
- // suffixed keys. All are attemptKey-scoped so rework passes stay
2021
- // disjoint (replay-key contract).
2022
- var pollKey = (chunk === 1)
2023
- ? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
2024
- : attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
2025
- try {
2026
- buildPoll = await agent(
2027
- "First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
2028
- "If an artifact_status call itself FAILS (rate limiting / TOO_MANY_REQUESTS / 429, network error, or any response that is not a build object): that check is inconclusive \u2014 do NOT record it as \"no build\". Stop polling immediately and return with status_error: true. A failed read is not evidence of absence and must never be folded into the no-build-observed signal.\n" +
2029
- "On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
2030
- "- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
2031
- "- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
2032
- "- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
2033
- "- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
2034
- "Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\", \"status_error\": <true ONLY when a status read failed as described above, false or omitted otherwise> } and nothing else.",
2035
- { key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
2036
- schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] }, status_error: { type: "boolean" } }, required: ["build_done"] },
2037
- timeoutMs: 270000 }
2038
- );
2039
- } catch (chunkErr) {
2040
- // See docs/decisions/publish-path.md#hung-chunk: a hung or failed chunk is inconclusive, never terminal.
2041
- chunkFailures.push("chunk " + chunk + ": " + (chunkErr && chunkErr.message ? chunkErr.message : chunkErr));
2042
- 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.");
2043
- }
2044
- if (buildPoll) {
2045
- pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
2046
- pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
2047
- lastObservedAgentId = buildPoll.observed_agent_id || null;
2048
- if (buildPoll.status_error === true) { pollStatusErrors++; log("Artifact build poll chunk " + chunk + " of 3 hit a status-read error \u2014 recorded as inconclusive, continuing to the next chunk; a failed read is not evidence of absence."); }
2049
- if (buildPoll.build_done) { break; }
2050
- }
2051
- }
2052
- if (!buildPoll || !buildPoll.build_done) {
2053
- buildPoll = { build_done: false, status: (buildPoll && buildPoll.status) || "build still running after the 10.5-minute bounded poll" };
2054
- }
2055
- if (buildPoll.build_done && pollSawOurBuild) {
2056
- // See docs/decisions/publish-path.md#step-1c-no-stamp: no provenance stamp in STEP 1c.
2057
- publishBuildLanded = true;
2058
- artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
2059
- log("Publish build landed for task " + taskId + " — provenance stamp deferred to parent content verification");
2060
- } else {
2061
- // STEP 1b durable audit-dir fallback (2026-09-15, task aadeccc3):
2062
- // the poll observes only in-flight builds; a build that finished
2063
- // inside the window leaves a durable audit dir. Diff the audit
2064
- // listing against the pre-trigger snapshot: a new timestamped dir
2065
- // is evidence a build completed. Attribution is by window, not by build identity: any
2066
- // observed stranger blocks it. Never re-issues, never stamps;
2067
- // ok=true routes to the parent's read-back (the verification).
2068
- //
2069
- // The poll end-state is read from the poll's own observations,
2070
- // not from build_done alone: a build in flight at the last check
2071
- // means the budget was shorter than the latency (or the build is
2072
- // stuck) — NOT that no build ever started; nothing observed at
2073
- // any check is the never-started signal.
2074
- var pollEndState = lastObservedAgentId ? "build-still-running-at-poll-end"
2075
- : (pollSawOurBuild ? "our-build-observed-then-unconfirmed" : "no-build-observed-in-window");
2076
- var strangerObserved = pollSawStranger;
2077
- var newAuditDirsAfterPoll = [];
2078
- try {
2079
- var auditAfterPoll = await agent(
2080
- "List the artifact audit directories for slug \"" + PUBLISH_SLUG + "\" (best-effort, never a gate).\n" +
2081
- "Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null\n" +
2082
- "Return JSON { \"dirs\": \"<newline-separated names, empty string when the audits directory does not exist or is empty>\" } and nothing else.",
2083
- { key: attemptKey("publish-audit-after-poll-" + taskId, totalReworkCount), label: "Re-listing audit dirs after build poll",
2084
- schema: { type: "object", properties: { dirs: { type: "string" } }, required: ["dirs"] } }
2085
- );
2086
- var auditDirsAfterPollList = String((auditAfterPoll && auditAfterPoll.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
2087
- // Gated on auditBeforeOk (critic finding 4): without a baseline
2088
- // every historical dir would look new.
2089
- newAuditDirsAfterPoll = auditBeforeOk ? auditDirsAfterPollList.filter(function (d) {
2090
- return auditDirsBeforeTrigger.indexOf(d) === -1 && /^20\d\d-\d\d-\d\dT\d\d-\d\d-\d\dZ-/.test(d);
2091
- }) : [];
2092
- log("Publish audit-dir re-list after build poll for task " + taskId + ": " + newAuditDirsAfterPoll.length + " new timestamped dir(s)");
2093
- } catch (auditAfterPollErr) {
2094
- log("Publish audit-dir re-list after build poll failed for task " + taskId + " (non-fatal, durable-evidence check degraded): " + (auditAfterPollErr && auditAfterPollErr.message ? auditAfterPollErr.message : auditAfterPollErr));
2095
- }
2096
- // The shared auditReportOk (defined with the immediate fallback
2097
- // above) interprets the raw body here too.
2098
- var auditOkAfterPoll = null;
2099
- var newestAuditDirAfterPoll = null;
2100
- if (newAuditDirsAfterPoll.length > 0 && !strangerObserved) {
2101
- newAuditDirsAfterPoll.sort();
2102
- newestAuditDirAfterPoll = newAuditDirsAfterPoll[newAuditDirsAfterPoll.length - 1];
2103
- try {
2104
- var auditOkRead = await agent(
2105
- "Read the build report for artifact slug \"" + PUBLISH_SLUG + "\", audit dir \"" + newestAuditDirAfterPoll + "\" (verbatim read, never interpreted, never a gate).\n" +
2106
- "Run: cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/" + newestAuditDirAfterPoll + "/report.json 2>/dev/null || echo MISSING\n" +
2107
- "Return JSON { \"raw\": \"<verbatim file contents, or the literal string MISSING when the file does not exist>\" } and nothing else.",
2108
- { key: attemptKey("publish-audit-ok-after-poll-" + taskId, totalReworkCount), label: "Reading build report after build poll",
2109
- schema: { type: "object", properties: { raw: { type: "string" } }, required: ["raw"] } }
2110
- );
2111
- auditOkAfterPoll = auditReportOk(auditOkRead && auditOkRead.raw);
2112
- } catch (auditOkReadErr) {
2113
- log("Publish build-report read after build poll failed for task " + taskId + " (non-fatal, treated as unknown): " + (auditOkReadErr && auditOkReadErr.message ? auditOkReadErr.message : auditOkReadErr));
2114
- auditOkAfterPoll = null;
2115
- }
2116
- }
2117
- if (auditOkAfterPoll === true) {
2118
- publishBuildLanded = true;
2119
- artifactPublish = { source_commit: mergeCommitForPublish, pending_parent_verification: true };
2120
- log("Publish build landed for task " + taskId + " via durable audit evidence — provenance stamp deferred to parent content verification");
2121
- await recordPublishLedger({
2122
- commit: mergeCommitForPublish,
2123
- attempt: rebuildAttemptKey,
2124
- agent_id: rebuildAgentId,
2125
- applied_report: publishAppliedObservation,
2126
- manifest_before: preTriggerManifest,
2127
- outcome: "build-observed",
2128
- detail: "durable audit evidence shows a build completed during the attempt window (audit dir " + newestAuditDirAfterPoll + ", report ok=true); routed to parent verification"
2129
- }, totalReworkCount);
2130
- } else if (auditOkAfterPoll === false) {
2131
- // Explicit negative evidence under the stranger guard: a build
2132
- // ran and failed during the poll window with no stranger in
2133
- // flight. This is an attributable failure — preserved verbatim
2134
- // through post-deploy. Message contract unchanged.
2135
- 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.";
2136
- await recordPublishLedger({
2137
- commit: mergeCommitForPublish,
2138
- attempt: rebuildAttemptKey,
2139
- agent_id: rebuildAgentId,
2140
- applied_report: publishAppliedObservation,
2141
- outcome: "failed",
2142
- detail: "a build ran and failed (attribution by window, not by build identity): audit dir " + newestAuditDirAfterPoll + " report ok=false; no stranger build observed in flight during the poll"
2143
- }, totalReworkCount);
2144
- } else {
2145
- var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
2146
- : ((pollStatusErrors > 0 && !pollSawOurBuild) ? "status-read-errors-during-poll"
2147
- : (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
2148
- : (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing")));
2149
- // Verdict UNKNOWN (poll path): the build cannot be attributed
2150
- // to this attempt. The fields feed the single composed
2151
- // fail-closed park reason after post-deploy — never a
2152
- // fabricated verdict, never a silent pass.
2153
- publishUnknownFields = {
2154
- taskId: taskId,
2155
- unattributableReason: unattributableReason,
2156
- pollEndState: pollEndState,
2157
- sawOurBuild: pollSawOurBuild,
2158
- newAuditDirCount: newAuditDirsAfterPoll.length,
2159
- chunkFailures: chunkFailures.length,
2160
- pollStatusErrors: pollStatusErrors
2161
- };
2162
- await recordPublishLedger({
2163
- commit: mergeCommitForPublish,
2164
- attempt: rebuildAttemptKey,
2165
- agent_id: rebuildAgentId,
2166
- applied_report: publishAppliedObservation,
2167
- outcome: "unknown",
2168
- detail: "durable audit-dir fallback could not attribute a completed build to this attempt (unattributable_reason=" + unattributableReason + ", poll_end_state=" + pollEndState + ")"
2169
- }, totalReworkCount);
2170
- }
2171
- }
2172
- }
2173
-
1651
+ outcome: "publish-intent",
1652
+ issuer: "workflow",
1653
+ diff_path: publishDiffFile,
1654
+ diff_sha256: publishDiffSha256,
1655
+ // (2026-09-20, critic-0145 F-A4) The base the diff was computed
1656
+ // against: scan-publish-unknown's intent branch compares it with
1657
+ // current stamped provenance and re-queues the task when it went
1658
+ // stale — never issues the stale diff.
1659
+ base: publishBase,
1660
+ detail: "publish intent: preflight, checksummed diff, toolcheck, and pre-trigger baselines are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
1661
+ }, totalReworkCount);
1662
+ return await parkTask("publish: publish-requested " + mergeCommitForPublish + " " + rebuildAttemptKey + " — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") for the crew's publish worker to issue. The worker verifies the build and stamps provenance before this task resumes; no action needed.");
2174
1663
  } // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
2175
1664
  // STEP 2 (mechanical, always — skip path included): post-deploy
2176
1665
  // commits builder leftovers if any, removes the worktree, and releases
@@ -2183,34 +1672,17 @@ while (i < STEPS.length) {
2183
1672
  { key: attemptKey("publish-postdeploy-" + taskId, totalReworkCount), label: "Finalizing publish (post-deploy)",
2184
1673
  schema: { type: "object", properties: { deployed: { type: "boolean" }, output: { type: "string" } }, required: ["deployed"] } }
2185
1674
  );
2186
- if (publishSkippedNoLock) {
1675
+ // (2026-09-20, one-party worker-owned publish) Verified re-entry:
1676
+ // the intent park's terminal-cleanup already released the lock and
1677
+ // reclaimed the worktree. Post-deploy above is best-effort hygiene
1678
+ // on this branch — never a gate, never a park.
1679
+ if (publishAlreadyVerified) {
1680
+ log("Publish verified re-entry for task " + taskId + " — post-deploy ran forgivingly; continuing to the verified work-agent framing");
1681
+ } else if (publishSkippedNoLock) {
2187
1682
  if (!postDeploy.deployed) {
2188
1683
  return await parkTask("Post-deploy failed after a skipped publish: " + (postDeploy.output || "no output") + ". Nothing was published; worktree cleanup and lock state unknown — human attention needed.");
2189
1684
  }
2190
1685
  log("Publish skipped cleanly for task " + taskId + " (no lock held) — post-deploy step finished");
2191
- } else {
2192
- if (publishFailure) {
2193
- return await parkTask(publishFailure + (postDeploy.deployed
2194
- ? " Post-deploy step finished (cleanup status unknown)."
2195
- : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
2196
- }
2197
- if (!publishBuildLanded) {
2198
- // Verdict UNKNOWN (single fail-closed park — post-deploy always
2199
- // runs first; no early parks anywhere above). Machine contract:
2200
- // "unattributable_reason=" and "poll_end_state=" are always
2201
- // present; attribution is by window, not identity.
2202
- // (2026-09-18, H2 message contract) Thread the commit short-sha
2203
- // through the unknown fields so the human line names the trigger
2204
- // commit (design §1.5: "the trigger was sent for commit <short-sha>").
2205
- if (publishUnknownFields) publishUnknownFields.commitShortSha = mergeCommitShortForPublish;
2206
- return await parkTask(composeUnattributedParkReason(publishUnknownFields) + (postDeploy.deployed
2207
- ? " Post-deploy step finished (cleanup status unknown)."
2208
- : " Post-deploy also failed (" + (postDeploy.output || "no output") + ") — worktree and lock state unknown."));
2209
- }
2210
- if (!postDeploy.deployed) {
2211
- 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.");
2212
- }
2213
- log("Deterministic artifact publish completed for task " + taskId + ": build at " + artifactPublish.source_commit + ", provenance PENDING parent content verification");
2214
1686
  }
2215
1687
  } catch (pubErr) {
2216
1688
  // Best-effort cleanup: if the lock was refreshed, try to release it
@@ -2231,7 +1703,16 @@ while (i < STEPS.length) {
2231
1703
  // The work agent no longer performs the publish — it reports on the
2232
1704
  // mechanical outcome above. It must not rebuild or re-stamp: a second
2233
1705
  // artifact_edit would trigger a duplicate build.
2234
- if (publishSkippedNoLock) {
1706
+ // (2026-09-20, one-party worker-owned publish) On a verified re-entry
1707
+ // the publish was issued by the crew worker, verified by the parent
1708
+ // read-back, and stamped — the work agent reports that, VERDICT: PASS.
1709
+ if (publishAlreadyVerified) {
1710
+ 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" +
1711
+ "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" +
1712
+ "For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
1713
+ "Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
1714
+ "The VERDICT line must be the last line of your report.";
1715
+ } else if (publishSkippedNoLock) {
2235
1716
  instructions = "Publish was skipped deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
2236
1717
  "Integrate reported MERGED_EMPTY (the task branch had no commits ahead of the integration target), so no merge lock was taken and there is nothing to ship. Post-deploy step finished per its return — do NOT run post-deploy yourself.\n\n" +
2237
1718
  "Write plain prose describing the skip, then on its own line: VERDICT: PASS\n" +
@@ -2959,20 +2440,6 @@ while (i < STEPS.length) {
2959
2440
  return { status: "failed", task_id: taskId, reason: "Publish failed: " + summary };
2960
2441
  }
2961
2442
 
2962
- // See docs/decisions/publish-path.md#verification-park: the build landed but provenance is unstamped until parent verification.
2963
- if (passed && step.name === "Publish" && PUBLISH_TYPE === "artifact" && PUBLISH_SLUG && !publishSkippedNoLock && publishBuildLanded) {
2964
- // Success contract (2026-09-18, H2): the parent asked for exactly
2965
- // one build for this publish. The build landed — do NOT republish: a
2966
- // duplicate build would re-publish the same change. content_check=pending
2967
- // means parent read-back is pending; this
2968
- // park is NOT proof the content is correct.
2969
- return await parkTask("publish: verification-requested " + mergeCommitForPublish +
2970
- " attempt=" + rebuildAttemptKey +
2971
- " Do NOT republish: a duplicate build would re-publish the same change. " +
2972
- "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. " +
2973
- "Appendix: build=" + (rebuildAgentId || "agent_id unobserved") + "; provenance=unstamped; content_check=pending.");
2974
- }
2975
-
2976
2443
  i++;
2977
2444
  }
2978
2445