muse-crew 0.7.20 → 0.8.0
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 +179 -7
- package/lib/AGENTS.md +2 -1
- package/lib/build-registry.js +3 -2
- package/lib/publish-npm.sh +33 -0
- package/lib/test-publish-preflight.sh +210 -0
- package/lib/test-worktree-backend.sh +51 -1
- package/lib/update-watch.js +633 -0
- package/lib/worktree-lifecycle.sh +68 -10
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-update-watch.md +13 -0
- package/seed/crons.json +14 -1
- package/seed/workflows/upgrade.md +21 -0
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +239 -14
- package/workflows/chore.js +44 -12
- package/workflows/crew-dispatch.js +43 -6
- package/workflows/crew-init.js +173 -10
- package/workflows/standard.js +44 -12
- package/workflows/upgrade.js +794 -0
package/workflows/bugfix.js
CHANGED
|
@@ -58,7 +58,10 @@ if (!inputs.crewHome) throw new Error("crewHome is required — pass the crew ho
|
|
|
58
58
|
const crewHome = inputs.crewHome;
|
|
59
59
|
// Crew API: the workflow calls the crew-owned CLI, not the dashboard.
|
|
60
60
|
// The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
|
|
61
|
-
const
|
|
61
|
+
const CREW_API_SRC = crewHome + "/current/lib/crew-api.js";
|
|
62
|
+
// Pinned at pinLifecycle: after the pin, CREW_API points into RUN_LIB so a
|
|
63
|
+
// mid-flight release swap cannot change the CLI under a running workflow.
|
|
64
|
+
let CREW_API = CREW_API_SRC;
|
|
62
65
|
// Build a shell command invoking the CLI. Args are JSON-encoded and
|
|
63
66
|
// single-quote-wrapped for safe shell passing. The agent runs this and
|
|
64
67
|
// returns the stdout verbatim (the CLI emits JSON on stdout).
|
|
@@ -75,9 +78,12 @@ const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
|
|
|
75
78
|
const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
|
|
76
79
|
const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
77
80
|
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
78
|
-
|
|
81
|
+
const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
|
|
82
|
+
const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
83
|
+
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
84
|
+
// The five basenames the pin step must materialize — asserted mechanically
|
|
79
85
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
80
|
-
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM].map(function (p) { return p.split("/").pop(); });
|
|
86
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED].map(function (p) { return p.split("/").pop(); });
|
|
81
87
|
|
|
82
88
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
83
89
|
const projectConfig = inputs.project_config || {};
|
|
@@ -223,7 +229,7 @@ function attemptKey(base, reworkCount) {
|
|
|
223
229
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
224
230
|
}
|
|
225
231
|
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
226
|
-
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the
|
|
232
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the five pinned
|
|
227
233
|
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
228
234
|
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
229
235
|
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
@@ -231,7 +237,7 @@ function attemptKey(base, reworkCount) {
|
|
|
231
237
|
function pinLifecycle(key) {
|
|
232
238
|
return agent(
|
|
233
239
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
234
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
240
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
235
241
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
236
242
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
237
243
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -522,7 +528,7 @@ function extractMarkerLines(workerText) {
|
|
|
522
528
|
var markers = [];
|
|
523
529
|
for (var i = 0; i < lines.length; i++) {
|
|
524
530
|
var line = lines[i].trim();
|
|
525
|
-
if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|capture_targets:|worktree:)/i.test(line)) {
|
|
531
|
+
if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|layer:|capture_targets:|worktree:)/i.test(line)) {
|
|
526
532
|
markers.push(line);
|
|
527
533
|
}
|
|
528
534
|
}
|
|
@@ -577,6 +583,16 @@ function extractExperiential(workerText) {
|
|
|
577
583
|
if (!r) return null;
|
|
578
584
|
return r[1].toLowerCase() === "yes";
|
|
579
585
|
}
|
|
586
|
+
// Layer flag: reads Sage's layer: artifact|engine|docs marker line.
|
|
587
|
+
// Missing or malformed degrades to null (unknown) — callers degrade to
|
|
588
|
+
// "artifact" (today's single-strategy behavior), never park a task on a
|
|
589
|
+
// garbled line.
|
|
590
|
+
function extractLayer(workerText) {
|
|
591
|
+
var t = workerText || "";
|
|
592
|
+
var r = /layer:\s*(artifact|engine|docs)\b/i.exec(t);
|
|
593
|
+
if (!r) return null;
|
|
594
|
+
return r[1].toLowerCase();
|
|
595
|
+
}
|
|
580
596
|
function buildVisualCapturePlan(taskTitle, taskDescription, kind, captureTargets) {
|
|
581
597
|
// Deterministic visual-capture frame. kind: "baseline" | "postchange".
|
|
582
598
|
// This string IS the capture script: fixed viewport matrix, scroll
|
|
@@ -634,6 +650,41 @@ async function resolveExperiential() {
|
|
|
634
650
|
else experientialResolved = "unknown";
|
|
635
651
|
return experientialResolved;
|
|
636
652
|
}
|
|
653
|
+
// Layer resolution: the task's layer is "artifact", "engine", or "docs" —
|
|
654
|
+
// the machine-read "layer:" marker Sage's Triage report ends with. Unknown
|
|
655
|
+
// (missing/garbled line, failed lookup) degrades to "artifact": today's
|
|
656
|
+
// single-strategy behavior, never a park. Mirrors resolveExperiential()
|
|
657
|
+
// (same cache shape, same Triage-notes re-read); the layer flag is captured
|
|
658
|
+
// at Triage closeout (bugLayer) so the common path needs no extra agent call.
|
|
659
|
+
async function resolveLayer() {
|
|
660
|
+
// Triage notes are immutable within a run: cache the resolved layer so
|
|
661
|
+
// the dashboard lookup runs at most once per run.
|
|
662
|
+
if (layerResolved !== null) return layerResolved;
|
|
663
|
+
if (bugLayer) return (layerResolved = bugLayer);
|
|
664
|
+
var layerCheck = null;
|
|
665
|
+
try {
|
|
666
|
+
layerCheck = await agent(
|
|
667
|
+
"Find this task's Triage step session notes from the crew API.\n" +
|
|
668
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
|
|
669
|
+
"Find the session with task_id \"" + taskId + "\" and step \"Triage\" (status completed) in the returned sessions array and read its notes field.\n" +
|
|
670
|
+
"Return JSON { \"layer_line\": \"<the exact text of the layer: marker line from the notes, or empty string if absent>\" } and nothing else.",
|
|
671
|
+
{
|
|
672
|
+
key: "resolve-layer-" + taskId,
|
|
673
|
+
label: "Resolving bug layer from Triage notes",
|
|
674
|
+
schema: { type: "object", properties: { layer_line: { type: "string" } }, required: ["layer_line"] }
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
} catch (e) {
|
|
678
|
+
log("resolveLayer: agent call failed (" + (e && e.message ? e.message : e) + ") — treating as unknown");
|
|
679
|
+
layerResolved = "artifact";
|
|
680
|
+
return layerResolved;
|
|
681
|
+
}
|
|
682
|
+
var layer = extractLayer(layerCheck && layerCheck.layer_line ? layerCheck.layer_line : "");
|
|
683
|
+
// Unknown degrades to "artifact" (today's behavior) — never park a task
|
|
684
|
+
// on a garbled line.
|
|
685
|
+
layerResolved = layer || "artifact";
|
|
686
|
+
return layerResolved;
|
|
687
|
+
}
|
|
637
688
|
// Baseline evidence status: reads the task's note events for the exact
|
|
638
689
|
// protocol prefixes (explicit state, never English matching). Returns
|
|
639
690
|
// { baseline_found, baseline_kind, baseline_refs, requested_count, evidence_count }.
|
|
@@ -718,6 +769,12 @@ let mapperSpec = "";
|
|
|
718
769
|
// plan, and the Map-gate bounce counter (keeps agent stable-keys unique when
|
|
719
770
|
// a Map-gate bounce re-runs Capture/Map in the same run).
|
|
720
771
|
let isExperiential = null;
|
|
772
|
+
// Sage's bug-layer flag ("artifact"|"engine"|"docs", null until the Triage
|
|
773
|
+
// report is read). Drives the Reproduce phase's strategy dispatch.
|
|
774
|
+
let bugLayer = null;
|
|
775
|
+
// Caches the resolveLayer() outcome; Triage notes are immutable within a
|
|
776
|
+
// run, so the lookup runs at most once.
|
|
777
|
+
let layerResolved = null;
|
|
721
778
|
// Caches all three resolveExperiential() outcomes (yes/no/unknown); Triage
|
|
722
779
|
// notes are immutable within a run, so the lookup runs at most once.
|
|
723
780
|
let experientialResolved = null;
|
|
@@ -803,7 +860,7 @@ let i = startStepIndex;
|
|
|
803
860
|
// ── Pin lifecycle scripts ────────────────────────────────────────────
|
|
804
861
|
// Copy lifecycle scripts into a per-task temp dir so this run is immune
|
|
805
862
|
// to upgrades that land while it's in flight. Verified mechanically:
|
|
806
|
-
// workflow code asserts the
|
|
863
|
+
// workflow code asserts the five basenames from the verbatim listing —
|
|
807
864
|
// the agent cannot self-certify. Any miss parks the task before Triage.
|
|
808
865
|
const initialPins = parsePinListing(await pinLifecycle("pin-lifecycle"));
|
|
809
866
|
const missingInitialPins = PIN_BASENAMES.filter(function (b) { return initialPins.indexOf(b) === -1; });
|
|
@@ -811,6 +868,10 @@ if (missingInitialPins.length > 0) {
|
|
|
811
868
|
return await parkTask("Lifecycle pin incomplete before Triage — missing " + missingInitialPins.join(", ") + " in " + RUN_LIB + ".");
|
|
812
869
|
}
|
|
813
870
|
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
871
|
+
// From here on, every crew-api.js invocation uses the pinned copy: immune
|
|
872
|
+
// to a release swap landing mid-flight.
|
|
873
|
+
CREW_API = CREW_API_PINNED;
|
|
874
|
+
log("Crew API pinned to " + CREW_API);
|
|
814
875
|
|
|
815
876
|
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
816
877
|
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
@@ -1128,7 +1189,9 @@ while (i < STEPS.length) {
|
|
|
1128
1189
|
var mapBaselineRefs = "";
|
|
1129
1190
|
var mapBaselineNone = false;
|
|
1130
1191
|
if (step.name === "Map") {
|
|
1131
|
-
|
|
1192
|
+
// Must match Capture's run condition (experiential + artifact publish):
|
|
1193
|
+
// when Capture skips, no baseline notes exist, so the gate must not apply.
|
|
1194
|
+
if ((await resolveExperiential()) === "yes" && PUBLISH_TYPE === "artifact") {
|
|
1132
1195
|
var gateStatus = await baselineStatus();
|
|
1133
1196
|
if (!gateStatus.baseline_found) {
|
|
1134
1197
|
log("Map gate: no baseline evidence for experiential task " + taskId + " — bouncing to Capture");
|
|
@@ -1161,14 +1224,26 @@ while (i < STEPS.length) {
|
|
|
1161
1224
|
qaVisual = (await resolveExperiential()) === "yes" && PUBLISH_TYPE === "artifact";
|
|
1162
1225
|
}
|
|
1163
1226
|
|
|
1227
|
+
// Reproduce-layer routing (2026-09-16): Sage's Triage classifies the bug's
|
|
1228
|
+
// layer (artifact|engine|docs) with the machine-read "layer:" marker; the
|
|
1229
|
+
// workflow dispatches the Reproduce strategy mechanically on it. Unknown
|
|
1230
|
+
// degrades to "artifact" (today's single-strategy behavior). resolveLayer
|
|
1231
|
+
// caches per run, so Reproduce entry costs one agent call at most.
|
|
1232
|
+
var reproLayer = "artifact";
|
|
1233
|
+
if (step.name === "Reproduce") {
|
|
1234
|
+
reproLayer = await resolveLayer();
|
|
1235
|
+
log("Reproduce dispatching at the bug's layer for task " + taskId + ": " + reproLayer);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1164
1238
|
var safeTitle = taskTitle.replace(/"/g, "'").replace(/\\/g, "\\\\").replace(/`/g, "'");
|
|
1165
1239
|
var instructions = "";
|
|
1166
1240
|
|
|
1167
1241
|
if (step.name === "Triage") {
|
|
1168
|
-
instructions = "Validate the task against the project's repo at " + REPO_PATH + " — that exact checkout, not any other copy of the project on disk. If you run git commands, cd " + REPO_PATH + " first.\nCheck clarity, note dependencies, confirm the bugfix workflow assignment.\nIf the task needs decomposition, note that in your assessment.\nReport back in plain prose — what you found.\nEXPERIENTIAL FLAG: does this task change anything rendered and visible in the project's user-facing artifact (pages, components, styles, layout, copy, visual states)? If yes it is experiential and gets baseline captures (plus a visual verdict where the workflow has a QA phase). End your report with exactly one line on its own, lowercase, unrephrased: experiential: yes — or experiential: no. This line is machine-read.";
|
|
1242
|
+
instructions = "Validate the task against the project's repo at " + REPO_PATH + " — that exact checkout, not any other copy of the project on disk. If you run git commands, cd " + REPO_PATH + " first.\nCheck clarity, note dependencies, confirm the bugfix workflow assignment.\nIf the task needs decomposition, note that in your assessment.\nReport back in plain prose — what you found.\nEXPERIENTIAL FLAG: does this task change anything rendered and visible in the project's user-facing artifact (pages, components, styles, layout, copy, visual states)? If yes it is experiential and gets baseline captures (plus a visual verdict where the workflow has a QA phase). End your report with exactly one line on its own, lowercase, unrephrased: experiential: yes — or experiential: no. This line is machine-read.\nLAYER FLAG: classify the bug's layer — where the reported misbehavior lives. artifact: user-facing behavior of the project's rendered artifact (something a user sees or clicks). engine: the crew's own machinery — workflows, lib scripts, shell scripts, tests, scheduler. docs: a documentation gap or error. End your report with exactly one line on its own, lowercase, unrephrased: layer: artifact — or layer: engine — or layer: docs. This line is machine-read. If the bug genuinely spans layers, pick the layer where reproduction must happen and note the ambiguity in prose.";
|
|
1169
1243
|
|
|
1170
1244
|
} else if (step.name === "Reproduce") {
|
|
1171
|
-
|
|
1245
|
+
if (reproLayer !== "engine" && reproLayer !== "docs") {
|
|
1246
|
+
instructions = "Reproduce the bug from a user's perspective — by USING the artifact, not by reading data. You are CODE-BLIND — do NOT read source code.\n" +
|
|
1172
1247
|
"You have a see-act driver: " + crewHome + "/current/lib/see-act.js (a node script; one browser action per invocation; it prints one JSON line to stdout). It launches its own Chromium through a self-contained loopback proxy — the ONLY url you may give it is the local artifact server you start below. Never point it at any other URL.\n" +
|
|
1173
1248
|
"Actions: aria | shot [--out <png>] [--full] | click [--out <png>] --selector <css> | scroll [--out <png>] --y <pixels|bottom> | type [--out <png>] --selector <css> --text <text>. Add --viewport mobile for a 390x844 frame. The JSON reports console_errors — treat any as a defect signal. Exit code 3 with not_possible set (a \"NOT POSSIBLE: <reason>\" string) means this environment cannot drive a browser: report NOT POSSIBLE: <reason> and fall back to the data-level investigation at the end.\n" +
|
|
1174
1249
|
"a. Verify the built artifact exists: test -d ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/client/dist && test -f ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/server/dist/actions.js — if either is missing, report NOT POSSIBLE: built artifact not present at ~/workspace/ts-spaces/" + PUBLISH_SLUG + " and fall back to the data-level investigation.\n" +
|
|
@@ -1184,6 +1259,18 @@ while (i < STEPS.length) {
|
|
|
1184
1259
|
"This returns current sessions, events, and tasks — capture concrete evidence from the data you retrieve.\n" +
|
|
1185
1260
|
"Report your reproduction steps and evidence as plain prose.\n" +
|
|
1186
1261
|
"End your report with exactly one line: VERDICT: PASS if you reproduced the reported bug (your frames show the reported misbehavior), VERDICT: FAIL if you could not. --expected names the bug as reported (its visible manifestation); --actual names what your frames actually showed. Checks you could not run are evidence gaps, not silent drops: name every one in --missing. First ensure the OODA log exists even if you logged zero steps (touch " + crewHome + "/task-evidence/" + taskId + "/repro/ooda-log.jsonl — an empty log is honest, an absent one is a broken report). Also write the same verdict machine-readably: node " + crewHome + "/current/lib/write-ooda-verdict.js --dir " + crewHome + "/task-evidence/" + taskId + "/repro/ --attempt \"1\" --verdict <PASS|FAIL|NOT_POSSIBLE> --summary \"<one line>\" --expected \"<the bug as reported — its visible manifestation>\" --actual \"<what your frames actually showed>\" --missing '[\"honest evidence gap, if any\"]' [--reason \"<why it failed — REQUIRED and non-empty when verdict is FAIL or NOT_POSSIBLE; the script rejects a reason-less negative verdict with exit 2>\"] — this writes verdict.json (the latest verdict) and appends to verdicts.jsonl (the append-only ledger: every attempt's verdict is preserved, never overwritten). A FAIL or NOT_POSSIBLE verdict without a machine-readable --reason cannot be written — state the reason.";
|
|
1262
|
+
} else if (reproLayer === "engine") {
|
|
1263
|
+
instructions = "REPRODUCE AT THE BUG'S LAYER. Triage classified this bug as layer: engine — it lives in the crew's own machinery (workflows, lib scripts, shell scripts, tests, scheduler), not in the rendered artifact. Reproduce it deterministically with shell commands in the repo checkout at " + REPO_PATH + " — that exact checkout, not any other copy of the project on disk. You are NOT code-blind here: reading source to find the failing mechanism is expected.\n" +
|
|
1264
|
+
"Do NOT start an artifact server. Do NOT invoke see-act.js or any browser loop — the experiential loop is for artifact-layer bugs only, and driving it here fails the phase loudly. If you cannot reproduce without a browser, say so and FAIL with a reason naming the layer you tried.\n" +
|
|
1265
|
+
"Reproduce with the commands that match the bug: run the failing script directly, re-run the failing test suite (bash tests/run.sh), grep the workflow source, query scheduler/crew state via the crew API. Concrete evidence only: the command lines you ran, their verbatim stdout, and their exit codes.\n" +
|
|
1266
|
+
"Report your reproduction steps and evidence as plain prose.\n" +
|
|
1267
|
+
"End your report with exactly one line: VERDICT: PASS if you reproduced the reported bug (your command output shows the reported misbehavior), VERDICT: FAIL if you could not. --expected names the bug as reported; --actual names what your commands actually showed. Checks you could not run are evidence gaps, not silent drops: name every one in --missing. Also write the same verdict machine-readably: node " + crewHome + "/current/lib/write-ooda-verdict.js --dir " + crewHome + "/task-evidence/" + taskId + "/repro/ --attempt \"1\" --verdict <PASS|FAIL|NOT_POSSIBLE> --summary \"<one line>\" --expected \"<the bug as reported>\" --actual \"<what your commands actually showed>\" --missing '[\"honest evidence gap, if any\"]' [--reason \"<why it failed — REQUIRED and non-empty when verdict is FAIL or NOT_POSSIBLE; the script rejects a reason-less negative verdict with exit 2>\"] — this writes verdict.json (the latest verdict) and appends to verdicts.jsonl (the append-only ledger: every attempt's verdict is preserved, never overwritten). A FAIL or NOT_POSSIBLE verdict without a machine-readable --reason cannot be written — state the reason.";
|
|
1268
|
+
} else {
|
|
1269
|
+
instructions = "REPRODUCE AT THE BUG'S LAYER. Triage classified this bug as layer: docs — a documentation gap or error. Reproduce it by reading the file, not the browser: open the document in the repo checkout at " + REPO_PATH + " (that exact checkout, not any other copy of the project on disk) and verify the reported gap or error is actually there (a missing section, a stale instruction, a wrong claim). Quote the exact lines you found — or their absence.\n" +
|
|
1270
|
+
"Do NOT start an artifact server. Do NOT invoke see-act.js or any browser loop — the experiential loop is for artifact-layer bugs only, and driving it here fails the phase loudly. If you cannot verify the gap without a browser, say so and FAIL with a reason naming the layer you tried.\n" +
|
|
1271
|
+
"Report your findings as plain prose: what the document says today versus what it should say.\n" +
|
|
1272
|
+
"End your report with exactly one line: VERDICT: PASS if you confirmed the reported docs gap, VERDICT: FAIL if you could not. Also write the same verdict machine-readably: node " + crewHome + "/current/lib/write-ooda-verdict.js --dir " + crewHome + "/task-evidence/" + taskId + "/repro/ --attempt \"1\" --verdict <PASS|FAIL|NOT_POSSIBLE> --summary \"<one line>\" --expected \"<the bug as reported>\" --actual \"<what the document actually says>\" --missing '[\"honest evidence gap, if any\"]' [--reason \"<why it failed — REQUIRED and non-empty when verdict is FAIL or NOT_POSSIBLE; the script rejects a reason-less negative verdict with exit 2>\"] — this writes verdict.json (the latest verdict) and appends to verdicts.jsonl (the append-only ledger: every attempt's verdict is preserved, never overwritten). A FAIL or NOT_POSSIBLE verdict without a machine-readable --reason cannot be written — state the reason.";
|
|
1273
|
+
}
|
|
1187
1274
|
|
|
1188
1275
|
} else if (step.name === "Map") {
|
|
1189
1276
|
var mapGatePara = "";
|
|
@@ -1315,6 +1402,10 @@ while (i < STEPS.length) {
|
|
|
1315
1402
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1316
1403
|
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
1317
1404
|
"VERDICT: PASS\n\n" +
|
|
1405
|
+
"If the script's output contains PUBLISH_SKIPPED=no-npm-publish, the publish was skipped gracefully: npm publish is not configured on this machine (helper or credential absent) — the merge stands, the version was not cut, nothing was shipped. Paste the marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
1406
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1407
|
+
"skipped: no-npm-publish (npm publish not configured — helper or credential missing; nothing versioned or published)\n" +
|
|
1408
|
+
"VERDICT: PASS\n\n" +
|
|
1318
1409
|
"If it exits zero, paste the script's COMPLETE marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
1319
1410
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1320
1411
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -1409,9 +1500,17 @@ while (i < STEPS.length) {
|
|
|
1409
1500
|
} else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
|
|
1410
1501
|
return await parkTask("Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
|
|
1411
1502
|
}
|
|
1503
|
+
// The empty tree is not a commit: git merge-base --is-ancestor fails on it.
|
|
1504
|
+
// The workflow knows publishBase == EMPTY_TREE_SHA (set above), so it
|
|
1505
|
+
// hardcodes ANCESTOR=yes for a first publish instead of asking the agent
|
|
1506
|
+
// to execute the conditional (clean-room 2026-09-16: the agent ran
|
|
1507
|
+
// merge-base on the empty tree directly and parked).
|
|
1508
|
+
var ancestorShell = (publishBase === EMPTY_TREE_SHA)
|
|
1509
|
+
? "ANCESTOR=yes && "
|
|
1510
|
+
: "git merge-base --is-ancestor \"$BASE\" \"$HEAD\" && ANCESTOR=yes || ANCESTOR=no && ";
|
|
1412
1511
|
var diffResult = await agent(
|
|
1413
1512
|
"Run: cd " + REPO_PATH + " && BASE='" + publishBase + "' && HEAD=$(git rev-parse HEAD) && " +
|
|
1414
|
-
|
|
1513
|
+
ancestorShell +
|
|
1415
1514
|
"echo '---COMMIT---' && echo \"$HEAD\" && echo '---BASE---' && echo \"$BASE\" && echo '---ANCESTOR---' && echo \"$ANCESTOR\" && " +
|
|
1416
1515
|
"if [ \"$ANCESTOR\" = yes ]; then echo '---DIFF---' && git diff \"$BASE\" \"$HEAD\" && echo '---NAMES---' && git diff-tree --no-commit-id --name-only -r \"$BASE\" \"$HEAD\"; fi\n" +
|
|
1417
1516
|
"Return JSON { \"commit\": \"<HEAD trimmed>\", \"base\": \"<BASE trimmed>\", \"ancestor\": \"<yes|no>\", \"diff\": \"<raw unified diff, may be multi-line>\", \"files\": \"<newline-separated paths>\" } and nothing else.",
|
|
@@ -2231,6 +2330,35 @@ while (i < STEPS.length) {
|
|
|
2231
2330
|
"Return your work as JSON in exactly this shape: {\"status\": \"ok\", \"result\": \"your report here\"}. " +
|
|
2232
2331
|
"The result is plain prose describing what you did and found. For verdict steps, end the report with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
|
|
2233
2332
|
var workKeyBase = "work-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : "");
|
|
2333
|
+
// Reproduce wrong-layer guard baseline (2026-09-17): snapshot the repro
|
|
2334
|
+
// evidence dir listing BEFORE the Reproduce work agent runs. The guard
|
|
2335
|
+
// after the run diffs against this baseline — only frames created by the
|
|
2336
|
+
// current attempt count as a wrong-layer violation; stale frames from
|
|
2337
|
+
// pre-fix attempts appear in both snapshots and are excluded (set diff,
|
|
2338
|
+
// no wall-clock, deterministic). One snapshot per phase attempt covers
|
|
2339
|
+
// all transport retries inside the dispatch loop. null = baseline
|
|
2340
|
+
// unavailable; the guard then fails closed for retry.
|
|
2341
|
+
var reproEvidenceDir = crewHome + "/task-evidence/" + taskId + "/repro";
|
|
2342
|
+
var reproFramesBefore = null; // null = baseline unavailable (guard must fail closed)
|
|
2343
|
+
if (step.name === "Reproduce" && (reproLayer === "engine" || reproLayer === "docs")) {
|
|
2344
|
+
try {
|
|
2345
|
+
var reproBefore = await agent(
|
|
2346
|
+
"Run: ls " + reproEvidenceDir + " 2>/dev/null | grep -E -- '-(shot|click|scroll|type)-' || echo NONE\n" +
|
|
2347
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
2348
|
+
{ key: attemptKey("repro-layer-guard-before-" + taskId, totalReworkCount),
|
|
2349
|
+
label: "Snapshotting repro evidence before Reproduce attempt",
|
|
2350
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
2351
|
+
);
|
|
2352
|
+
var reproBeforeOut = (reproBefore && reproBefore.output ? reproBefore.output : "").trim();
|
|
2353
|
+
reproFramesBefore = (reproBeforeOut === "NONE" || reproBeforeOut === "")
|
|
2354
|
+
? []
|
|
2355
|
+
: reproBeforeOut.split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
2356
|
+
log("Reproduce guard baseline for task " + taskId + ": " + reproFramesBefore.length + " pre-existing frame(s)");
|
|
2357
|
+
} catch (e) {
|
|
2358
|
+
reproFramesBefore = null;
|
|
2359
|
+
log("Reproduce guard baseline snapshot failed for task " + taskId + " — guard will fail closed for retry");
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2234
2362
|
var workerResult = null;
|
|
2235
2363
|
var workAttempts = [];
|
|
2236
2364
|
for (var workAttempt = 0; workAttempt <= 2; workAttempt++) {
|
|
@@ -2432,6 +2560,91 @@ while (i < STEPS.length) {
|
|
|
2432
2560
|
log("QA visual-loop guard passed: experiential evidence present");
|
|
2433
2561
|
}
|
|
2434
2562
|
|
|
2563
|
+
// Reproduce wrong-layer guard (2026-09-16): for engine/docs tasks the
|
|
2564
|
+
// experiential see-act loop is forbidden — a reproducer that drives the
|
|
2565
|
+
// browser burns the run and emits a wrong-layer verdict (three tasks
|
|
2566
|
+
// parked on exactly this in the 2026-09-17 UTC shift). The workflow
|
|
2567
|
+
// checks the repro evidence dir mechanically for see-act frame artifacts
|
|
2568
|
+
// (the driver's archive naming: <n>-<action>-<desktop|mobile>.png).
|
|
2569
|
+
// Attempt-scoped (2026-09-17): the after-listing is diffed against the
|
|
2570
|
+
// baseline taken before this phase attempt's dispatch loop — only frames
|
|
2571
|
+
// CREATED BY the current attempt count as a violation; stale frames from
|
|
2572
|
+
// earlier attempts appear in both snapshots and are excluded. Without a
|
|
2573
|
+
// baseline the guard is inconclusive and fails closed for retry (passing
|
|
2574
|
+
// could route a genuinely wrong-layer verdict to Map).
|
|
2575
|
+
if (step.name === "Reproduce" && (reproLayer === "engine" || reproLayer === "docs") && verdictPassed !== null) {
|
|
2576
|
+
var reproEvidenceDir = crewHome + "/task-evidence/" + taskId + "/repro";
|
|
2577
|
+
var reproFramesOut = "";
|
|
2578
|
+
try {
|
|
2579
|
+
var reproFramesCheck = await agent(
|
|
2580
|
+
"Run: ls " + reproEvidenceDir + " 2>/dev/null | grep -E -- '-(shot|click|scroll|type)-' || echo NONE\n" +
|
|
2581
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
2582
|
+
{ key: attemptKey("repro-layer-guard-" + taskId, totalReworkCount), label: "Checking for wrong-layer driver invocation",
|
|
2583
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
2584
|
+
);
|
|
2585
|
+
reproFramesOut = (reproFramesCheck && reproFramesCheck.output ? reproFramesCheck.output : "").trim();
|
|
2586
|
+
} catch (e) {
|
|
2587
|
+
reproFramesOut = "";
|
|
2588
|
+
}
|
|
2589
|
+
var reproAfterList = (reproFramesOut && reproFramesOut !== "NONE")
|
|
2590
|
+
? reproFramesOut.split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; })
|
|
2591
|
+
: [];
|
|
2592
|
+
var reproGuardFailKind = null; // "wrong-layer" | "baseline-unavailable" | null
|
|
2593
|
+
var reproGuardDetail = "";
|
|
2594
|
+
if (reproFramesBefore === null) {
|
|
2595
|
+
reproGuardFailKind = "baseline-unavailable";
|
|
2596
|
+
} else {
|
|
2597
|
+
var reproBeforeSet = new Set(reproFramesBefore);
|
|
2598
|
+
var reproNewFrames = reproAfterList.filter(function (f) { return !reproBeforeSet.has(f); });
|
|
2599
|
+
if (reproNewFrames.length > 0) {
|
|
2600
|
+
reproGuardFailKind = "wrong-layer";
|
|
2601
|
+
reproGuardDetail = reproNewFrames.slice(0, 5).join(", ").slice(0, 300);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
if (reproGuardFailKind !== null) {
|
|
2605
|
+
var reproGuardNotes, reproGuardMessage;
|
|
2606
|
+
if (reproGuardFailKind === "baseline-unavailable") {
|
|
2607
|
+
log("Reproduce wrong-layer guard failed (baseline unavailable) — cannot attribute frames to this attempt on task " + taskId + " — marking failed for retry");
|
|
2608
|
+
reproGuardNotes = "Reproduce wrong-layer guard baseline unavailable: task is classified layer: " + reproLayer + " and the before-snapshot of " + reproEvidenceDir + " failed, so frames cannot be attributed to this attempt (fail-closed — passing could route a wrong-layer verdict to Map). Phase failed for retry; the retry re-snapshots";
|
|
2609
|
+
reproGuardMessage = "Reproduce wrong-layer guard baseline unavailable for a " + reproLayer + "-layer task — frames in " + reproEvidenceDir + " cannot be attributed to this attempt, phase failed, dispatcher will retry Reproduce";
|
|
2610
|
+
} else {
|
|
2611
|
+
log("Reproduce wrong-layer guard failed — see-act frames from THIS attempt on a " + reproLayer + "-layer task " + taskId + " (" + reproGuardDetail + ") — marking failed for retry");
|
|
2612
|
+
reproGuardNotes = "Reproduce wrong-layer guard failed: task is classified layer: " + reproLayer + ", but the experiential see-act browser loop was invoked (frames attributable to this attempt in " + reproEvidenceDir + ": " + reproGuardDetail + "). Engine bugs reproduce with deterministic repo commands, docs bugs by reading the file — the browser loop is for artifact-layer bugs only. Phase failed for retry";
|
|
2613
|
+
reproGuardMessage = "Reproduce wrong-layer guard failed — see-act loop invoked on a " + reproLayer + "-layer task (" + reproGuardDetail + " attributable to this attempt), phase failed, dispatcher will retry Reproduce";
|
|
2614
|
+
}
|
|
2615
|
+
await agent(
|
|
2616
|
+
"Record wrong-layer driver invocation.\n" +
|
|
2617
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
|
|
2618
|
+
task_id: taskId,
|
|
2619
|
+
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name,
|
|
2620
|
+
status: "failed", notes: reproGuardNotes },
|
|
2621
|
+
event: { task_id: taskId, type: "failed",
|
|
2622
|
+
message: reproGuardMessage }
|
|
2623
|
+
}),
|
|
2624
|
+
{ key: "record-repro-layer-guard-fail-" + step.name, label: "Recording wrong-layer guard failure" }
|
|
2625
|
+
);
|
|
2626
|
+
if (reproGuardFailKind === "baseline-unavailable") {
|
|
2627
|
+
return {
|
|
2628
|
+
__hatchWorkflowControl: "blocked",
|
|
2629
|
+
result: {
|
|
2630
|
+
"blocked_reason": "Reproduce wrong-layer guard baseline unavailable — cannot attribute frames to this attempt; retrying",
|
|
2631
|
+
message: reproGuardMessage,
|
|
2632
|
+
task_id: taskId
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
return {
|
|
2637
|
+
__hatchWorkflowControl: "blocked",
|
|
2638
|
+
result: {
|
|
2639
|
+
"blocked_reason": "Reproduce invoked the experiential browser loop on a " + reproLayer + "-layer task",
|
|
2640
|
+
message: reproGuardMessage,
|
|
2641
|
+
task_id: taskId
|
|
2642
|
+
}
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
log("Reproduce wrong-layer guard passed: no see-act frames from this attempt on " + reproLayer + "-layer task " + taskId);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2435
2648
|
|
|
2436
2649
|
// Worktree confinement (Build only): the declared worktree path must
|
|
2437
2650
|
// match WORKTREE_HINT exactly. A builder that worked in any other
|
|
@@ -2570,16 +2783,24 @@ while (i < STEPS.length) {
|
|
|
2570
2783
|
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
2571
2784
|
// no merge lock held (empty-diff Integrate), it skips the publish path
|
|
2572
2785
|
// gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
|
|
2573
|
-
// marker.
|
|
2786
|
+
// marker. The preflight (bugfix 2026-09-17) emits
|
|
2787
|
+
// PUBLISH_SKIPPED=no-npm-publish when npm publish is not configured on
|
|
2788
|
+
// this machine (helper or credential absent) — also before any mutation.
|
|
2789
|
+
// Verification is then vacuous — nothing was shipped, and the
|
|
2574
2790
|
// registry must NOT have moved. The marker is script-emitted explicit state
|
|
2575
2791
|
// (pasted verbatim per the Publish agent instructions), not agent prose; a
|
|
2576
2792
|
// report without the marker still runs the full verification fail-closed.
|
|
2577
2793
|
var publishVerified = false;
|
|
2578
2794
|
var npmPublishSkipped = false;
|
|
2579
2795
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
2580
|
-
|
|
2796
|
+
var publishSkipMatch = /^PUBLISH_SKIPPED=(no-lock-held|no-npm-publish)$/m.exec(workerText || "");
|
|
2797
|
+
if (publishSkipMatch) {
|
|
2581
2798
|
npmPublishSkipped = true;
|
|
2582
|
-
|
|
2799
|
+
if (publishSkipMatch[1] === "no-npm-publish") {
|
|
2800
|
+
log("Publish skipped for task " + taskId + " (no-npm-publish — npm publish not configured): publish verification vacuous, nothing was shipped");
|
|
2801
|
+
} else {
|
|
2802
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
2803
|
+
}
|
|
2583
2804
|
}
|
|
2584
2805
|
if (!npmPublishSkipped) {
|
|
2585
2806
|
try {
|
|
@@ -2699,6 +2920,10 @@ while (i < STEPS.length) {
|
|
|
2699
2920
|
// Capture Sage's experiential flag (machine-read marker line).
|
|
2700
2921
|
if (step.name === "Triage" && passed) {
|
|
2701
2922
|
isExperiential = extractExperiential(workerText);
|
|
2923
|
+
// Capture Sage's bug-layer flag (machine-read marker line): drives the
|
|
2924
|
+
// Reproduce strategy dispatch. Unknown (null) degrades to "artifact" at
|
|
2925
|
+
// resolveLayer — never parks on a garbled line.
|
|
2926
|
+
bugLayer = extractLayer(workerText);
|
|
2702
2927
|
}
|
|
2703
2928
|
|
|
2704
2929
|
// Capture the accepted Build report's machine-readable release decision.
|
package/workflows/chore.js
CHANGED
|
@@ -55,7 +55,10 @@ if (!inputs.crewHome) throw new Error("crewHome is required — pass the crew ho
|
|
|
55
55
|
const crewHome = inputs.crewHome;
|
|
56
56
|
// Crew API: the workflow calls the crew-owned CLI, not the dashboard.
|
|
57
57
|
// The CLI implements the API.md contract against $CREW_HOME/crew-state.db.
|
|
58
|
-
const
|
|
58
|
+
const CREW_API_SRC = crewHome + "/current/lib/crew-api.js";
|
|
59
|
+
// Pinned at pinLifecycle: after the pin, CREW_API points into RUN_LIB so a
|
|
60
|
+
// mid-flight release swap cannot change the CLI under a running workflow.
|
|
61
|
+
let CREW_API = CREW_API_SRC;
|
|
59
62
|
// Build a shell command invoking the CLI. Args are JSON-encoded and
|
|
60
63
|
// single-quote-wrapped for safe shell passing. The agent runs this and
|
|
61
64
|
// returns the stdout verbatim (the CLI emits JSON on stdout).
|
|
@@ -132,9 +135,12 @@ const LIFECYCLE = RUN_LIB + "/worktree-lifecycle.sh";
|
|
|
132
135
|
const MERGE_LOCK = RUN_LIB + "/merge-lock.sh";
|
|
133
136
|
const PUBLISH_NPM_SRC = crewHome + "/lib/publish-npm.sh";
|
|
134
137
|
const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
135
|
-
|
|
138
|
+
const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
|
|
139
|
+
const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
140
|
+
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
141
|
+
// The five basenames the pin step must materialize — asserted mechanically
|
|
136
142
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
137
|
-
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM].map(function (p) { return p.split("/").pop(); });
|
|
143
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED].map(function (p) { return p.split("/").pop(); });
|
|
138
144
|
|
|
139
145
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
140
146
|
const projectConfig = inputs.project_config || {};
|
|
@@ -280,7 +286,7 @@ function attemptKey(base, reworkCount) {
|
|
|
280
286
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
281
287
|
}
|
|
282
288
|
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
283
|
-
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the
|
|
289
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the five pinned
|
|
284
290
|
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
285
291
|
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
286
292
|
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
@@ -288,7 +294,7 @@ function attemptKey(base, reworkCount) {
|
|
|
288
294
|
function pinLifecycle(key) {
|
|
289
295
|
return agent(
|
|
290
296
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
291
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
297
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
292
298
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
293
299
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
294
300
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -579,7 +585,7 @@ function extractMarkerLines(workerText) {
|
|
|
579
585
|
var markers = [];
|
|
580
586
|
for (var i = 0; i < lines.length; i++) {
|
|
581
587
|
var line = lines[i].trim();
|
|
582
|
-
if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|capture_targets:|worktree:)/i.test(line)) {
|
|
588
|
+
if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|layer:|capture_targets:|worktree:)/i.test(line)) {
|
|
583
589
|
markers.push(line);
|
|
584
590
|
}
|
|
585
591
|
}
|
|
@@ -854,7 +860,7 @@ await telemetryStart("chore");
|
|
|
854
860
|
// ── Pin lifecycle scripts ────────────────────────────────────────────
|
|
855
861
|
// Copy lifecycle scripts into a per-task temp dir so this run is immune
|
|
856
862
|
// to upgrades that land while it's in flight. Verified mechanically:
|
|
857
|
-
// workflow code asserts the
|
|
863
|
+
// workflow code asserts the five basenames from the verbatim listing —
|
|
858
864
|
// the agent cannot self-certify. Any miss parks the task before Triage.
|
|
859
865
|
await telemetryEvent("pin_start");
|
|
860
866
|
const initialPins = parsePinListing(await pinLifecycle("pin-lifecycle"));
|
|
@@ -864,6 +870,10 @@ if (missingInitialPins.length > 0) {
|
|
|
864
870
|
return await parkTask("Lifecycle pin incomplete before Triage — missing " + missingInitialPins.join(", ") + " in " + RUN_LIB + ".");
|
|
865
871
|
}
|
|
866
872
|
log("Lifecycle scripts pinned to " + RUN_LIB);
|
|
873
|
+
// From here on, every crew-api.js invocation uses the pinned copy: immune
|
|
874
|
+
// to a release swap landing mid-flight.
|
|
875
|
+
CREW_API = CREW_API_PINNED;
|
|
876
|
+
log("Crew API pinned to " + CREW_API);
|
|
867
877
|
|
|
868
878
|
// Merge-lock holder identity (bug 2fc8f52f): the opaque task+run identity
|
|
869
879
|
// minted at this run's first claim (never a PID — short-lived agent PIDs
|
|
@@ -1170,7 +1180,9 @@ while (i < STEPS.length) {
|
|
|
1170
1180
|
var mapBaselineRefs = "";
|
|
1171
1181
|
var mapBaselineNone = false;
|
|
1172
1182
|
if (step.name === "Map") {
|
|
1173
|
-
|
|
1183
|
+
// Must match Capture's run condition (experiential + artifact publish):
|
|
1184
|
+
// when Capture skips, no baseline notes exist, so the gate must not apply.
|
|
1185
|
+
if ((await resolveExperiential()) === "yes" && PUBLISH_TYPE === "artifact") {
|
|
1174
1186
|
var gateStatus = await baselineStatus();
|
|
1175
1187
|
if (!gateStatus.baseline_found) {
|
|
1176
1188
|
log("Map gate: no baseline evidence for experiential task " + taskId + " — bouncing to Capture");
|
|
@@ -1328,6 +1340,10 @@ while (i < STEPS.length) {
|
|
|
1328
1340
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1329
1341
|
"skipped: no-lock-held (empty-diff Integrate — nothing merged, nothing to ship)\n" +
|
|
1330
1342
|
"VERDICT: PASS\n\n" +
|
|
1343
|
+
"If the script's output contains PUBLISH_SKIPPED=no-npm-publish, the publish was skipped gracefully: npm publish is not configured on this machine (helper or credential absent) — the merge stands, the version was not cut, nothing was shipped. Paste the marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
1344
|
+
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1345
|
+
"skipped: no-npm-publish (npm publish not configured — helper or credential missing; nothing versioned or published)\n" +
|
|
1346
|
+
"VERDICT: PASS\n\n" +
|
|
1331
1347
|
"If it exits zero, paste the script's COMPLETE marker block verbatim into your report, then end your report with exactly these three lines, in this order — lowercase, no trailing period, do not rephrase:\n" +
|
|
1332
1348
|
"TARGET_VERSION=" + publishTarget.target + " computed as " + publishTarget.base + " + " + publishTarget.scope + " → " + publishTarget.target + "\n" +
|
|
1333
1349
|
"published: muse-crew@" + publishTarget.target + "\n" +
|
|
@@ -1422,9 +1438,17 @@ while (i < STEPS.length) {
|
|
|
1422
1438
|
} else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
|
|
1423
1439
|
return await parkTask("Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
|
|
1424
1440
|
}
|
|
1441
|
+
// The empty tree is not a commit: git merge-base --is-ancestor fails on it.
|
|
1442
|
+
// The workflow knows publishBase == EMPTY_TREE_SHA (set above), so it
|
|
1443
|
+
// hardcodes ANCESTOR=yes for a first publish instead of asking the agent
|
|
1444
|
+
// to execute the conditional (clean-room 2026-09-16: the agent ran
|
|
1445
|
+
// merge-base on the empty tree directly and parked).
|
|
1446
|
+
var ancestorShell = (publishBase === EMPTY_TREE_SHA)
|
|
1447
|
+
? "ANCESTOR=yes && "
|
|
1448
|
+
: "git merge-base --is-ancestor \"$BASE\" \"$HEAD\" && ANCESTOR=yes || ANCESTOR=no && ";
|
|
1425
1449
|
var diffResult = await agent(
|
|
1426
1450
|
"Run: cd " + REPO_PATH + " && BASE='" + publishBase + "' && HEAD=$(git rev-parse HEAD) && " +
|
|
1427
|
-
|
|
1451
|
+
ancestorShell +
|
|
1428
1452
|
"echo '---COMMIT---' && echo \"$HEAD\" && echo '---BASE---' && echo \"$BASE\" && echo '---ANCESTOR---' && echo \"$ANCESTOR\" && " +
|
|
1429
1453
|
"if [ \"$ANCESTOR\" = yes ]; then echo '---DIFF---' && git diff \"$BASE\" \"$HEAD\" && echo '---NAMES---' && git diff-tree --no-commit-id --name-only -r \"$BASE\" \"$HEAD\"; fi\n" +
|
|
1430
1454
|
"Return JSON { \"commit\": \"<HEAD trimmed>\", \"base\": \"<BASE trimmed>\", \"ancestor\": \"<yes|no>\", \"diff\": \"<raw unified diff, may be multi-line>\", \"files\": \"<newline-separated paths>\" } and nothing else.",
|
|
@@ -2426,16 +2450,24 @@ while (i < STEPS.length) {
|
|
|
2426
2450
|
// Skip-aware (park 2026-09-11): when the deterministic publish script found
|
|
2427
2451
|
// no merge lock held (empty-diff Integrate), it skips the publish path
|
|
2428
2452
|
// gracefully and emits the machine-readable PUBLISH_SKIPPED=no-lock-held
|
|
2429
|
-
// marker.
|
|
2453
|
+
// marker. The preflight (bugfix 2026-09-17) emits
|
|
2454
|
+
// PUBLISH_SKIPPED=no-npm-publish when npm publish is not configured on
|
|
2455
|
+
// this machine (helper or credential absent) — also before any mutation.
|
|
2456
|
+
// Verification is then vacuous — nothing was shipped, and the
|
|
2430
2457
|
// registry must NOT have moved. The marker is script-emitted explicit state
|
|
2431
2458
|
// (pasted verbatim per the Publish agent instructions), not agent prose; a
|
|
2432
2459
|
// report without the marker still runs the full verification fail-closed.
|
|
2433
2460
|
var publishVerified = false;
|
|
2434
2461
|
var npmPublishSkipped = false;
|
|
2435
2462
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget) {
|
|
2436
|
-
|
|
2463
|
+
var publishSkipMatch = /^PUBLISH_SKIPPED=(no-lock-held|no-npm-publish)$/m.exec(workerText || "");
|
|
2464
|
+
if (publishSkipMatch) {
|
|
2437
2465
|
npmPublishSkipped = true;
|
|
2438
|
-
|
|
2466
|
+
if (publishSkipMatch[1] === "no-npm-publish") {
|
|
2467
|
+
log("Publish skipped for task " + taskId + " (no-npm-publish — npm publish not configured): publish verification vacuous, nothing was shipped");
|
|
2468
|
+
} else {
|
|
2469
|
+
log("Publish skipped for task " + taskId + " (no merge lock held — empty-diff Integrate): publish verification vacuous, nothing was shipped");
|
|
2470
|
+
}
|
|
2439
2471
|
}
|
|
2440
2472
|
if (!npmPublishSkipped) {
|
|
2441
2473
|
try {
|