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.
- package/docs/publish-verification.md +1 -1
- package/package.json +1 -1
- package/seed/cron-body-template.md +1 -1
- package/workflows/bugfix.js +60 -10
- package/workflows/chore.js +60 -10
- package/workflows/crew-dispatch.js +29 -1
- package/workflows/standard.js +60 -10
- package/workflows/tests/read-board-parse.test.mjs +34 -0
|
@@ -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
|
|
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
|
@@ -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
|
|
package/workflows/bugfix.js
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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
|
|
1583
|
-
//
|
|
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 + "\"
|
|
1754
|
-
"
|
|
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; }
|
package/workflows/chore.js
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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
|
|
1572
|
-
//
|
|
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 + "\"
|
|
1743
|
-
"
|
|
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
|
);
|
package/workflows/standard.js
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
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
|
|
1555
|
-
//
|
|
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 + "\"
|
|
1726
|
-
"
|
|
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": ['));
|