muse-crew 0.12.0 → 0.13.1
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 +38 -4
- package/docs/guide.md +1 -1
- package/docs/publish-verification.md +17 -5
- package/lib/AGENTS.md +4 -4
- package/lib/advance-publish-base.js +36 -15
- package/lib/compose-evidence-caption.js +4 -3
- package/lib/crew-api.js +303 -52
- package/lib/schema.sql +25 -1
- package/lib/test-worktree-backend.sh +29 -0
- package/lib/verify-publish.js +4 -3
- package/lib/worktree-lifecycle.sh +9 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +5 -3
- package/workflows/bugfix.js +184 -36
- package/workflows/chore.js +183 -35
- package/workflows/crew-init.js +59 -4
- package/workflows/standard.js +184 -36
- package/workflows/upgrade.js +8 -3
|
@@ -40,11 +40,13 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
40
40
|
4.5. **Parent publish verification (docs/publish-verification.md):** The publisher parks instead of stamping provenance; the parent — this tick, the live root agent — verifies content and stamps. Deterministic code detects, reads back, and certifies; you are only the ferry between the deterministic steps (scan → sensor → verifier).
|
|
41
41
|
- Scan (code): `node {crewHome}/lib/crew-api.js --crew-home {crewHome} scan-verification-pending`
|
|
42
42
|
This atomically claims each verification-pending task (1-hour lease, so a second tick cannot double-verify) and reconciles verified-but-still-parked tasks to `in_progress`. It returns `{ to_verify: [...], reconciled: [...] }`. Log both lists. If the scan exits 2 (e.g. the active release cannot be resolved), log the error loudly and continue — do NOT work around it.
|
|
43
|
-
- **Read-back (deterministic sensor, 2026-09-15):** no platform inspection tool is needed — `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source (`~/workspace/ts-spaces/<slug>/`) and emits the machine-readable findings block the verifier parses. For each entry in `to_verify`, resolve the base via `get-provenance` (`source_commit`; the empty-tree sha `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when nothing is stamped yet — a first publish)
|
|
43
|
+
- **Read-back (deterministic sensor, 2026-09-15):** no platform inspection tool is needed — `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source (`~/workspace/ts-spaces/<slug>/`) and emits the machine-readable findings block the verifier parses. For each entry in `to_verify`, resolve the base via `get-provenance` (`source_commit`; the empty-tree sha `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when nothing is stamped yet — a first publish):
|
|
44
|
+
`node {crewHome}/lib/crew-api.js --crew-home {crewHome} get-provenance --json '{"project_id": "<project_id>"}'`
|
|
45
|
+
(use the entry's `project_id` verbatim), then run:
|
|
44
46
|
`node {crewHome}/lib/readback-disk.js --repo-path "<repo_path>" --commit <commit> --base <base> --slug "<deploy_slug>" --task-id <task_id> > /tmp/readback-<task_id>.txt 2> /tmp/readback-<task_id>.err`
|
|
45
47
|
Use the entry's `repo_path` and `deploy_slug` verbatim. If the sensor exits 0, the result file holds the findings block — hand it to the verify step below. If it exits non-zero, do NOT save or use stdout: log `publish: verification-procedural-error <commit> <first line of the .err file>` and leave the task parked — the next tick retries. A sensor failure is procedural (the read could not be performed), never a content verdict. Do NOT judge content yourself, and do NOT stamp provenance.
|
|
46
|
-
- **Verify (code):** `node {crewHome}/lib/verify-publish.js --crew-home {crewHome} --task-id <task_id> --commit <commit> --base <base> --repo-path "<repo_path>" --slug "<deploy_slug>" --crew-release <crew_release> --inspection-id <task_id>-disk --result-file /tmp/readback-<task_id>.txt`
|
|
47
|
-
Pass `--build-agent-id <id>` from the entry's `build_agent_id` when it is present. Use the SAME `<base>` the sensor ran with. The verifier parses the findings, compares mechanically against the base..commit diff, checks supersession, and stamps only on a match. Its terminal verdicts (`publish: verified` → task re-queued; `publish: verification-failed` → stays parked) are final — log them and continue.
|
|
48
|
+
- **Verify (code):** `node {crewHome}/lib/verify-publish.js --crew-home {crewHome} --task-id <task_id> --commit <commit> --base <base> --repo-path "<repo_path>" --slug "<deploy_slug>" --crew-release <crew_release> --project-id <project_id> --inspection-id <task_id>-disk --result-file /tmp/readback-<task_id>.txt`
|
|
49
|
+
Pass `--build-agent-id <id>` from the entry's `build_agent_id` when it is present. Use the SAME `<base>` the sensor ran with and the entry's `project_id` verbatim. The verifier parses the findings, compares mechanically against the base..commit diff, checks supersession, and stamps only on a match. Its terminal verdicts (`publish: verified` → task re-queued; `publish: verification-failed` → stays parked) are final — log them and continue.
|
|
48
50
|
- Never stamp provenance from prose. Never infer a verdict from an inspector's summary text. The verify script's machine-checked comparison is the only certification.
|
|
49
51
|
|
|
50
52
|
5. **Monitor launched workflows until terminal (stay-alive — 2026-09-13):** The platform ties async workflow `agent()` authorization to the launcher's lifetime: if THIS tick ends while a workflow is still running, the workflow's next `agent()` call fails with "subagent bootstrap is no longer authorized" / "subagent reservation owner is terminal". Prevention beats recovery here, so this tick is configured with a 90-minute execution timeout (`timeout_secs: 5400` in seed/crons.json) and you MUST stay alive until every launched run reaches a terminal state. Do not exit early while a launched run is still `running` — your death is what kills it.
|
package/workflows/bugfix.js
CHANGED
|
@@ -83,13 +83,18 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
|
83
83
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
84
84
|
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
85
85
|
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
86
|
-
|
|
86
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
87
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
88
|
+
// The seven basenames the pin step must materialize — asserted mechanically
|
|
87
89
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
88
90
|
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
89
91
|
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
90
92
|
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
91
93
|
// mid-run release swap cannot change it under the workflow.
|
|
92
|
-
|
|
94
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
95
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
96
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
97
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
|
|
93
98
|
|
|
94
99
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
95
100
|
const projectConfig = inputs.project_config || {};
|
|
@@ -273,7 +278,7 @@ function attemptKey(base, reworkCount) {
|
|
|
273
278
|
function pinLifecycle(key) {
|
|
274
279
|
return agent(
|
|
275
280
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
276
|
-
"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 + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
281
|
+
"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 + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && cp " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
277
282
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
278
283
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
279
284
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -544,14 +549,34 @@ function extractMarkerLines(workerText) {
|
|
|
544
549
|
// builder correctly makes no commit because the deliverable is already on
|
|
545
550
|
// main (a prior merge or hand-repair landed it), it declares
|
|
546
551
|
// `repo_diff: none (already-merged: <sha>)` naming the main commit that
|
|
547
|
-
// carries the work.
|
|
548
|
-
//
|
|
549
|
-
//
|
|
552
|
+
// carries the work. Room #16 blocker 11 (2026-09-18): the line anchor
|
|
553
|
+
// missed Wren's mid-paragraph declaration, and the persisted notes truncated
|
|
554
|
+
// the tail — so the anchor is gone and a sha followed by `)`, whitespace, or
|
|
555
|
+
// end-of-string (truncation) is accepted. The sha is hex-only (7-40 chars)
|
|
556
|
+
// so the workflow can interpolate it into the mechanical ancestor check
|
|
557
|
+
// without injection risk; a over-long hex run never matches (the lookahead
|
|
558
|
+
// fails on the extra hex char). Pure — pinned byte-identical across
|
|
559
|
+
// standard/bugfix/chore.
|
|
550
560
|
function extractAlreadyMerged(workerText) {
|
|
551
|
-
var m =
|
|
561
|
+
var m = /repo_diff:\s*none\s*\(already-merged:\s*([0-9a-f]{7,40})(?=[\s)]|$)/i.exec(workerText || "");
|
|
552
562
|
return m ? { sha: m[1].toLowerCase() } : { sha: null };
|
|
553
563
|
}
|
|
554
564
|
|
|
565
|
+
// Explicit artifact refusal (room #16 blocker 10, 2026-09-18): the rebuild
|
|
566
|
+
// trigger child ends its turn with `ARTIFACT_EDIT_REFUSED: <text>` when
|
|
567
|
+
// artifact_edit explicitly refuses the edit (e.g. the artifact does not
|
|
568
|
+
// exist). A refusal is conclusive negative evidence — the edit provably did
|
|
569
|
+
// NOT go through — distinct from an unconsumed trigger return (unknown).
|
|
570
|
+
// Pure — pinned byte-identical across standard/bugfix/chore.
|
|
571
|
+
// The signal must be the ENTIRE trimmed turn output (not a line within prose):
|
|
572
|
+
// the trigger child is instructed to end its turn with exactly this line and
|
|
573
|
+
// nothing else. A confused child quoting the instructions back in prose must
|
|
574
|
+
// NOT produce a conclusive negative — that degrades to unknown (fail-closed).
|
|
575
|
+
function extractRefusal(workerText) {
|
|
576
|
+
var m = /^ARTIFACT_EDIT_REFUSED:\s*(.+?)\s*$/.exec(String(workerText || "").trim());
|
|
577
|
+
return m ? m[1].slice(0, 300) : null;
|
|
578
|
+
}
|
|
579
|
+
|
|
555
580
|
// Worktree confinement: the Build agent must declare the exact worktree
|
|
556
581
|
// path it built in on a `worktree:` marker line. The workflow compares it
|
|
557
582
|
// against WORKTREE_HINT mechanically (exact string match) — never by
|
|
@@ -1373,23 +1398,40 @@ while (i < STEPS.length) {
|
|
|
1373
1398
|
} else if (step.name === "Review") {
|
|
1374
1399
|
// Already-merged hydration: when this run did not execute Build itself
|
|
1375
1400
|
// (dispatcher resume at Review after a platform death between phases),
|
|
1376
|
-
// recover the workflow-
|
|
1377
|
-
//
|
|
1378
|
-
//
|
|
1379
|
-
//
|
|
1380
|
-
//
|
|
1401
|
+
// recover the workflow-verified sha. Room #16 blocker 11: the structured
|
|
1402
|
+
// session field is read FIRST — the `already_merged_verified:` notes line
|
|
1403
|
+
// is only a fallback, because session notes are hard-capped at 3000
|
|
1404
|
+
// chars and a truthful declaration at the report's tail was silently
|
|
1405
|
+
// truncated. The structured value was written by the workflow after a
|
|
1406
|
+
// mechanical ancestor check — it is trusted; the builder's bare
|
|
1407
|
+
// declaration never is. Absent both, the mechanical fact below reads
|
|
1408
|
+
// "none declared" and Cass fails closed. The hydration read is best-effort:
|
|
1409
|
+
// a transport throw degrades to "none declared" rather than crashing Review.
|
|
1381
1410
|
if (!alreadyMergedSha) {
|
|
1382
|
-
var
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1411
|
+
var hydResult = null;
|
|
1412
|
+
try {
|
|
1413
|
+
hydResult = await agent(
|
|
1414
|
+
"Read the latest completed Build session for task " + taskId + ".\n" +
|
|
1415
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
|
|
1416
|
+
"In the returned sessions array, find the most recent session (by started_at) with task_id \"" + taskId + "\", step \"Build\", and status \"completed\". Return exactly two sections, verbatim, with no commentary:\n" +
|
|
1417
|
+
"SHA: <the session's already_merged_sha field value, or the word null when it is null>\n" +
|
|
1418
|
+
"NOTES:\n<the session's notes field, verbatim>",
|
|
1419
|
+
{ key: "hydrate-already-merged" + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""), label: "Hydrating already-merged verification" }
|
|
1420
|
+
);
|
|
1421
|
+
} catch (hydErr) {
|
|
1422
|
+
log("Hydration read failed (" + String(hydErr && hydErr.message || hydErr) + "); treating as none declared.");
|
|
1423
|
+
}
|
|
1424
|
+
var hydStr = hydResult ? ((typeof hydResult === "string") ? hydResult : JSON.stringify(hydResult)) : "";
|
|
1425
|
+
var hydSha = /^SHA:\s*([0-9a-f]{7,40})\s*$/im.exec(hydStr);
|
|
1426
|
+
if (hydSha) {
|
|
1427
|
+
alreadyMergedSha = hydSha[1].toLowerCase();
|
|
1428
|
+
log("Hydrated already-merged verification from structured session field: " + alreadyMergedSha);
|
|
1429
|
+
} else {
|
|
1430
|
+
var hvm = /already_merged_verified:\s*([0-9a-f]{7,40})/i.exec(hydStr);
|
|
1431
|
+
if (hvm) {
|
|
1432
|
+
alreadyMergedSha = hvm[1].toLowerCase();
|
|
1433
|
+
log("Hydrated already-merged verification from Build session notes (fallback): " + alreadyMergedSha);
|
|
1434
|
+
}
|
|
1393
1435
|
}
|
|
1394
1436
|
}
|
|
1395
1437
|
instructions = "Review independently and cold. You have NOT seen any reasoning from the builder.\nDo NOT access the task dashboard, event log, or any comments. Your review is based solely on the spec and the code.\n\n" +
|
|
@@ -1532,6 +1574,85 @@ while (i < STEPS.length) {
|
|
|
1532
1574
|
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
1533
1575
|
// to ship.
|
|
1534
1576
|
if (!publishSkippedNoLock) {
|
|
1577
|
+
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1578
|
+
// Minted once here (not re-minted per use site) so the ledger always
|
|
1579
|
+
// records the exact key that was issued — and so a re-minted duplicate
|
|
1580
|
+
// can never drift from it. Defined before the preflight so pre-trigger
|
|
1581
|
+
// parks (room #16 blocker 10) record the same attempt key.
|
|
1582
|
+
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, totalReworkCount);
|
|
1583
|
+
// STEP 0.5 (mechanical, room #16 blocker 10): assert the artifact
|
|
1584
|
+
// target exists before any artifact_status / artifact_edit call. The
|
|
1585
|
+
// project was classified as an artifact surface (deploy_slug set),
|
|
1586
|
+
// but setup never provisioned the artifact — Publish then entered
|
|
1587
|
+
// the trigger path against a slug with no on-disk target and the
|
|
1588
|
+
// edit failed opaquely ("web artifact <slug> was not found on
|
|
1589
|
+
// disk"), which the ledger could only record as unknown. A missing
|
|
1590
|
+
// target is conclusive negative evidence: the edit provably did NOT
|
|
1591
|
+
// go through, so this parks rejected (not unknown) with the actual
|
|
1592
|
+
// missing path — no trigger issued, no blind retry, no observation
|
|
1593
|
+
// polling. The check is a pure filesystem stat; the path is
|
|
1594
|
+
// workflow-computed, never agent prose. An inconclusive check
|
|
1595
|
+
// (throw / unparseable signal) is fail-closed unknown: without
|
|
1596
|
+
// proof the target exists, no edit is issued. The slug is
|
|
1597
|
+
// interpolated into a shell command — a slug outside [a-zA-Z0-9_-]
|
|
1598
|
+
// (e.g. from a hand-edited space.json) is treated as inconclusive
|
|
1599
|
+
// rather than risking shell injection.
|
|
1600
|
+
var artifactTargetDir = "~/workspace/ts-spaces/" + PUBLISH_SLUG + "/";
|
|
1601
|
+
var preflightSignal = "";
|
|
1602
|
+
var preflightInconclusive = false;
|
|
1603
|
+
// Misconfiguration fast path: artifact surface with no slug is not a
|
|
1604
|
+
// signal problem — it's a project setup defect. Park rejected with a
|
|
1605
|
+
// truthful reason, not "inconclusive."
|
|
1606
|
+
if (!PUBLISH_SLUG) {
|
|
1607
|
+
await recordPublishLedger({
|
|
1608
|
+
commit: mergeCommitForPublish,
|
|
1609
|
+
attempt: rebuildAttemptKey,
|
|
1610
|
+
agent_id: null,
|
|
1611
|
+
applied_report: null,
|
|
1612
|
+
outcome: "rejected",
|
|
1613
|
+
detail: "artifact surface with empty deploy_slug (preflight): the project is classified as artifact but has no deploy_slug — misconfiguration, not a missing artifact. No edit was issued."
|
|
1614
|
+
}, totalReworkCount);
|
|
1615
|
+
return await parkTask("Publish cannot proceed for task " + taskId + ": the project is classified as an artifact surface but has no deploy_slug. This is a project configuration defect — set a deploy_slug for the project, then re-run Publish. Human attention needed.");
|
|
1616
|
+
}
|
|
1617
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(PUBLISH_SLUG)) {
|
|
1618
|
+
preflightInconclusive = true;
|
|
1619
|
+
log("Publish artifact preflight for task " + taskId + ": PUBLISH_SLUG has an unsafe shape — inconclusive, fail-closed");
|
|
1620
|
+
} else try {
|
|
1621
|
+
var preflight = await agent(
|
|
1622
|
+
"Check whether the artifact target directory exists.\n" +
|
|
1623
|
+
"Run in shell: test -d ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/ && echo ARTIFACT_TARGET: present || echo ARTIFACT_TARGET: missing\n" +
|
|
1624
|
+
"Return JSON { \"signal\": \"<the exact ARTIFACT_TARGET line>\" } and nothing else.",
|
|
1625
|
+
{ key: attemptKey("publish-artifact-preflight-" + taskId, totalReworkCount), label: "Checking artifact target exists",
|
|
1626
|
+
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1627
|
+
);
|
|
1628
|
+
preflightSignal = String((preflight && preflight.signal) || "");
|
|
1629
|
+
} catch (preflightErr) {
|
|
1630
|
+
preflightInconclusive = true;
|
|
1631
|
+
log("Publish artifact preflight for task " + taskId + " threw (" + (preflightErr && preflightErr.message ? preflightErr.message : preflightErr) + ") — inconclusive, fail-closed");
|
|
1632
|
+
}
|
|
1633
|
+
if (!preflightInconclusive && /ARTIFACT_TARGET:\s*missing/.test(preflightSignal)) {
|
|
1634
|
+
await recordPublishLedger({
|
|
1635
|
+
commit: mergeCommitForPublish,
|
|
1636
|
+
attempt: rebuildAttemptKey,
|
|
1637
|
+
agent_id: null,
|
|
1638
|
+
applied_report: null,
|
|
1639
|
+
outcome: "rejected",
|
|
1640
|
+
detail: "artifact target directory missing (preflight): " + artifactTargetDir + " does not exist — setup never provisioned the artifact for deploy_slug " + PUBLISH_SLUG + ". The edit provably did not go through: no trigger issued, no blind retry"
|
|
1641
|
+
}, totalReworkCount);
|
|
1642
|
+
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact target directory " + artifactTargetDir + " does not exist. The project is classified as an artifact surface (deploy_slug " + PUBLISH_SLUG + ") but setup never provisioned the artifact — this is conclusive (rejected, not unknown): no edit was issued. Create the artifact via the Muse UI (Publish edits an existing artifact; it never creates one), then re-run init and Publish. Human attention needed.");
|
|
1643
|
+
}
|
|
1644
|
+
if (preflightInconclusive || !/ARTIFACT_TARGET:\s*present/.test(preflightSignal)) {
|
|
1645
|
+
await recordPublishLedger({
|
|
1646
|
+
commit: mergeCommitForPublish,
|
|
1647
|
+
attempt: rebuildAttemptKey,
|
|
1648
|
+
agent_id: null,
|
|
1649
|
+
applied_report: null,
|
|
1650
|
+
outcome: "unknown",
|
|
1651
|
+
detail: "artifact preflight inconclusive (no parsable ARTIFACT_TARGET signal): target existence unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
|
|
1652
|
+
}, totalReworkCount);
|
|
1653
|
+
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact target preflight was inconclusive (no parsable signal). Target existence is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
|
|
1654
|
+
}
|
|
1655
|
+
log("Publish artifact preflight for task " + taskId + ": target " + artifactTargetDir + " present");
|
|
1535
1656
|
// (below) the diff computation, rebuild trigger, application
|
|
1536
1657
|
// verification, bounded poll, and provenance stamp. The builder
|
|
1537
1658
|
// only makes the artifact_edit call and reports the applied
|
|
@@ -1548,7 +1669,7 @@ while (i < STEPS.length) {
|
|
|
1548
1669
|
// first publish (no provenance stamped yet).
|
|
1549
1670
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1550
1671
|
var provResult = await agent(
|
|
1551
|
-
crewCmd("get-provenance", {}) + "\n" +
|
|
1672
|
+
crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1552
1673
|
"Return JSON { \"provenance\": <the CLI's provenance object, or null when nothing is stamped> } and nothing else. Do not interpret it.",
|
|
1553
1674
|
{ key: attemptKey("publish-provenance-base-" + taskId, totalReworkCount), label: "Reading stamped publish base",
|
|
1554
1675
|
schema: { type: "object", properties: { provenance: { type: ["object", "null"] } }, required: ["provenance"] } }
|
|
@@ -1642,14 +1763,10 @@ while (i < STEPS.length) {
|
|
|
1642
1763
|
"- After applying, rebuild and deploy.'\n" +
|
|
1643
1764
|
"Edit-request contract (read carefully):\n" +
|
|
1644
1765
|
"- Call artifact_edit exactly once with the slug and verbatim_request above. Never retry the edit yourself: if the edit is not accepted, do NOT call artifact_edit again — end your turn.\n" +
|
|
1766
|
+
"- If artifact_edit explicitly refuses the edit (the call is rejected — e.g. the artifact does not exist), do NOT call artifact_edit again: end your turn with exactly one line and nothing else: ARTIFACT_EDIT_REFUSED: <the refusal text, one line>.\n" +
|
|
1645
1767
|
"- If artifact_edit is not available after the load, do NOT improvise — end your turn.\n" +
|
|
1646
1768
|
"- You do NOT call setprovenance, artifact_inspect, or post-deploy yourself.\n" +
|
|
1647
1769
|
"No report is needed: do not return JSON, do not summarize what you did, do not echo the diff. End your turn after the artifact_edit call.\n";
|
|
1648
|
-
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1649
|
-
// Minted once here (not re-minted per use site) so the ledger always
|
|
1650
|
-
// records the exact key that was issued — and so a re-minted duplicate
|
|
1651
|
-
// can never drift from it.
|
|
1652
|
-
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, totalReworkCount);
|
|
1653
1770
|
// The artifact build's agent_id, attributed to this edit by the
|
|
1654
1771
|
// workflow-owned observation below. The agent_id is the artifact
|
|
1655
1772
|
// system's in-flight correlation ID (research 2026-09-12):
|
|
@@ -1815,9 +1932,27 @@ while (i < STEPS.length) {
|
|
|
1815
1932
|
// re-trigger duplicated the edit on 2026-09-12).
|
|
1816
1933
|
var rebuildTrigger = null;
|
|
1817
1934
|
try {
|
|
1818
|
-
var
|
|
1819
|
-
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "")
|
|
1820
|
-
log("Publish rebuild trigger for task " + taskId + " returned (" +
|
|
1935
|
+
var triggerText = String(await agent(rebuildPrompt,
|
|
1936
|
+
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
|
|
1937
|
+
log("Publish rebuild trigger for task " + taskId + " returned (" + triggerText.length + " chars; awaited; scanned only for the explicit refusal signal)");
|
|
1938
|
+
// Explicit refusal (room #16 blocker 10): the child ends its turn
|
|
1939
|
+
// with ARTIFACT_EDIT_REFUSED when artifact_edit explicitly refused.
|
|
1940
|
+
// Conclusive negative evidence — the edit provably did NOT go
|
|
1941
|
+
// through — so this parks rejected and skips observation polling.
|
|
1942
|
+
// A missing/unparseable signal is NOT a refusal: it stays unknown
|
|
1943
|
+
// and fail-closed below.
|
|
1944
|
+
var refusalText = extractRefusal(triggerText);
|
|
1945
|
+
if (refusalText) {
|
|
1946
|
+
await recordPublishLedger({
|
|
1947
|
+
commit: mergeCommitForPublish,
|
|
1948
|
+
attempt: rebuildAttemptKey,
|
|
1949
|
+
agent_id: null,
|
|
1950
|
+
applied_report: null,
|
|
1951
|
+
outcome: "rejected",
|
|
1952
|
+
detail: "artifact_edit explicitly refused the edit (parsed ARTIFACT_EDIT_REFUSED signal): " + refusalText + " — conclusive negative: the edit provably did not go through, no observation polling, no blind retry"
|
|
1953
|
+
}, totalReworkCount);
|
|
1954
|
+
return await parkTask("Publish cannot proceed for task " + taskId + ": artifact_edit explicitly refused the edit (" + refusalText + "). This is conclusive (rejected, not unknown): the edit did not go through. Repair or provision the artifact target, then re-run Publish. Human attention needed.");
|
|
1955
|
+
}
|
|
1821
1956
|
} catch (triggerErr) {
|
|
1822
1957
|
log("Publish rebuild trigger for task " + taskId + " threw (" + (triggerErr && triggerErr.message ? triggerErr.message : triggerErr) + ") — outcome unknown until observation confirms it; the edit may have gone through");
|
|
1823
1958
|
}
|
|
@@ -2399,7 +2534,7 @@ while (i < STEPS.length) {
|
|
|
2399
2534
|
if (PUBLISH_TYPE === "artifact") {
|
|
2400
2535
|
instructions = "PROVENANCE CHECK (this project publishes to a dashboard artifact).\n" +
|
|
2401
2536
|
"Provenance is crew-owned state: read it from the Crew API, never from the artifact's own getprovenance action (a different, non-authoritative store).\n" +
|
|
2402
|
-
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2537
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2403
2538
|
"If provenance is null, report 'provenance missing — publish did not stamp source/crew release', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
2404
2539
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD — call this LIVE_HEAD.\n" +
|
|
2405
2540
|
"Run: test -d " + crewHome + "/releases/<provenance.crew_release> (substitute the real stamped hash; do not run the literal placeholder). If the directory does not exist, report 'provenance mismatch: crew_release [value from get-provenance] not found in release registry', then end your report with exactly this line: VERDICT: FAIL.\n" +
|
|
@@ -2944,11 +3079,11 @@ while (i < STEPS.length) {
|
|
|
2944
3079
|
// dashboard QA source check).
|
|
2945
3080
|
try {
|
|
2946
3081
|
var provRefresh = await agent(
|
|
2947
|
-
"Run in shell and read the stdout JSON:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
3082
|
+
"Run in shell and read the stdout JSON:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2948
3083
|
"If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
|
|
2949
3084
|
"Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
|
|
2950
3085
|
"run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
|
|
2951
|
-
"Then run in shell: node " + CREW_API + " --crew-home " + crewHome + " set-provenance --json '{\"source_commit\":\"<the existing provenance.source_commit value>\",\"crew_release\":\"<REL>\",\"published_at\":\"<TS>\",\"task_id\":\"" + taskId + "\"}' " +
|
|
3086
|
+
"Then run in shell: node " + CREW_API + " --crew-home " + crewHome + " set-provenance --json '{\"project_id\":\"" + LAUNCH_PROJECT_ID + "\",\"source_commit\":\"<the existing provenance.source_commit value>\",\"crew_release\":\"<REL>\",\"published_at\":\"<TS>\",\"task_id\":\"" + taskId + "\"}' " +
|
|
2952
3087
|
"(substitute the real values; the JSON must be single-quote-wrapped for the shell). " +
|
|
2953
3088
|
"Return JSON { \"refreshed\": <true if the set-provenance stdout contains ok: true, false otherwise>, \"crew_release\": \"<REL trimmed>\", \"published_at\": \"<TS trimmed>\" } and nothing else.",
|
|
2954
3089
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
|
|
@@ -3057,8 +3192,12 @@ while (i < STEPS.length) {
|
|
|
3057
3192
|
"Update the session and log the event.\n" +
|
|
3058
3193
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
|
|
3059
3194
|
task_id: taskId,
|
|
3195
|
+
// Room #16 blocker 11: the workflow-verified already-merged sha as
|
|
3196
|
+
// structured control state. Only the Build gate sets alreadyMergedSha
|
|
3197
|
+
// (after the mechanical ancestor check); the API validates the shape
|
|
3198
|
+
// and a later write without the field never clears it (COALESCE).
|
|
3060
3199
|
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name,
|
|
3061
|
-
status: status, notes: summary },
|
|
3200
|
+
status: status, notes: summary, already_merged_sha: (step.name === "Build" ? alreadyMergedSha : null) },
|
|
3062
3201
|
event: { task_id: taskId, type: status, identity: step.identity,
|
|
3063
3202
|
message: step.name + " " + status + " by " + step.identity }
|
|
3064
3203
|
}),
|
|
@@ -3091,6 +3230,15 @@ while (i < STEPS.length) {
|
|
|
3091
3230
|
return await parkTask("Exceeded shared rework budget (" + MAX_TOTAL_REWORK + " total rework attempts across Review and QA) after " + step.name + " rejection. Worktree preserved.");
|
|
3092
3231
|
}
|
|
3093
3232
|
rejectionNotes = summary;
|
|
3233
|
+
// Already-merged corrective (room #16 blocker 11): when Review rejected
|
|
3234
|
+
// an empty branch but the work is already on main (the workflow verified
|
|
3235
|
+
// the sha), Wren must declare it — not re-implement or re-commit
|
|
3236
|
+
// already-landed work. Scoped to the empty-branch rejection; any other
|
|
3237
|
+
// rejection already carries its own specific notes.
|
|
3238
|
+
if (step.name === "Review" && alreadyMergedSha && /no commits ahead of main/i.test(summary)) {
|
|
3239
|
+
rejectionNotes += "\n\nCORRECTIVE (from the workflow, not the reviewer): the deliverable is already on main — the workflow mechanically verified that " + alreadyMergedSha + " is an ancestor of main. Do NOT re-implement the work and do NOT create a new commit for it. In your Build report, declare exactly: repo_diff: none (already-merged: " + alreadyMergedSha + ") — then end with VERDICT: PASS.";
|
|
3240
|
+
log("Rework corrective appended for task " + taskId + ": already-merged " + alreadyMergedSha + " — Wren must declare, not rebuild");
|
|
3241
|
+
}
|
|
3094
3242
|
i = BUILD_INDEX;
|
|
3095
3243
|
log(step.name + " rejected — bouncing to Build (rework #" + totalReworkCount + " of " + MAX_TOTAL_REWORK + ")");
|
|
3096
3244
|
continue;
|