muse-crew 0.7.8 → 0.7.9

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.
@@ -72,7 +72,7 @@ The parked message is stored as `Parked: publish: verification-requested
72
72
  `publish: verification-requested`. The `<commit>` is the merged commit whose
73
73
  content must be verified. The `(build …)` suffix carries the
74
74
  `build.agent_id` the workflow observed for this publish attempt (the
75
- artifact system's durable build identifier — the parent uses it for the
75
+ artifact system's in-flight build correlation ID — the parent uses it for the
76
76
  build-ID correlation in step 4b); `agent_id unobserved` means the edit was
77
77
  accepted but the workflow never correlated it to a builder run. The merge
78
78
  lock is already released (post-deploy ran before the park), so the parked
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -21,7 +21,7 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
21
21
 
22
22
  1. **Load tools:** Call tool_search_load_tool_namespace with paths ["workflow_launch"].
23
23
 
24
- 2. **Load the workflow registry:** Read the file "{crewHome}/workflows/registry.json" with the read tool and parse it as JSON. If the file does not exist (the live release predates the registry), proceed without it — omit the `registry` arg and the dispatcher will load the registry the slow way and log a warning.
24
+ 2. **Load the workflow registry:** Read the file "{crewHome}/workflows/registry.json" with the read tool and parse it as JSON. If the file does not exist (the live release predates the registry), proceed without it — omit the `registry` arg and the dispatcher will load the registry the slow way and log a warning. If the read FAILS on a file that exists (transient read error — observed 2026-09-13; the file itself was healthy and later ticks read it fine), retry the read once; if it still fails, write the error text into your run summary (observability — never silently swallow a failed read) and proceed without the registry the same way.
25
25
 
26
26
  3. **Run the dispatcher:** Call workflow_launch with scriptPath "{crewHome}/workflows/crew-dispatch.js" and args {"crewHome": "{crewHome}", "registry": <parsed registry JSON, or omit the key when the file was missing>}.
27
27
 
@@ -486,8 +486,8 @@ function verifyAppliedChanges(expected, applied) {
486
486
  // it must match the artifact's real content.
487
487
  function buildPublishReadbackRequest(taskId, commit, diff, buildAgentId) {
488
488
  // Build-ID correlation (2026-09-12): buildAgentId is the build.agent_id the
489
- // workflow observed for the publish attempt (the artifact system's durable
490
- // build identifier). The read-back request carries it so the parent can
489
+ // workflow observed for the publish attempt (the artifact system's in-flight
490
+ // build correlation ID — not a durable post-completion identifier). The read-back request carries it so the parent can
491
491
  // prove the read-back inspected the live build of THIS attempt — not a
492
492
  // different build's output. Null/empty means the edit was accepted but
493
493
  // never correlated to a builder run. Pure function of inputs — no I/O,
@@ -1521,12 +1521,22 @@ while (i < STEPS.length) {
1521
1521
  "- Report, for each file you changed: its path, the exact lines you added, and the exact lines you removed.\n" +
1522
1522
  buildPreHashInstruction(expectedChanges) + "'\n" +
1523
1523
  ARTIFACT_LOAD_PREAMBLE +
1524
- "If artifact_edit is still not available after the load, do NOT improvise — return { \"edit_started\": false, \"error\": \"artifact_tools missing after load\", \"applied\": [] }.\n" +
1525
- "Make no other calls. Return JSON { \"edit_started\": <true if the edit was accepted, false otherwise>, \"error\": \"<details or empty string>\", \"applied\": [{\"path\": \"<file path>\", \"added\": [\"<added lines>\"], \"removed\": [\"<removed lines>\"]}], \"pre_hashes\": {\"<file path>\": \"<sha256 of that file's content BEFORE you applied the diff, or \"MISSING\">\"} } and nothing else.";
1524
+ "If artifact_edit is still not available after the load, do NOT improvise — return { \"edit_started\": false, \"build_agent_id\": null, \"error\": \"artifact_tools missing after load\", \"applied\": [] }.\n" +
1525
+ "RECEIPT CAPTURE (receipt-chained publish, 2026-09-13): the edit is only half the contract you must also capture the platform build's receipt, the in-flight correlation ID the follow-up poll chains to.\n" +
1526
+ "- The artifact namespace is already loaded (see below). BEFORE calling artifact_edit, call artifact_status with slug \"" + PUBLISH_SLUG + "\" and note the running build's agent_id (or null when no build is running). This is the pre-edit baseline.\n" +
1527
+ "- Call artifact_edit as instructed above.\n" +
1528
+ "- IMMEDIATELY after artifact_edit returns, call artifact_status again. If a build is running whose agent_id DIFFERS from the pre-edit baseline (or the baseline was null), that build is this edit's — its agent_id is the receipt.\n" +
1529
+ "- If the post-edit status shows the SAME agent_id as the baseline, or no build at all, wait about 15 seconds and check artifact_status again, up to 4 more times. If a build with a NEW agent_id appears, that is the receipt.\n" +
1530
+ "- If no new build appears, the receipt is null: the build may be pending_init-invisible, may have finished before the capture, or may be queued behind the earlier build. Return null — do NOT guess, and do NOT substitute the baseline build's agent_id.\n" +
1531
+ "Make no other calls. Return JSON { \"edit_started\": <true if the edit was accepted, false otherwise>, \"build_agent_id\": <the receipt agent_id string, or null when no build could be attributed to this edit>, \"error\": \"<details or empty string>\", \"applied\": [{\"path\": \"<file path>\", \"added\": [\"<added lines>\"], \"removed\": [\"<removed lines>\"]}], \"pre_hashes\": {\"<file path>\": \"<sha256 of that file's content BEFORE you applied the diff, or \"MISSING\">\"} } and nothing else.";
1526
1532
  var rebuildSchema =
1527
1533
  { type: "object",
1528
1534
  properties: {
1529
1535
  edit_started: { type: "boolean" },
1536
+ build_agent_id: {
1537
+ type: ["string", "null"],
1538
+ description: "Receipt-chained publish (2026-09-13): the platform build's in-flight correlation ID (build.agent_id from artifact_status) captured immediately after the edit was accepted — the receipt the follow-up poll chains to. Null when no build could be attributed to this edit. Required: an accepted edit with a null receipt parks fail-closed as unknown."
1539
+ },
1530
1540
  error: { type: "string" },
1531
1541
  applied: {
1532
1542
  type: "array",
@@ -1545,7 +1555,7 @@ while (i < STEPS.length) {
1545
1555
  description: "Diagnostic (2026-09-12): sha256 of each touched file's content BEFORE the builder applied the diff, as reported by the builder. Compared against the workflow-computed expected base hashes (merge parent) — observation only, never gating."
1546
1556
  }
1547
1557
  },
1548
- required: ["edit_started", "applied"] };
1558
+ required: ["edit_started", "build_agent_id", "applied"] };
1549
1559
  var rebuildTrigger = null;
1550
1560
  var rebuildReportMissing = false; // true if the edit went through but the agent returned no applied report (structured-output failure) — the smoke-check is skipped; the parent's independent read-back is the verification
1551
1561
  // The trigger key of the attempt that last ran, for the publish ledger.
@@ -1579,8 +1589,9 @@ while (i < STEPS.length) {
1579
1589
  //
1580
1590
  // Build-ID research (2026-09-12) corrected the model: artifact.edit
1581
1591
  // returns pending_init with NO agent_id, but artifact_status exposes
1582
- // the build's agent_id (the artifact system's durable build
1583
- // identifier, stable across polls) immediately after acceptance.
1592
+ // the build's agent_id (the artifact system's in-flight build
1593
+ // correlation ID, stable across polls while the build runs)
1594
+ // immediately after acceptance.
1584
1595
  // So the recovery no longer asks the child to derive booleans —
1585
1596
  // the layer where the 2026-09-12 signal was lost. It reads the RAW
1586
1597
  // build object and extracts build.agent_id mechanically in the
@@ -1647,6 +1658,42 @@ while (i < STEPS.length) {
1647
1658
  { key: attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount), label: "Triggering artifact rebuild (retry)", schema: rebuildSchema });
1648
1659
  rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount);
1649
1660
  }
1661
+ // Receipt chaining (2026-09-13): adopt the trigger's build receipt,
1662
+ // or park on an uncorrelated acceptance. The trigger's closeout schema
1663
+ // requires build_agent_id — the platform build's in-flight correlation
1664
+ // ID captured immediately after the edit was accepted.
1665
+ // An accepted edit (edit_started=true) with a null receipt is UNKNOWN,
1666
+ // not "did not go through": the build may be pending_init-invisible,
1667
+ // may have finished before the capture window, or may be queued behind
1668
+ // a still-running earlier build. No re-trigger is issued on unknown —
1669
+ // a blind re-trigger duplicated the edit on 2026-09-12, and the platform
1670
+ // offers no idempotency proof that would make re-issue safe.
1671
+ // (Retry-semantics reconciliation, 2026-09-13: the "no receipt → safe
1672
+ // re-trigger" sketch assumed the edit command idempotently publishes
1673
+ // what's on git; the duplicate-edit incident disproves the assumption,
1674
+ // and the standing rule retries only on explicit negative evidence.
1675
+ // Re-trigger stays exactly where it was: the edit_started=false
1676
+ // explicit-rejection path above.) Record the attempt and park
1677
+ // fail-closed; correlate via the ledger and the parent's content
1678
+ // read-back before re-driving Publish.
1679
+ if (rebuildTrigger && rebuildTrigger.edit_started && !rebuildReportMissing) {
1680
+ var triggerAgentId = (typeof rebuildTrigger.build_agent_id === "string" && rebuildTrigger.build_agent_id.length > 0) ? rebuildTrigger.build_agent_id : null;
1681
+ if (!triggerAgentId) {
1682
+ var uncorrelatedObservation = (function () { var c = verifyAppliedChanges(expectedChanges, rebuildTrigger.applied); return c.ok ? "match" : "mismatch: " + c.reason; })();
1683
+ log("Publish receipt missing for task " + taskId + ": the trigger reported edit_started=true but captured no build receipt (build_agent_id null) — no build attributable to this edit. Parking fail-closed without re-triggering.");
1684
+ await recordPublishLedger({
1685
+ commit: mergeCommitForPublish,
1686
+ attempt: rebuildAttemptKey,
1687
+ agent_id: null,
1688
+ applied_report: uncorrelatedObservation,
1689
+ outcome: "unknown",
1690
+ detail: "edit accepted (edit_started=true) but the trigger captured no build receipt in its capture window: no build attributable to this edit (pending_init-invisible, finished before capture, or queued behind an earlier build). No re-trigger issued — a blind re-trigger on an unknown outcome duplicated the edit on 2026-09-12 and the platform offers no idempotency proof."
1691
+ }, totalReworkCount);
1692
+ return await parkTask("Publish outcome unknown: the rebuild trigger reported the edit was accepted but captured no build receipt (build_agent_id null) — no build could be attributed to this edit in the capture window. The edit may be pending_init-invisible, already finished, or queued behind an earlier build, so no re-trigger 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 via the ledger and re-drive Publish only after the parent's content read-back resolves what actually landed. Fail-closed.");
1693
+ }
1694
+ rebuildAgentId = triggerAgentId;
1695
+ log("Publish receipt chained for task " + taskId + ": build " + triggerAgentId + " — the follow-up poll waits on this build only.");
1696
+ }
1650
1697
  // Durable publish-attempt ledger: record the trigger outcome while the
1651
1698
  // attempt key and commit are in scope. Every attempt lands here with
1652
1699
  // its outcome — submitted, rejected, or unknown (unknown is recorded
@@ -1750,10 +1797,13 @@ while (i < STEPS.length) {
1750
1797
  ? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
1751
1798
  : attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
1752
1799
  buildPoll = await agent(
1753
- "First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
1754
- "Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
1800
+ "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" +
1801
+ "- If no build is running (build is null): OUR build finished. Stop and report done.\n" +
1802
+ "- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
1803
+ "- If the running build's agent_id is present but DIFFERENT: our build is gone (it finished before this one started). Do NOT wait on the stranger's build and do NOT attribute its completion to our attempt \u2014 stop and report done.\n" +
1804
+ "Return JSON { \"build_done\": <true if our build is no longer running within budget, false on timeout>, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
1755
1805
  { key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
1756
- schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
1806
+ schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
1757
1807
  timeoutMs: 270000 }
1758
1808
  );
1759
1809
  if (buildPoll && buildPoll.build_done) { break; }
@@ -543,8 +543,8 @@ function verifyAppliedChanges(expected, applied) {
543
543
  // it must match the artifact's real content.
544
544
  function buildPublishReadbackRequest(taskId, commit, diff, buildAgentId) {
545
545
  // Build-ID correlation (2026-09-12): buildAgentId is the build.agent_id the
546
- // workflow observed for the publish attempt (the artifact system's durable
547
- // build identifier). The read-back request carries it so the parent can
546
+ // workflow observed for the publish attempt (the artifact system's in-flight
547
+ // build correlation ID — not a durable post-completion identifier). The read-back request carries it so the parent can
548
548
  // prove the read-back inspected the live build of THIS attempt — not a
549
549
  // different build's output. Null/empty means the edit was accepted but
550
550
  // never correlated to a builder run. Pure function of inputs — no I/O,
@@ -1510,12 +1510,22 @@ while (i < STEPS.length) {
1510
1510
  "- Report, for each file you changed: its path, the exact lines you added, and the exact lines you removed.\n" +
1511
1511
  buildPreHashInstruction(expectedChanges) + "'\n" +
1512
1512
  ARTIFACT_LOAD_PREAMBLE +
1513
- "If artifact_edit is still not available after the load, do NOT improvise — return { \"edit_started\": false, \"error\": \"artifact_tools missing after load\", \"applied\": [] }.\n" +
1514
- "Make no other calls. Return JSON { \"edit_started\": <true if the edit was accepted, false otherwise>, \"error\": \"<details or empty string>\", \"applied\": [{\"path\": \"<file path>\", \"added\": [\"<added lines>\"], \"removed\": [\"<removed lines>\"]}], \"pre_hashes\": {\"<file path>\": \"<sha256 of that file's content BEFORE you applied the diff, or \"MISSING\">\"} } and nothing else.";
1513
+ "If artifact_edit is still not available after the load, do NOT improvise — return { \"edit_started\": false, \"build_agent_id\": null, \"error\": \"artifact_tools missing after load\", \"applied\": [] }.\n" +
1514
+ "RECEIPT CAPTURE (receipt-chained publish, 2026-09-13): the edit is only half the contract you must also capture the platform build's receipt, the in-flight correlation ID the follow-up poll chains to.\n" +
1515
+ "- The artifact namespace is already loaded (see below). BEFORE calling artifact_edit, call artifact_status with slug \"" + PUBLISH_SLUG + "\" and note the running build's agent_id (or null when no build is running). This is the pre-edit baseline.\n" +
1516
+ "- Call artifact_edit as instructed above.\n" +
1517
+ "- IMMEDIATELY after artifact_edit returns, call artifact_status again. If a build is running whose agent_id DIFFERS from the pre-edit baseline (or the baseline was null), that build is this edit's — its agent_id is the receipt.\n" +
1518
+ "- If the post-edit status shows the SAME agent_id as the baseline, or no build at all, wait about 15 seconds and check artifact_status again, up to 4 more times. If a build with a NEW agent_id appears, that is the receipt.\n" +
1519
+ "- If no new build appears, the receipt is null: the build may be pending_init-invisible, may have finished before the capture, or may be queued behind the earlier build. Return null — do NOT guess, and do NOT substitute the baseline build's agent_id.\n" +
1520
+ "Make no other calls. Return JSON { \"edit_started\": <true if the edit was accepted, false otherwise>, \"build_agent_id\": <the receipt agent_id string, or null when no build could be attributed to this edit>, \"error\": \"<details or empty string>\", \"applied\": [{\"path\": \"<file path>\", \"added\": [\"<added lines>\"], \"removed\": [\"<removed lines>\"]}], \"pre_hashes\": {\"<file path>\": \"<sha256 of that file's content BEFORE you applied the diff, or \"MISSING\">\"} } and nothing else.";
1515
1521
  var rebuildSchema =
1516
1522
  { type: "object",
1517
1523
  properties: {
1518
1524
  edit_started: { type: "boolean" },
1525
+ build_agent_id: {
1526
+ type: ["string", "null"],
1527
+ description: "Receipt-chained publish (2026-09-13): the platform build's in-flight correlation ID (build.agent_id from artifact_status) captured immediately after the edit was accepted — the receipt the follow-up poll chains to. Null when no build could be attributed to this edit. Required: an accepted edit with a null receipt parks fail-closed as unknown."
1528
+ },
1519
1529
  error: { type: "string" },
1520
1530
  applied: {
1521
1531
  type: "array",
@@ -1534,7 +1544,7 @@ while (i < STEPS.length) {
1534
1544
  description: "Diagnostic (2026-09-12): sha256 of each touched file's content BEFORE the builder applied the diff, as reported by the builder. Compared against the workflow-computed expected base hashes (merge parent) — observation only, never gating."
1535
1545
  }
1536
1546
  },
1537
- required: ["edit_started", "applied"] };
1547
+ required: ["edit_started", "build_agent_id", "applied"] };
1538
1548
  var rebuildTrigger = null;
1539
1549
  var rebuildReportMissing = false; // true if the edit went through but the agent returned no applied report (structured-output failure) — the smoke-check is skipped; the parent's independent read-back is the verification
1540
1550
  // The trigger key of the attempt that last ran, for the publish ledger.
@@ -1568,8 +1578,9 @@ while (i < STEPS.length) {
1568
1578
  //
1569
1579
  // Build-ID research (2026-09-12) corrected the model: artifact.edit
1570
1580
  // returns pending_init with NO agent_id, but artifact_status exposes
1571
- // the build's agent_id (the artifact system's durable build
1572
- // identifier, stable across polls) immediately after acceptance.
1581
+ // the build's agent_id (the artifact system's in-flight build
1582
+ // correlation ID, stable across polls while the build runs)
1583
+ // immediately after acceptance.
1573
1584
  // So the recovery no longer asks the child to derive booleans —
1574
1585
  // the layer where the 2026-09-12 signal was lost. It reads the RAW
1575
1586
  // build object and extracts build.agent_id mechanically in the
@@ -1636,6 +1647,42 @@ while (i < STEPS.length) {
1636
1647
  { key: attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", reworkCount), label: "Triggering artifact rebuild (retry)", schema: rebuildSchema });
1637
1648
  rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", reworkCount);
1638
1649
  }
1650
+ // Receipt chaining (2026-09-13): adopt the trigger's build receipt,
1651
+ // or park on an uncorrelated acceptance. The trigger's closeout schema
1652
+ // requires build_agent_id — the platform build's in-flight correlation
1653
+ // ID captured immediately after the edit was accepted.
1654
+ // An accepted edit (edit_started=true) with a null receipt is UNKNOWN,
1655
+ // not "did not go through": the build may be pending_init-invisible,
1656
+ // may have finished before the capture window, or may be queued behind
1657
+ // a still-running earlier build. No re-trigger is issued on unknown —
1658
+ // a blind re-trigger duplicated the edit on 2026-09-12, and the platform
1659
+ // offers no idempotency proof that would make re-issue safe.
1660
+ // (Retry-semantics reconciliation, 2026-09-13: the "no receipt → safe
1661
+ // re-trigger" sketch assumed the edit command idempotently publishes
1662
+ // what's on git; the duplicate-edit incident disproves the assumption,
1663
+ // and the standing rule retries only on explicit negative evidence.
1664
+ // Re-trigger stays exactly where it was: the edit_started=false
1665
+ // explicit-rejection path above.) Record the attempt and park
1666
+ // fail-closed; correlate via the ledger and the parent's content
1667
+ // read-back before re-driving Publish.
1668
+ if (rebuildTrigger && rebuildTrigger.edit_started && !rebuildReportMissing) {
1669
+ var triggerAgentId = (typeof rebuildTrigger.build_agent_id === "string" && rebuildTrigger.build_agent_id.length > 0) ? rebuildTrigger.build_agent_id : null;
1670
+ if (!triggerAgentId) {
1671
+ var uncorrelatedObservation = (function () { var c = verifyAppliedChanges(expectedChanges, rebuildTrigger.applied); return c.ok ? "match" : "mismatch: " + c.reason; })();
1672
+ log("Publish receipt missing for task " + taskId + ": the trigger reported edit_started=true but captured no build receipt (build_agent_id null) — no build attributable to this edit. Parking fail-closed without re-triggering.");
1673
+ await recordPublishLedger({
1674
+ commit: mergeCommitForPublish,
1675
+ attempt: rebuildAttemptKey,
1676
+ agent_id: null,
1677
+ applied_report: uncorrelatedObservation,
1678
+ outcome: "unknown",
1679
+ detail: "edit accepted (edit_started=true) but the trigger captured no build receipt in its capture window: no build attributable to this edit (pending_init-invisible, finished before capture, or queued behind an earlier build). No re-trigger issued — a blind re-trigger on an unknown outcome duplicated the edit on 2026-09-12 and the platform offers no idempotency proof."
1680
+ }, reworkCount);
1681
+ return await parkTask("Publish outcome unknown: the rebuild trigger reported the edit was accepted but captured no build receipt (build_agent_id null) — no build could be attributed to this edit in the capture window. The edit may be pending_init-invisible, already finished, or queued behind an earlier build, so no re-trigger 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 via the ledger and re-drive Publish only after the parent's content read-back resolves what actually landed. Fail-closed.");
1682
+ }
1683
+ rebuildAgentId = triggerAgentId;
1684
+ log("Publish receipt chained for task " + taskId + ": build " + triggerAgentId + " — the follow-up poll waits on this build only.");
1685
+ }
1639
1686
  // Durable publish-attempt ledger: record the trigger outcome while the
1640
1687
  // attempt key and commit are in scope. Every attempt lands here with
1641
1688
  // its outcome — submitted, rejected, or unknown (unknown is recorded
@@ -1739,10 +1786,13 @@ while (i < STEPS.length) {
1739
1786
  ? attemptKey("publish-artifact-poll-" + taskId, reworkCount)
1740
1787
  : attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, reworkCount);
1741
1788
  buildPoll = await agent(
1742
- "First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
1743
- "Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
1789
+ "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" +
1790
+ "- If no build is running (build is null): OUR build finished. Stop and report done.\n" +
1791
+ "- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
1792
+ "- If the running build's agent_id is present but DIFFERENT: our build is gone (it finished before this one started). Do NOT wait on the stranger's build and do NOT attribute its completion to our attempt \u2014 stop and report done.\n" +
1793
+ "Return JSON { \"build_done\": <true if our build is no longer running within budget, false on timeout>, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
1744
1794
  { key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
1745
- schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
1795
+ schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
1746
1796
  timeoutMs: 270000 }
1747
1797
  );
1748
1798
  if (buildPoll && buildPoll.build_done) { break; }
@@ -248,6 +248,34 @@ function unwrapBoardResult(boardResult) {
248
248
  }
249
249
  }
250
250
  }
251
+ // The read-board agent may return the whole board stdout in a
252
+ // {"status":"ok","result":"<board JSON string>"} envelope — the
253
+ // get-dispatch-state stdout placed in .result as a string instead of a
254
+ // parsed object (17:00 PDT tick, 2026-09-13). Parse it deterministically
255
+ // in JS: a string that parses to an array is the ready_tasks array
256
+ // itself; a string that parses to an object is the whole board stdout,
257
+ // so re-run the unwrap on it. Unparseable or scalar strings fall through
258
+ // to the fail-closed throw below.
259
+ if (boardData && typeof boardData === 'object' && typeof boardData.result === 'string' && !Array.isArray(boardData.ready_tasks)) {
260
+ var parsedResult = null, resultParsed = false;
261
+ try {
262
+ parsedResult = JSON.parse(boardData.result);
263
+ resultParsed = true;
264
+ } catch (e) {
265
+ // Unparseable — fall through to fail-closed below.
266
+ }
267
+ if (resultParsed) {
268
+ if (Array.isArray(parsedResult)) {
269
+ boardData.ready_tasks = parsedResult;
270
+ } else if (parsedResult && typeof parsedResult === 'object') {
271
+ // Recursion happens outside the try: a fail-closed throw from the
272
+ // inner unwrap must propagate with its own precise message, not be
273
+ // swallowed into the generic throw below.
274
+ return unwrapBoardResult(parsedResult);
275
+ }
276
+ // Scalar — fall through to fail-closed below.
277
+ }
278
+ }
251
279
  // The read-board agent may hand back the envelope with ready_tasks as a
252
280
  // JSON string — {"ready_tasks": "<json string>"} — the get-dispatch-state
253
281
  // stdout placed in the envelope instead of parsed (14:24 PDT tick,
@@ -272,7 +300,7 @@ function unwrapBoardResult(boardResult) {
272
300
  if (!boardData || typeof boardData !== 'object' || !Array.isArray(boardData.ready_tasks)) {
273
301
  throw new Error(
274
302
  "unwrapBoardResult: unknown board envelope — expected ready_tasks at top level, " +
275
- "in .data, in .result, in .result.data, or as a JSON string of a ready_tasks array " +
303
+ "in .data, in .result (object or JSON string), in .result.data, or as a JSON string of a ready_tasks array " +
276
304
  "or of a whole board object. Got keys: " +
277
305
  (boardData && typeof boardData === 'object' ? Object.keys(boardData).join(",") : typeof boardData)
278
306
  );
@@ -486,8 +486,8 @@ function verifyAppliedChanges(expected, applied) {
486
486
  // it must match the artifact's real content.
487
487
  function buildPublishReadbackRequest(taskId, commit, diff, buildAgentId) {
488
488
  // Build-ID correlation (2026-09-12): buildAgentId is the build.agent_id the
489
- // workflow observed for the publish attempt (the artifact system's durable
490
- // build identifier). The read-back request carries it so the parent can
489
+ // workflow observed for the publish attempt (the artifact system's in-flight
490
+ // build correlation ID — not a durable post-completion identifier). The read-back request carries it so the parent can
491
491
  // prove the read-back inspected the live build of THIS attempt — not a
492
492
  // different build's output. Null/empty means the edit was accepted but
493
493
  // never correlated to a builder run. Pure function of inputs — no I/O,
@@ -1493,12 +1493,22 @@ while (i < STEPS.length) {
1493
1493
  "- Report, for each file you changed: its path, the exact lines you added, and the exact lines you removed.\n" +
1494
1494
  buildPreHashInstruction(expectedChanges) + "'\n" +
1495
1495
  ARTIFACT_LOAD_PREAMBLE +
1496
- "If artifact_edit is still not available after the load, do NOT improvise — return { \"edit_started\": false, \"error\": \"artifact_tools missing after load\", \"applied\": [] }.\n" +
1497
- "Make no other calls. Return JSON { \"edit_started\": <true if the edit was accepted, false otherwise>, \"error\": \"<details or empty string>\", \"applied\": [{\"path\": \"<file path>\", \"added\": [\"<added lines>\"], \"removed\": [\"<removed lines>\"]}], \"pre_hashes\": {\"<file path>\": \"<sha256 of that file's content BEFORE you applied the diff, or \"MISSING\">\"} } and nothing else.";
1496
+ "If artifact_edit is still not available after the load, do NOT improvise — return { \"edit_started\": false, \"build_agent_id\": null, \"error\": \"artifact_tools missing after load\", \"applied\": [] }.\n" +
1497
+ "RECEIPT CAPTURE (receipt-chained publish, 2026-09-13): the edit is only half the contract you must also capture the platform build's receipt, the in-flight correlation ID the follow-up poll chains to.\n" +
1498
+ "- The artifact namespace is already loaded (see below). BEFORE calling artifact_edit, call artifact_status with slug \"" + PUBLISH_SLUG + "\" and note the running build's agent_id (or null when no build is running). This is the pre-edit baseline.\n" +
1499
+ "- Call artifact_edit as instructed above.\n" +
1500
+ "- IMMEDIATELY after artifact_edit returns, call artifact_status again. If a build is running whose agent_id DIFFERS from the pre-edit baseline (or the baseline was null), that build is this edit's — its agent_id is the receipt.\n" +
1501
+ "- If the post-edit status shows the SAME agent_id as the baseline, or no build at all, wait about 15 seconds and check artifact_status again, up to 4 more times. If a build with a NEW agent_id appears, that is the receipt.\n" +
1502
+ "- If no new build appears, the receipt is null: the build may be pending_init-invisible, may have finished before the capture, or may be queued behind the earlier build. Return null — do NOT guess, and do NOT substitute the baseline build's agent_id.\n" +
1503
+ "Make no other calls. Return JSON { \"edit_started\": <true if the edit was accepted, false otherwise>, \"build_agent_id\": <the receipt agent_id string, or null when no build could be attributed to this edit>, \"error\": \"<details or empty string>\", \"applied\": [{\"path\": \"<file path>\", \"added\": [\"<added lines>\"], \"removed\": [\"<removed lines>\"]}], \"pre_hashes\": {\"<file path>\": \"<sha256 of that file's content BEFORE you applied the diff, or \"MISSING\">\"} } and nothing else.";
1498
1504
  var rebuildSchema =
1499
1505
  { type: "object",
1500
1506
  properties: {
1501
1507
  edit_started: { type: "boolean" },
1508
+ build_agent_id: {
1509
+ type: ["string", "null"],
1510
+ description: "Receipt-chained publish (2026-09-13): the platform build's in-flight correlation ID (build.agent_id from artifact_status) captured immediately after the edit was accepted — the receipt the follow-up poll chains to. Null when no build could be attributed to this edit. Required: an accepted edit with a null receipt parks fail-closed as unknown."
1511
+ },
1502
1512
  error: { type: "string" },
1503
1513
  applied: {
1504
1514
  type: "array",
@@ -1517,7 +1527,7 @@ while (i < STEPS.length) {
1517
1527
  description: "Diagnostic (2026-09-12): sha256 of each touched file's content BEFORE the builder applied the diff, as reported by the builder. Compared against the workflow-computed expected base hashes (merge parent) — observation only, never gating."
1518
1528
  }
1519
1529
  },
1520
- required: ["edit_started", "applied"] };
1530
+ required: ["edit_started", "build_agent_id", "applied"] };
1521
1531
  var rebuildTrigger = null;
1522
1532
  var rebuildReportMissing = false; // true if the edit went through but the agent returned no applied report (structured-output failure) — the smoke-check is skipped; the parent's independent read-back is the verification
1523
1533
  // The trigger key of the attempt that last ran, for the publish ledger.
@@ -1551,8 +1561,9 @@ while (i < STEPS.length) {
1551
1561
  //
1552
1562
  // Build-ID research (2026-09-12) corrected the model: artifact.edit
1553
1563
  // returns pending_init with NO agent_id, but artifact_status exposes
1554
- // the build's agent_id (the artifact system's durable build
1555
- // identifier, stable across polls) immediately after acceptance.
1564
+ // the build's agent_id (the artifact system's in-flight build
1565
+ // correlation ID, stable across polls while the build runs)
1566
+ // immediately after acceptance.
1556
1567
  // So the recovery no longer asks the child to derive booleans —
1557
1568
  // the layer where the 2026-09-12 signal was lost. It reads the RAW
1558
1569
  // build object and extracts build.agent_id mechanically in the
@@ -1619,6 +1630,42 @@ while (i < STEPS.length) {
1619
1630
  { key: attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount), label: "Triggering artifact rebuild (retry)", schema: rebuildSchema });
1620
1631
  rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount);
1621
1632
  }
1633
+ // Receipt chaining (2026-09-13): adopt the trigger's build receipt,
1634
+ // or park on an uncorrelated acceptance. The trigger's closeout schema
1635
+ // requires build_agent_id — the platform build's in-flight correlation
1636
+ // ID captured immediately after the edit was accepted.
1637
+ // An accepted edit (edit_started=true) with a null receipt is UNKNOWN,
1638
+ // not "did not go through": the build may be pending_init-invisible,
1639
+ // may have finished before the capture window, or may be queued behind
1640
+ // a still-running earlier build. No re-trigger is issued on unknown —
1641
+ // a blind re-trigger duplicated the edit on 2026-09-12, and the platform
1642
+ // offers no idempotency proof that would make re-issue safe.
1643
+ // (Retry-semantics reconciliation, 2026-09-13: the "no receipt → safe
1644
+ // re-trigger" sketch assumed the edit command idempotently publishes
1645
+ // what's on git; the duplicate-edit incident disproves the assumption,
1646
+ // and the standing rule retries only on explicit negative evidence.
1647
+ // Re-trigger stays exactly where it was: the edit_started=false
1648
+ // explicit-rejection path above.) Record the attempt and park
1649
+ // fail-closed; correlate via the ledger and the parent's content
1650
+ // read-back before re-driving Publish.
1651
+ if (rebuildTrigger && rebuildTrigger.edit_started && !rebuildReportMissing) {
1652
+ var triggerAgentId = (typeof rebuildTrigger.build_agent_id === "string" && rebuildTrigger.build_agent_id.length > 0) ? rebuildTrigger.build_agent_id : null;
1653
+ if (!triggerAgentId) {
1654
+ var uncorrelatedObservation = (function () { var c = verifyAppliedChanges(expectedChanges, rebuildTrigger.applied); return c.ok ? "match" : "mismatch: " + c.reason; })();
1655
+ log("Publish receipt missing for task " + taskId + ": the trigger reported edit_started=true but captured no build receipt (build_agent_id null) — no build attributable to this edit. Parking fail-closed without re-triggering.");
1656
+ await recordPublishLedger({
1657
+ commit: mergeCommitForPublish,
1658
+ attempt: rebuildAttemptKey,
1659
+ agent_id: null,
1660
+ applied_report: uncorrelatedObservation,
1661
+ outcome: "unknown",
1662
+ detail: "edit accepted (edit_started=true) but the trigger captured no build receipt in its capture window: no build attributable to this edit (pending_init-invisible, finished before capture, or queued behind an earlier build). No re-trigger issued — a blind re-trigger on an unknown outcome duplicated the edit on 2026-09-12 and the platform offers no idempotency proof."
1663
+ }, totalReworkCount);
1664
+ return await parkTask("Publish outcome unknown: the rebuild trigger reported the edit was accepted but captured no build receipt (build_agent_id null) — no build could be attributed to this edit in the capture window. The edit may be pending_init-invisible, already finished, or queued behind an earlier build, so no re-trigger 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 via the ledger and re-drive Publish only after the parent's content read-back resolves what actually landed. Fail-closed.");
1665
+ }
1666
+ rebuildAgentId = triggerAgentId;
1667
+ log("Publish receipt chained for task " + taskId + ": build " + triggerAgentId + " — the follow-up poll waits on this build only.");
1668
+ }
1622
1669
  // Durable publish-attempt ledger: record the trigger outcome while the
1623
1670
  // attempt key and commit are in scope. Every attempt lands here with
1624
1671
  // its outcome — submitted, rejected, or unknown (unknown is recorded
@@ -1722,10 +1769,13 @@ while (i < STEPS.length) {
1722
1769
  ? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
1723
1770
  : attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
1724
1771
  buildPoll = await agent(
1725
- "First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" until no build is running. Check every 30 seconds, up to 7 checks (3.5 minutes max).\n" +
1726
- "Return JSON { \"build_done\": <true if no build is running within budget, false on timeout>, \"status\": \"<final status or timeout note>\" } and nothing else.",
1772
+ "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" +
1773
+ "- If no build is running (build is null): OUR build finished. Stop and report done.\n" +
1774
+ "- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
1775
+ "- If the running build's agent_id is present but DIFFERENT: our build is gone (it finished before this one started). Do NOT wait on the stranger's build and do NOT attribute its completion to our attempt \u2014 stop and report done.\n" +
1776
+ "Return JSON { \"build_done\": <true if our build is no longer running within budget, false on timeout>, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
1727
1777
  { key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
1728
- schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" } }, required: ["build_done"] },
1778
+ schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
1729
1779
  timeoutMs: 270000 }
1730
1780
  );
1731
1781
  if (buildPoll && buildPoll.build_done) { break; }
@@ -125,6 +125,40 @@ ok("string and object returns converge identically (deep equal)", () => {
125
125
  assert.deepStrictEqual(fromString, fromObject);
126
126
  });
127
127
 
128
+ // ── result-as-string envelope (2026-09-13 17:00 PDT tick) ───────────
129
+ // The read-board agent returned {"status":"ok","result":"<board JSON>"} —
130
+ // the whole board stdout as a string in .result. unwrapBoardResult must
131
+ // converge it the same way it converges every other envelope.
132
+ ok("envelope string {status, result: board-JSON-string} converges", () => {
133
+ const boardData = converge(JSON.stringify({ status: "ok", result: JSON.stringify(BOARD) }));
134
+ assert.deepStrictEqual(boardData.ready_tasks, TASKS);
135
+ assert.deepStrictEqual(boardData.projects, PROJECTS);
136
+ });
137
+
138
+ ok("envelope object {status, result: board-JSON-string} converges", () => {
139
+ const boardData = converge({ status: "ok", result: JSON.stringify(BOARD) });
140
+ assert.deepStrictEqual(boardData.ready_tasks, TASKS);
141
+ assert.deepStrictEqual(boardData.projects, PROJECTS);
142
+ });
143
+
144
+ ok("envelope {status, result: tasks-array-JSON-string} converges", () => {
145
+ const boardData = converge({ status: "ok", result: JSON.stringify(TASKS) });
146
+ assert.deepStrictEqual(boardData.ready_tasks, TASKS);
147
+ });
148
+
149
+ ok("doubly-nested envelope {status, result: \"{result:{ready_tasks}}\"} converges", () => {
150
+ const inner = JSON.stringify({ result: { ready_tasks: TASKS } });
151
+ const boardData = converge(JSON.stringify({ status: "ok", result: inner }));
152
+ assert.deepStrictEqual(boardData.ready_tasks, TASKS);
153
+ });
154
+
155
+ throwsClosed("envelope {status, result: unparseable-string} throws", () =>
156
+ converge(JSON.stringify({ status: "ok", result: "{not json" })));
157
+ throwsClosed("envelope {status, result: scalar-string} throws", () =>
158
+ converge(JSON.stringify({ status: "ok", result: "42" })));
159
+ throwsClosed("envelope {status, result: JSON-without-ready_tasks} throws", () =>
160
+ converge(JSON.stringify({ status: "ok", result: JSON.stringify({ foo: 1 }) })));
161
+
128
162
  // ── Fail-closed: never a silent empty task set ──────────────────────
129
163
  throwsClosed("invalid JSON string throws", () => converge("{not json"));
130
164
  throwsClosed("truncated JSON throws", () => converge('{"ready_tasks": ['));