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
package/workflows/standard.js
CHANGED
|
@@ -91,13 +91,18 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
|
91
91
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
92
92
|
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
93
93
|
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
94
|
-
|
|
94
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
95
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
96
|
+
// The seven basenames the pin step must materialize — asserted mechanically
|
|
95
97
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
96
98
|
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
97
99
|
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
98
100
|
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
99
101
|
// mid-run release swap cannot change it under the workflow.
|
|
100
|
-
|
|
102
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
103
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
104
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
105
|
+
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(); });
|
|
101
106
|
|
|
102
107
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
103
108
|
const projectConfig = inputs.project_config || {};
|
|
@@ -281,7 +286,7 @@ function attemptKey(base, reworkCount) {
|
|
|
281
286
|
function pinLifecycle(key) {
|
|
282
287
|
return agent(
|
|
283
288
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
284
|
-
"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" +
|
|
289
|
+
"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" +
|
|
285
290
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
286
291
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
287
292
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -556,14 +561,34 @@ function extractMarkerLines(workerText) {
|
|
|
556
561
|
// builder correctly makes no commit because the deliverable is already on
|
|
557
562
|
// main (a prior merge or hand-repair landed it), it declares
|
|
558
563
|
// `repo_diff: none (already-merged: <sha>)` naming the main commit that
|
|
559
|
-
// carries the work.
|
|
560
|
-
//
|
|
561
|
-
//
|
|
564
|
+
// carries the work. Room #16 blocker 11 (2026-09-18): the line anchor
|
|
565
|
+
// missed Wren's mid-paragraph declaration, and the persisted notes truncated
|
|
566
|
+
// the tail — so the anchor is gone and a sha followed by `)`, whitespace, or
|
|
567
|
+
// end-of-string (truncation) is accepted. The sha is hex-only (7-40 chars)
|
|
568
|
+
// so the workflow can interpolate it into the mechanical ancestor check
|
|
569
|
+
// without injection risk; a over-long hex run never matches (the lookahead
|
|
570
|
+
// fails on the extra hex char). Pure — pinned byte-identical across
|
|
571
|
+
// standard/bugfix/chore.
|
|
562
572
|
function extractAlreadyMerged(workerText) {
|
|
563
|
-
var m =
|
|
573
|
+
var m = /repo_diff:\s*none\s*\(already-merged:\s*([0-9a-f]{7,40})(?=[\s)]|$)/i.exec(workerText || "");
|
|
564
574
|
return m ? { sha: m[1].toLowerCase() } : { sha: null };
|
|
565
575
|
}
|
|
566
576
|
|
|
577
|
+
// Explicit artifact refusal (room #16 blocker 10, 2026-09-18): the rebuild
|
|
578
|
+
// trigger child ends its turn with `ARTIFACT_EDIT_REFUSED: <text>` when
|
|
579
|
+
// artifact_edit explicitly refuses the edit (e.g. the artifact does not
|
|
580
|
+
// exist). A refusal is conclusive negative evidence — the edit provably did
|
|
581
|
+
// NOT go through — distinct from an unconsumed trigger return (unknown).
|
|
582
|
+
// Pure — pinned byte-identical across standard/bugfix/chore.
|
|
583
|
+
// The signal must be the ENTIRE trimmed turn output (not a line within prose):
|
|
584
|
+
// the trigger child is instructed to end its turn with exactly this line and
|
|
585
|
+
// nothing else. A confused child quoting the instructions back in prose must
|
|
586
|
+
// NOT produce a conclusive negative — that degrades to unknown (fail-closed).
|
|
587
|
+
function extractRefusal(workerText) {
|
|
588
|
+
var m = /^ARTIFACT_EDIT_REFUSED:\s*(.+?)\s*$/.exec(String(workerText || "").trim());
|
|
589
|
+
return m ? m[1].slice(0, 300) : null;
|
|
590
|
+
}
|
|
591
|
+
|
|
567
592
|
// Worktree confinement: the Build agent must declare the exact worktree
|
|
568
593
|
// path it built in on a `worktree:` marker line. The workflow compares it
|
|
569
594
|
// against WORKTREE_HINT mechanically (exact string match) — never by
|
|
@@ -1254,23 +1279,40 @@ while (i < STEPS.length) {
|
|
|
1254
1279
|
} else if (step.name === "Review") {
|
|
1255
1280
|
// Already-merged hydration: when this run did not execute Build itself
|
|
1256
1281
|
// (dispatcher resume at Review after a platform death between phases),
|
|
1257
|
-
// recover the workflow-
|
|
1258
|
-
//
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
//
|
|
1282
|
+
// recover the workflow-verified sha. Room #16 blocker 11: the structured
|
|
1283
|
+
// session field is read FIRST — the `already_merged_verified:` notes line
|
|
1284
|
+
// is only a fallback, because session notes are hard-capped at 3000
|
|
1285
|
+
// chars and a truthful declaration at the report's tail was silently
|
|
1286
|
+
// truncated. The structured value was written by the workflow after a
|
|
1287
|
+
// mechanical ancestor check — it is trusted; the builder's bare
|
|
1288
|
+
// declaration never is. Absent both, the mechanical fact below reads
|
|
1289
|
+
// "none declared" and Cass fails closed. The hydration read is best-effort:
|
|
1290
|
+
// a transport throw degrades to "none declared" rather than crashing Review.
|
|
1262
1291
|
if (!alreadyMergedSha) {
|
|
1263
|
-
var
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1292
|
+
var hydResult = null;
|
|
1293
|
+
try {
|
|
1294
|
+
hydResult = await agent(
|
|
1295
|
+
"Read the latest completed Build session for task " + taskId + ".\n" +
|
|
1296
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
|
|
1297
|
+
"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" +
|
|
1298
|
+
"SHA: <the session's already_merged_sha field value, or the word null when it is null>\n" +
|
|
1299
|
+
"NOTES:\n<the session's notes field, verbatim>",
|
|
1300
|
+
{ key: "hydrate-already-merged" + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""), label: "Hydrating already-merged verification" }
|
|
1301
|
+
);
|
|
1302
|
+
} catch (hydErr) {
|
|
1303
|
+
log("Hydration read failed (" + String(hydErr && hydErr.message || hydErr) + "); treating as none declared.");
|
|
1304
|
+
}
|
|
1305
|
+
var hydStr = hydResult ? ((typeof hydResult === "string") ? hydResult : JSON.stringify(hydResult)) : "";
|
|
1306
|
+
var hydSha = /^SHA:\s*([0-9a-f]{7,40})\s*$/im.exec(hydStr);
|
|
1307
|
+
if (hydSha) {
|
|
1308
|
+
alreadyMergedSha = hydSha[1].toLowerCase();
|
|
1309
|
+
log("Hydrated already-merged verification from structured session field: " + alreadyMergedSha);
|
|
1310
|
+
} else {
|
|
1311
|
+
var hvm = /already_merged_verified:\s*([0-9a-f]{7,40})/i.exec(hydStr);
|
|
1312
|
+
if (hvm) {
|
|
1313
|
+
alreadyMergedSha = hvm[1].toLowerCase();
|
|
1314
|
+
log("Hydrated already-merged verification from Build session notes (fallback): " + alreadyMergedSha);
|
|
1315
|
+
}
|
|
1274
1316
|
}
|
|
1275
1317
|
}
|
|
1276
1318
|
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" +
|
|
@@ -1413,6 +1455,85 @@ while (i < STEPS.length) {
|
|
|
1413
1455
|
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
1414
1456
|
// to ship.
|
|
1415
1457
|
if (!publishSkippedNoLock) {
|
|
1458
|
+
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1459
|
+
// Minted once here (not re-minted per use site) so the ledger always
|
|
1460
|
+
// records the exact key that was issued — and so a re-minted duplicate
|
|
1461
|
+
// can never drift from it. Defined before the preflight so pre-trigger
|
|
1462
|
+
// parks (room #16 blocker 10) record the same attempt key.
|
|
1463
|
+
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, totalReworkCount);
|
|
1464
|
+
// STEP 0.5 (mechanical, room #16 blocker 10): assert the artifact
|
|
1465
|
+
// target exists before any artifact_status / artifact_edit call. The
|
|
1466
|
+
// project was classified as an artifact surface (deploy_slug set),
|
|
1467
|
+
// but setup never provisioned the artifact — Publish then entered
|
|
1468
|
+
// the trigger path against a slug with no on-disk target and the
|
|
1469
|
+
// edit failed opaquely ("web artifact <slug> was not found on
|
|
1470
|
+
// disk"), which the ledger could only record as unknown. A missing
|
|
1471
|
+
// target is conclusive negative evidence: the edit provably did NOT
|
|
1472
|
+
// go through, so this parks rejected (not unknown) with the actual
|
|
1473
|
+
// missing path — no trigger issued, no blind retry, no observation
|
|
1474
|
+
// polling. The check is a pure filesystem stat; the path is
|
|
1475
|
+
// workflow-computed, never agent prose. An inconclusive check
|
|
1476
|
+
// (throw / unparseable signal) is fail-closed unknown: without
|
|
1477
|
+
// proof the target exists, no edit is issued. The slug is
|
|
1478
|
+
// interpolated into a shell command — a slug outside [a-zA-Z0-9_-]
|
|
1479
|
+
// (e.g. from a hand-edited space.json) is treated as inconclusive
|
|
1480
|
+
// rather than risking shell injection.
|
|
1481
|
+
var artifactTargetDir = "~/workspace/ts-spaces/" + PUBLISH_SLUG + "/";
|
|
1482
|
+
var preflightSignal = "";
|
|
1483
|
+
var preflightInconclusive = false;
|
|
1484
|
+
// Misconfiguration fast path: artifact surface with no slug is not a
|
|
1485
|
+
// signal problem — it's a project setup defect. Park rejected with a
|
|
1486
|
+
// truthful reason, not "inconclusive."
|
|
1487
|
+
if (!PUBLISH_SLUG) {
|
|
1488
|
+
await recordPublishLedger({
|
|
1489
|
+
commit: mergeCommitForPublish,
|
|
1490
|
+
attempt: rebuildAttemptKey,
|
|
1491
|
+
agent_id: null,
|
|
1492
|
+
applied_report: null,
|
|
1493
|
+
outcome: "rejected",
|
|
1494
|
+
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."
|
|
1495
|
+
}, totalReworkCount);
|
|
1496
|
+
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.");
|
|
1497
|
+
}
|
|
1498
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(PUBLISH_SLUG)) {
|
|
1499
|
+
preflightInconclusive = true;
|
|
1500
|
+
log("Publish artifact preflight for task " + taskId + ": PUBLISH_SLUG has an unsafe shape — inconclusive, fail-closed");
|
|
1501
|
+
} else try {
|
|
1502
|
+
var preflight = await agent(
|
|
1503
|
+
"Check whether the artifact target directory exists.\n" +
|
|
1504
|
+
"Run in shell: test -d ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/ && echo ARTIFACT_TARGET: present || echo ARTIFACT_TARGET: missing\n" +
|
|
1505
|
+
"Return JSON { \"signal\": \"<the exact ARTIFACT_TARGET line>\" } and nothing else.",
|
|
1506
|
+
{ key: attemptKey("publish-artifact-preflight-" + taskId, totalReworkCount), label: "Checking artifact target exists",
|
|
1507
|
+
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1508
|
+
);
|
|
1509
|
+
preflightSignal = String((preflight && preflight.signal) || "");
|
|
1510
|
+
} catch (preflightErr) {
|
|
1511
|
+
preflightInconclusive = true;
|
|
1512
|
+
log("Publish artifact preflight for task " + taskId + " threw (" + (preflightErr && preflightErr.message ? preflightErr.message : preflightErr) + ") — inconclusive, fail-closed");
|
|
1513
|
+
}
|
|
1514
|
+
if (!preflightInconclusive && /ARTIFACT_TARGET:\s*missing/.test(preflightSignal)) {
|
|
1515
|
+
await recordPublishLedger({
|
|
1516
|
+
commit: mergeCommitForPublish,
|
|
1517
|
+
attempt: rebuildAttemptKey,
|
|
1518
|
+
agent_id: null,
|
|
1519
|
+
applied_report: null,
|
|
1520
|
+
outcome: "rejected",
|
|
1521
|
+
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"
|
|
1522
|
+
}, totalReworkCount);
|
|
1523
|
+
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.");
|
|
1524
|
+
}
|
|
1525
|
+
if (preflightInconclusive || !/ARTIFACT_TARGET:\s*present/.test(preflightSignal)) {
|
|
1526
|
+
await recordPublishLedger({
|
|
1527
|
+
commit: mergeCommitForPublish,
|
|
1528
|
+
attempt: rebuildAttemptKey,
|
|
1529
|
+
agent_id: null,
|
|
1530
|
+
applied_report: null,
|
|
1531
|
+
outcome: "unknown",
|
|
1532
|
+
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"
|
|
1533
|
+
}, totalReworkCount);
|
|
1534
|
+
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.");
|
|
1535
|
+
}
|
|
1536
|
+
log("Publish artifact preflight for task " + taskId + ": target " + artifactTargetDir + " present");
|
|
1416
1537
|
// (below) the diff computation, rebuild trigger, application
|
|
1417
1538
|
// verification, bounded poll, and provenance stamp. The builder
|
|
1418
1539
|
// only makes the artifact_edit call and reports the applied
|
|
@@ -1429,7 +1550,7 @@ while (i < STEPS.length) {
|
|
|
1429
1550
|
// first publish (no provenance stamped yet).
|
|
1430
1551
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1431
1552
|
var provResult = await agent(
|
|
1432
|
-
crewCmd("get-provenance", {}) + "\n" +
|
|
1553
|
+
crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1433
1554
|
"Return JSON { \"provenance\": <the CLI's provenance object, or null when nothing is stamped> } and nothing else. Do not interpret it.",
|
|
1434
1555
|
{ key: attemptKey("publish-provenance-base-" + taskId, totalReworkCount), label: "Reading stamped publish base",
|
|
1435
1556
|
schema: { type: "object", properties: { provenance: { type: ["object", "null"] } }, required: ["provenance"] } }
|
|
@@ -1523,14 +1644,10 @@ while (i < STEPS.length) {
|
|
|
1523
1644
|
"- After applying, rebuild and deploy.'\n" +
|
|
1524
1645
|
"Edit-request contract (read carefully):\n" +
|
|
1525
1646
|
"- 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" +
|
|
1647
|
+
"- 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" +
|
|
1526
1648
|
"- If artifact_edit is not available after the load, do NOT improvise — end your turn.\n" +
|
|
1527
1649
|
"- You do NOT call setprovenance, artifact_inspect, or post-deploy yourself.\n" +
|
|
1528
1650
|
"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";
|
|
1529
|
-
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1530
|
-
// Minted once here (not re-minted per use site) so the ledger always
|
|
1531
|
-
// records the exact key that was issued — and so a re-minted duplicate
|
|
1532
|
-
// can never drift from it.
|
|
1533
|
-
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, totalReworkCount);
|
|
1534
1651
|
// The artifact build's agent_id, attributed to this edit by the
|
|
1535
1652
|
// workflow-owned observation below. The agent_id is the artifact
|
|
1536
1653
|
// system's in-flight correlation ID (research 2026-09-12):
|
|
@@ -1696,9 +1813,27 @@ while (i < STEPS.length) {
|
|
|
1696
1813
|
// re-trigger duplicated the edit on 2026-09-12).
|
|
1697
1814
|
var rebuildTrigger = null;
|
|
1698
1815
|
try {
|
|
1699
|
-
var
|
|
1700
|
-
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "")
|
|
1701
|
-
log("Publish rebuild trigger for task " + taskId + " returned (" +
|
|
1816
|
+
var triggerText = String(await agent(rebuildPrompt,
|
|
1817
|
+
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
|
|
1818
|
+
log("Publish rebuild trigger for task " + taskId + " returned (" + triggerText.length + " chars; awaited; scanned only for the explicit refusal signal)");
|
|
1819
|
+
// Explicit refusal (room #16 blocker 10): the child ends its turn
|
|
1820
|
+
// with ARTIFACT_EDIT_REFUSED when artifact_edit explicitly refused.
|
|
1821
|
+
// Conclusive negative evidence — the edit provably did NOT go
|
|
1822
|
+
// through — so this parks rejected and skips observation polling.
|
|
1823
|
+
// A missing/unparseable signal is NOT a refusal: it stays unknown
|
|
1824
|
+
// and fail-closed below.
|
|
1825
|
+
var refusalText = extractRefusal(triggerText);
|
|
1826
|
+
if (refusalText) {
|
|
1827
|
+
await recordPublishLedger({
|
|
1828
|
+
commit: mergeCommitForPublish,
|
|
1829
|
+
attempt: rebuildAttemptKey,
|
|
1830
|
+
agent_id: null,
|
|
1831
|
+
applied_report: null,
|
|
1832
|
+
outcome: "rejected",
|
|
1833
|
+
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"
|
|
1834
|
+
}, totalReworkCount);
|
|
1835
|
+
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.");
|
|
1836
|
+
}
|
|
1702
1837
|
} catch (triggerErr) {
|
|
1703
1838
|
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");
|
|
1704
1839
|
}
|
|
@@ -2226,7 +2361,7 @@ while (i < STEPS.length) {
|
|
|
2226
2361
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
|
|
2227
2362
|
"Use the returned tasks, sessions, and events to check the task's data-level effects.\n" +
|
|
2228
2363
|
"DOCS GATE: If the change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale for a public-affecting change, report 'public docs missing/stale for [the change]', then end your report with exactly this line: VERDICT: FAIL. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n\n" +
|
|
2229
|
-
"PROVENANCE CHECK: Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2364
|
+
"PROVENANCE CHECK: Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2230
2365
|
"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" +
|
|
2231
2366
|
"Run: cd " + REPO_PATH + " && git rev-parse HEAD — call this LIVE_HEAD.\n" +
|
|
2232
2367
|
"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, FAIL: { \"passed\": false, \"summary\": \"provenance mismatch: crew_release [value from get-provenance] not found in release registry\" }.\n" +
|
|
@@ -2703,11 +2838,11 @@ while (i < STEPS.length) {
|
|
|
2703
2838
|
// dashboard QA source check).
|
|
2704
2839
|
try {
|
|
2705
2840
|
var provRefresh = await agent(
|
|
2706
|
-
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2841
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2707
2842
|
"If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
|
|
2708
2843
|
"Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
|
|
2709
2844
|
"run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
|
|
2710
|
-
"Then run in shell:\n" + crewCmd("set-provenance", { source_commit: "<existing provenance.source_commit>", crew_release: "<REL trimmed>", published_at: "<TS trimmed>", task_id: taskId }) + "\n" +
|
|
2845
|
+
"Then run in shell:\n" + crewCmd("set-provenance", { project_id: LAUNCH_PROJECT_ID, source_commit: "<existing provenance.source_commit>", crew_release: "<REL trimmed>", published_at: "<TS trimmed>", task_id: taskId }) + "\n" +
|
|
2711
2846
|
"(substitute the real existing source_commit, REL, and TS for the placeholders). " +
|
|
2712
2847
|
"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.",
|
|
2713
2848
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, totalReworkCount), label: "Refreshing dashboard provenance after crew release",
|
|
@@ -2813,7 +2948,11 @@ while (i < STEPS.length) {
|
|
|
2813
2948
|
"Update the session and log the event.\n" +
|
|
2814
2949
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
|
|
2815
2950
|
task_id: taskId,
|
|
2816
|
-
|
|
2951
|
+
// Room #16 blocker 11: the workflow-verified already-merged sha as
|
|
2952
|
+
// structured control state. Only the Build gate sets alreadyMergedSha
|
|
2953
|
+
// (after the mechanical ancestor check); the API validates the shape
|
|
2954
|
+
// and a later write without the field never clears it (COALESCE).
|
|
2955
|
+
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: status, notes: summary, already_merged_sha: (step.name === "Build" ? alreadyMergedSha : null) },
|
|
2817
2956
|
event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity }
|
|
2818
2957
|
}),
|
|
2819
2958
|
{
|
|
@@ -2830,6 +2969,15 @@ while (i < STEPS.length) {
|
|
|
2830
2969
|
return await parkTask("Exceeded shared rework budget (" + MAX_TOTAL_REWORK + " total rework attempts across Review and QA) after " + step.name + " rejection. Worktree preserved.");
|
|
2831
2970
|
}
|
|
2832
2971
|
rejectionNotes = summary;
|
|
2972
|
+
// Already-merged corrective (room #16 blocker 11): when Review rejected
|
|
2973
|
+
// an empty branch but the work is already on main (the workflow verified
|
|
2974
|
+
// the sha), Wren must declare it — not re-implement or re-commit
|
|
2975
|
+
// already-landed work. Scoped to the empty-branch rejection; any other
|
|
2976
|
+
// rejection already carries its own specific notes.
|
|
2977
|
+
if (step.name === "Review" && alreadyMergedSha && /no commits ahead of main/i.test(summary)) {
|
|
2978
|
+
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.";
|
|
2979
|
+
log("Rework corrective appended for task " + taskId + ": already-merged " + alreadyMergedSha + " — Wren must declare, not rebuild");
|
|
2980
|
+
}
|
|
2833
2981
|
i = BUILD_INDEX;
|
|
2834
2982
|
log(step.name + " rejected — bouncing to Build (rework #" + totalReworkCount + " of " + MAX_TOTAL_REWORK + ")");
|
|
2835
2983
|
continue;
|
package/workflows/upgrade.js
CHANGED
|
@@ -157,9 +157,14 @@ const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
|
157
157
|
const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
|
|
158
158
|
const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
159
159
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
160
|
-
|
|
160
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
161
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
162
|
+
// The six basenames the pin step must materialize — asserted mechanically
|
|
161
163
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
162
|
-
|
|
164
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
165
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
166
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
167
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, CLASSIFY_SURFACE].map(function (p) { return p.split("/").pop(); });
|
|
163
168
|
|
|
164
169
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
165
170
|
const projectConfig = inputs.project_config || {};
|
|
@@ -190,7 +195,7 @@ if (!taskId) {
|
|
|
190
195
|
function pinLifecycle(key) {
|
|
191
196
|
return agent(
|
|
192
197
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
193
|
-
"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" +
|
|
198
|
+
"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 " + CLASSIFY_SURFACE_SRC + " " + CLASSIFY_SURFACE + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
194
199
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
195
200
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
196
201
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|