muse-crew 0.7.8 → 0.7.10
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 +68 -14
- package/workflows/chore.js +68 -14
- package/workflows/crew-dispatch.js +29 -1
- package/workflows/standard.js +68 -14
- 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,
|
|
@@ -1469,11 +1469,15 @@ while (i < STEPS.length) {
|
|
|
1469
1469
|
if (/^rename from /m.test(mergeDiff)) {
|
|
1470
1470
|
return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
1471
1471
|
}
|
|
1472
|
-
var mergeDiffLines = mergeDiff.split("\n").length;
|
|
1473
|
-
if (mergeDiffLines > 200) {
|
|
1474
|
-
return await parkTask("Publish diff is " + mergeDiffLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1475
|
-
}
|
|
1476
1472
|
var expectedChanges = parseUnifiedDiff(mergeDiff);
|
|
1473
|
+
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1474
|
+
// output lines: context lines and file headers inflated the old
|
|
1475
|
+
// split("\n").length count ~2x, parking a 95-line change against a
|
|
1476
|
+
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13).
|
|
1477
|
+
var mergeDiffChangedLines = expectedChanges.reduce(function (n, f) { return n + f.added.length + f.removed.length; }, 0);
|
|
1478
|
+
if (mergeDiffChangedLines > 200) {
|
|
1479
|
+
return await parkTask("Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1480
|
+
}
|
|
1477
1481
|
if (expectedChanges.length === 0) {
|
|
1478
1482
|
return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
1479
1483
|
}
|
|
@@ -1521,12 +1525,22 @@ while (i < STEPS.length) {
|
|
|
1521
1525
|
"- Report, for each file you changed: its path, the exact lines you added, and the exact lines you removed.\n" +
|
|
1522
1526
|
buildPreHashInstruction(expectedChanges) + "'\n" +
|
|
1523
1527
|
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
|
-
"
|
|
1528
|
+
"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" +
|
|
1529
|
+
"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" +
|
|
1530
|
+
"- 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" +
|
|
1531
|
+
"- Call artifact_edit as instructed above.\n" +
|
|
1532
|
+
"- 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" +
|
|
1533
|
+
"- 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" +
|
|
1534
|
+
"- 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" +
|
|
1535
|
+
"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
1536
|
var rebuildSchema =
|
|
1527
1537
|
{ type: "object",
|
|
1528
1538
|
properties: {
|
|
1529
1539
|
edit_started: { type: "boolean" },
|
|
1540
|
+
build_agent_id: {
|
|
1541
|
+
type: ["string", "null"],
|
|
1542
|
+
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."
|
|
1543
|
+
},
|
|
1530
1544
|
error: { type: "string" },
|
|
1531
1545
|
applied: {
|
|
1532
1546
|
type: "array",
|
|
@@ -1545,7 +1559,7 @@ while (i < STEPS.length) {
|
|
|
1545
1559
|
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
1560
|
}
|
|
1547
1561
|
},
|
|
1548
|
-
required: ["edit_started", "applied"] };
|
|
1562
|
+
required: ["edit_started", "build_agent_id", "applied"] };
|
|
1549
1563
|
var rebuildTrigger = null;
|
|
1550
1564
|
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
1565
|
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
@@ -1579,8 +1593,9 @@ while (i < STEPS.length) {
|
|
|
1579
1593
|
//
|
|
1580
1594
|
// Build-ID research (2026-09-12) corrected the model: artifact.edit
|
|
1581
1595
|
// returns pending_init with NO agent_id, but artifact_status exposes
|
|
1582
|
-
// the build's agent_id (the artifact system's
|
|
1583
|
-
//
|
|
1596
|
+
// the build's agent_id (the artifact system's in-flight build
|
|
1597
|
+
// correlation ID, stable across polls while the build runs)
|
|
1598
|
+
// immediately after acceptance.
|
|
1584
1599
|
// So the recovery no longer asks the child to derive booleans —
|
|
1585
1600
|
// the layer where the 2026-09-12 signal was lost. It reads the RAW
|
|
1586
1601
|
// build object and extracts build.agent_id mechanically in the
|
|
@@ -1647,6 +1662,42 @@ while (i < STEPS.length) {
|
|
|
1647
1662
|
{ key: attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount), label: "Triggering artifact rebuild (retry)", schema: rebuildSchema });
|
|
1648
1663
|
rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount);
|
|
1649
1664
|
}
|
|
1665
|
+
// Receipt chaining (2026-09-13): adopt the trigger's build receipt,
|
|
1666
|
+
// or park on an uncorrelated acceptance. The trigger's closeout schema
|
|
1667
|
+
// requires build_agent_id — the platform build's in-flight correlation
|
|
1668
|
+
// ID captured immediately after the edit was accepted.
|
|
1669
|
+
// An accepted edit (edit_started=true) with a null receipt is UNKNOWN,
|
|
1670
|
+
// not "did not go through": the build may be pending_init-invisible,
|
|
1671
|
+
// may have finished before the capture window, or may be queued behind
|
|
1672
|
+
// a still-running earlier build. No re-trigger is issued on unknown —
|
|
1673
|
+
// a blind re-trigger duplicated the edit on 2026-09-12, and the platform
|
|
1674
|
+
// offers no idempotency proof that would make re-issue safe.
|
|
1675
|
+
// (Retry-semantics reconciliation, 2026-09-13: the "no receipt → safe
|
|
1676
|
+
// re-trigger" sketch assumed the edit command idempotently publishes
|
|
1677
|
+
// what's on git; the duplicate-edit incident disproves the assumption,
|
|
1678
|
+
// and the standing rule retries only on explicit negative evidence.
|
|
1679
|
+
// Re-trigger stays exactly where it was: the edit_started=false
|
|
1680
|
+
// explicit-rejection path above.) Record the attempt and park
|
|
1681
|
+
// fail-closed; correlate via the ledger and the parent's content
|
|
1682
|
+
// read-back before re-driving Publish.
|
|
1683
|
+
if (rebuildTrigger && rebuildTrigger.edit_started && !rebuildReportMissing) {
|
|
1684
|
+
var triggerAgentId = (typeof rebuildTrigger.build_agent_id === "string" && rebuildTrigger.build_agent_id.length > 0) ? rebuildTrigger.build_agent_id : null;
|
|
1685
|
+
if (!triggerAgentId) {
|
|
1686
|
+
var uncorrelatedObservation = (function () { var c = verifyAppliedChanges(expectedChanges, rebuildTrigger.applied); return c.ok ? "match" : "mismatch: " + c.reason; })();
|
|
1687
|
+
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.");
|
|
1688
|
+
await recordPublishLedger({
|
|
1689
|
+
commit: mergeCommitForPublish,
|
|
1690
|
+
attempt: rebuildAttemptKey,
|
|
1691
|
+
agent_id: null,
|
|
1692
|
+
applied_report: uncorrelatedObservation,
|
|
1693
|
+
outcome: "unknown",
|
|
1694
|
+
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."
|
|
1695
|
+
}, totalReworkCount);
|
|
1696
|
+
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.");
|
|
1697
|
+
}
|
|
1698
|
+
rebuildAgentId = triggerAgentId;
|
|
1699
|
+
log("Publish receipt chained for task " + taskId + ": build " + triggerAgentId + " — the follow-up poll waits on this build only.");
|
|
1700
|
+
}
|
|
1650
1701
|
// Durable publish-attempt ledger: record the trigger outcome while the
|
|
1651
1702
|
// attempt key and commit are in scope. Every attempt lands here with
|
|
1652
1703
|
// its outcome — submitted, rejected, or unknown (unknown is recorded
|
|
@@ -1750,10 +1801,13 @@ while (i < STEPS.length) {
|
|
|
1750
1801
|
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1751
1802
|
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1752
1803
|
buildPoll = await agent(
|
|
1753
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\"
|
|
1754
|
-
"
|
|
1804
|
+
"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" +
|
|
1805
|
+
"- If no build is running (build is null): OUR build finished. Stop and report done.\n" +
|
|
1806
|
+
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1807
|
+
"- 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" +
|
|
1808
|
+
"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
1809
|
{ 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"] },
|
|
1810
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1757
1811
|
timeoutMs: 270000 }
|
|
1758
1812
|
);
|
|
1759
1813
|
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,
|
|
@@ -1458,11 +1458,15 @@ while (i < STEPS.length) {
|
|
|
1458
1458
|
if (/^rename from /m.test(mergeDiff)) {
|
|
1459
1459
|
return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
1460
1460
|
}
|
|
1461
|
-
var mergeDiffLines = mergeDiff.split("\n").length;
|
|
1462
|
-
if (mergeDiffLines > 200) {
|
|
1463
|
-
return await parkTask("Publish diff is " + mergeDiffLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1464
|
-
}
|
|
1465
1461
|
var expectedChanges = parseUnifiedDiff(mergeDiff);
|
|
1462
|
+
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1463
|
+
// output lines: context lines and file headers inflated the old
|
|
1464
|
+
// split("\n").length count ~2x, parking a 95-line change against a
|
|
1465
|
+
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13).
|
|
1466
|
+
var mergeDiffChangedLines = expectedChanges.reduce(function (n, f) { return n + f.added.length + f.removed.length; }, 0);
|
|
1467
|
+
if (mergeDiffChangedLines > 200) {
|
|
1468
|
+
return await parkTask("Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1469
|
+
}
|
|
1466
1470
|
if (expectedChanges.length === 0) {
|
|
1467
1471
|
return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
1468
1472
|
}
|
|
@@ -1510,12 +1514,22 @@ while (i < STEPS.length) {
|
|
|
1510
1514
|
"- Report, for each file you changed: its path, the exact lines you added, and the exact lines you removed.\n" +
|
|
1511
1515
|
buildPreHashInstruction(expectedChanges) + "'\n" +
|
|
1512
1516
|
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
|
-
"
|
|
1517
|
+
"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" +
|
|
1518
|
+
"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" +
|
|
1519
|
+
"- 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" +
|
|
1520
|
+
"- Call artifact_edit as instructed above.\n" +
|
|
1521
|
+
"- 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" +
|
|
1522
|
+
"- 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" +
|
|
1523
|
+
"- 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" +
|
|
1524
|
+
"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
1525
|
var rebuildSchema =
|
|
1516
1526
|
{ type: "object",
|
|
1517
1527
|
properties: {
|
|
1518
1528
|
edit_started: { type: "boolean" },
|
|
1529
|
+
build_agent_id: {
|
|
1530
|
+
type: ["string", "null"],
|
|
1531
|
+
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."
|
|
1532
|
+
},
|
|
1519
1533
|
error: { type: "string" },
|
|
1520
1534
|
applied: {
|
|
1521
1535
|
type: "array",
|
|
@@ -1534,7 +1548,7 @@ while (i < STEPS.length) {
|
|
|
1534
1548
|
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
1549
|
}
|
|
1536
1550
|
},
|
|
1537
|
-
required: ["edit_started", "applied"] };
|
|
1551
|
+
required: ["edit_started", "build_agent_id", "applied"] };
|
|
1538
1552
|
var rebuildTrigger = null;
|
|
1539
1553
|
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
1554
|
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
@@ -1568,8 +1582,9 @@ while (i < STEPS.length) {
|
|
|
1568
1582
|
//
|
|
1569
1583
|
// Build-ID research (2026-09-12) corrected the model: artifact.edit
|
|
1570
1584
|
// returns pending_init with NO agent_id, but artifact_status exposes
|
|
1571
|
-
// the build's agent_id (the artifact system's
|
|
1572
|
-
//
|
|
1585
|
+
// the build's agent_id (the artifact system's in-flight build
|
|
1586
|
+
// correlation ID, stable across polls while the build runs)
|
|
1587
|
+
// immediately after acceptance.
|
|
1573
1588
|
// So the recovery no longer asks the child to derive booleans —
|
|
1574
1589
|
// the layer where the 2026-09-12 signal was lost. It reads the RAW
|
|
1575
1590
|
// build object and extracts build.agent_id mechanically in the
|
|
@@ -1636,6 +1651,42 @@ while (i < STEPS.length) {
|
|
|
1636
1651
|
{ key: attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", reworkCount), label: "Triggering artifact rebuild (retry)", schema: rebuildSchema });
|
|
1637
1652
|
rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", reworkCount);
|
|
1638
1653
|
}
|
|
1654
|
+
// Receipt chaining (2026-09-13): adopt the trigger's build receipt,
|
|
1655
|
+
// or park on an uncorrelated acceptance. The trigger's closeout schema
|
|
1656
|
+
// requires build_agent_id — the platform build's in-flight correlation
|
|
1657
|
+
// ID captured immediately after the edit was accepted.
|
|
1658
|
+
// An accepted edit (edit_started=true) with a null receipt is UNKNOWN,
|
|
1659
|
+
// not "did not go through": the build may be pending_init-invisible,
|
|
1660
|
+
// may have finished before the capture window, or may be queued behind
|
|
1661
|
+
// a still-running earlier build. No re-trigger is issued on unknown —
|
|
1662
|
+
// a blind re-trigger duplicated the edit on 2026-09-12, and the platform
|
|
1663
|
+
// offers no idempotency proof that would make re-issue safe.
|
|
1664
|
+
// (Retry-semantics reconciliation, 2026-09-13: the "no receipt → safe
|
|
1665
|
+
// re-trigger" sketch assumed the edit command idempotently publishes
|
|
1666
|
+
// what's on git; the duplicate-edit incident disproves the assumption,
|
|
1667
|
+
// and the standing rule retries only on explicit negative evidence.
|
|
1668
|
+
// Re-trigger stays exactly where it was: the edit_started=false
|
|
1669
|
+
// explicit-rejection path above.) Record the attempt and park
|
|
1670
|
+
// fail-closed; correlate via the ledger and the parent's content
|
|
1671
|
+
// read-back before re-driving Publish.
|
|
1672
|
+
if (rebuildTrigger && rebuildTrigger.edit_started && !rebuildReportMissing) {
|
|
1673
|
+
var triggerAgentId = (typeof rebuildTrigger.build_agent_id === "string" && rebuildTrigger.build_agent_id.length > 0) ? rebuildTrigger.build_agent_id : null;
|
|
1674
|
+
if (!triggerAgentId) {
|
|
1675
|
+
var uncorrelatedObservation = (function () { var c = verifyAppliedChanges(expectedChanges, rebuildTrigger.applied); return c.ok ? "match" : "mismatch: " + c.reason; })();
|
|
1676
|
+
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.");
|
|
1677
|
+
await recordPublishLedger({
|
|
1678
|
+
commit: mergeCommitForPublish,
|
|
1679
|
+
attempt: rebuildAttemptKey,
|
|
1680
|
+
agent_id: null,
|
|
1681
|
+
applied_report: uncorrelatedObservation,
|
|
1682
|
+
outcome: "unknown",
|
|
1683
|
+
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."
|
|
1684
|
+
}, reworkCount);
|
|
1685
|
+
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.");
|
|
1686
|
+
}
|
|
1687
|
+
rebuildAgentId = triggerAgentId;
|
|
1688
|
+
log("Publish receipt chained for task " + taskId + ": build " + triggerAgentId + " — the follow-up poll waits on this build only.");
|
|
1689
|
+
}
|
|
1639
1690
|
// Durable publish-attempt ledger: record the trigger outcome while the
|
|
1640
1691
|
// attempt key and commit are in scope. Every attempt lands here with
|
|
1641
1692
|
// its outcome — submitted, rejected, or unknown (unknown is recorded
|
|
@@ -1739,10 +1790,13 @@ while (i < STEPS.length) {
|
|
|
1739
1790
|
? attemptKey("publish-artifact-poll-" + taskId, reworkCount)
|
|
1740
1791
|
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, reworkCount);
|
|
1741
1792
|
buildPoll = await agent(
|
|
1742
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\"
|
|
1743
|
-
"
|
|
1793
|
+
"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" +
|
|
1794
|
+
"- If no build is running (build is null): OUR build finished. Stop and report done.\n" +
|
|
1795
|
+
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1796
|
+
"- 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" +
|
|
1797
|
+
"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
1798
|
{ 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"] },
|
|
1799
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1746
1800
|
timeoutMs: 270000 }
|
|
1747
1801
|
);
|
|
1748
1802
|
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,
|
|
@@ -1441,11 +1441,15 @@ while (i < STEPS.length) {
|
|
|
1441
1441
|
if (/^rename from /m.test(mergeDiff)) {
|
|
1442
1442
|
return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
1443
1443
|
}
|
|
1444
|
-
var mergeDiffLines = mergeDiff.split("\n").length;
|
|
1445
|
-
if (mergeDiffLines > 200) {
|
|
1446
|
-
return await parkTask("Publish diff is " + mergeDiffLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1447
|
-
}
|
|
1448
1444
|
var expectedChanges = parseUnifiedDiff(mergeDiff);
|
|
1445
|
+
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1446
|
+
// output lines: context lines and file headers inflated the old
|
|
1447
|
+
// split("\n").length count ~2x, parking a 95-line change against a
|
|
1448
|
+
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13).
|
|
1449
|
+
var mergeDiffChangedLines = expectedChanges.reduce(function (n, f) { return n + f.added.length + f.removed.length; }, 0);
|
|
1450
|
+
if (mergeDiffChangedLines > 200) {
|
|
1451
|
+
return await parkTask("Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1452
|
+
}
|
|
1449
1453
|
if (expectedChanges.length === 0) {
|
|
1450
1454
|
return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
1451
1455
|
}
|
|
@@ -1493,12 +1497,22 @@ while (i < STEPS.length) {
|
|
|
1493
1497
|
"- Report, for each file you changed: its path, the exact lines you added, and the exact lines you removed.\n" +
|
|
1494
1498
|
buildPreHashInstruction(expectedChanges) + "'\n" +
|
|
1495
1499
|
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
|
-
"
|
|
1500
|
+
"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" +
|
|
1501
|
+
"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" +
|
|
1502
|
+
"- 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" +
|
|
1503
|
+
"- Call artifact_edit as instructed above.\n" +
|
|
1504
|
+
"- 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" +
|
|
1505
|
+
"- 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" +
|
|
1506
|
+
"- 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" +
|
|
1507
|
+
"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
1508
|
var rebuildSchema =
|
|
1499
1509
|
{ type: "object",
|
|
1500
1510
|
properties: {
|
|
1501
1511
|
edit_started: { type: "boolean" },
|
|
1512
|
+
build_agent_id: {
|
|
1513
|
+
type: ["string", "null"],
|
|
1514
|
+
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."
|
|
1515
|
+
},
|
|
1502
1516
|
error: { type: "string" },
|
|
1503
1517
|
applied: {
|
|
1504
1518
|
type: "array",
|
|
@@ -1517,7 +1531,7 @@ while (i < STEPS.length) {
|
|
|
1517
1531
|
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
1532
|
}
|
|
1519
1533
|
},
|
|
1520
|
-
required: ["edit_started", "applied"] };
|
|
1534
|
+
required: ["edit_started", "build_agent_id", "applied"] };
|
|
1521
1535
|
var rebuildTrigger = null;
|
|
1522
1536
|
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
1537
|
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
@@ -1551,8 +1565,9 @@ while (i < STEPS.length) {
|
|
|
1551
1565
|
//
|
|
1552
1566
|
// Build-ID research (2026-09-12) corrected the model: artifact.edit
|
|
1553
1567
|
// returns pending_init with NO agent_id, but artifact_status exposes
|
|
1554
|
-
// the build's agent_id (the artifact system's
|
|
1555
|
-
//
|
|
1568
|
+
// the build's agent_id (the artifact system's in-flight build
|
|
1569
|
+
// correlation ID, stable across polls while the build runs)
|
|
1570
|
+
// immediately after acceptance.
|
|
1556
1571
|
// So the recovery no longer asks the child to derive booleans —
|
|
1557
1572
|
// the layer where the 2026-09-12 signal was lost. It reads the RAW
|
|
1558
1573
|
// build object and extracts build.agent_id mechanically in the
|
|
@@ -1619,6 +1634,42 @@ while (i < STEPS.length) {
|
|
|
1619
1634
|
{ key: attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount), label: "Triggering artifact rebuild (retry)", schema: rebuildSchema });
|
|
1620
1635
|
rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId + "-retry2", totalReworkCount);
|
|
1621
1636
|
}
|
|
1637
|
+
// Receipt chaining (2026-09-13): adopt the trigger's build receipt,
|
|
1638
|
+
// or park on an uncorrelated acceptance. The trigger's closeout schema
|
|
1639
|
+
// requires build_agent_id — the platform build's in-flight correlation
|
|
1640
|
+
// ID captured immediately after the edit was accepted.
|
|
1641
|
+
// An accepted edit (edit_started=true) with a null receipt is UNKNOWN,
|
|
1642
|
+
// not "did not go through": the build may be pending_init-invisible,
|
|
1643
|
+
// may have finished before the capture window, or may be queued behind
|
|
1644
|
+
// a still-running earlier build. No re-trigger is issued on unknown —
|
|
1645
|
+
// a blind re-trigger duplicated the edit on 2026-09-12, and the platform
|
|
1646
|
+
// offers no idempotency proof that would make re-issue safe.
|
|
1647
|
+
// (Retry-semantics reconciliation, 2026-09-13: the "no receipt → safe
|
|
1648
|
+
// re-trigger" sketch assumed the edit command idempotently publishes
|
|
1649
|
+
// what's on git; the duplicate-edit incident disproves the assumption,
|
|
1650
|
+
// and the standing rule retries only on explicit negative evidence.
|
|
1651
|
+
// Re-trigger stays exactly where it was: the edit_started=false
|
|
1652
|
+
// explicit-rejection path above.) Record the attempt and park
|
|
1653
|
+
// fail-closed; correlate via the ledger and the parent's content
|
|
1654
|
+
// read-back before re-driving Publish.
|
|
1655
|
+
if (rebuildTrigger && rebuildTrigger.edit_started && !rebuildReportMissing) {
|
|
1656
|
+
var triggerAgentId = (typeof rebuildTrigger.build_agent_id === "string" && rebuildTrigger.build_agent_id.length > 0) ? rebuildTrigger.build_agent_id : null;
|
|
1657
|
+
if (!triggerAgentId) {
|
|
1658
|
+
var uncorrelatedObservation = (function () { var c = verifyAppliedChanges(expectedChanges, rebuildTrigger.applied); return c.ok ? "match" : "mismatch: " + c.reason; })();
|
|
1659
|
+
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.");
|
|
1660
|
+
await recordPublishLedger({
|
|
1661
|
+
commit: mergeCommitForPublish,
|
|
1662
|
+
attempt: rebuildAttemptKey,
|
|
1663
|
+
agent_id: null,
|
|
1664
|
+
applied_report: uncorrelatedObservation,
|
|
1665
|
+
outcome: "unknown",
|
|
1666
|
+
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."
|
|
1667
|
+
}, totalReworkCount);
|
|
1668
|
+
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.");
|
|
1669
|
+
}
|
|
1670
|
+
rebuildAgentId = triggerAgentId;
|
|
1671
|
+
log("Publish receipt chained for task " + taskId + ": build " + triggerAgentId + " — the follow-up poll waits on this build only.");
|
|
1672
|
+
}
|
|
1622
1673
|
// Durable publish-attempt ledger: record the trigger outcome while the
|
|
1623
1674
|
// attempt key and commit are in scope. Every attempt lands here with
|
|
1624
1675
|
// its outcome — submitted, rejected, or unknown (unknown is recorded
|
|
@@ -1722,10 +1773,13 @@ while (i < STEPS.length) {
|
|
|
1722
1773
|
? attemptKey("publish-artifact-poll-" + taskId, totalReworkCount)
|
|
1723
1774
|
: attemptKey("publish-artifact-poll-" + taskId + "-c" + chunk, totalReworkCount);
|
|
1724
1775
|
buildPoll = await agent(
|
|
1725
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\"
|
|
1726
|
-
"
|
|
1776
|
+
"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" +
|
|
1777
|
+
"- If no build is running (build is null): OUR build finished. Stop and report done.\n" +
|
|
1778
|
+
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1779
|
+
"- 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" +
|
|
1780
|
+
"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
1781
|
{ 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"] },
|
|
1782
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1729
1783
|
timeoutMs: 270000 }
|
|
1730
1784
|
);
|
|
1731
1785
|
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": ['));
|