muse-crew 0.11.1 → 0.12.0

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.
@@ -941,11 +941,17 @@ await agent(
941
941
  // not timestamps — no wall-clock calls (see tests/determinism.test.js).
942
942
  // Fire-and-forget: the script swallows its own errors and exits 0, the
943
943
  // agent call carries no schema, and the whole call is wrapped in
944
- // try/catch — a failed write can never fail the tick. Placement: every run
945
- // that completes the poll (dispatching, NO_DISPATCH, partial-cap) reaches
946
- // this point. Concurrency edge: overlapping ticks could duplicate seq
947
- // (count+append is not atomic) but appends are atomic, so lines never
948
- // interleave — seq is advisory, file order is the proof.
944
+ // try/catch — a failed write can never fail the tick. The script echoes
945
+ // the appended line to stdout (2026-09-17, rooms #12–#14): the fire-and-forget
946
+ // agent() call carries no schema and its return is discarded, but the
947
+ // platform's JSON-candidate scan throws on an EMPTY schema-less result —
948
+ // which is what the writer returned before it echoed. Every other
949
+ // fire-and-forget call in this file returns the CLI's non-empty JSON
950
+ // stdout verbatim, so the writer now speaks the same proven shape.
951
+ // Placement: every run that completes the poll (dispatching, NO_DISPATCH,
952
+ // partial-cap) reaches this point. Concurrency edge: overlapping ticks
953
+ // could duplicate seq (count+append is not atomic) but appends are atomic,
954
+ // so lines never interleave — seq is advisory, file order is the proof.
949
955
  var tickReleaseScript = [
950
956
  "var crewHome=process.argv[1];",
951
957
  "try{",
@@ -954,7 +960,16 @@ var tickReleaseScript = [
954
960
  "var file=path.join(crewHome,\".tick-releases.jsonl\");",
955
961
  "var count=0;",
956
962
  "try{var lines=fs.readFileSync(file,\"utf8\").split(\"\\n\");for(var i=0;i<lines.length;i++){if(lines[i].trim()!==\"\"){count++;}}}catch(e){}",
957
- "fs.appendFileSync(file,JSON.stringify({seq:count+1,release:release})+\"\\n\");",
963
+ "var line=JSON.stringify({seq:count+1,release:release});",
964
+ "fs.appendFileSync(file,line+\"\\n\");",
965
+ // Rooms #12–#14: echo the appended line to stdout. The fire-and-forget
966
+ // agent() call below carries no schema and its return is discarded — but
967
+ // the platform's JSON-candidate scan throws on an EMPTY schema-less
968
+ // result, which is exactly what this script returned before (exit 0, no
969
+ // output). Every other fire-and-forget call in this file returns the
970
+ // CLI's non-empty JSON stdout verbatim, so the writer now speaks the same
971
+ // proven shape: the echoed line is the evidence of the write.
972
+ "process.stdout.write(line+\"\\n\");",
958
973
  "}catch(e){}",
959
974
  "process.exit(0);"
960
975
  ].join("");
@@ -492,6 +492,56 @@ if (dashboardRepoPath) {
492
492
  log("Repo setup: no repo (CLI-only mode) — skipping.");
493
493
  }
494
494
 
495
+ // ── Publish-base advance (2026-09-18) ─────────────────────────────────
496
+ // Room #14: create-project stamps provenance.source_commit at the
497
+ // pre-scaffold repo HEAD, then the scaffold commits .orchestration/ (and the
498
+ // consented .gitignore) on top. The first task's publish diff then carries
499
+ // the whole scaffold (517 insertions in room #14) and blows the 200-line
500
+ // diff-transport budget. The scaffold is crew-owned infrastructure the
501
+ // dashboard artifact never consumes, so the correct publish base is the
502
+ // scaffold commit itself. Advance it deterministically via
503
+ // lib/advance-publish-base.js — only when this init created the project
504
+ // (the stamp is ours, never a real publish's), the scaffold commit's parent
505
+ // is exactly the stamped base (nothing slipped between), and the scaffold
506
+ // touches only crew-owned paths. Best-effort: a failed advance never blocks
507
+ // init — the first task then publishes from the stamped base as before.
508
+ var publishBaseResult = null;
509
+ if (dashboardRepoPath && projectResult && projectResult.action === "created" &&
510
+ setupRepoResult && setupRepoResult.commit && setupRepoResult.commit.committed) {
511
+ try {
512
+ publishBaseResult = await agent(
513
+ "Run the deterministic publish-base advance script. Do not decide anything — just run the command and return its JSON output.\n\n" +
514
+ "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" +
516
+ "The script prints one JSON object: { ok, action, reason?, from?, to? }.\n" +
517
+ "Return that JSON verbatim.",
518
+ {
519
+ key: "advance-publish-base",
520
+ label: "Advance publish base past scaffold commit",
521
+ schema: {
522
+ type: "object",
523
+ properties: {
524
+ ok: { type: "boolean" },
525
+ action: { type: "string" },
526
+ reason: {},
527
+ from: {},
528
+ to: {}
529
+ },
530
+ required: ["ok", "action"]
531
+ }
532
+ }
533
+ );
534
+ log("Publish base: " + publishBaseResult.action +
535
+ (publishBaseResult.reason ? " (" + publishBaseResult.reason + ")" : "") +
536
+ (publishBaseResult.from ? " " + publishBaseResult.from + " -> " + publishBaseResult.to : ""));
537
+ } catch (e) {
538
+ log("Publish base: advance script failed (" + String(e.message || e) + ") — continuing with the stamped base (non-fatal).");
539
+ }
540
+ } else {
541
+ log("Publish base: no advance (project " + (projectResult ? projectResult.action : "unknown") +
542
+ ", scaffold " + (setupRepoResult && setupRepoResult.commit && setupRepoResult.commit.committed ? "committed" : "not committed") + ").");
543
+ }
544
+
495
545
  // ── Scheduler identity (2026-09-16) ──────────────────────────────────
496
546
  // The crew's scheduler identity is dashboard-independent and chosen once.
497
547
  // First init uses the crew-home basename; a re-run keeps the instanceId
@@ -89,9 +89,15 @@ const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
89
89
  const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
90
90
  const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
91
91
  const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
92
- // The five basenames the pin step must materialize — asserted mechanically
92
+ const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
93
+ const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
94
+ // The six basenames the pin step must materialize — asserted mechanically
93
95
  // by workflow code from the verbatim listing, never from agent prose.
94
- const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED].map(function (p) { return p.split("/").pop(); });
96
+ // COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
97
+ // 2026-09-17): the diff is computed by this script, never ferried as an
98
+ // agent JSON string. Pinned like the other publish-critical modules so a
99
+ // mid-run release swap cannot change it under the workflow.
100
+ const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF].map(function (p) { return p.split("/").pop(); });
95
101
 
96
102
  // Project config — passed by dispatcher, falls back to dashboard defaults
97
103
  const projectConfig = inputs.project_config || {};
@@ -267,7 +273,7 @@ function attemptKey(base, reworkCount) {
267
273
  return base + (reworkCount > 0 ? "-r" + reworkCount : "");
268
274
  }
269
275
  // pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
270
- // the verbatim `ls -1` listing so WORKFLOW CODE asserts the five pinned
276
+ // the verbatim `ls -1` listing so WORKFLOW CODE asserts the six pinned
271
277
  // basenames; the agent cannot self-certify. (The pin step was the one place
272
278
  // the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
273
279
  // empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
@@ -275,7 +281,7 @@ function attemptKey(base, reworkCount) {
275
281
  function pinLifecycle(key) {
276
282
  return agent(
277
283
  "Snapshot lifecycle scripts for version pinning.\n" +
278
- "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
284
+ "Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
279
285
  "Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
280
286
  { key: key, label: "Pinning lifecycle scripts",
281
287
  schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
@@ -456,37 +462,6 @@ function hydrateReleaseDecision(rec) {
456
462
  return null;
457
463
  }
458
464
 
459
- // Publish diff transport: parse a unified diff into per-file {path, added,
460
- // removed} line lists. Pure function — no I/O, no clock. Used by Publish to
461
- // verify the artifact builder applied the carried change (canary run 4,
462
- // 2026-09-11: the builder's source tree was stale and disconnected from the
463
- // crew's repo; "rebuild from current source" rebuilt stale code and the
464
- // workflow stamped the new commit hash on it — provenance fiction).
465
- function parseUnifiedDiff(diffText) {
466
- var files = [];
467
- var current = null;
468
- var lines = (diffText || "").split("\n");
469
- for (var i = 0; i < lines.length; i++) {
470
- var line = lines[i];
471
- var m = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
472
- if (m) {
473
- current = { path: m[2], added: [], removed: [] };
474
- files.push(current);
475
- continue;
476
- }
477
- if (!current) continue;
478
- if (/^--- /.test(line) || /^\+\+\+ /.test(line)) continue;
479
- if (/^@@ /.test(line)) continue;
480
- if (line.charAt(0) === "+") {
481
- current.added.push(line.slice(1));
482
- } else if (line.charAt(0) === "-") {
483
- current.removed.push(line.slice(1));
484
- }
485
- }
486
- return files;
487
- }
488
-
489
-
490
465
  // Publish read-back request (agent path currently unavailable): the
491
466
  // verbatim_request the parent protocol (docs/publish-verification.md) would
492
467
  // hand to an independent read-back tool after the artifact build lands.
@@ -1467,59 +1442,77 @@ while (i < STEPS.length) {
1467
1442
  } else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
1468
1443
  return await parkTask("Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
1469
1444
  }
1470
- // The empty tree is not a commit: git merge-base --is-ancestor fails on it.
1471
- // The workflow knows publishBase == EMPTY_TREE_SHA (set above), so it
1472
- // hardcodes ANCESTOR=yes for a first publish instead of asking the agent
1473
- // to execute the conditional (clean-room 2026-09-16: the agent ran
1474
- // merge-base on the empty tree directly and parked).
1475
- var ancestorShell = (publishBase === EMPTY_TREE_SHA)
1476
- ? "ANCESTOR=yes && "
1477
- : "git merge-base --is-ancestor \"$BASE\" \"$HEAD\" && ANCESTOR=yes || ANCESTOR=no && ";
1445
+ // Publish diff transport (room #14, 2026-09-17): the diff used to be
1446
+ // ferried as a JSON string field in the agent's response — the agent
1447
+ // produced a valid 700-line diff on disk but the JSON ferry dropped
1448
+ // it, and the parse saw zero files ("Publish diff parsed to zero
1449
+ // files"). The diff now travels git -> file -> deterministic script
1450
+ // summary; the LLM never carries diff bytes. The agent is pure hands:
1451
+ // it runs exactly one command (the PINNED compute-publish-diff.js —
1452
+ // a mid-run release swap cannot change it under the workflow) and
1453
+ // returns the small JSON summary verbatim. All fail-closed parks
1454
+ // below are unchanged in meaning.
1455
+ var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + ".diff";
1478
1456
  var diffResult = await agent(
1479
- "Run: cd " + REPO_PATH + " && BASE='" + publishBase + "' && HEAD=$(git rev-parse HEAD) && " +
1480
- ancestorShell +
1481
- "echo '---COMMIT---' && echo \"$HEAD\" && echo '---BASE---' && echo \"$BASE\" && echo '---ANCESTOR---' && echo \"$ANCESTOR\" && " +
1482
- "if [ \"$ANCESTOR\" = yes ]; then echo '---DIFF---' && git diff \"$BASE\" \"$HEAD\" && echo '---NAMES---' && git diff-tree --no-commit-id --name-only -r \"$BASE\" \"$HEAD\"; fi\n" +
1483
- "Return JSON { \"commit\": \"<HEAD trimmed>\", \"base\": \"<BASE trimmed>\", \"ancestor\": \"<yes|no>\", \"diff\": \"<raw unified diff, may be multi-line>\", \"files\": \"<newline-separated paths>\" } and nothing else.",
1457
+ "Run exactly one command and nothing else:\n" +
1458
+ "node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
1459
+ "The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
1484
1460
  { key: attemptKey("publish-artifact-diff-" + taskId, totalReworkCount), label: "Computing publish diff from stamped base",
1485
- schema: { type: "object", properties: { commit: { type: "string" }, base: { type: "string" }, ancestor: { type: "string" }, diff: { type: "string" }, files: { type: "string" } }, required: ["commit", "base", "ancestor", "diff"] } }
1461
+ schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
1486
1462
  );
1487
- if ((diffResult.ancestor || "").trim() !== "yes") {
1488
- return await parkTask("Publish base " + publishBase.slice(0, 12) + " is not an ancestor of HEAD " + (diffResult.commit || "").trim().slice(0, 12) + " — the stamped provenance does not lead to the integrated commit. Human attention needed.");
1463
+ var diffSummary = null;
1464
+ try {
1465
+ diffSummary = JSON.parse((diffResult.output || "").trim());
1466
+ } catch (e) {
1467
+ return await parkTask("Publish diff computation returned unparseable output — cannot verify application. Human attention needed.");
1468
+ }
1469
+ if (!diffSummary || diffSummary.ok !== true) {
1470
+ if (diffSummary && diffSummary.error === "not-ancestor") {
1471
+ return await parkTask("Publish base " + publishBase.slice(0, 12) + " is not an ancestor of HEAD " + String(diffSummary.commit || "").slice(0, 12) + " — the stamped provenance does not lead to the integrated commit. Human attention needed.");
1472
+ }
1473
+ return await parkTask("Publish diff computation failed (" + ((diffSummary && diffSummary.error) || "unknown error") + ") — cannot verify application. Human attention needed.");
1489
1474
  }
1490
- if ((diffResult.base || "").trim() !== publishBase) {
1491
- return await parkTask("Publish diff base mismatch: agent reported '" + (diffResult.base || "").trim().slice(0, 12) + "' but the stamped base is '" + publishBase.slice(0, 12) + "'. Human attention needed.");
1475
+ if (diffSummary.base !== publishBase) {
1476
+ return await parkTask("Publish diff base mismatch: script reported '" + String(diffSummary.base || "").slice(0, 12) + "' but the stamped base is '" + publishBase.slice(0, 12) + "'. Human attention needed.");
1492
1477
  }
1493
- var mergeCommitForPublish = (diffResult.commit || "").trim();
1494
- var mergeDiff = diffResult.diff || "";
1495
- if (!mergeDiff.trim()) {
1478
+ var mergeCommitForPublish = String(diffSummary.commit || "").trim();
1479
+ var publishDiffSha256 = String(diffSummary.sha256 || "");
1480
+ if (!diffSummary.bytes) {
1496
1481
  return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
1497
1482
  }
1498
- if (/^Binary files /m.test(mergeDiff)) {
1499
- return await parkTask("Publish diff contains binary files — the text diff transport cannot carry them. Human attention needed.");
1483
+ if (diffSummary.has_binary) {
1484
+ return await parkTask("Publish diff contains binary files — the diff transport cannot carry them. Human attention needed.");
1500
1485
  }
1501
- if (/^rename from /m.test(mergeDiff)) {
1486
+ if (diffSummary.has_rename) {
1502
1487
  return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
1503
1488
  }
1504
- var expectedChanges = parseUnifiedDiff(mergeDiff);
1505
1489
  // Budget counts CHANGED lines (added + removed), not raw unified-diff
1506
1490
  // output lines: context lines and file headers inflated the old
1507
1491
  // split("\n").length count ~2x, parking a 95-line change against a
1508
- // 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13).
1509
- var mergeDiffChangedLines = expectedChanges.reduce(function (n, f) { return n + f.added.length + f.removed.length; }, 0);
1492
+ // 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13). The count
1493
+ // now comes from the deterministic script summary, never from the LLM.
1494
+ var mergeDiffChangedLines = diffSummary.changed_lines || 0;
1510
1495
  if (mergeDiffChangedLines > 200) {
1511
1496
  return await parkTask("Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
1512
1497
  }
1513
- if (expectedChanges.length === 0) {
1498
+ if (!diffSummary.file_count) {
1514
1499
  return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
1515
1500
  }
1501
+ // The trigger agent reads the diff from the checksummed file — the
1502
+ // workflow never holds diff bytes (room #14, 2026-09-17). The file
1503
+ // is small by construction: the 200-line budget above gates the
1504
+ // trigger. The sha256 check is the agent's only verification step;
1505
+ // a mismatch stops the trigger before any artifact_edit call.
1516
1506
  var rebuildPrompt =
1517
1507
  ARTIFACT_LOAD_PREAMBLE +
1518
- "Call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
1508
+ "The change to apply is the unified diff in the file \"" + publishDiffFile + "\" (sha256 " + publishDiffSha256 + ").\n" +
1509
+ "1. Verify the file: run sha256sum on it. If the printed hash is not exactly " + publishDiffSha256 + ", STOP and end your turn — do not call artifact_edit.\n" +
1510
+ "2. Read the file's full content.\n" +
1511
+ "3. Call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
1519
1512
  "'Apply the following change to your source tree, then rebuild and deploy.\n" +
1520
1513
  "\n" +
1521
1514
  "UNIFIED DIFF (relative to your source tree):\n" +
1522
- "```diff\n" + mergeDiff + "\n```\n" +
1515
+ "```diff\n<the full content of the verified file, pasted verbatim>\n```\n" +
1523
1516
  "\n" +
1524
1517
  "Rules:\n" +
1525
1518
  "- For each file in the diff, apply its hunks to the same path in your source tree (use git apply or equivalent).\n" +