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/chore.js
CHANGED
|
@@ -140,13 +140,18 @@ const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
|
140
140
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
141
141
|
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
142
142
|
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
143
|
-
|
|
143
|
+
const CLASSIFY_SURFACE_SRC = crewHome + "/current/lib/classify-surface.js";
|
|
144
|
+
const CLASSIFY_SURFACE = RUN_LIB + "/classify-surface.js";
|
|
145
|
+
// The seven basenames the pin step must materialize — asserted mechanically
|
|
144
146
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
145
147
|
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
146
148
|
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
147
149
|
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
148
150
|
// mid-run release swap cannot change it under the workflow.
|
|
149
|
-
|
|
151
|
+
// CLASSIFY_SURFACE is the surface classifier (room #15, 2026-09-18):
|
|
152
|
+
// crew-api.js statically imports it, so the pin must carry it — a pin
|
|
153
|
+
// without it kills every claim with ERR_MODULE_NOT_FOUND.
|
|
154
|
+
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(); });
|
|
150
155
|
|
|
151
156
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
152
157
|
const projectConfig = inputs.project_config || {};
|
|
@@ -325,7 +330,7 @@ function attemptKey(base, reworkCount) {
|
|
|
325
330
|
function pinLifecycle(key) {
|
|
326
331
|
return agent(
|
|
327
332
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
328
|
-
"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" +
|
|
333
|
+
"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" +
|
|
329
334
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
330
335
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
331
336
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -596,14 +601,34 @@ function extractMarkerLines(workerText) {
|
|
|
596
601
|
// builder correctly makes no commit because the deliverable is already on
|
|
597
602
|
// main (a prior merge or hand-repair landed it), it declares
|
|
598
603
|
// `repo_diff: none (already-merged: <sha>)` naming the main commit that
|
|
599
|
-
// carries the work.
|
|
600
|
-
//
|
|
601
|
-
//
|
|
604
|
+
// carries the work. Room #16 blocker 11 (2026-09-18): the line anchor
|
|
605
|
+
// missed Wren's mid-paragraph declaration, and the persisted notes truncated
|
|
606
|
+
// the tail — so the anchor is gone and a sha followed by `)`, whitespace, or
|
|
607
|
+
// end-of-string (truncation) is accepted. The sha is hex-only (7-40 chars)
|
|
608
|
+
// so the workflow can interpolate it into the mechanical ancestor check
|
|
609
|
+
// without injection risk; a over-long hex run never matches (the lookahead
|
|
610
|
+
// fails on the extra hex char). Pure — pinned byte-identical across
|
|
611
|
+
// standard/bugfix/chore.
|
|
602
612
|
function extractAlreadyMerged(workerText) {
|
|
603
|
-
var m =
|
|
613
|
+
var m = /repo_diff:\s*none\s*\(already-merged:\s*([0-9a-f]{7,40})(?=[\s)]|$)/i.exec(workerText || "");
|
|
604
614
|
return m ? { sha: m[1].toLowerCase() } : { sha: null };
|
|
605
615
|
}
|
|
606
616
|
|
|
617
|
+
// Explicit artifact refusal (room #16 blocker 10, 2026-09-18): the rebuild
|
|
618
|
+
// trigger child ends its turn with `ARTIFACT_EDIT_REFUSED: <text>` when
|
|
619
|
+
// artifact_edit explicitly refuses the edit (e.g. the artifact does not
|
|
620
|
+
// exist). A refusal is conclusive negative evidence — the edit provably did
|
|
621
|
+
// NOT go through — distinct from an unconsumed trigger return (unknown).
|
|
622
|
+
// Pure — pinned byte-identical across standard/bugfix/chore.
|
|
623
|
+
// The signal must be the ENTIRE trimmed turn output (not a line within prose):
|
|
624
|
+
// the trigger child is instructed to end its turn with exactly this line and
|
|
625
|
+
// nothing else. A confused child quoting the instructions back in prose must
|
|
626
|
+
// NOT produce a conclusive negative — that degrades to unknown (fail-closed).
|
|
627
|
+
function extractRefusal(workerText) {
|
|
628
|
+
var m = /^ARTIFACT_EDIT_REFUSED:\s*(.+?)\s*$/.exec(String(workerText || "").trim());
|
|
629
|
+
return m ? m[1].slice(0, 300) : null;
|
|
630
|
+
}
|
|
631
|
+
|
|
607
632
|
// Worktree confinement: the Build agent must declare the exact worktree
|
|
608
633
|
// path it built in on a `worktree:` marker line. The workflow compares it
|
|
609
634
|
// against WORKTREE_HINT mechanically (exact string match) — never by
|
|
@@ -1260,23 +1285,40 @@ while (i < STEPS.length) {
|
|
|
1260
1285
|
} else if (step.name === "Review") {
|
|
1261
1286
|
// Already-merged hydration: when this run did not execute Build itself
|
|
1262
1287
|
// (dispatcher resume at Review after a platform death between phases),
|
|
1263
|
-
// recover the workflow-
|
|
1264
|
-
//
|
|
1265
|
-
//
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1288
|
+
// recover the workflow-verified sha. Room #16 blocker 11: the structured
|
|
1289
|
+
// session field is read FIRST — the `already_merged_verified:` notes line
|
|
1290
|
+
// is only a fallback, because session notes are hard-capped at 3000
|
|
1291
|
+
// chars and a truthful declaration at the report's tail was silently
|
|
1292
|
+
// truncated. The structured value was written by the workflow after a
|
|
1293
|
+
// mechanical ancestor check — it is trusted; the builder's bare
|
|
1294
|
+
// declaration never is. Absent both, the mechanical fact below reads
|
|
1295
|
+
// "none declared" and Cass fails closed. The hydration read is best-effort:
|
|
1296
|
+
// a transport throw degrades to "none declared" rather than crashing Review.
|
|
1268
1297
|
if (!alreadyMergedSha) {
|
|
1269
|
-
var
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1298
|
+
var hydResult = null;
|
|
1299
|
+
try {
|
|
1300
|
+
hydResult = await agent(
|
|
1301
|
+
"Read the latest completed Build session for task " + taskId + ".\n" +
|
|
1302
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
|
|
1303
|
+
"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" +
|
|
1304
|
+
"SHA: <the session's already_merged_sha field value, or the word null when it is null>\n" +
|
|
1305
|
+
"NOTES:\n<the session's notes field, verbatim>",
|
|
1306
|
+
{ key: "hydrate-already-merged" + (reworkCount > 0 ? "-r" + reworkCount : ""), label: "Hydrating already-merged verification" }
|
|
1307
|
+
);
|
|
1308
|
+
} catch (hydErr) {
|
|
1309
|
+
log("Hydration read failed (" + String(hydErr && hydErr.message || hydErr) + "); treating as none declared.");
|
|
1310
|
+
}
|
|
1311
|
+
var hydStr = hydResult ? ((typeof hydResult === "string") ? hydResult : JSON.stringify(hydResult)) : "";
|
|
1312
|
+
var hydSha = /^SHA:\s*([0-9a-f]{7,40})\s*$/im.exec(hydStr);
|
|
1313
|
+
if (hydSha) {
|
|
1314
|
+
alreadyMergedSha = hydSha[1].toLowerCase();
|
|
1315
|
+
log("Hydrated already-merged verification from structured session field: " + alreadyMergedSha);
|
|
1316
|
+
} else {
|
|
1317
|
+
var hvm = /already_merged_verified:\s*([0-9a-f]{7,40})/i.exec(hydStr);
|
|
1318
|
+
if (hvm) {
|
|
1319
|
+
alreadyMergedSha = hvm[1].toLowerCase();
|
|
1320
|
+
log("Hydrated already-merged verification from Build session notes (fallback): " + alreadyMergedSha);
|
|
1321
|
+
}
|
|
1280
1322
|
}
|
|
1281
1323
|
}
|
|
1282
1324
|
instructions = "Review independently and cold. No prior context 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" +
|
|
@@ -1419,6 +1461,85 @@ while (i < STEPS.length) {
|
|
|
1419
1461
|
// Skipped entirely when no lock was held — nothing merged, nothing
|
|
1420
1462
|
// to ship.
|
|
1421
1463
|
if (!publishSkippedNoLock) {
|
|
1464
|
+
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1465
|
+
// Minted once here (not re-minted per use site) so the ledger always
|
|
1466
|
+
// records the exact key that was issued — and so a re-minted duplicate
|
|
1467
|
+
// can never drift from it. Defined before the preflight so pre-trigger
|
|
1468
|
+
// parks (room #16 blocker 10) record the same attempt key.
|
|
1469
|
+
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, reworkCount);
|
|
1470
|
+
// STEP 0.5 (mechanical, room #16 blocker 10): assert the artifact
|
|
1471
|
+
// target exists before any artifact_status / artifact_edit call. The
|
|
1472
|
+
// project was classified as an artifact surface (deploy_slug set),
|
|
1473
|
+
// but setup never provisioned the artifact — Publish then entered
|
|
1474
|
+
// the trigger path against a slug with no on-disk target and the
|
|
1475
|
+
// edit failed opaquely ("web artifact <slug> was not found on
|
|
1476
|
+
// disk"), which the ledger could only record as unknown. A missing
|
|
1477
|
+
// target is conclusive negative evidence: the edit provably did NOT
|
|
1478
|
+
// go through, so this parks rejected (not unknown) with the actual
|
|
1479
|
+
// missing path — no trigger issued, no blind retry, no observation
|
|
1480
|
+
// polling. The check is a pure filesystem stat; the path is
|
|
1481
|
+
// workflow-computed, never agent prose. An inconclusive check
|
|
1482
|
+
// (throw / unparseable signal) is fail-closed unknown: without
|
|
1483
|
+
// proof the target exists, no edit is issued. The slug is
|
|
1484
|
+
// interpolated into a shell command — a slug outside [a-zA-Z0-9_-]
|
|
1485
|
+
// (e.g. from a hand-edited space.json) is treated as inconclusive
|
|
1486
|
+
// rather than risking shell injection.
|
|
1487
|
+
var artifactTargetDir = "~/workspace/ts-spaces/" + PUBLISH_SLUG + "/";
|
|
1488
|
+
var preflightSignal = "";
|
|
1489
|
+
var preflightInconclusive = false;
|
|
1490
|
+
// Misconfiguration fast path: artifact surface with no slug is not a
|
|
1491
|
+
// signal problem — it's a project setup defect. Park rejected with a
|
|
1492
|
+
// truthful reason, not "inconclusive."
|
|
1493
|
+
if (!PUBLISH_SLUG) {
|
|
1494
|
+
await recordPublishLedger({
|
|
1495
|
+
commit: mergeCommitForPublish,
|
|
1496
|
+
attempt: rebuildAttemptKey,
|
|
1497
|
+
agent_id: null,
|
|
1498
|
+
applied_report: null,
|
|
1499
|
+
outcome: "rejected",
|
|
1500
|
+
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."
|
|
1501
|
+
}, reworkCount);
|
|
1502
|
+
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.");
|
|
1503
|
+
}
|
|
1504
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(PUBLISH_SLUG)) {
|
|
1505
|
+
preflightInconclusive = true;
|
|
1506
|
+
log("Publish artifact preflight for task " + taskId + ": PUBLISH_SLUG has an unsafe shape — inconclusive, fail-closed");
|
|
1507
|
+
} else try {
|
|
1508
|
+
var preflight = await agent(
|
|
1509
|
+
"Check whether the artifact target directory exists.\n" +
|
|
1510
|
+
"Run in shell: test -d ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/ && echo ARTIFACT_TARGET: present || echo ARTIFACT_TARGET: missing\n" +
|
|
1511
|
+
"Return JSON { \"signal\": \"<the exact ARTIFACT_TARGET line>\" } and nothing else.",
|
|
1512
|
+
{ key: attemptKey("publish-artifact-preflight-" + taskId, reworkCount), label: "Checking artifact target exists",
|
|
1513
|
+
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1514
|
+
);
|
|
1515
|
+
preflightSignal = String((preflight && preflight.signal) || "");
|
|
1516
|
+
} catch (preflightErr) {
|
|
1517
|
+
preflightInconclusive = true;
|
|
1518
|
+
log("Publish artifact preflight for task " + taskId + " threw (" + (preflightErr && preflightErr.message ? preflightErr.message : preflightErr) + ") — inconclusive, fail-closed");
|
|
1519
|
+
}
|
|
1520
|
+
if (!preflightInconclusive && /ARTIFACT_TARGET:\s*missing/.test(preflightSignal)) {
|
|
1521
|
+
await recordPublishLedger({
|
|
1522
|
+
commit: mergeCommitForPublish,
|
|
1523
|
+
attempt: rebuildAttemptKey,
|
|
1524
|
+
agent_id: null,
|
|
1525
|
+
applied_report: null,
|
|
1526
|
+
outcome: "rejected",
|
|
1527
|
+
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"
|
|
1528
|
+
}, reworkCount);
|
|
1529
|
+
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.");
|
|
1530
|
+
}
|
|
1531
|
+
if (preflightInconclusive || !/ARTIFACT_TARGET:\s*present/.test(preflightSignal)) {
|
|
1532
|
+
await recordPublishLedger({
|
|
1533
|
+
commit: mergeCommitForPublish,
|
|
1534
|
+
attempt: rebuildAttemptKey,
|
|
1535
|
+
agent_id: null,
|
|
1536
|
+
applied_report: null,
|
|
1537
|
+
outcome: "unknown",
|
|
1538
|
+
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"
|
|
1539
|
+
}, reworkCount);
|
|
1540
|
+
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.");
|
|
1541
|
+
}
|
|
1542
|
+
log("Publish artifact preflight for task " + taskId + ": target " + artifactTargetDir + " present");
|
|
1422
1543
|
// (below) the diff computation, rebuild trigger, application
|
|
1423
1544
|
// verification, bounded poll, and provenance stamp. The builder
|
|
1424
1545
|
// only makes the artifact_edit call and reports the applied
|
|
@@ -1435,7 +1556,7 @@ while (i < STEPS.length) {
|
|
|
1435
1556
|
// first publish (no provenance stamped yet).
|
|
1436
1557
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1437
1558
|
var provResult = await agent(
|
|
1438
|
-
crewCmd("get-provenance", {}) + "\n" +
|
|
1559
|
+
crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
1439
1560
|
"Return JSON { \"provenance\": <the CLI's provenance object, or null when nothing is stamped> } and nothing else. Do not interpret it.",
|
|
1440
1561
|
{ key: attemptKey("publish-provenance-base-" + taskId, reworkCount), label: "Reading stamped publish base",
|
|
1441
1562
|
schema: { type: "object", properties: { provenance: { type: ["object", "null"] } }, required: ["provenance"] } }
|
|
@@ -1529,14 +1650,10 @@ while (i < STEPS.length) {
|
|
|
1529
1650
|
"- After applying, rebuild and deploy.'\n" +
|
|
1530
1651
|
"Edit-request contract (read carefully):\n" +
|
|
1531
1652
|
"- 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" +
|
|
1653
|
+
"- 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" +
|
|
1532
1654
|
"- If artifact_edit is not available after the load, do NOT improvise — end your turn.\n" +
|
|
1533
1655
|
"- You do NOT call setprovenance, artifact_inspect, or post-deploy yourself.\n" +
|
|
1534
1656
|
"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";
|
|
1535
|
-
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
1536
|
-
// Minted once here (not re-minted per use site) so the ledger always
|
|
1537
|
-
// records the exact key that was issued — and so a re-minted duplicate
|
|
1538
|
-
// can never drift from it.
|
|
1539
|
-
var rebuildAttemptKey = attemptKey("publish-artifact-rebuild-" + taskId, reworkCount);
|
|
1540
1657
|
// The artifact build's agent_id, attributed to this edit by the
|
|
1541
1658
|
// workflow-owned observation below. The agent_id is the artifact
|
|
1542
1659
|
// system's in-flight correlation ID (research 2026-09-12):
|
|
@@ -1702,9 +1819,27 @@ while (i < STEPS.length) {
|
|
|
1702
1819
|
// re-trigger duplicated the edit on 2026-09-12).
|
|
1703
1820
|
var rebuildTrigger = null;
|
|
1704
1821
|
try {
|
|
1705
|
-
var
|
|
1706
|
-
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "")
|
|
1707
|
-
log("Publish rebuild trigger for task " + taskId + " returned (" +
|
|
1822
|
+
var triggerText = String(await agent(rebuildPrompt,
|
|
1823
|
+
{ key: rebuildAttemptKey, label: "Triggering artifact rebuild" }) || "");
|
|
1824
|
+
log("Publish rebuild trigger for task " + taskId + " returned (" + triggerText.length + " chars; awaited; scanned only for the explicit refusal signal)");
|
|
1825
|
+
// Explicit refusal (room #16 blocker 10): the child ends its turn
|
|
1826
|
+
// with ARTIFACT_EDIT_REFUSED when artifact_edit explicitly refused.
|
|
1827
|
+
// Conclusive negative evidence — the edit provably did NOT go
|
|
1828
|
+
// through — so this parks rejected and skips observation polling.
|
|
1829
|
+
// A missing/unparseable signal is NOT a refusal: it stays unknown
|
|
1830
|
+
// and fail-closed below.
|
|
1831
|
+
var refusalText = extractRefusal(triggerText);
|
|
1832
|
+
if (refusalText) {
|
|
1833
|
+
await recordPublishLedger({
|
|
1834
|
+
commit: mergeCommitForPublish,
|
|
1835
|
+
attempt: rebuildAttemptKey,
|
|
1836
|
+
agent_id: null,
|
|
1837
|
+
applied_report: null,
|
|
1838
|
+
outcome: "rejected",
|
|
1839
|
+
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"
|
|
1840
|
+
}, reworkCount);
|
|
1841
|
+
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.");
|
|
1842
|
+
}
|
|
1708
1843
|
} catch (triggerErr) {
|
|
1709
1844
|
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");
|
|
1710
1845
|
}
|
|
@@ -2521,11 +2656,11 @@ while (i < STEPS.length) {
|
|
|
2521
2656
|
// dashboard QA source check).
|
|
2522
2657
|
try {
|
|
2523
2658
|
var provRefresh = await agent(
|
|
2524
|
-
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", {}) + "\n" +
|
|
2659
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("get-provenance", { project_id: LAUNCH_PROJECT_ID }) + "\n" +
|
|
2525
2660
|
"If the response has no provenance (null), return JSON { \"refreshed\": false, \"reason\": \"no-record\" } and stop. " +
|
|
2526
2661
|
"Otherwise run: basename $(readlink " + crewHome + "/current) — call this REL; " +
|
|
2527
2662
|
"run: date -u +%Y-%m-%dT%H:%M:%SZ — call this TS. " +
|
|
2528
|
-
"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" +
|
|
2663
|
+
"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" +
|
|
2529
2664
|
"(substitute the real existing source_commit, REL, and TS for the placeholders). " +
|
|
2530
2665
|
"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.",
|
|
2531
2666
|
{ key: attemptKey("publish-provenance-refresh-" + taskId, reworkCount), label: "Refreshing dashboard provenance after crew release",
|
|
@@ -2619,7 +2754,11 @@ while (i < STEPS.length) {
|
|
|
2619
2754
|
"Update session and log event.\n" +
|
|
2620
2755
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
|
|
2621
2756
|
task_id: taskId,
|
|
2622
|
-
|
|
2757
|
+
// Room #16 blocker 11: the workflow-verified already-merged sha as
|
|
2758
|
+
// structured control state. Only the Build gate sets alreadyMergedSha
|
|
2759
|
+
// (after the mechanical ancestor check); the API validates the shape
|
|
2760
|
+
// and a later write without the field never clears it (COALESCE).
|
|
2761
|
+
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: status, notes: summary, already_merged_sha: (step.name === "Build" ? alreadyMergedSha : null) },
|
|
2623
2762
|
event: { task_id: taskId, type: status, identity: step.identity, message: step.name + " " + status + " by " + step.identity }
|
|
2624
2763
|
}) + "\n",
|
|
2625
2764
|
{
|
|
@@ -2635,6 +2774,15 @@ while (i < STEPS.length) {
|
|
|
2635
2774
|
return await parkTask("Exceeded " + MAX_REWORK + " rework attempts after Review rejection. Worktree preserved.");
|
|
2636
2775
|
}
|
|
2637
2776
|
rejectionNotes = summary;
|
|
2777
|
+
// Already-merged corrective (room #16 blocker 11): when Review rejected
|
|
2778
|
+
// an empty branch but the work is already on main (the workflow verified
|
|
2779
|
+
// the sha), Wren must declare it — not re-implement or re-commit
|
|
2780
|
+
// already-landed work. Scoped to the empty-branch rejection; any other
|
|
2781
|
+
// rejection already carries its own specific notes.
|
|
2782
|
+
if (step.name === "Review" && alreadyMergedSha && /no commits ahead of main/i.test(summary)) {
|
|
2783
|
+
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.";
|
|
2784
|
+
log("Rework corrective appended for task " + taskId + ": already-merged " + alreadyMergedSha + " — Wren must declare, not rebuild");
|
|
2785
|
+
}
|
|
2638
2786
|
i = BUILD_INDEX;
|
|
2639
2787
|
log("Review rejected — bouncing to Build (rework #" + reworkCount + ")");
|
|
2640
2788
|
continue;
|
package/workflows/crew-init.js
CHANGED
|
@@ -350,6 +350,7 @@ log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped"
|
|
|
350
350
|
// projects later via crew-api.js create-project.
|
|
351
351
|
phase("project");
|
|
352
352
|
var projectResult;
|
|
353
|
+
var initialProvenanceWarning = null;
|
|
353
354
|
if (!dashboardSlug) {
|
|
354
355
|
log("Project: skipped (CLI-only mode — no dashboard; create projects via crew-api.js create-project)");
|
|
355
356
|
projectResult = { action: "skipped", project_id: null };
|
|
@@ -380,7 +381,8 @@ try {
|
|
|
380
381
|
" Return { action: \"repaired\", project_id: \"" + dashboardSlug + "\" }.\n" +
|
|
381
382
|
"3. If not found (error), create it:\n" +
|
|
382
383
|
" node " + crewHome + "/current/lib/crew-api.js --crew-home " + crewHome + " create-project --json '{\"id\": \"" + dashboardSlug + "\", \"display_name\": \"" + safeDashboardName + "\", \"repo_path\": \"" + safeDashboardRepo + "\", \"deploy_type\": \"artifact\", \"deploy_slug\": \"" + dashboardSlug + "\", \"environment_type\": \"artifact\", \"description\": \"The dashboard task service — the crew\\u0027s first project\"}'\n" +
|
|
383
|
-
"
|
|
384
|
+
" The create-project response carries initial_provenance { stamped, reason?, source_commit?, crew_release? } — pass it through verbatim.\n" +
|
|
385
|
+
" Return { action: \"created\", project_id: \"" + dashboardSlug + "\", initial_provenance: <the verbatim initial_provenance object> }.\n\n" +
|
|
384
386
|
"4. Seed the update watcher's trusted dashboard base (enrollment) — the\n" +
|
|
385
387
|
" commit this installation starts from. Only ever seeds when no base\n" +
|
|
386
388
|
" exists; an established base is never moved by a re-init:\n" +
|
|
@@ -394,7 +396,7 @@ try {
|
|
|
394
396
|
" dashboard upgrade with oldSha unknown and park it with enrollment\n" +
|
|
395
397
|
" guidance, fail-closed.\n\n" +
|
|
396
398
|
"The repo_path must be the dashboard git repository — never the crew home.\n" +
|
|
397
|
-
"Return JSON with action (\"exists\", \"repaired\", or \"created\")
|
|
399
|
+
"Return JSON with action (\"exists\", \"repaired\", or \"created\"), project_id (string), and — only when action is \"created\" — the create-project response's initial_provenance object verbatim.",
|
|
398
400
|
{
|
|
399
401
|
key: "project-1",
|
|
400
402
|
label: "Register dashboard project",
|
|
@@ -402,7 +404,8 @@ try {
|
|
|
402
404
|
type: "object",
|
|
403
405
|
properties: {
|
|
404
406
|
action: { type: "string", enum: ["exists", "repaired", "created"] },
|
|
405
|
-
project_id: { type: "string" }
|
|
407
|
+
project_id: { type: "string" },
|
|
408
|
+
initial_provenance: { type: "object" }
|
|
406
409
|
},
|
|
407
410
|
required: ["action", "project_id"]
|
|
408
411
|
}
|
|
@@ -412,8 +415,59 @@ try {
|
|
|
412
415
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
|
|
413
416
|
}
|
|
414
417
|
log("Project: " + dashboardSlug + " " + projectResult.action + " (repo_path=" + gateFacts.dashboardRepoExpanded + ")");
|
|
418
|
+
if (projectResult && projectResult.action === "created" &&
|
|
419
|
+
projectResult.initial_provenance && !projectResult.initial_provenance.stamped) {
|
|
420
|
+
// Room #15 (reliability): create-project's initial stamp is best-effort,
|
|
421
|
+
// but "reported" used to mean "returned into the void" — zero production
|
|
422
|
+
// code read it, so a skipped stamp (the J1/J2 shape) stayed invisible.
|
|
423
|
+
// Surface it here; the publish-base advance below re-stamps right after,
|
|
424
|
+
// and fails loudly (exit 1) if the re-stamp itself cannot stamp.
|
|
425
|
+
initialProvenanceWarning = "Project created but its initial provenance stamp was skipped (" +
|
|
426
|
+
(projectResult.initial_provenance.reason || "unknown reason") +
|
|
427
|
+
") — the publish-base advance re-stamps it next; if that also fails, the first task's publish diffs from the empty tree.";
|
|
428
|
+
log("WARNING: " + initialProvenanceWarning);
|
|
429
|
+
}
|
|
415
430
|
} // end dashboard-mode project registration
|
|
416
431
|
|
|
432
|
+
// ── Artifact target verification (room #16 blocker 10, 2026-09-18) ──
|
|
433
|
+
// Setup owns the invariant: deploy_slug ⇒ artifact exists. The dashboard
|
|
434
|
+
// project is registered with deploy_slug = dashboardSlug, but registration
|
|
435
|
+
// alone does not create the artifact — the artifact is a platform primitive
|
|
436
|
+
// created by the human via the Muse UI (docs/guide.md prerequisite: "A task
|
|
437
|
+
// service artifact already created"). If the target directory is missing,
|
|
438
|
+
// init blocks loudly: a dangling deploy_slug would otherwise let the first
|
|
439
|
+
// task reach Publish and fail opaquely. This is not a provisioner — the
|
|
440
|
+
// human creates the artifact; init only verifies and refuses to proceed
|
|
441
|
+
// without it.
|
|
442
|
+
if (dashboardSlug) {
|
|
443
|
+
// The slug is interpolated into a shell command — reject an unsafe shape
|
|
444
|
+
// rather than risking injection (dashboardSlug comes from init args).
|
|
445
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(dashboardSlug)) {
|
|
446
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
447
|
+
blocked_reason: "Dashboard slug has an unsafe shape",
|
|
448
|
+
message: "dashboardSlug \"" + dashboardSlug + "\" contains characters outside [a-zA-Z0-9_-]. The slug is used in shell paths and as the artifact key — choose a safe slug and re-run init."
|
|
449
|
+
} };
|
|
450
|
+
}
|
|
451
|
+
var artifactTargetCheck = await agent(
|
|
452
|
+
"Verify the dashboard artifact target exists.\n" +
|
|
453
|
+
"Run in shell: test -d ~/workspace/ts-spaces/" + dashboardSlug + "/ && echo ARTIFACT_TARGET: present || echo ARTIFACT_TARGET: missing\n" +
|
|
454
|
+
"Return JSON { \"signal\": \"<the exact ARTIFACT_TARGET line>\" } and nothing else.",
|
|
455
|
+
{
|
|
456
|
+
key: "artifact-target-verify",
|
|
457
|
+
label: "Verifying dashboard artifact target exists",
|
|
458
|
+
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] }
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
var artifactSignal = String((artifactTargetCheck && artifactTargetCheck.signal) || "");
|
|
462
|
+
if (!/ARTIFACT_TARGET:\s*present/.test(artifactSignal)) {
|
|
463
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
464
|
+
blocked_reason: "Dashboard artifact target missing",
|
|
465
|
+
message: "The dashboard project is registered with deploy_slug \"" + dashboardSlug + "\", but the artifact target directory ~/workspace/ts-spaces/" + dashboardSlug + "/ does not exist. An artifact is a Muse platform primitive (a hosted web app) — the crew never creates artifacts, it only edits existing ones. Create the artifact from your dashboard repo via the Muse UI first (see docs/guide.md prerequisites), then re-run init. Init will not complete with a deploy_slug that points to nothing: the first task's Publish would otherwise fail with a confusing error."
|
|
466
|
+
} };
|
|
467
|
+
}
|
|
468
|
+
log("Artifact target verified: ~/workspace/ts-spaces/" + dashboardSlug + "/ exists");
|
|
469
|
+
}
|
|
470
|
+
|
|
417
471
|
// ── Project repo setup (2026-09-18) ─────────────────────────────────
|
|
418
472
|
// One deterministic composer — lib/setup-project-repo.js — scaffolds
|
|
419
473
|
// $REPO/.orchestration/, applies the consented .gitignore update, and COMMITS
|
|
@@ -512,7 +566,7 @@ if (dashboardRepoPath && projectResult && projectResult.action === "created" &&
|
|
|
512
566
|
publishBaseResult = await agent(
|
|
513
567
|
"Run the deterministic publish-base advance script. Do not decide anything — just run the command and return its JSON output.\n\n" +
|
|
514
568
|
"Command:\n" +
|
|
515
|
-
"node " + crewHome + "/current/lib/advance-publish-base.js --crew-home \"" + crewHome + "\" --repo \"" + gateFacts.dashboardRepoExpanded + "\" --scaffold-sha " + setupRepoResult.commit.sha + " --project-created\n\n" +
|
|
569
|
+
"node " + crewHome + "/current/lib/advance-publish-base.js --crew-home \"" + crewHome + "\" --repo \"" + gateFacts.dashboardRepoExpanded + "\" --scaffold-sha " + setupRepoResult.commit.sha + " --project-id " + projectResult.project_id + " --project-created\n\n" +
|
|
516
570
|
"The script prints one JSON object: { ok, action, reason?, from?, to? }.\n" +
|
|
517
571
|
"Return that JSON verbatim.",
|
|
518
572
|
{
|
|
@@ -801,6 +855,7 @@ return {
|
|
|
801
855
|
scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
802
856
|
project: projectResult.action,
|
|
803
857
|
repoSetupWarning: setupRepoWarning,
|
|
858
|
+
initialProvenanceWarning: initialProvenanceWarning,
|
|
804
859
|
crons: cronsResult.summary.crons,
|
|
805
860
|
registry: cronsResult.summary.registry,
|
|
806
861
|
autoUpdate: {
|