muse-crew 0.13.3 → 0.14.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/docs/publish-unknown-recovery.md +113 -0
- package/lib/AGENTS.md +7 -2
- package/lib/advance-publish-base.js +74 -28
- package/lib/classify-publish-absence.js +451 -0
- package/lib/crew-api.js +584 -21
- package/lib/publish-content.js +154 -0
- package/lib/retry-publish.js +370 -0
- package/lib/scaffold-crew.js +129 -0
- package/lib/verify-publish.js +101 -84
- package/package.json +2 -2
- package/seed/cron-body-template.md +57 -2
- package/workflows/bugfix.js +38 -23
- package/workflows/chore.js +37 -22
- package/workflows/crew-dispatch.js +43 -8
- package/workflows/crew-init.js +333 -78
- package/workflows/crew-uninstall.js +57 -10
- package/workflows/standard.js +35 -16
- package/workflows/upgrade.js +28 -14
package/workflows/crew-init.js
CHANGED
|
@@ -6,7 +6,8 @@ export const meta = {
|
|
|
6
6
|
{ name: "scaffold", title: "Scaffold orchestration directory" },
|
|
7
7
|
{ name: "project", title: "Register first project" },
|
|
8
8
|
{ name: "crons", title: "Create and converge cron jobs" },
|
|
9
|
-
{ name: "policy", title: "Persist update policy" }
|
|
9
|
+
{ name: "policy", title: "Persist update policy" },
|
|
10
|
+
{ name: "verify", title: "Independently verify init end state" }
|
|
10
11
|
]
|
|
11
12
|
};
|
|
12
13
|
|
|
@@ -248,7 +249,8 @@ try {
|
|
|
248
249
|
} catch (e) {
|
|
249
250
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Release bootstrap failed", message: String(e.message || e) } };
|
|
250
251
|
}
|
|
251
|
-
log("Release: " + (releaseResult.existed ? "existing" : "bootstrapped") +
|
|
252
|
+
log("Release: bootstrap agent reported " + (releaseResult.existed ? "existing" : "bootstrapped") +
|
|
253
|
+
" release at " + releaseResult.hash + " (testimony — the verify phase re-reads the release identity)");
|
|
252
254
|
|
|
253
255
|
// ── Dependency node_modules symlink ───────────────────────────────────
|
|
254
256
|
// The release bootstrap (crew-release.sh init -> deploy) already links
|
|
@@ -287,7 +289,9 @@ try {
|
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
);
|
|
290
|
-
|
|
292
|
+
// Testimony only — the verify phase re-reads the symlink target independently.
|
|
293
|
+
log("Dependency node_modules: agent reported " + (depLinkResult.note || "unknown") + " -> " + depNodeModules +
|
|
294
|
+
" (witness verification pending)");
|
|
291
295
|
} catch (e) {
|
|
292
296
|
log("Dependency node_modules link step errored (non-fatal): " + String(e.message || e));
|
|
293
297
|
}
|
|
@@ -295,47 +299,51 @@ try {
|
|
|
295
299
|
// ── Phase 2: Scaffold ─────────────────────────────────────────────────
|
|
296
300
|
// Copies identities (with portraits), personas, workflow docs, and
|
|
297
301
|
// feedback convention from the crew repo into .orchestration/ under crewHome.
|
|
298
|
-
//
|
|
302
|
+
// (2026-09-19, Room #19 D1 step-back): copying files is deterministic work —
|
|
303
|
+
// no agent holds our data. The deterministic lib/scaffold-crew.js does the
|
|
304
|
+
// copies, counts them, verifies every file on disk, and writes
|
|
305
|
+
// .scaffold-receipt.json. The agent below is a byte courier only: it runs
|
|
306
|
+
// the exact command and prints the receipt file's exact bytes. The receipt
|
|
307
|
+
// bytes are script-authored (a paraphrased ferry fails the mechanical parse
|
|
308
|
+
// loudly — explicit ferry failure, never a fabricated 0/0), but the ferry
|
|
309
|
+
// itself is the agent's REPORT that the script ran: it does not prove the
|
|
310
|
+
// side effect. The verify phase re-runs this same idempotent composer and
|
|
311
|
+
// its disk-verification holds the end state; the ferry's counts are logged
|
|
312
|
+
// as reported, the witness's counts are the state. Init is idempotent, so
|
|
313
|
+
// re-running retries cheaply.
|
|
299
314
|
phase("scaffold");
|
|
300
|
-
var
|
|
315
|
+
var scaffoldCreated = 0;
|
|
316
|
+
var scaffoldSkipped = 0;
|
|
317
|
+
var scaffoldReceiptPath = orchDir + "/.scaffold-receipt.json";
|
|
318
|
+
var safeScaffoldRepo = String(crewRepoPath).split('"').join('\\"');
|
|
319
|
+
var safeScaffoldOrch = String(orchDir).split('"').join('\\"');
|
|
301
320
|
try {
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
" mkdir -p " + orchDir + "/identities " + orchDir + "/personas " + orchDir + "/workflows " + orchDir + "/feedback\n" +
|
|
309
|
-
"2. Copy identities (.md AND .png — portraits are in the repo):\n" +
|
|
310
|
-
" cp -n " + crewRepoPath + "/identities/*.md " + crewRepoPath + "/identities/*.png " + orchDir + "/identities/\n" +
|
|
311
|
-
" Also copy personality-notes.md if it exists.\n" +
|
|
312
|
-
"3. Copy personas:\n" +
|
|
313
|
-
" cp -n " + crewRepoPath + "/personas/*.md " + orchDir + "/personas/\n" +
|
|
314
|
-
"4. Copy workflow documentation (markdown descriptions, not JS scripts):\n" +
|
|
315
|
-
" cp -n " + crewRepoPath + "/seed/workflows/*.md " + orchDir + "/workflows/\n" +
|
|
316
|
-
"5. Copy feedback convention:\n" +
|
|
317
|
-
" cp -n " + crewRepoPath + "/seed/feedback/README.md " + orchDir + "/feedback/README.md\n" +
|
|
318
|
-
"6. List what ended up in " + orchDir + " to confirm.\n\n" +
|
|
319
|
-
"Return JSON with created (array of relative paths that were newly copied) and skipped (array that already existed).",
|
|
321
|
+
var scaffoldFerry = await agent(
|
|
322
|
+
"Run the deterministic scaffold composer. Do not decide anything — run the exact command below, then print the exact bytes of the receipt file it writes, and nothing else.\n\n" +
|
|
323
|
+
"Command:\n" +
|
|
324
|
+
"node \"" + crewHome + "/current/lib/scaffold-crew.js\" --repo \"" + safeScaffoldRepo + "\" --orch-dir \"" + safeScaffoldOrch + "\"\n\n" +
|
|
325
|
+
"The script copies the crew's identities, personas, workflow docs, and feedback convention into " + orchDir + " (never overwriting existing files), verifies every file on disk, and writes " + scaffoldReceiptPath + " containing { created, skipped } (integer counts).\n" +
|
|
326
|
+
"Print the exact bytes of " + scaffoldReceiptPath + " and nothing else.",
|
|
320
327
|
{
|
|
321
328
|
key: "scaffold-1",
|
|
322
|
-
label: "Scaffold .orchestration directory"
|
|
323
|
-
schema: {
|
|
324
|
-
type: "object",
|
|
325
|
-
properties: {
|
|
326
|
-
created: { type: "array", items: { type: "string" } },
|
|
327
|
-
skipped: { type: "array", items: { type: "string" } }
|
|
328
|
-
},
|
|
329
|
-
required: ["created", "skipped"]
|
|
330
|
-
}
|
|
329
|
+
label: "Scaffold .orchestration directory"
|
|
331
330
|
}
|
|
332
331
|
);
|
|
332
|
+
var scaffoldReceipt = JSON.parse(String(scaffoldFerry).trim());
|
|
333
|
+
if (!scaffoldReceipt || !Number.isInteger(scaffoldReceipt.created) || !Number.isInteger(scaffoldReceipt.skipped)) {
|
|
334
|
+
throw new Error("scaffold receipt did not carry integer created/skipped counts");
|
|
335
|
+
}
|
|
336
|
+
if (scaffoldReceipt.created < 0 || scaffoldReceipt.skipped < 0) {
|
|
337
|
+
throw new Error("scaffold receipt carried a negative count (created=" +
|
|
338
|
+
scaffoldReceipt.created + ", skipped=" + scaffoldReceipt.skipped + ")");
|
|
339
|
+
}
|
|
340
|
+
// Reported counts only — the verify phase corroborates the side effect.
|
|
341
|
+
scaffoldCreated = scaffoldReceipt.created;
|
|
342
|
+
scaffoldSkipped = scaffoldReceipt.skipped;
|
|
333
343
|
} catch (e) {
|
|
334
344
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Scaffold failed", message: String(e.message || e) } };
|
|
335
345
|
}
|
|
336
|
-
|
|
337
|
-
var scaffoldSkipped = scaffoldResult.skipped ? scaffoldResult.skipped.length : 0;
|
|
338
|
-
log("Scaffold: " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped");
|
|
346
|
+
log("Scaffold: ferry reported " + scaffoldCreated + " created, " + scaffoldSkipped + " skipped (witness verification pending)");
|
|
339
347
|
|
|
340
348
|
// ── Phase 3: Project registration ─────────────────────────────────────
|
|
341
349
|
// Registers the first project in crew-state.db via the Crew API. In
|
|
@@ -414,19 +422,12 @@ try {
|
|
|
414
422
|
} catch (e) {
|
|
415
423
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Project registration failed", message: String(e.message || e) } };
|
|
416
424
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
}
|
|
425
|
+
// Testimony only: the registration agent's report. The verify phase re-reads
|
|
426
|
+
// the project row (and its provenance) independently; the reported action is
|
|
427
|
+
// never used as a gate — the publish-base advance below decides from the
|
|
428
|
+
// deterministic task_id invariant inside lib/advance-publish-base.js.
|
|
429
|
+
log("Project: registration agent reported action=" + projectResult.action + " for " + dashboardSlug +
|
|
430
|
+
" (repo_path=" + gateFacts.dashboardRepoExpanded + ") — witness verification pending");
|
|
430
431
|
} // end dashboard-mode project registration
|
|
431
432
|
|
|
432
433
|
// ── Artifact target verification (room #16 blocker 10, 2026-09-18) ──
|
|
@@ -465,7 +466,10 @@ if (dashboardSlug) {
|
|
|
465
466
|
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
|
} };
|
|
467
468
|
}
|
|
468
|
-
|
|
469
|
+
// Fail-closed in the safe direction: a false "missing" blocks init
|
|
470
|
+
// unnecessarily, but a false "present" is re-checked by the verify-phase
|
|
471
|
+
// witness before the summary asserts anything.
|
|
472
|
+
log("Artifact target: agent reported ~/workspace/ts-spaces/" + dashboardSlug + "/ present (witness re-confirms)");
|
|
469
473
|
}
|
|
470
474
|
|
|
471
475
|
// ── Project repo setup (2026-09-18) ─────────────────────────────────
|
|
@@ -521,19 +525,18 @@ if (dashboardRepoPath) {
|
|
|
521
525
|
}
|
|
522
526
|
}
|
|
523
527
|
);
|
|
528
|
+
// Testimony only: the composer's report via the agent. The verify phase
|
|
529
|
+
// observes the repo state (HEAD, .orchestration/ present, clean tree)
|
|
530
|
+
// independently; the uncommitted-scaffold warning is driven by that
|
|
531
|
+
// observation, never by the reported commit flag.
|
|
524
532
|
var orchDirs = (setupRepoResult.orchestration.dirsCreated || []).join(", ");
|
|
525
533
|
var commitMsg = setupRepoResult.commit.committed
|
|
526
534
|
? "committed " + setupRepoResult.commit.sha
|
|
527
535
|
: "not committed (" + setupRepoResult.commit.reason + ")";
|
|
528
|
-
log("Repo setup: scaffolded " + setupRepoResult.orchestration.orchDir +
|
|
536
|
+
log("Repo setup: composer agent reported scaffolded " + setupRepoResult.orchestration.orchDir +
|
|
529
537
|
" (dirs: [" + orchDirs + "]); gitignore " +
|
|
530
538
|
(gitignoreConsent ? "update consented" : "declined by user — add '.worktrees/' and '.orchestration/user/' manually") +
|
|
531
|
-
"; scaffold " + commitMsg);
|
|
532
|
-
if (!setupRepoResult.commit.committed && setupRepoResult.commit.reason !== "nothing-to-commit") {
|
|
533
|
-
setupRepoWarning = "Project repo setup left the scaffold uncommitted (" +
|
|
534
|
-
setupRepoResult.commit.reason + ") — the first task's Integrate will refuse to merge until it is committed.";
|
|
535
|
-
log("WARNING: " + setupRepoWarning);
|
|
536
|
-
}
|
|
539
|
+
"; scaffold " + commitMsg + " (witness verification pending)");
|
|
537
540
|
} catch (e) {
|
|
538
541
|
var setupErr = String(e.message || e);
|
|
539
542
|
setupRepoWarning = "Project repo setup failed: " + setupErr + " — continuing without the repo scaffold.";
|
|
@@ -553,21 +556,31 @@ if (dashboardRepoPath) {
|
|
|
553
556
|
// the whole scaffold (517 insertions in room #14) and blows the 200-line
|
|
554
557
|
// diff-transport budget. The scaffold is crew-owned infrastructure the
|
|
555
558
|
// dashboard artifact never consumes, so the correct publish base is the
|
|
556
|
-
// scaffold commit itself.
|
|
557
|
-
//
|
|
558
|
-
// (
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
//
|
|
559
|
+
// scaffold commit itself.
|
|
560
|
+
//
|
|
561
|
+
// (2026-09-19 REVIEW, agent-boundary razor): the old invocation passed
|
|
562
|
+
// --scaffold-sha and --project-created — both agent testimony (the ferried
|
|
563
|
+
// commit sha, the registration agent's "created" action). The script now
|
|
564
|
+
// resolves the scaffold commit itself (git rev-parse HEAD in --repo) and
|
|
565
|
+
// decides from the deterministic task_id invariant (a real publish's
|
|
566
|
+
// provenance carries task_id; initial stamps and base advances leave it
|
|
567
|
+
// null — see stampProvenance in lib/crew-api.js). The workflow passes no
|
|
568
|
+
// testimony into the script and gates only on "there is a dashboard repo";
|
|
569
|
+
// the script's own fail-closed checks own every consequential decision.
|
|
570
|
+
// The script's return is its report of those checks (logged honestly); the
|
|
571
|
+
// verify phase confirms the stamped base independently.
|
|
572
|
+
// Best-effort: a failed advance never blocks init — the first task then
|
|
573
|
+
// publishes from the stamped base as before.
|
|
562
574
|
var publishBaseResult = null;
|
|
563
|
-
|
|
564
|
-
|
|
575
|
+
var safeAdvanceCrewHome = String(crewHome).split('"').join('\\"');
|
|
576
|
+
var safeAdvanceRepo = String(gateFacts.dashboardRepoExpanded).split('"').join('\\"');
|
|
577
|
+
if (dashboardRepoPath && projectResult && projectResult.project_id) {
|
|
565
578
|
try {
|
|
566
579
|
publishBaseResult = await agent(
|
|
567
580
|
"Run the deterministic publish-base advance script. Do not decide anything — just run the command and return its JSON output.\n\n" +
|
|
568
581
|
"Command:\n" +
|
|
569
|
-
"node " +
|
|
570
|
-
"The script prints one JSON object: { ok, action, reason?, from?, to? }.\n" +
|
|
582
|
+
"node \"" + safeAdvanceCrewHome + "/current/lib/advance-publish-base.js\" --crew-home \"" + safeAdvanceCrewHome + "\" --repo \"" + safeAdvanceRepo + "\" --project-id " + projectResult.project_id + "\n\n" +
|
|
583
|
+
"The script prints one JSON object: { ok, action, reason?, from?, to?, repo_clean? }.\n" +
|
|
571
584
|
"Return that JSON verbatim.",
|
|
572
585
|
{
|
|
573
586
|
key: "advance-publish-base",
|
|
@@ -579,21 +592,22 @@ if (dashboardRepoPath && projectResult && projectResult.action === "created" &&
|
|
|
579
592
|
action: { type: "string" },
|
|
580
593
|
reason: {},
|
|
581
594
|
from: {},
|
|
582
|
-
to: {}
|
|
595
|
+
to: {},
|
|
596
|
+
repo_clean: {}
|
|
583
597
|
},
|
|
584
598
|
required: ["ok", "action"]
|
|
585
599
|
}
|
|
586
600
|
}
|
|
587
601
|
);
|
|
588
|
-
log("Publish base: " + publishBaseResult.action +
|
|
602
|
+
log("Publish base: advance script reported action=" + publishBaseResult.action +
|
|
589
603
|
(publishBaseResult.reason ? " (" + publishBaseResult.reason + ")" : "") +
|
|
590
|
-
(publishBaseResult.from ? " " + publishBaseResult.from + " -> " + publishBaseResult.to : "")
|
|
604
|
+
(publishBaseResult.from ? " " + publishBaseResult.from + " -> " + publishBaseResult.to : "") +
|
|
605
|
+
" (witness verification pending)");
|
|
591
606
|
} catch (e) {
|
|
592
607
|
log("Publish base: advance script failed (" + String(e.message || e) + ") — continuing with the stamped base (non-fatal).");
|
|
593
608
|
}
|
|
594
609
|
} else {
|
|
595
|
-
log("Publish base: no advance (
|
|
596
|
-
", scaffold " + (setupRepoResult && setupRepoResult.commit && setupRepoResult.commit.committed ? "committed" : "not committed") + ").");
|
|
610
|
+
log("Publish base: no advance (no dashboard repo — CLI-only mode).");
|
|
597
611
|
}
|
|
598
612
|
|
|
599
613
|
// ── Scheduler identity (2026-09-16) ──────────────────────────────────
|
|
@@ -762,8 +776,9 @@ if (!cronsResult.passed || !cronsResult.summary || !Array.isArray(cronsResult.su
|
|
|
762
776
|
!cronsResult.summary.registry || !Array.isArray(cronsResult.summary.registry.ids)) {
|
|
763
777
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Cron manifest invalid", message: "seed/crons.json failed validation, produced no crons list, or wrote no cron registry" } };
|
|
764
778
|
}
|
|
765
|
-
log("Crons: " + cronsResult.summary.crons.map(function (c) { return c.id + "=" + c.action; }).join(", ") +
|
|
766
|
-
"; registry: " + cronsResult.summary.registry.path + " (" + cronsResult.summary.registry.ids.join(", ") + ")"
|
|
779
|
+
log("Crons: agent reported " + cronsResult.summary.crons.map(function (c) { return c.id + "=" + c.action; }).join(", ") +
|
|
780
|
+
"; registry: " + cronsResult.summary.registry.path + " (" + cronsResult.summary.registry.ids.join(", ") + ")" +
|
|
781
|
+
" (witness verification pending)");
|
|
767
782
|
|
|
768
783
|
|
|
769
784
|
// ── Phase 5: Update policy ────────────────────────────────────────────
|
|
@@ -838,12 +853,229 @@ var persistedPolicy = policyResult.persisted || {};
|
|
|
838
853
|
if (!persistedPolicy.auto_update_crew || !persistedPolicy.auto_update_dashboard || !persistedPolicy.update_channel) {
|
|
839
854
|
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Update policy incomplete", message: "policy phase returned no persisted policy values" } };
|
|
840
855
|
}
|
|
841
|
-
log("Update policy: auto_update_crew=" + persistedPolicy.auto_update_crew +
|
|
856
|
+
log("Update policy: agent reported auto_update_crew=" + persistedPolicy.auto_update_crew +
|
|
842
857
|
" auto_update_dashboard=" + persistedPolicy.auto_update_dashboard +
|
|
843
858
|
" update_channel=" + persistedPolicy.update_channel +
|
|
844
|
-
" state_file=" + policyResult.state_file);
|
|
859
|
+
" state_file=" + policyResult.state_file + " (witness verification pending)");
|
|
860
|
+
|
|
861
|
+
// ── Phase: verify — independent witness (2026-09-19) ────────────────────
|
|
862
|
+
// Agent-boundary razor: every agent return above is testimony — routed,
|
|
863
|
+
// never trusted. One fresh agent re-reads the end state deterministically;
|
|
864
|
+
// the workflow checks each claim mechanically below. Fail-closed: a
|
|
865
|
+
// mismatch on a phase that promised a state blocks init; the two
|
|
866
|
+
// best-effort phases (repo setup, publish-base advance) warn loudly
|
|
867
|
+
// instead of blocking.
|
|
868
|
+
phase("verify");
|
|
869
|
+
var wCrewHome = String(crewHome).split('"').join('\\"');
|
|
870
|
+
var wCrewRepo = String(crewRepoPath).split('"').join('\\"');
|
|
871
|
+
var wOrchDir = String(orchDir).split('"').join('\\"');
|
|
872
|
+
var wDashRepo = String(gateFacts.dashboardRepoExpanded || "").split('"').join('\\"');
|
|
873
|
+
var witnessResult;
|
|
874
|
+
try {
|
|
875
|
+
witnessResult = await agent(
|
|
876
|
+
"Verify the Muse Crew installation end state. Do not decide anything — run each read exactly as written and return one JSON object with exactly the keys below.\n\n" +
|
|
877
|
+
(dashboardSlug
|
|
878
|
+
? "There IS a dashboard project (id \"" + dashboardSlug + "\"). Run the dashboard-only checks.\n\n"
|
|
879
|
+
: "This is CLI-only mode (no dashboard project). Put null for every dashboard-only key without running its command.\n\n") +
|
|
880
|
+
"1. release_hash — run: \"" + wCrewHome + "/crew-release.sh\" current \"" + wCrewHome + "\" — put its stdout trimmed, or null when the command fails.\n" +
|
|
881
|
+
"2. node_modules_target — run: readlink \"" + wCrewHome + "/node_modules\" || echo NOT_A_SYMLINK — put its stdout trimmed.\n" +
|
|
882
|
+
"3. scaffold_receipt — re-run the idempotent composer: node \"" + wCrewHome + "/current/lib/scaffold-crew.js\" --repo \"" + wCrewRepo + "\" --orch-dir \"" + wOrchDir + "\" — then put the exact bytes of \"" + wOrchDir + "/.scaffold-receipt.json\".\n" +
|
|
883
|
+
"4. scaffold_error — null when step 3 exited 0; otherwise the failure message.\n" +
|
|
884
|
+
"5. project_row [dashboard-only] — run: node \"" + wCrewHome + "/current/lib/crew-api.js\" --crew-home \"" + wCrewHome + "\" get-project --json '{\"id\": \"" + dashboardSlug + "\"}' — put its stdout trimmed.\n" +
|
|
885
|
+
"6. repo_head [dashboard-only] — run: git -C \"" + wDashRepo + "\" rev-parse HEAD — put its stdout trimmed, or null on failure.\n" +
|
|
886
|
+
"7. repo_orch_present [dashboard-only] — put \"yes\" when \"" + wDashRepo + "/.orchestration\" is a directory, else \"no\".\n" +
|
|
887
|
+
"8. repo_clean [dashboard-only] — run: git -C \"" + wDashRepo + "\" status --porcelain — put \"yes\" when it prints nothing, else \"no\".\n" +
|
|
888
|
+
"9. provenance_row [dashboard-only] — run: node \"" + wCrewHome + "/current/lib/crew-api.js\" --crew-home \"" + wCrewHome + "\" get-provenance --json '{\"project_id\": \"" + dashboardSlug + "\"}' — put its stdout trimmed.\n" +
|
|
889
|
+
"10. artifact_target [dashboard-only] — run: test -d ~/workspace/ts-spaces/" + dashboardSlug + "/ && echo present || echo missing — put the word printed.\n" +
|
|
890
|
+
"11. cron_ids — call cron_list and put the JSON array of live job ids.\n" +
|
|
891
|
+
"12. config_row — run: node \"" + wCrewHome + "/current/lib/crew-api.js\" --crew-home \"" + wCrewHome + "\" get-config — put its stdout trimmed.\n\n" +
|
|
892
|
+
"Return the JSON object and nothing else.",
|
|
893
|
+
{
|
|
894
|
+
key: "init-verify",
|
|
895
|
+
label: "Independently verify init end state",
|
|
896
|
+
schema: {
|
|
897
|
+
type: "object",
|
|
898
|
+
properties: {
|
|
899
|
+
release_hash: { type: ["string", "null"] },
|
|
900
|
+
node_modules_target: { type: "string" },
|
|
901
|
+
scaffold_receipt: { type: ["string", "null"] },
|
|
902
|
+
scaffold_error: { type: ["string", "null"] },
|
|
903
|
+
project_row: { type: ["string", "null"] },
|
|
904
|
+
repo_head: { type: ["string", "null"] },
|
|
905
|
+
repo_orch_present: { type: ["string", "null"] },
|
|
906
|
+
repo_clean: { type: ["string", "null"] },
|
|
907
|
+
provenance_row: { type: ["string", "null"] },
|
|
908
|
+
artifact_target: { type: ["string", "null"] },
|
|
909
|
+
cron_ids: { type: "array", items: { type: "string" } },
|
|
910
|
+
config_row: { type: "string" }
|
|
911
|
+
},
|
|
912
|
+
required: ["release_hash", "node_modules_target", "scaffold_receipt", "scaffold_error",
|
|
913
|
+
"project_row", "repo_head", "repo_orch_present", "repo_clean",
|
|
914
|
+
"provenance_row", "artifact_target", "cron_ids", "config_row"]
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
);
|
|
918
|
+
} catch (e) {
|
|
919
|
+
return { __hatchWorkflowControl: "blocked", result: { blocked_reason: "Init verification failed", message: String(e.message || e) } };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// 1. Release identity: the release system must self-identify with the hash
|
|
923
|
+
// the bootstrap reported. A mismatch means the bootstrap report was fiction.
|
|
924
|
+
if (String(witnessResult.release_hash || "").trim() !== String(releaseResult.hash || "").trim() ||
|
|
925
|
+
!witnessResult.release_hash) {
|
|
926
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
927
|
+
blocked_reason: "Init verification failed: release identity mismatch",
|
|
928
|
+
message: "The release bootstrap reported hash \"" + releaseResult.hash + "\", but the release system now self-identifies as \"" +
|
|
929
|
+
(witnessResult.release_hash || "null") + "\". The bootstrap report did not match reality — refusing to assert a release identity that cannot identify itself. Re-running init is safe (idempotent)."
|
|
930
|
+
} };
|
|
931
|
+
}
|
|
932
|
+
log("Verify: release identity matches (" + witnessResult.release_hash + ")");
|
|
933
|
+
|
|
934
|
+
// 2. Dependency symlink: the observed target must match the reported outcome.
|
|
935
|
+
var nmTarget = String(witnessResult.node_modules_target || "");
|
|
936
|
+
if (depLinkResult && depLinkResult.note === "LINKED" && nmTarget !== depNodeModules) {
|
|
937
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
938
|
+
blocked_reason: "Init verification failed: node_modules symlink mismatch",
|
|
939
|
+
message: "The link step reported LINKED into \"" + depNodeModules + "\", but readlink now shows \"" + nmTarget + "\"."
|
|
940
|
+
} };
|
|
941
|
+
}
|
|
942
|
+
if (depLinkResult && depLinkResult.note === "REAL_DIR_KEPT" && nmTarget !== "NOT_A_SYMLINK") {
|
|
943
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
944
|
+
blocked_reason: "Init verification failed: node_modules is not a real dir",
|
|
945
|
+
message: "The link step reported REAL_DIR_KEPT, but readlink now shows \"" + nmTarget + "\"."
|
|
946
|
+
} };
|
|
947
|
+
}
|
|
948
|
+
log("Verify: node_modules link state matches report (" + nmTarget + ")");
|
|
949
|
+
|
|
950
|
+
// 3. Scaffold: the witness re-ran the idempotent composer — its receipt is
|
|
951
|
+
// the state. The composer's exit 0 is its own disk-verification.
|
|
952
|
+
if (witnessResult.scaffold_error) {
|
|
953
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
954
|
+
blocked_reason: "Init verification failed: scaffold re-run failed",
|
|
955
|
+
message: String(witnessResult.scaffold_error)
|
|
956
|
+
} };
|
|
957
|
+
}
|
|
958
|
+
var wScaffold = null;
|
|
959
|
+
try {
|
|
960
|
+
wScaffold = JSON.parse(String(witnessResult.scaffold_receipt || "").trim());
|
|
961
|
+
if (!wScaffold || !Number.isInteger(wScaffold.created) || !Number.isInteger(wScaffold.skipped) ||
|
|
962
|
+
wScaffold.created < 0 || wScaffold.skipped < 0) throw new Error("bad receipt");
|
|
963
|
+
} catch (e) {
|
|
964
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
965
|
+
blocked_reason: "Init verification failed: scaffold receipt unparseable",
|
|
966
|
+
message: "The scaffold witness re-run did not produce a parseable receipt with non-negative integer created/skipped counts."
|
|
967
|
+
} };
|
|
968
|
+
}
|
|
969
|
+
if (scaffoldCreated > 0 && wScaffold.created > 0) {
|
|
970
|
+
log("WARNING: the scaffold ferry reported " + scaffoldCreated + " created, but the witness re-run still created " +
|
|
971
|
+
wScaffold.created + " — the ferry's report did not match disk state. The end state is now verified; the ferry report is not trusted.");
|
|
972
|
+
}
|
|
973
|
+
log("Verify: scaffold composer re-run verified " + (wScaffold.created + wScaffold.skipped) + " files on disk");
|
|
974
|
+
|
|
975
|
+
// 4-8. Project row, provenance, repo state, publish-base consistency,
|
|
976
|
+
// artifact target (dashboard mode only).
|
|
977
|
+
var wProject = null;
|
|
978
|
+
var wProvenance = null;
|
|
979
|
+
if (dashboardSlug) {
|
|
980
|
+
try {
|
|
981
|
+
wProject = JSON.parse(String(witnessResult.project_row || "").trim()).project;
|
|
982
|
+
} catch (e) { wProject = null; }
|
|
983
|
+
if (!wProject || wProject.id !== dashboardSlug ||
|
|
984
|
+
wProject.repo_path !== gateFacts.dashboardRepoExpanded ||
|
|
985
|
+
wProject.environment_type !== "artifact") {
|
|
986
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
987
|
+
blocked_reason: "Init verification failed: project row mismatch",
|
|
988
|
+
message: "The project registration reported action=" + projectResult.action + " for \"" + dashboardSlug +
|
|
989
|
+
"\", but the re-read project row does not match (id, repo_path, or environment_type differ). The registration report did not land as specified."
|
|
990
|
+
} };
|
|
991
|
+
}
|
|
992
|
+
log("Verify: project row matches (id=" + wProject.id + ", repo_path and environment_type confirmed)");
|
|
993
|
+
|
|
994
|
+
try {
|
|
995
|
+
wProvenance = JSON.parse(String(witnessResult.provenance_row || "").trim()).provenance;
|
|
996
|
+
} catch (e) { wProvenance = null; }
|
|
997
|
+
// Room #15: a missing stamp used to stay invisible ("reported into the
|
|
998
|
+
// void"). The observable condition — no provenance row after init —
|
|
999
|
+
// warns loudly here, driven by the re-read, never by testimony.
|
|
1000
|
+
if (!wProvenance) {
|
|
1001
|
+
initialProvenanceWarning = "Init finished with no provenance stamp on project \"" + dashboardSlug +
|
|
1002
|
+
"\" — the first task's publish diffs from the empty tree. Re-running init is safe (idempotent).";
|
|
1003
|
+
log("WARNING: " + initialProvenanceWarning);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Repo state: observation drives the warning (best-effort phase, never
|
|
1007
|
+
// the composer's reported commit flag).
|
|
1008
|
+
if (witnessResult.repo_orch_present !== "yes" || witnessResult.repo_clean !== "yes") {
|
|
1009
|
+
var repoStateBits = [];
|
|
1010
|
+
if (witnessResult.repo_orch_present !== "yes") repoStateBits.push(".orchestration/ not present");
|
|
1011
|
+
if (witnessResult.repo_clean !== "yes") repoStateBits.push("working tree not clean");
|
|
1012
|
+
if (witnessResult.repo_head) repoStateBits.push("HEAD=" + String(witnessResult.repo_head).slice(0, 12));
|
|
1013
|
+
setupRepoWarning = "Project repo setup left the repo in an unexpected state (" + repoStateBits.join("; ") +
|
|
1014
|
+
") — the first task's Integrate will refuse to merge until the scaffold is committed and the tree is clean.";
|
|
1015
|
+
log("WARNING: " + setupRepoWarning);
|
|
1016
|
+
} else {
|
|
1017
|
+
log("Verify: repo scaffold present at " + String(witnessResult.repo_head || "").slice(0, 12) + ", tree clean");
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Publish-base consistency: a reported "advanced" must be observable in
|
|
1021
|
+
// provenance. Best-effort — warns loudly instead of blocking.
|
|
1022
|
+
if (publishBaseResult && publishBaseResult.action === "advanced") {
|
|
1023
|
+
var advTo = String(publishBaseResult.to || "");
|
|
1024
|
+
var provSha = wProvenance && wProvenance.source_commit ? String(wProvenance.source_commit) : "";
|
|
1025
|
+
if (!advTo || provSha.indexOf(advTo) !== 0) {
|
|
1026
|
+
log("WARNING: the publish-base advance script reported action=advanced (-> " + advTo +
|
|
1027
|
+
") but provenance now points at " + (provSha ? provSha.slice(0, 12) : "nothing") +
|
|
1028
|
+
" — the report did not match state. The first task's publish diff may carry the scaffold; watch its diff-transport budget.");
|
|
1029
|
+
} else {
|
|
1030
|
+
log("Verify: publish base advanced -> " + advTo + " (provenance confirms)");
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Artifact target: the early check's "present" is re-confirmed. A
|
|
1035
|
+
// "missing" here means the early agent's report was fiction — block with
|
|
1036
|
+
// the same guidance.
|
|
1037
|
+
if (witnessResult.artifact_target !== "present") {
|
|
1038
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
1039
|
+
blocked_reason: "Dashboard artifact target missing",
|
|
1040
|
+
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."
|
|
1041
|
+
} };
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// 9. Crons: every reported live id must exist in cron_list.
|
|
1046
|
+
var liveIds = Array.isArray(witnessResult.cron_ids) ? witnessResult.cron_ids : [];
|
|
1047
|
+
var expectedIds = cronsResult.summary.registry.ids;
|
|
1048
|
+
var missingCrons = expectedIds.filter(function (id) { return liveIds.indexOf(id) === -1; });
|
|
1049
|
+
if (missingCrons.length > 0) {
|
|
1050
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
1051
|
+
blocked_reason: "Init verification failed: cron jobs missing",
|
|
1052
|
+
message: "The cron setup reported " + missingCrons.join(", ") + " as installed, but cron_list does not show them. The setup report did not match the scheduler state."
|
|
1053
|
+
} };
|
|
1054
|
+
}
|
|
1055
|
+
log("Verify: all " + expectedIds.length + " cron jobs present in cron_list");
|
|
1056
|
+
|
|
1057
|
+
// 10. Policy: the persisted config must read back the reported values.
|
|
1058
|
+
var wConfig = {};
|
|
1059
|
+
try {
|
|
1060
|
+
wConfig = JSON.parse(String(witnessResult.config_row || "").trim()).config || {};
|
|
1061
|
+
} catch (e) { wConfig = {}; }
|
|
1062
|
+
if (wConfig.auto_update_crew !== persistedPolicy.auto_update_crew ||
|
|
1063
|
+
wConfig.auto_update_dashboard !== persistedPolicy.auto_update_dashboard ||
|
|
1064
|
+
wConfig.update_channel !== persistedPolicy.update_channel) {
|
|
1065
|
+
return { __hatchWorkflowControl: "blocked", result: {
|
|
1066
|
+
blocked_reason: "Init verification failed: update policy mismatch",
|
|
1067
|
+
message: "The policy phase reported auto_update_crew=" + persistedPolicy.auto_update_crew +
|
|
1068
|
+
" auto_update_dashboard=" + persistedPolicy.auto_update_dashboard +
|
|
1069
|
+
" update_channel=" + persistedPolicy.update_channel + ", but get-config reads back auto_update_crew=" +
|
|
1070
|
+
wConfig.auto_update_crew + " auto_update_dashboard=" + wConfig.auto_update_dashboard +
|
|
1071
|
+
" update_channel=" + wConfig.update_channel + "."
|
|
1072
|
+
} };
|
|
1073
|
+
}
|
|
1074
|
+
log("Verify: update policy reads back as reported");
|
|
845
1075
|
|
|
846
1076
|
// ── Summary ───────────────────────────────────────────────────────────
|
|
1077
|
+
// Built from the verify-phase witness's re-reads. Anything the witness
|
|
1078
|
+
// could not corroborate is labeled *_reported — testimony, not fact.
|
|
847
1079
|
return {
|
|
848
1080
|
message: "Muse Crew initialized.",
|
|
849
1081
|
crewHome: crewHome,
|
|
@@ -851,14 +1083,37 @@ return {
|
|
|
851
1083
|
dashboardSlug: dashboardSlug,
|
|
852
1084
|
dashboardRepoPath: gateFacts.dashboardRepoExpanded,
|
|
853
1085
|
dashboardName: dashboardName,
|
|
854
|
-
releaseHash:
|
|
855
|
-
|
|
856
|
-
|
|
1086
|
+
releaseHash: witnessResult.release_hash,
|
|
1087
|
+
releaseBootstrapReported: releaseResult.existed ? "existing" : "bootstrapped",
|
|
1088
|
+
scaffold: { created: wScaffold.created, skipped: wScaffold.skipped },
|
|
1089
|
+
scaffoldFerryReported: { created: scaffoldCreated, skipped: scaffoldSkipped },
|
|
1090
|
+
project: dashboardSlug ? {
|
|
1091
|
+
actionReported: projectResult.action,
|
|
1092
|
+
id: wProject.id,
|
|
1093
|
+
repo_path: wProject.repo_path,
|
|
1094
|
+
environment_type: wProject.environment_type
|
|
1095
|
+
} : { action: "skipped" },
|
|
1096
|
+
repoSetup: dashboardRepoPath ? {
|
|
1097
|
+
head: witnessResult.repo_head,
|
|
1098
|
+
orch_dir_present: witnessResult.repo_orch_present === "yes",
|
|
1099
|
+
tree_clean: witnessResult.repo_clean === "yes"
|
|
1100
|
+
} : null,
|
|
857
1101
|
repoSetupWarning: setupRepoWarning,
|
|
858
1102
|
initialProvenanceWarning: initialProvenanceWarning,
|
|
1103
|
+
publishBase: publishBaseResult ? {
|
|
1104
|
+
actionReported: publishBaseResult.action,
|
|
1105
|
+
reasonReported: publishBaseResult.reason || null,
|
|
1106
|
+
verified_source_commit: wProvenance ? wProvenance.source_commit : null
|
|
1107
|
+
} : { actionReported: "not attempted" },
|
|
859
1108
|
crons: cronsResult.summary.crons,
|
|
1109
|
+
cronsVerifiedIds: liveIds,
|
|
860
1110
|
registry: cronsResult.summary.registry,
|
|
861
1111
|
autoUpdate: {
|
|
1112
|
+
crew: wConfig.auto_update_crew === "true",
|
|
1113
|
+
dashboard: wConfig.auto_update_dashboard === "true",
|
|
1114
|
+
channel: wConfig.update_channel
|
|
1115
|
+
},
|
|
1116
|
+
autoUpdateReported: {
|
|
862
1117
|
crew: persistedPolicy.auto_update_crew === "true",
|
|
863
1118
|
dashboard: persistedPolicy.auto_update_dashboard === "true",
|
|
864
1119
|
channel: persistedPolicy.update_channel
|