muse-crew 0.14.5 → 0.14.6
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/API.md +61 -7
- package/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +191 -1
- package/docs/guide.md +41 -5
- package/docs/publish-unknown-recovery.md +28 -120
- package/docs/publish-verification.md +148 -501
- package/lib/AGENTS.md +4 -9
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +82 -5
- package/lib/crew-api.js +734 -1023
- package/lib/publish-note-vocabulary.js +135 -42
- package/lib/schema.sql +19 -0
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-ack-scan.md +44 -0
- package/seed/cron-body-template.md +53 -87
- package/seed/crons.json +12 -0
- package/workflows/bugfix.js +55 -118
- package/workflows/chore.js +55 -118
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/standard.js +55 -140
- package/lib/build-readback-request.js +0 -140
- package/lib/check-intent-freshness.js +0 -101
- package/lib/classify-publish-absence.js +0 -462
- package/lib/publish-content.js +0 -154
- package/lib/readback-disk.js +0 -195
- package/lib/retry-publish.js +0 -417
- package/lib/verify-publish.js +0 -416
package/workflows/bugfix.js
CHANGED
|
@@ -237,14 +237,6 @@ var TOOL_CHECK_PREAMBLE =
|
|
|
237
237
|
"2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
|
|
238
238
|
"3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
|
|
239
239
|
"Then do the assignment below.\n\n";
|
|
240
|
-
// ARTIFACT_LOAD_PREAMBLE - the one-line load instruction that precedes every
|
|
241
|
-
// special-purpose prompt calling artifact tools (rebuild trigger, provenance
|
|
242
|
-
// stamp, publish verification). The artifact tool namespace is deferred for
|
|
243
|
-
// workflow children: present but invisible until the child loads it via
|
|
244
|
-
// tool_search.load_tool_namespace (bug 3472bf36 root cause). Byte-identical
|
|
245
|
-
// across standard/bugfix/chore - pinned by tests/artifact-tools.test.js.
|
|
246
|
-
var ARTIFACT_LOAD_PREAMBLE =
|
|
247
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"] - the artifact tool namespace is deferred and invisible until loaded.\n";
|
|
248
240
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
249
241
|
// reason: "discarded" (the runtime threw the output away - it could not be
|
|
250
242
|
// machine-read), "empty" (agent() returned without throwing but produced
|
|
@@ -422,6 +414,11 @@ async function recordPublishLedger(entry, rework) {
|
|
|
422
414
|
diff_path: entry.diff_path || null,
|
|
423
415
|
diff_sha256: entry.diff_sha256 || null,
|
|
424
416
|
base: entry.base || null,
|
|
417
|
+
// (0.14.6) Numeric publication attempt + the attempt's deterministic
|
|
418
|
+
// version. Attempt 1 is staged here by the workflow; attempts 2-3 are
|
|
419
|
+
// staged by scan-ack-pending's re-issue path with fresh versions.
|
|
420
|
+
publish_attempt: entry.publish_attempt || null,
|
|
421
|
+
version: entry.version || null,
|
|
425
422
|
});
|
|
426
423
|
var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
|
|
427
424
|
var res = await agent(
|
|
@@ -433,9 +430,15 @@ async function recordPublishLedger(entry, rework) {
|
|
|
433
430
|
label: "Recording publish attempt in ledger",
|
|
434
431
|
schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
|
|
435
432
|
);
|
|
436
|
-
|
|
433
|
+
// (2026-09-20 REVIEW) The intent entry is the machine's anchor — the
|
|
434
|
+
// caller gates on this return. LEDGER_OK in the agent's verbatim output
|
|
435
|
+
// is the only mechanical proof the line landed.
|
|
436
|
+
var ok = res && typeof res.result === "string" && res.result.indexOf("LEDGER_OK") !== -1;
|
|
437
|
+
log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger" + (ok ? "" : " (WRITE UNCONFIRMED)"));
|
|
438
|
+
return ok;
|
|
437
439
|
} catch (e) {
|
|
438
440
|
log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
|
|
441
|
+
return false;
|
|
439
442
|
}
|
|
440
443
|
}
|
|
441
444
|
|
|
@@ -1485,7 +1488,7 @@ while (i < STEPS.length) {
|
|
|
1485
1488
|
var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + ".diff";
|
|
1486
1489
|
var diffResult = await agent(
|
|
1487
1490
|
"Run exactly one command and nothing else:\n" +
|
|
1488
|
-
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
|
|
1491
|
+
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --task-id \"" + taskId + "\" --attempt 1 --out \"" + publishDiffFile + "\"\n" +
|
|
1489
1492
|
"The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
|
|
1490
1493
|
{ key: attemptKey("publish-artifact-diff-" + taskId, totalReworkCount), label: "Computing publish diff from stamped base",
|
|
1491
1494
|
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
@@ -1508,6 +1511,18 @@ while (i < STEPS.length) {
|
|
|
1508
1511
|
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
1509
1512
|
var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
|
|
1510
1513
|
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
1514
|
+
// (0.14.6) The per-attempt publish version, synthesized into the
|
|
1515
|
+
// staged diff as .crew-publish-version-<task_id> (per-task receipt —
|
|
1516
|
+
// two same-project tasks can publish concurrently) by the
|
|
1517
|
+
// deterministic script.
|
|
1518
|
+
// The workflow never invents it: the script derives it from
|
|
1519
|
+
// task+commit+attempt, and the intent scan re-derives it before
|
|
1520
|
+
// claiming. A missing/malformed version parks — never publish blind.
|
|
1521
|
+
var publishVersion = String(diffSummary.version || "");
|
|
1522
|
+
// Mirrors isPublishVersion() in lib/crew-api.js: <40-hex commit>:<attempt>.
|
|
1523
|
+
if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
|
|
1524
|
+
return await parkTask("Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.");
|
|
1525
|
+
}
|
|
1511
1526
|
if (!diffSummary.bytes) {
|
|
1512
1527
|
return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
|
|
1513
1528
|
}
|
|
@@ -1535,131 +1550,53 @@ while (i < STEPS.length) {
|
|
|
1535
1550
|
// worker verifies the sha256 from the intent entry before issuing;
|
|
1536
1551
|
// a mismatch stops issuance. (2026-09-20, one-party worker-owned
|
|
1537
1552
|
// publish: the workflow no longer issues the edit itself.)
|
|
1538
|
-
// See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
|
|
1539
|
-
var auditDirsBeforeTrigger = [];
|
|
1540
|
-
var auditBeforeOk = false;
|
|
1541
|
-
// Pre-trigger baselines (design §1.9): the audit-dir listing and the
|
|
1542
|
-
// manifest snapshot. The verified-path freshness check compares the
|
|
1543
|
-
// post-trigger manifest against the baseline (built_at advance +
|
|
1544
|
-
// content_sha256 change). Best-effort, never gates — a missing
|
|
1545
|
-
// manifest baseline fails the verified path closed.
|
|
1546
|
-
var preTriggerManifest = null;
|
|
1547
|
-
try {
|
|
1548
|
-
var auditBefore = await agent(
|
|
1549
|
-
"Capture pre-trigger baselines for slug \"" + PUBLISH_SLUG + "\" (best-effort snapshots, never gates).\n" +
|
|
1550
|
-
"Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null; echo ---MANIFEST---; cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/.space-build/manifest.json 2>/dev/null\n" +
|
|
1551
|
-
"Return JSON { \"dirs\": \"<newline-separated names, empty string when missing>\", \"manifest\": \"<the manifest's full text, or empty string when missing/unreadable>\" } and nothing else.",
|
|
1552
|
-
{ key: attemptKey("publish-baseline-before-" + taskId, totalReworkCount), label: "Snapshotting baselines before rebuild trigger",
|
|
1553
|
-
schema: { type: "object", properties: { dirs: { type: "string" }, manifest: { type: "string" } }, required: ["dirs", "manifest"] } }
|
|
1554
|
-
);
|
|
1555
|
-
auditDirsBeforeTrigger = String((auditBefore && auditBefore.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
1556
|
-
var manifestText = String((auditBefore && auditBefore.manifest) || "").trim();
|
|
1557
|
-
if (manifestText) {
|
|
1558
|
-
var manifestJson = JSON.parse(manifestText);
|
|
1559
|
-
preTriggerManifest = {
|
|
1560
|
-
built_at: typeof manifestJson.built_at === "string" ? manifestJson.built_at : null,
|
|
1561
|
-
content_sha256: typeof manifestJson.content_sha256 === "string" ? manifestJson.content_sha256 : null,
|
|
1562
|
-
};
|
|
1563
|
-
}
|
|
1564
|
-
// Arm only after both baselines parse.
|
|
1565
|
-
auditBeforeOk = true;
|
|
1566
|
-
log("Publish pre-trigger baselines for task " + taskId + ": " + auditDirsBeforeTrigger.length + " audit dirs, manifest " + (preTriggerManifest ? "built_at=" + preTriggerManifest.built_at : "none"));
|
|
1567
|
-
} catch (auditBeforeErr) {
|
|
1568
|
-
log("Publish pre-trigger baselines failed for task " + taskId + " (non-fatal): audit fallback DISABLED for this attempt — without a baseline, historical dirs would look new: " + (auditBeforeErr && auditBeforeErr.message ? auditBeforeErr.message : auditBeforeErr));
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// See docs/decisions/publish-path.md#fire-and-forget-trigger: the trigger child returns immediately; the workflow owns observation and verdict.
|
|
1572
|
-
var publishToolsOk = false;
|
|
1573
|
-
var publishToolsMissing = false;
|
|
1574
|
-
for (var toolcheckAttempt = 1; toolcheckAttempt <= 2 && !publishToolsOk; toolcheckAttempt++) {
|
|
1575
|
-
try {
|
|
1576
|
-
var toolcheckResult = await agent(
|
|
1577
|
-
"Check whether the artifact tool namespace is available.\n" +
|
|
1578
|
-
"Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
|
|
1579
|
-
"Then emit exactly one line and nothing else: ARTIFACT_TOOLS: <ok if the load succeeded and artifact_edit and artifact_status are now functions, missing otherwise>.\n" +
|
|
1580
|
-
"Return JSON { \"signal\": \"<the exact ARTIFACT_TOOLS line>\" } and nothing else.",
|
|
1581
|
-
{ key: attemptKey("publish-artifact-toolcheck-" + taskId + (toolcheckAttempt === 1 ? "" : "-retry2"), totalReworkCount),
|
|
1582
|
-
label: "Checking artifact tool availability" + (toolcheckAttempt === 1 ? "" : " (retry)"),
|
|
1583
|
-
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1584
|
-
);
|
|
1585
|
-
var toolSignal = String((toolcheckResult && toolcheckResult.signal) || "");
|
|
1586
|
-
if (/ARTIFACT_TOOLS:\s*ok/.test(toolSignal)) {
|
|
1587
|
-
publishToolsOk = true;
|
|
1588
|
-
} else if (/ARTIFACT_TOOLS:\s*missing/.test(toolSignal)) {
|
|
1589
|
-
publishToolsMissing = true;
|
|
1590
|
-
}
|
|
1591
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2): " +
|
|
1592
|
-
(publishToolsOk ? "tools ok" : publishToolsMissing ? "tools missing (explicit parsed signal)" : "inconclusive (no ARTIFACT_TOOLS signal parsed)"));
|
|
1593
|
-
} catch (toolcheckErr) {
|
|
1594
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2) threw (" + (toolcheckErr && toolcheckErr.message ? toolcheckErr.message : toolcheckErr) + ") — inconclusive: a throw proves nothing about tool availability, never counted as missing");
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
if (!publishToolsOk && !publishToolsMissing) {
|
|
1598
|
-
await recordPublishLedger({
|
|
1599
|
-
commit: mergeCommitForPublish,
|
|
1600
|
-
attempt: rebuildAttemptKey,
|
|
1601
|
-
agent_id: null,
|
|
1602
|
-
applied_report: null,
|
|
1603
|
-
outcome: "unknown",
|
|
1604
|
-
detail: "artifact toolcheck inconclusive after two attempts (throws or unparseable signals — never an explicit ARTIFACT_TOOLS: missing): tool availability unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
|
|
1605
|
-
}, totalReworkCount);
|
|
1606
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact toolcheck was inconclusive after two attempts (no explicit ARTIFACT_TOOLS signal parsed — a throw is transport noise, not evidence). Tool availability is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
|
|
1607
|
-
}
|
|
1608
|
-
if (!publishToolsOk) {
|
|
1609
|
-
await recordPublishLedger({
|
|
1610
|
-
commit: mergeCommitForPublish,
|
|
1611
|
-
attempt: rebuildAttemptKey,
|
|
1612
|
-
agent_id: null,
|
|
1613
|
-
applied_report: null,
|
|
1614
|
-
outcome: "rejected",
|
|
1615
|
-
detail: "artifact tool namespace explicitly missing (parsed ARTIFACT_TOOLS: missing signal, one bounded retry spent): the edit provably did not go through — no trigger issued, no blind retry"
|
|
1616
|
-
}, totalReworkCount);
|
|
1617
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact tool namespace was explicitly missing (parsed signal — the edit provably did not go through, so no trigger was issued and nothing was retried blindly). Human attention needed.");
|
|
1618
|
-
}
|
|
1619
|
-
// See docs/decisions/publish-path.md#pretrigger-baseline: a tiny schema'd baseline is captured before the trigger.
|
|
1620
|
-
var baselineAgentId = null;
|
|
1621
|
-
var baselineFailed = false;
|
|
1622
|
-
try {
|
|
1623
|
-
var publishBaseline = await agent(
|
|
1624
|
-
ARTIFACT_LOAD_PREAMBLE +
|
|
1625
|
-
"Call artifact_status with slug \"" + PUBLISH_SLUG + "\" once.\n" +
|
|
1626
|
-
"Return JSON { \"build\": <the raw \"build\" value exactly as returned, or null when there is none> } and nothing else.",
|
|
1627
|
-
{ key: attemptKey("publish-artifact-baseline-" + taskId, totalReworkCount), label: "Reading pre-trigger build state",
|
|
1628
|
-
schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
|
|
1629
|
-
);
|
|
1630
|
-
baselineAgentId = (publishBaseline && publishBaseline.build && typeof publishBaseline.build.agent_id === "string" && publishBaseline.build.agent_id) || null;
|
|
1631
|
-
log("Publish pre-trigger baseline for task " + taskId + ": " + (baselineAgentId ? "build " + baselineAgentId + " already in flight (predates the trigger — never attributed to this edit)" : "no build in flight"));
|
|
1632
|
-
} catch (baselineErr) {
|
|
1633
|
-
baselineFailed = true;
|
|
1634
|
-
log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
|
|
1635
|
-
}
|
|
1636
1553
|
// (2026-09-20, one-party worker-owned publish) The workflow exits
|
|
1637
1554
|
// Publish at intent. Everything above — preflight, provenance base,
|
|
1555
|
+
// checksummed diff — is read-only. (2026-09-20 REVIEW: the
|
|
1556
|
+
// pre-trigger baselines, artifact toolcheck, and pre-trigger build
|
|
1557
|
+
// state agent steps were deleted — their outputs fed only the
|
|
1558
|
+
// retired content-verdict/receipt-attribution machinery, and the
|
|
1559
|
+
// toolcheck ran in a workflow child that cannot reach artifact_edit.
|
|
1560
|
+
// Issuance belongs to the session-carrying tick worker, which
|
|
1561
|
+
// handles tool-unavailability as inconclusive per the tick protocol.)
|
|
1562
|
+
// Publish at intent. Everything above — preflight, provenance base,
|
|
1638
1563
|
// checksummed diff, toolcheck, pre-trigger baselines — is read-only.
|
|
1639
1564
|
// Issuance belongs to the session-carrying tick worker
|
|
1640
|
-
// (scan-publish-
|
|
1565
|
+
// (scan-publish-intent's intent bucket), the only caller class that
|
|
1641
1566
|
// can reach artifact_edit. This entry is issuer-written BY THE
|
|
1642
1567
|
// WORKFLOW and names the intent: outcome "publish-intent", never
|
|
1643
1568
|
// "submitted" — the workflow issued nothing. The worker claims it
|
|
1644
1569
|
// via the park note below.
|
|
1645
|
-
|
|
1570
|
+
// (2026-09-20 REVIEW) The intent ledger entry is the anchor the
|
|
1571
|
+
// whole machine depends on. If the ferry drops it, the task would
|
|
1572
|
+
// park with publish-requested and no anchor — unclaimable forever.
|
|
1573
|
+
// Fail loudly instead of parking an orphan intent.
|
|
1574
|
+
var intentAnchored = await recordPublishLedger({
|
|
1646
1575
|
commit: mergeCommitForPublish,
|
|
1647
1576
|
attempt: rebuildAttemptKey,
|
|
1577
|
+
// (0.14.6) Attempt 1, staged here. The version is the attempt's
|
|
1578
|
+
// deterministic receipt string, synthesized into the staged diff;
|
|
1579
|
+
// the ack scan re-derives it from task+commit+attempt before the
|
|
1580
|
+
// tick worker may issue the edit.
|
|
1581
|
+
publish_attempt: 1,
|
|
1582
|
+
version: publishVersion,
|
|
1648
1583
|
agent_id: null,
|
|
1649
1584
|
applied_report: null,
|
|
1650
|
-
manifest_before: preTriggerManifest,
|
|
1651
1585
|
outcome: "publish-intent",
|
|
1652
1586
|
issuer: "workflow",
|
|
1653
1587
|
diff_path: publishDiffFile,
|
|
1654
1588
|
diff_sha256: publishDiffSha256,
|
|
1655
1589
|
// (2026-09-20, critic-0145 F-A4) The base the diff was computed
|
|
1656
|
-
// against: scan-publish-
|
|
1590
|
+
// against: scan-publish-intent's intent branch compares it with
|
|
1657
1591
|
// current stamped provenance and re-queues the task when it went
|
|
1658
1592
|
// stale — never issues the stale diff.
|
|
1659
1593
|
base: publishBase,
|
|
1660
|
-
detail: "publish intent: preflight
|
|
1594
|
+
detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
|
|
1661
1595
|
}, totalReworkCount);
|
|
1662
|
-
|
|
1596
|
+
if (!intentAnchored) {
|
|
1597
|
+
throw new Error("publish-intent ledger anchor write unconfirmed for task " + taskId + " — refusing to park an unclaimable intent; the Publish phase fails loudly instead");
|
|
1598
|
+
}
|
|
1599
|
+
return await parkTask("publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") carrying the per-attempt publish version for the crew's publish worker to issue. The worker issues the edit directly and the acknowledgement scan owns the task from there; no action needed.");
|
|
1663
1600
|
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1664
1601
|
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
1665
1602
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
@@ -2320,10 +2257,10 @@ while (i < STEPS.length) {
|
|
|
2320
2257
|
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
2321
2258
|
}
|
|
2322
2259
|
|
|
2323
|
-
//
|
|
2324
|
-
// scan
|
|
2325
|
-
// the
|
|
2326
|
-
//
|
|
2260
|
+
// Version acknowledgement (docs/publish-verification.md): the tick worker
|
|
2261
|
+
// issues the edit directly and the ack scan owns the task from there —
|
|
2262
|
+
// the exact per-attempt version on the artifact's disk is the sole positive
|
|
2263
|
+
// completion criterion. A skipped or failed publish has nothing to ack.
|
|
2327
2264
|
let summary;
|
|
2328
2265
|
var workerMarkers = extractMarkerLines(workerText);
|
|
2329
2266
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
package/workflows/chore.js
CHANGED
|
@@ -296,14 +296,6 @@ var TOOL_CHECK_PREAMBLE =
|
|
|
296
296
|
"2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
|
|
297
297
|
"3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
|
|
298
298
|
"Then do the assignment below.\n\n";
|
|
299
|
-
// ARTIFACT_LOAD_PREAMBLE - the one-line load instruction that precedes every
|
|
300
|
-
// special-purpose prompt calling artifact tools (rebuild trigger, provenance
|
|
301
|
-
// stamp, publish verification). The artifact tool namespace is deferred for
|
|
302
|
-
// workflow children: present but invisible until the child loads it via
|
|
303
|
-
// tool_search.load_tool_namespace (bug 3472bf36 root cause). Byte-identical
|
|
304
|
-
// across standard/bugfix/chore - pinned by tests/artifact-tools.test.js.
|
|
305
|
-
var ARTIFACT_LOAD_PREAMBLE =
|
|
306
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"] - the artifact tool namespace is deferred and invisible until loaded.\n";
|
|
307
299
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
308
300
|
// reason: "discarded" (the runtime threw the output away - it could not be
|
|
309
301
|
// machine-read), "empty" (agent() returned without throwing but produced
|
|
@@ -481,6 +473,11 @@ async function recordPublishLedger(entry, rework) {
|
|
|
481
473
|
diff_path: entry.diff_path || null,
|
|
482
474
|
diff_sha256: entry.diff_sha256 || null,
|
|
483
475
|
base: entry.base || null,
|
|
476
|
+
// (0.14.6) Numeric publication attempt + the attempt's deterministic
|
|
477
|
+
// version. Attempt 1 is staged here by the workflow; attempts 2-3 are
|
|
478
|
+
// staged by scan-ack-pending's re-issue path with fresh versions.
|
|
479
|
+
publish_attempt: entry.publish_attempt || null,
|
|
480
|
+
version: entry.version || null,
|
|
484
481
|
});
|
|
485
482
|
var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
|
|
486
483
|
var res = await agent(
|
|
@@ -492,9 +489,15 @@ async function recordPublishLedger(entry, rework) {
|
|
|
492
489
|
label: "Recording publish attempt in ledger",
|
|
493
490
|
schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
|
|
494
491
|
);
|
|
495
|
-
|
|
492
|
+
// (2026-09-20 REVIEW) The intent entry is the machine's anchor — the
|
|
493
|
+
// caller gates on this return. LEDGER_OK in the agent's verbatim output
|
|
494
|
+
// is the only mechanical proof the line landed.
|
|
495
|
+
var ok = res && typeof res.result === "string" && res.result.indexOf("LEDGER_OK") !== -1;
|
|
496
|
+
log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger" + (ok ? "" : " (WRITE UNCONFIRMED)"));
|
|
497
|
+
return ok;
|
|
496
498
|
} catch (e) {
|
|
497
499
|
log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
|
|
500
|
+
return false;
|
|
498
501
|
}
|
|
499
502
|
}
|
|
500
503
|
|
|
@@ -1391,7 +1394,7 @@ while (i < STEPS.length) {
|
|
|
1391
1394
|
var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (reworkCount > 0 ? "-r" + reworkCount : "") + ".diff";
|
|
1392
1395
|
var diffResult = await agent(
|
|
1393
1396
|
"Run exactly one command and nothing else:\n" +
|
|
1394
|
-
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
|
|
1397
|
+
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --task-id \"" + taskId + "\" --attempt 1 --out \"" + publishDiffFile + "\"\n" +
|
|
1395
1398
|
"The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
|
|
1396
1399
|
{ key: attemptKey("publish-artifact-diff-" + taskId, reworkCount), label: "Computing publish diff from stamped base",
|
|
1397
1400
|
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
@@ -1414,6 +1417,18 @@ while (i < STEPS.length) {
|
|
|
1414
1417
|
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
1415
1418
|
var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
|
|
1416
1419
|
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
1420
|
+
// (0.14.6) The per-attempt publish version, synthesized into the
|
|
1421
|
+
// staged diff as .crew-publish-version-<task_id> (per-task receipt —
|
|
1422
|
+
// two same-project tasks can publish concurrently) by the
|
|
1423
|
+
// deterministic script.
|
|
1424
|
+
// The workflow never invents it: the script derives it from
|
|
1425
|
+
// task+commit+attempt, and the intent scan re-derives it before
|
|
1426
|
+
// claiming. A missing/malformed version parks — never publish blind.
|
|
1427
|
+
var publishVersion = String(diffSummary.version || "");
|
|
1428
|
+
// Mirrors isPublishVersion() in lib/crew-api.js: <40-hex commit>:<attempt>.
|
|
1429
|
+
if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
|
|
1430
|
+
return await parkTask("Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.");
|
|
1431
|
+
}
|
|
1417
1432
|
if (!diffSummary.bytes) {
|
|
1418
1433
|
return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
|
|
1419
1434
|
}
|
|
@@ -1441,131 +1456,53 @@ while (i < STEPS.length) {
|
|
|
1441
1456
|
// worker verifies the sha256 from the intent entry before issuing;
|
|
1442
1457
|
// a mismatch stops issuance. (2026-09-20, one-party worker-owned
|
|
1443
1458
|
// publish: the workflow no longer issues the edit itself.)
|
|
1444
|
-
// See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
|
|
1445
|
-
var auditDirsBeforeTrigger = [];
|
|
1446
|
-
var auditBeforeOk = false;
|
|
1447
|
-
// Pre-trigger baselines (design §1.9): the audit-dir listing and the
|
|
1448
|
-
// manifest snapshot. The verified-path freshness check compares the
|
|
1449
|
-
// post-trigger manifest against the baseline (built_at advance +
|
|
1450
|
-
// content_sha256 change). Best-effort, never gates — a missing
|
|
1451
|
-
// manifest baseline fails the verified path closed.
|
|
1452
|
-
var preTriggerManifest = null;
|
|
1453
|
-
try {
|
|
1454
|
-
var auditBefore = await agent(
|
|
1455
|
-
"Capture pre-trigger baselines for slug \"" + PUBLISH_SLUG + "\" (best-effort snapshots, never gates).\n" +
|
|
1456
|
-
"Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null; echo ---MANIFEST---; cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/.space-build/manifest.json 2>/dev/null\n" +
|
|
1457
|
-
"Return JSON { \"dirs\": \"<newline-separated names, empty string when missing>\", \"manifest\": \"<the manifest's full text, or empty string when missing/unreadable>\" } and nothing else.",
|
|
1458
|
-
{ key: attemptKey("publish-baseline-before-" + taskId, reworkCount), label: "Snapshotting baselines before rebuild trigger",
|
|
1459
|
-
schema: { type: "object", properties: { dirs: { type: "string" }, manifest: { type: "string" } }, required: ["dirs", "manifest"] } }
|
|
1460
|
-
);
|
|
1461
|
-
auditDirsBeforeTrigger = String((auditBefore && auditBefore.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
1462
|
-
var manifestText = String((auditBefore && auditBefore.manifest) || "").trim();
|
|
1463
|
-
if (manifestText) {
|
|
1464
|
-
var manifestJson = JSON.parse(manifestText);
|
|
1465
|
-
preTriggerManifest = {
|
|
1466
|
-
built_at: typeof manifestJson.built_at === "string" ? manifestJson.built_at : null,
|
|
1467
|
-
content_sha256: typeof manifestJson.content_sha256 === "string" ? manifestJson.content_sha256 : null,
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
// Arm only after both baselines parse.
|
|
1471
|
-
auditBeforeOk = true;
|
|
1472
|
-
log("Publish pre-trigger baselines for task " + taskId + ": " + auditDirsBeforeTrigger.length + " audit dirs, manifest " + (preTriggerManifest ? "built_at=" + preTriggerManifest.built_at : "none"));
|
|
1473
|
-
} catch (auditBeforeErr) {
|
|
1474
|
-
log("Publish pre-trigger baselines failed for task " + taskId + " (non-fatal): audit fallback DISABLED for this attempt — without a baseline, historical dirs would look new: " + (auditBeforeErr && auditBeforeErr.message ? auditBeforeErr.message : auditBeforeErr));
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
// See docs/decisions/publish-path.md#fire-and-forget-trigger: the trigger child returns immediately; the workflow owns observation and verdict.
|
|
1478
|
-
var publishToolsOk = false;
|
|
1479
|
-
var publishToolsMissing = false;
|
|
1480
|
-
for (var toolcheckAttempt = 1; toolcheckAttempt <= 2 && !publishToolsOk; toolcheckAttempt++) {
|
|
1481
|
-
try {
|
|
1482
|
-
var toolcheckResult = await agent(
|
|
1483
|
-
"Check whether the artifact tool namespace is available.\n" +
|
|
1484
|
-
"Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
|
|
1485
|
-
"Then emit exactly one line and nothing else: ARTIFACT_TOOLS: <ok if the load succeeded and artifact_edit and artifact_status are now functions, missing otherwise>.\n" +
|
|
1486
|
-
"Return JSON { \"signal\": \"<the exact ARTIFACT_TOOLS line>\" } and nothing else.",
|
|
1487
|
-
{ key: attemptKey("publish-artifact-toolcheck-" + taskId + (toolcheckAttempt === 1 ? "" : "-retry2"), reworkCount),
|
|
1488
|
-
label: "Checking artifact tool availability" + (toolcheckAttempt === 1 ? "" : " (retry)"),
|
|
1489
|
-
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1490
|
-
);
|
|
1491
|
-
var toolSignal = String((toolcheckResult && toolcheckResult.signal) || "");
|
|
1492
|
-
if (/ARTIFACT_TOOLS:\s*ok/.test(toolSignal)) {
|
|
1493
|
-
publishToolsOk = true;
|
|
1494
|
-
} else if (/ARTIFACT_TOOLS:\s*missing/.test(toolSignal)) {
|
|
1495
|
-
publishToolsMissing = true;
|
|
1496
|
-
}
|
|
1497
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2): " +
|
|
1498
|
-
(publishToolsOk ? "tools ok" : publishToolsMissing ? "tools missing (explicit parsed signal)" : "inconclusive (no ARTIFACT_TOOLS signal parsed)"));
|
|
1499
|
-
} catch (toolcheckErr) {
|
|
1500
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2) threw (" + (toolcheckErr && toolcheckErr.message ? toolcheckErr.message : toolcheckErr) + ") — inconclusive: a throw proves nothing about tool availability, never counted as missing");
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
if (!publishToolsOk && !publishToolsMissing) {
|
|
1504
|
-
await recordPublishLedger({
|
|
1505
|
-
commit: mergeCommitForPublish,
|
|
1506
|
-
attempt: rebuildAttemptKey,
|
|
1507
|
-
agent_id: null,
|
|
1508
|
-
applied_report: null,
|
|
1509
|
-
outcome: "unknown",
|
|
1510
|
-
detail: "artifact toolcheck inconclusive after two attempts (throws or unparseable signals — never an explicit ARTIFACT_TOOLS: missing): tool availability unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
|
|
1511
|
-
}, reworkCount);
|
|
1512
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact toolcheck was inconclusive after two attempts (no explicit ARTIFACT_TOOLS signal parsed — a throw is transport noise, not evidence). Tool availability is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
|
|
1513
|
-
}
|
|
1514
|
-
if (!publishToolsOk) {
|
|
1515
|
-
await recordPublishLedger({
|
|
1516
|
-
commit: mergeCommitForPublish,
|
|
1517
|
-
attempt: rebuildAttemptKey,
|
|
1518
|
-
agent_id: null,
|
|
1519
|
-
applied_report: null,
|
|
1520
|
-
outcome: "rejected",
|
|
1521
|
-
detail: "artifact tool namespace explicitly missing (parsed ARTIFACT_TOOLS: missing signal, one bounded retry spent): the edit provably did not go through — no trigger issued, no blind retry"
|
|
1522
|
-
}, reworkCount);
|
|
1523
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact tool namespace was explicitly missing (parsed signal — the edit provably did not go through, so no trigger was issued and nothing was retried blindly). Human attention needed.");
|
|
1524
|
-
}
|
|
1525
|
-
// See docs/decisions/publish-path.md#pretrigger-baseline: a tiny schema'd baseline is captured before the trigger.
|
|
1526
|
-
var baselineAgentId = null;
|
|
1527
|
-
var baselineFailed = false;
|
|
1528
|
-
try {
|
|
1529
|
-
var publishBaseline = await agent(
|
|
1530
|
-
ARTIFACT_LOAD_PREAMBLE +
|
|
1531
|
-
"Call artifact_status with slug \"" + PUBLISH_SLUG + "\" once.\n" +
|
|
1532
|
-
"Return JSON { \"build\": <the raw \"build\" value exactly as returned, or null when there is none> } and nothing else.",
|
|
1533
|
-
{ key: attemptKey("publish-artifact-baseline-" + taskId, reworkCount), label: "Reading pre-trigger build state",
|
|
1534
|
-
schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
|
|
1535
|
-
);
|
|
1536
|
-
baselineAgentId = (publishBaseline && publishBaseline.build && typeof publishBaseline.build.agent_id === "string" && publishBaseline.build.agent_id) || null;
|
|
1537
|
-
log("Publish pre-trigger baseline for task " + taskId + ": " + (baselineAgentId ? "build " + baselineAgentId + " already in flight (predates the trigger — never attributed to this edit)" : "no build in flight"));
|
|
1538
|
-
} catch (baselineErr) {
|
|
1539
|
-
baselineFailed = true;
|
|
1540
|
-
log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
|
|
1541
|
-
}
|
|
1542
1459
|
// (2026-09-20, one-party worker-owned publish) The workflow exits
|
|
1543
1460
|
// Publish at intent. Everything above — preflight, provenance base,
|
|
1461
|
+
// checksummed diff — is read-only. (2026-09-20 REVIEW: the
|
|
1462
|
+
// pre-trigger baselines, artifact toolcheck, and pre-trigger build
|
|
1463
|
+
// state agent steps were deleted — their outputs fed only the
|
|
1464
|
+
// retired content-verdict/receipt-attribution machinery, and the
|
|
1465
|
+
// toolcheck ran in a workflow child that cannot reach artifact_edit.
|
|
1466
|
+
// Issuance belongs to the session-carrying tick worker, which
|
|
1467
|
+
// handles tool-unavailability as inconclusive per the tick protocol.)
|
|
1468
|
+
// Publish at intent. Everything above — preflight, provenance base,
|
|
1544
1469
|
// checksummed diff, toolcheck, pre-trigger baselines — is read-only.
|
|
1545
1470
|
// Issuance belongs to the session-carrying tick worker
|
|
1546
|
-
// (scan-publish-
|
|
1471
|
+
// (scan-publish-intent's intent bucket), the only caller class that
|
|
1547
1472
|
// can reach artifact_edit. This entry is issuer-written BY THE
|
|
1548
1473
|
// WORKFLOW and names the intent: outcome "publish-intent", never
|
|
1549
1474
|
// "submitted" — the workflow issued nothing. The worker claims it
|
|
1550
1475
|
// via the park note below.
|
|
1551
|
-
|
|
1476
|
+
// (2026-09-20 REVIEW) The intent ledger entry is the anchor the
|
|
1477
|
+
// whole machine depends on. If the ferry drops it, the task would
|
|
1478
|
+
// park with publish-requested and no anchor — unclaimable forever.
|
|
1479
|
+
// Fail loudly instead of parking an orphan intent.
|
|
1480
|
+
var intentAnchored = await recordPublishLedger({
|
|
1552
1481
|
commit: mergeCommitForPublish,
|
|
1553
1482
|
attempt: rebuildAttemptKey,
|
|
1483
|
+
// (0.14.6) Attempt 1, staged here. The version is the attempt's
|
|
1484
|
+
// deterministic receipt string, synthesized into the staged diff;
|
|
1485
|
+
// the ack scan re-derives it from task+commit+attempt before the
|
|
1486
|
+
// tick worker may issue the edit.
|
|
1487
|
+
publish_attempt: 1,
|
|
1488
|
+
version: publishVersion,
|
|
1554
1489
|
agent_id: null,
|
|
1555
1490
|
applied_report: null,
|
|
1556
|
-
manifest_before: preTriggerManifest,
|
|
1557
1491
|
outcome: "publish-intent",
|
|
1558
1492
|
issuer: "workflow",
|
|
1559
1493
|
diff_path: publishDiffFile,
|
|
1560
1494
|
diff_sha256: publishDiffSha256,
|
|
1561
1495
|
// (2026-09-20, critic-0145 F-A4) The base the diff was computed
|
|
1562
|
-
// against: scan-publish-
|
|
1496
|
+
// against: scan-publish-intent's intent branch compares it with
|
|
1563
1497
|
// current stamped provenance and re-queues the task when it went
|
|
1564
1498
|
// stale — never issues the stale diff.
|
|
1565
1499
|
base: publishBase,
|
|
1566
|
-
detail: "publish intent: preflight
|
|
1500
|
+
detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
|
|
1567
1501
|
}, reworkCount);
|
|
1568
|
-
|
|
1502
|
+
if (!intentAnchored) {
|
|
1503
|
+
throw new Error("publish-intent ledger anchor write unconfirmed for task " + taskId + " — refusing to park an unclaimable intent; the Publish phase fails loudly instead");
|
|
1504
|
+
}
|
|
1505
|
+
return await parkTask("publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") carrying the per-attempt publish version for the crew's publish worker to issue. The worker issues the edit directly and the acknowledgement scan owns the task from there; no action needed.");
|
|
1569
1506
|
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1570
1507
|
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
1571
1508
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
@@ -1947,10 +1884,10 @@ while (i < STEPS.length) {
|
|
|
1947
1884
|
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
1948
1885
|
}
|
|
1949
1886
|
|
|
1950
|
-
//
|
|
1951
|
-
// scan
|
|
1952
|
-
// the
|
|
1953
|
-
//
|
|
1887
|
+
// Version acknowledgement (docs/publish-verification.md): the tick worker
|
|
1888
|
+
// issues the edit directly and the ack scan owns the task from there —
|
|
1889
|
+
// the exact per-attempt version on the artifact's disk is the sole positive
|
|
1890
|
+
// completion criterion. A skipped or failed publish has nothing to ack.
|
|
1954
1891
|
let summary;
|
|
1955
1892
|
var workerMarkers = extractMarkerLines(workerText);
|
|
1956
1893
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
package/workflows/crew-init.js
CHANGED
|
@@ -58,6 +58,29 @@ if (!crewHome) throw new Error("crewHome is required — e.g. ~/workspace/.crew
|
|
|
58
58
|
// create projects later via crew-api.js). One without the other is an error.
|
|
59
59
|
if (!!dashboardSlug !== !!dashboardRepoPath) throw new Error("dashboardSlug and dashboardRepoPath must be provided together, or neither (omit both for a CLI-only install without a dashboard)");
|
|
60
60
|
|
|
61
|
+
// ── Cron id override validation (2026-09-20, 0.14.6 REVIEW) ──
|
|
62
|
+
// Uninstall's discovery fallback only recognizes live ids that start with
|
|
63
|
+
// a manifest entry id + "-" (crew-poll-, crew-ack-scan-,
|
|
64
|
+
// crew-update-watch-). An override that breaks that convention would
|
|
65
|
+
// survive uninstall if .cron-registry.json were ever lost — so overrides
|
|
66
|
+
// must keep the manifest-id prefix. Fail closed at init, before any cron
|
|
67
|
+
// is created.
|
|
68
|
+
for (const manifestId of Object.keys(cronIds)) {
|
|
69
|
+
const liveId = cronIds[manifestId];
|
|
70
|
+
if (typeof liveId !== "string" || !liveId.startsWith(manifestId + "-")) {
|
|
71
|
+
return {
|
|
72
|
+
__hatchWorkflowControl: "blocked",
|
|
73
|
+
result: {
|
|
74
|
+
blocked_reason: "Invalid cronIds override",
|
|
75
|
+
message: "cronIds[\"" + manifestId + "\"] must start with \"" + manifestId + "-\" — got " +
|
|
76
|
+
JSON.stringify(liveId) + ". Uninstall's discovery fallback only recognizes live ids under " +
|
|
77
|
+
"the manifest-id prefix; an arbitrary id could survive uninstall if the cron registry " +
|
|
78
|
+
"were lost. Re-run init with a prefixed id."
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
61
84
|
const orchDir = crewHome + "/.orchestration";
|
|
62
85
|
|
|
63
86
|
// ── Dependency node_modules resolution (clean-room defect 2026-09-16) ──
|
|
@@ -158,11 +158,14 @@ try {
|
|
|
158
158
|
"Steps:\n" +
|
|
159
159
|
"1. Registry: if " + sq(crewHomeExpanded + "/.cron-registry.json") + " exists, read it\n" +
|
|
160
160
|
" and take its crons[].id list as registry candidates.\n" +
|
|
161
|
-
"2. Discovery: call cron_list. For every job whose id starts with
|
|
161
|
+
"2. Discovery: call cron_list. For every job whose id starts with one of\n" +
|
|
162
|
+
" 'crew-poll-', 'crew-ack-scan-', or 'crew-update-watch-' (the three\n" +
|
|
163
|
+
" seed/crons.json manifest entry ids, each suffixed with the instance id),\n" +
|
|
162
164
|
" call cron_view and keep it as a discovery candidate when the job body\n" +
|
|
163
|
-
" contains the crewHome string '" + crewHomeExpanded + "' (the
|
|
164
|
-
"
|
|
165
|
-
" is never a live crew job
|
|
165
|
+
" contains the crewHome string '" + crewHomeExpanded + "' (the cron body templates\n" +
|
|
166
|
+
" embed the crew home path). A bare manifest id ('crew-poll', 'crew-ack-scan',\n" +
|
|
167
|
+
" 'crew-update-watch') with no instance suffix is never a live crew job —\n" +
|
|
168
|
+
" never remove it.\n" +
|
|
166
169
|
"3. Removal list = registry candidates ∪ discovery candidates, deduped.\n" +
|
|
167
170
|
" SAFETY: remove only jobs on this list. When in doubt about any job, fail\n" +
|
|
168
171
|
" closed (passed: false) instead of guessing.\n" +
|