muse-crew 0.7.7 → 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/guide.md +24 -0
- 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/crew-init.js +28 -8
- package/workflows/crew-uninstall.js +264 -0
- package/workflows/standard.js +60 -10
- package/workflows/tests/read-board-parse.test.mjs +34 -0
package/docs/guide.md
CHANGED
|
@@ -99,6 +99,30 @@ After init completes, it returns a summary:
|
|
|
99
99
|
|
|
100
100
|
Re-running init against an existing setup returns `"unchanged"` (or `"updated"` with the converged field names) for each cron, with no other mutations.
|
|
101
101
|
|
|
102
|
+
### Uninstall
|
|
103
|
+
|
|
104
|
+
Uninstalling a crew removes its scheduler crons and deletes the crew home — one flow, never a manual two-step. Launch it as a workflow through the agent's workflow tools:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
workflow_launch with:
|
|
108
|
+
scriptPath: "<install-path>/workflows/crew-uninstall.js"
|
|
109
|
+
args: {
|
|
110
|
+
crewHome: "~/workspace/.my-crew",
|
|
111
|
+
confirm: true
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`crewHome` is required; `confirm` must be exactly `true` or the run blocks. `force` (default `false`) overrides the live-work check described below.
|
|
116
|
+
|
|
117
|
+
The run has four phases:
|
|
118
|
+
|
|
119
|
+
1. **Gates** — the crew home must be inside the workspace (uninstall refuses to delete anything outside it), and `confirm: true` must be present. Either missing blocks the run before anything is touched.
|
|
120
|
+
2. **Safety** — if any task is `in_progress`, the run blocks unless `force: true`. Uninstalling under live workflows would strand them.
|
|
121
|
+
3. **Crons** — removes the crew's scheduler jobs, and only those: the exact ids from `$CREW_HOME/.cron-registry.json` (written by init; covers id overrides), plus discovery of `crew-poll-*` jobs whose body contains the crew home path (covers installs from before the registry existed). Anything not on that union is never touched — shared crons are safe by construction, and a missing home still gets its orphaned crons removed.
|
|
122
|
+
4. **Home** — best-effort `git worktree prune` on the crew's project repos, then deletes the crew home directory and verifies it is gone.
|
|
123
|
+
|
|
124
|
+
Dashboard artifacts are left untouched — uninstall removes the crew instance (its crons and its home), never the user's artifacts.
|
|
125
|
+
|
|
102
126
|
## Connecting an existing project
|
|
103
127
|
|
|
104
128
|
Projects are registered through the API, not through init. Init sets up the infrastructure and registers the task service as the first project; you register additional projects separately.
|
|
@@ -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/crew-init.js
CHANGED
|
@@ -363,9 +363,18 @@ try {
|
|
|
363
363
|
" Call cron_update with only the fields that differ. If nothing differs,\n" +
|
|
364
364
|
" do not call cron_update. Record action 'updated' with the updated field\n" +
|
|
365
365
|
" names, or 'unchanged' with no updated fields.\n" +
|
|
366
|
-
"3.
|
|
367
|
-
"
|
|
368
|
-
"
|
|
366
|
+
"3. Write the cron registry at " + crewHome + "/.cron-registry.json — the exact\n" +
|
|
367
|
+
" record a later uninstall uses to find these jobs. Build this JSON (one entry\n" +
|
|
368
|
+
" per manifest entry, in manifest order, using the live ids from step 2):\n" +
|
|
369
|
+
" {\"version\":1,\"instanceId\":\"" + instanceId + "\",\"owner\":\"" + cronOwner + "\",\n" +
|
|
370
|
+
" \"crons\":[{\"id\":<live id>,\"manifestId\":<entry.id>}, ...]}\n" +
|
|
371
|
+
" Write it with printf '%s' and a single-quoted heredoc so the shell\n" +
|
|
372
|
+
" interpolates nothing, then read it back to confirm it parses.\n" +
|
|
373
|
+
"4. Return { passed: true, summary: { crons: [...], registry: {...} } } with one\n" +
|
|
374
|
+
" crons entry per manifest entry, in manifest order:\n" +
|
|
375
|
+
" { id, action: 'created' | 'updated' | 'unchanged', updated_fields: [...] },\n" +
|
|
376
|
+
" and registry: { path: \"" + crewHome + "/.cron-registry.json\",\n" +
|
|
377
|
+
" ids: [<live id>, ...] } echoing the ids written to disk.",
|
|
369
378
|
{
|
|
370
379
|
key: "crons-1",
|
|
371
380
|
label: "Create and converge cron jobs",
|
|
@@ -387,9 +396,17 @@ try {
|
|
|
387
396
|
},
|
|
388
397
|
required: ["id", "action", "updated_fields"]
|
|
389
398
|
}
|
|
399
|
+
},
|
|
400
|
+
registry: {
|
|
401
|
+
type: "object",
|
|
402
|
+
properties: {
|
|
403
|
+
path: { type: "string" },
|
|
404
|
+
ids: { type: "array", items: { type: "string" } }
|
|
405
|
+
},
|
|
406
|
+
required: ["path", "ids"]
|
|
390
407
|
}
|
|
391
408
|
},
|
|
392
|
-
required: ["crons"]
|
|
409
|
+
required: ["crons", "registry"]
|
|
393
410
|
}
|
|
394
411
|
},
|
|
395
412
|
required: ["passed", "summary"]
|
|
@@ -399,10 +416,12 @@ try {
|
|
|
399
416
|
} catch (e) {
|
|
400
417
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron setup failed", message: String(e.message || e) } };
|
|
401
418
|
}
|
|
402
|
-
if (!cronsResult.passed || !cronsResult.summary || !Array.isArray(cronsResult.summary.crons)
|
|
403
|
-
|
|
419
|
+
if (!cronsResult.passed || !cronsResult.summary || !Array.isArray(cronsResult.summary.crons) ||
|
|
420
|
+
!cronsResult.summary.registry || !Array.isArray(cronsResult.summary.registry.ids)) {
|
|
421
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron manifest invalid", message: "seed/crons.json failed validation, produced no crons list, or wrote no cron registry" } };
|
|
404
422
|
}
|
|
405
|
-
log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" + c.action; }).join(", ")
|
|
423
|
+
log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" + c.action; }).join(", ") +
|
|
424
|
+
"; registry: " + cronsResult.summary.registry.path + " (" + cronsResult.summary.registry.ids.join(", ") + ")");
|
|
406
425
|
|
|
407
426
|
|
|
408
427
|
// ── Summary ───────────────────────────────────────────────────────────
|
|
@@ -416,5 +435,6 @@ return {
|
|
|
416
435
|
releaseHash: releaseResult.hash,
|
|
417
436
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
418
437
|
project: projectResult.action,
|
|
419
|
-
crons: cronsResult.summary.crons
|
|
438
|
+
crons: cronsResult.summary.crons,
|
|
439
|
+
registry: cronsResult.summary.registry
|
|
420
440
|
};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
export const meta = {
|
|
2
|
+
name: "crew-uninstall",
|
|
3
|
+
description: "Uninstall a Muse Crew instance: remove its scheduler cron jobs, then delete the crew home directory. Destructive and irreversible — requires confirm: true, and refuses to run while any task is in_progress unless force: true. Only the crew's own crons are removed (exact registry plus body-match discovery); shared crons are never touched. Dashboard artifacts are left alone.",
|
|
4
|
+
phases: [
|
|
5
|
+
{ name: "gates", title: "Containment gate and explicit confirmation" },
|
|
6
|
+
{ name: "safety", title: "Refuse while work is live (unless forced)" },
|
|
7
|
+
{ name: "crons", title: "Remove the crew's scheduler crons" },
|
|
8
|
+
{ name: "home", title: "Delete the crew home directory" }
|
|
9
|
+
]
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ── Arguments ──────────────────────────────────────────────────────────
|
|
13
|
+
const inputs = args ?? {};
|
|
14
|
+
const crewHome = inputs.crewHome;
|
|
15
|
+
const confirm = inputs.confirm;
|
|
16
|
+
const force = inputs.force;
|
|
17
|
+
|
|
18
|
+
if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew");
|
|
19
|
+
|
|
20
|
+
// ── Pure decision helpers ──────────────────────────────────────────────
|
|
21
|
+
// The agent is a sensor, not a judge: it reports raw facts (HOME, expanded
|
|
22
|
+
// paths, existence, in-progress counts) and the verdicts below are computed
|
|
23
|
+
// here, deterministically. (Prompt hardening is a smell — this is the
|
|
24
|
+
// mechanical version.)
|
|
25
|
+
function expandTilde(p, home) {
|
|
26
|
+
if (p === "~") return home;
|
|
27
|
+
if (p.indexOf("~/") === 0) return home + p.slice(1);
|
|
28
|
+
return p;
|
|
29
|
+
}
|
|
30
|
+
function uninstallGateDecide(facts) {
|
|
31
|
+
// facts: { home, crewHomeExpanded }
|
|
32
|
+
// Uninstall deletes a directory tree — it must never run against a home
|
|
33
|
+
// outside the workspace. Same prefix rule as crew-init's Gate 0.
|
|
34
|
+
var workspace = facts.home + "/workspace";
|
|
35
|
+
var launchable = facts.crewHomeExpanded.indexOf(workspace + "/") === 0;
|
|
36
|
+
return { workspace: workspace, launchable: launchable };
|
|
37
|
+
}
|
|
38
|
+
function confirmDecide(c) {
|
|
39
|
+
// Strict: only the boolean true counts. "true", 1, and undefined do not.
|
|
40
|
+
if (c === true) return { ok: true, message: "" };
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
message: "crew-uninstall is destructive and irreversible: it removes the crew's scheduler cron jobs and deletes the crew home directory. Re-run with confirm: true to proceed."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function safetyDecide(inProgressCount, f) {
|
|
47
|
+
// Live work strands if its home disappears mid-run. Refuse unless forced.
|
|
48
|
+
if (inProgressCount > 0 && f !== true) {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
message: inProgressCount + " task(s) are in_progress. Uninstalling now would strand their workflows. Re-run with force: true to uninstall anyway, or finish/park the work first."
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { ok: true, message: "" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Gates: containment + confirmation ──────────────────────────────────
|
|
58
|
+
phase("gates");
|
|
59
|
+
var gateFacts;
|
|
60
|
+
try {
|
|
61
|
+
gateFacts = await agent(
|
|
62
|
+
"Report filesystem facts for a crew uninstall. Do not judge — just report.\n\n" +
|
|
63
|
+
"Requested crewHome: " + crewHome + "\n\n" +
|
|
64
|
+
"Steps (run in shell):\n" +
|
|
65
|
+
"1. home=$(echo $HOME)\n" +
|
|
66
|
+
"2. Expand a leading ~/ in the requested crewHome against $HOME (leave other paths untouched).\n" +
|
|
67
|
+
"3. Report whether the expanded path exists: test -e \"<expanded>\"; echo EXISTS:$?\n" +
|
|
68
|
+
"4. Return JSON { home, crewHomeExpanded, homeExists } where homeExists is true\n" +
|
|
69
|
+
" when the path exists.",
|
|
70
|
+
{
|
|
71
|
+
key: "uninstall-gates",
|
|
72
|
+
label: "Validate uninstall target",
|
|
73
|
+
schema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
home: { type: "string" },
|
|
77
|
+
crewHomeExpanded: { type: "string" },
|
|
78
|
+
homeExists: { type: "boolean" }
|
|
79
|
+
},
|
|
80
|
+
required: ["home", "crewHomeExpanded", "homeExists"]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Uninstall validation failed", message: String(e.message || e) } };
|
|
86
|
+
}
|
|
87
|
+
var gateResult = uninstallGateDecide(gateFacts);
|
|
88
|
+
if (!gateResult.launchable) {
|
|
89
|
+
return {
|
|
90
|
+
__hatchWorkflowControl: "blocked",
|
|
91
|
+
result: {
|
|
92
|
+
blocked_reason: "crewHome is not workspace-contained",
|
|
93
|
+
message: "crewHome must be inside the workspace (" + gateResult.workspace + "). Requested: " + gateFacts.crewHomeExpanded + ". Uninstall refuses to delete anything outside the workspace."
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
var crewHomeExpanded = gateFacts.crewHomeExpanded;
|
|
98
|
+
var confirmation = confirmDecide(confirm);
|
|
99
|
+
if (!confirmation.ok) {
|
|
100
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Uninstall not confirmed", message: confirmation.message } };
|
|
101
|
+
}
|
|
102
|
+
log("Uninstall confirmed for " + crewHomeExpanded + (gateFacts.homeExists ? "" : " (crew home already gone — removing orphaned crons only)"));
|
|
103
|
+
|
|
104
|
+
// ── Safety: refuse while work is live (unless forced) ───────────────────
|
|
105
|
+
phase("safety");
|
|
106
|
+
var safetyFacts = { homeExists: gateFacts.homeExists, inProgress: 0 };
|
|
107
|
+
if (gateFacts.homeExists) {
|
|
108
|
+
try {
|
|
109
|
+
safetyFacts = await agent(
|
|
110
|
+
"Report whether this crew has live work. Do not judge — just report.\n\n" +
|
|
111
|
+
"crewHome: " + crewHomeExpanded + "\n\n" +
|
|
112
|
+
"Steps (run in shell):\n" +
|
|
113
|
+
"1. If \"" + crewHomeExpanded + "/crew-state.db\" exists, run:\n" +
|
|
114
|
+
" sqlite3 \"" + crewHomeExpanded + "/crew-state.db\" \"SELECT COUNT(*) FROM tasks WHERE state='in_progress';\"\n" +
|
|
115
|
+
" Report the number. When the database is missing, the count is 0.\n" +
|
|
116
|
+
"2. Return JSON { homeExists: true, inProgress: <number> }.",
|
|
117
|
+
{
|
|
118
|
+
key: "uninstall-safety",
|
|
119
|
+
label: "Check for live work",
|
|
120
|
+
schema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
homeExists: { type: "boolean" },
|
|
124
|
+
inProgress: { type: "number" }
|
|
125
|
+
},
|
|
126
|
+
required: ["homeExists", "inProgress"]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Safety check failed", message: String(e.message || e) } };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
var safety = safetyDecide(safetyFacts.inProgress, force);
|
|
135
|
+
if (!safety.ok) {
|
|
136
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Live work in progress", message: safety.message } };
|
|
137
|
+
}
|
|
138
|
+
log("Safety: " + safetyFacts.inProgress + " in_progress task(s)" + (force === true ? " (forced)" : ""));
|
|
139
|
+
|
|
140
|
+
// ── Remove the crew's scheduler crons ──────────────────────────────────
|
|
141
|
+
// Two sources, unioned: the exact registry written by crew-init (covers id
|
|
142
|
+
// overrides), and discovery (covers installs from before the registry
|
|
143
|
+
// existed, and a registry that was never written). A job is only removed
|
|
144
|
+
// when it is on that union — never by guesswork.
|
|
145
|
+
phase("crons");
|
|
146
|
+
var cronsResult;
|
|
147
|
+
try {
|
|
148
|
+
cronsResult = await agent(
|
|
149
|
+
"Remove this crew instance's scheduler cron jobs — and nothing else.\n\n" +
|
|
150
|
+
"crewHome: " + crewHomeExpanded + "\n\n" +
|
|
151
|
+
"Steps:\n" +
|
|
152
|
+
"1. Registry: if \"" + crewHomeExpanded + "/.cron-registry.json\" exists, read it\n" +
|
|
153
|
+
" and take its crons[].id list as registry candidates.\n" +
|
|
154
|
+
"2. Discovery: call cron_list. For every job whose id starts with 'crew-poll-',\n" +
|
|
155
|
+
" call cron_view and keep it as a discovery candidate when the job body\n" +
|
|
156
|
+
" contains the crewHome string '" + crewHomeExpanded + "' (the poll body template\n" +
|
|
157
|
+
" embeds the crew home path). The bare id 'crew-poll' with no instance suffix\n" +
|
|
158
|
+
" is never a live crew job — never remove it.\n" +
|
|
159
|
+
"3. Removal list = registry candidates ∪ discovery candidates, deduped.\n" +
|
|
160
|
+
" SAFETY: remove only jobs on this list. When in doubt about any job, fail\n" +
|
|
161
|
+
" closed (passed: false) instead of guessing.\n" +
|
|
162
|
+
"4. For each id on the removal list: call cron_remove. A job that is already\n" +
|
|
163
|
+
" gone records action 'already_gone'.\n" +
|
|
164
|
+
"5. Call cron_list again and confirm none of the removed ids remain. If any\n" +
|
|
165
|
+
" remain, return passed: false.\n" +
|
|
166
|
+
"6. Return { passed: true, summary: { removed: [...] } } with one entry per\n" +
|
|
167
|
+
" removal-list id: { id, source: 'registry' | 'discovery',\n" +
|
|
168
|
+
" action: 'removed' | 'already_gone' }. An empty removal list is a pass\n" +
|
|
169
|
+
" with removed: [].",
|
|
170
|
+
{
|
|
171
|
+
key: "uninstall-crons",
|
|
172
|
+
label: "Remove the crew's crons",
|
|
173
|
+
schema: {
|
|
174
|
+
type: "object",
|
|
175
|
+
properties: {
|
|
176
|
+
passed: { type: "boolean" },
|
|
177
|
+
summary: {
|
|
178
|
+
type: "object",
|
|
179
|
+
properties: {
|
|
180
|
+
removed: {
|
|
181
|
+
type: "array",
|
|
182
|
+
items: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
id: { type: "string" },
|
|
186
|
+
source: { type: "string", enum: ["registry", "discovery"] },
|
|
187
|
+
action: { type: "string", enum: ["removed", "already_gone"] }
|
|
188
|
+
},
|
|
189
|
+
required: ["id", "source", "action"]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ["removed"]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
required: ["passed", "summary"]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron removal failed", message: String(e.message || e) } };
|
|
202
|
+
}
|
|
203
|
+
if (!cronsResult.passed || !cronsResult.summary || !Array.isArray(cronsResult.summary.removed)) {
|
|
204
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron removal failed", message: "The cron-removal agent did not pass. No further teardown was attempted; the crew home is untouched." } };
|
|
205
|
+
}
|
|
206
|
+
var removedDesc = cronsResult.summary.removed.map(function (r) { return r.id + "=" + r.action; }).join(", ");
|
|
207
|
+
log("Crons: " + (removedDesc || "(none belonged to this crew)"));
|
|
208
|
+
|
|
209
|
+
// ── Delete the crew home directory ─────────────────────────────────────
|
|
210
|
+
phase("home");
|
|
211
|
+
var homeRemoved = false;
|
|
212
|
+
if (gateFacts.homeExists) {
|
|
213
|
+
var homeResult;
|
|
214
|
+
try {
|
|
215
|
+
homeResult = await agent(
|
|
216
|
+
"Delete the crew home directory.\n\n" +
|
|
217
|
+
"crewHome: " + crewHomeExpanded + "\n\n" +
|
|
218
|
+
"Steps:\n" +
|
|
219
|
+
"1. Best-effort worktree hygiene (failures do not block): if\n" +
|
|
220
|
+
" \"" + crewHomeExpanded + "/crew-state.db\" exists, list the repo_path values\n" +
|
|
221
|
+
" from its projects table and run 'git worktree prune' in each. Ignore errors.\n" +
|
|
222
|
+
"2. Run: rm -rf \"" + crewHomeExpanded + "\"\n" +
|
|
223
|
+
"3. Verify: test ! -e \"" + crewHomeExpanded + "\" — fail closed (passed: false)\n" +
|
|
224
|
+
" if the path still exists.\n" +
|
|
225
|
+
"4. Return { passed: true, summary: { removed: true } }.",
|
|
226
|
+
{
|
|
227
|
+
key: "uninstall-home",
|
|
228
|
+
label: "Delete the crew home",
|
|
229
|
+
schema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
passed: { type: "boolean" },
|
|
233
|
+
summary: {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
removed: { type: "boolean" }
|
|
237
|
+
},
|
|
238
|
+
required: ["removed"]
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
required: ["passed", "summary"]
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Crew home removal failed", message: String(e.message || e) } };
|
|
247
|
+
}
|
|
248
|
+
if (!homeResult.passed || !homeResult.summary || homeResult.summary.removed !== true) {
|
|
249
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Crew home removal failed", message: "The crew home could not be deleted. Its crons were already removed; delete " + crewHomeExpanded + " manually." } };
|
|
250
|
+
}
|
|
251
|
+
homeRemoved = true;
|
|
252
|
+
log("Crew home deleted: " + crewHomeExpanded);
|
|
253
|
+
} else {
|
|
254
|
+
log("Crew home already gone — nothing to delete.");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Summary ────────────────────────────────────────────────────────────
|
|
258
|
+
return {
|
|
259
|
+
message: "Muse Crew uninstalled.",
|
|
260
|
+
crewHome: crewHomeExpanded,
|
|
261
|
+
removed_crons: cronsResult.summary.removed,
|
|
262
|
+
crew_home_removed: homeRemoved,
|
|
263
|
+
note: "Dashboard artifacts were left untouched — uninstall removes the crew instance (its crons and its home), never the user's artifacts."
|
|
264
|
+
};
|
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": ['));
|