muse-crew 0.14.0 → 0.14.2

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.
@@ -794,13 +794,44 @@ if (eligible.length === 0) {
794
794
  );
795
795
 
796
796
  if (fileResult.task_id) {
797
- eligible.push({
798
- task: { id: fileResult.task_id, title: ptTitle, description: ptDesc, project: ptProject, workflow: "standard", state: "todo" },
799
- startStep: qaStepIndex("standard"),
800
- reason: "playtest",
801
- workflow: "standard"
802
- });
803
- log("Filed idle playtest \"" + ptTitle + "\" [" + ptProject + "] journey " + pti + "/" + J + ", persona " + ppi + "/" + P + ", max_filings " + maxFilings);
797
+ // Razor: the filed task_id is testimony. Do an independent witness
798
+ // re-read of the board before entering eligible — the task must be
799
+ // observed in todo state, not assumed. The downstream atomic claim
800
+ // remains the authoritative guard; this is fail-fast, fail-closed.
801
+ var ptWitnessOk = false;
802
+ try {
803
+ var ptBoardRaw = await agent(
804
+ "Re-read the dispatch state to verify the filed playtest task.\\n" +
805
+ "Run in shell and return the stdout as a raw string:\\n" + crewCmd("get-dispatch-state", {}) + "\\n" +
806
+ "Return the command's stdout JSON as a plain string, byte-for-byte, unmodified. " +
807
+ "Do NOT parse the JSON — your return value must be the raw stdout string, never an object.",
808
+ {
809
+ key: "verify-playtest",
810
+ label: "Verifying filed playtest task"
811
+ }
812
+ );
813
+ var ptBoard = unwrapBoardResult(parseBoardJson(ptBoardRaw));
814
+ var ptTasks = (ptBoard.ready_tasks || []).map(projectTaskRecord).filter(function (t) { return t !== null; });
815
+ for (var pti2 = 0; pti2 < ptTasks.length; pti2++) {
816
+ if (ptTasks[pti2].id === fileResult.task_id && ptTasks[pti2].state === "todo") {
817
+ ptWitnessOk = true;
818
+ break;
819
+ }
820
+ }
821
+ } catch (e) {
822
+ log("Playtest witness re-read failed (" + String(e.message || e) + ") — task not verified, skipping eligible");
823
+ }
824
+ if (ptWitnessOk) {
825
+ eligible.push({
826
+ task: { id: fileResult.task_id, title: ptTitle, description: ptDesc, project: ptProject, workflow: "standard", state: "todo" },
827
+ startStep: qaStepIndex("standard"),
828
+ reason: "playtest",
829
+ workflow: "standard"
830
+ });
831
+ log("Filed idle playtest \"" + ptTitle + "\" [" + ptProject + "] journey " + pti + "/" + J + ", persona " + ppi + "/" + P + ", max_filings " + maxFilings + " (witness observed todo)");
832
+ } else {
833
+ log("Idle playtest task " + fileResult.task_id + " not observed in todo state on re-read — skipping eligible (fail-closed)");
834
+ }
804
835
  } else {
805
836
  log("Idle playtest filing failed — cursors already advanced, assignment skipped");
806
837
  }
@@ -1022,7 +1053,11 @@ if (recommended.length > 0) {
1022
1053
  );
1023
1054
  var reserveParsed = extractJsonObject(reserveOut);
1024
1055
  if (reserveParsed && reserveParsed.acquired) {
1025
- log("Reserved task " + rec.task_id.slice(0, 8));
1056
+ // Razor: the acquired flag is testimony from the reservation agent's
1057
+ // stdout ferry, not proof. The downstream atomic claim-task remains
1058
+ // the authoritative guard. We route on the flag (fail-closed if
1059
+ // absent) but do not assert the reservation as established fact.
1060
+ log("Reservation agent returned acquired=true for task " + rec.task_id.slice(0, 8) + " (not independently verified; claim-task is authoritative)");
1026
1061
  acquired.push(rec);
1027
1062
  } else {
1028
1063
  // FAIL CLOSED: Another dispatcher owns this task. Do not recommend it.
@@ -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") + " at " + releaseResult.hash);
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
- log("Dependency node_modules: " + (depLinkResult.note || "unknown") + " -> " + depNodeModules);
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
- // Uses cp --no-clobber so existing files are never overwritten.
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 scaffoldResult;
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
- scaffoldResult = await agent(
303
- "Scaffold the .orchestration directory from the crew repo.\n\n" +
304
- "Source repo: " + crewRepoPath + "\n" +
305
- "Destination: " + orchDir + "\n\n" +
306
- "Steps:\n" +
307
- "1. Create directories:\n" +
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
- var scaffoldCreated = scaffoldResult.created ? scaffoldResult.created.length : 0;
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
- 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
- }
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
- log("Artifact target verified: ~/workspace/ts-spaces/" + dashboardSlug + "/ exists");
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. Advance it deterministically via
557
- // lib/advance-publish-base.js — only when this init created the project
558
- // (the stamp is ours, never a real publish's), the scaffold commit's parent
559
- // is exactly the stamped base (nothing slipped between), and the scaffold
560
- // touches only crew-owned paths. Best-effort: a failed advance never blocks
561
- // init — the first task then publishes from the stamped base as before.
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
- if (dashboardRepoPath && projectResult && projectResult.action === "created" &&
564
- setupRepoResult && setupRepoResult.commit && setupRepoResult.commit.committed) {
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 " + 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" +
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 (project " + (projectResult ? projectResult.action : "unknown") +
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: releaseResult.hash,
855
- scaffold: { created: scaffoldCreated, skipped: scaffoldSkipped },
856
- project: projectResult.action,
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