muse-crew 0.14.4 → 0.14.5

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.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // check-intent-freshness.js — deterministic manifest-freshness check for a
3
+ // re-claimed one-party publish intent (2026-09-20, critic-0145 F-O3/F-R2).
4
+ //
5
+ // A re-claiming tick must decide: did the dead tick issue the edit (record
6
+ // "recovered") or not (issue it now)? The decision is mechanical, never
7
+ // prose: read the on-disk artifact manifest at the same path the verifier
8
+ // uses (<spaces-root>/<slug>/.space-build/manifest.json — see
9
+ // lib/verify-publish.js) and compare its content_sha256 / built_at against
10
+ // the intent entry's pre-issuance baseline (manifest_before).
11
+ //
12
+ // The manifest ADVANCED — a new build identity, so the dead tick issued —
13
+ // when either:
14
+ // current.content_sha256 !== baseline.content_sha256 (a new content hash), OR
15
+ // Date.parse(current.built_at) > Date.parse(baseline.built_at) (a later build).
16
+ // Otherwise it still shows the pre-edit state and the tick issues fresh.
17
+ //
18
+ // Stdout contract: exactly one JSON object, always small:
19
+ // { ok:true, advanced:<bool>, baseline:{built_at, content_sha256}, current:{built_at, content_sha256} }
20
+ // { ok:false, error:"no-baseline"|"unreadable-manifest"|"unparseable-manifest" }
21
+ //
22
+ // Exit codes: 0 ok (JSON on stdout) · 2 usage · 1 IO failure (nothing on
23
+ // stdout — a caller must never treat stdout as a result). Errors go to
24
+ // stderr as JSON.
25
+ //
26
+ // A non-ok result is never a verdict — the caller fails closed: record
27
+ // nothing and let the claim expire into the reclaim path (the scan's F-R3
28
+ // terminal park covers the null-baseline reclaim; an unreadable manifest
29
+ // leaves the task parked for the next tick). Reclaim decisions never rest
30
+ // on prose.
31
+
32
+ import { readFileSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { join } from "node:path";
35
+
36
+ // --help: before required-arg parsing (shebang⇔CLI contract).
37
+ if (process.argv.slice(2).includes("--help")) {
38
+ console.log(
39
+ "usage: node check-intent-freshness.js --slug <slug> --manifest-before '<json>' [--spaces-root <path>]\n" +
40
+ "compares the on-disk artifact manifest's content_sha256/built_at against the pre-issuance baseline; prints {ok, advanced} JSON on stdout"
41
+ );
42
+ process.exit(0);
43
+ }
44
+
45
+ function arg(name, required = true) {
46
+ const i = process.argv.indexOf(name);
47
+ if (i < 0 || i + 1 >= process.argv.length) {
48
+ if (!required) return null;
49
+ fail("usage", `${name} is required.`, 2);
50
+ }
51
+ return process.argv[i + 1];
52
+ }
53
+ function fail(code, message, exitCode) {
54
+ process.stderr.write(JSON.stringify({ ok: false, error: code, message }) + "\n");
55
+ process.exit(exitCode);
56
+ }
57
+
58
+ const slug = arg("--slug");
59
+ const baselineRaw = arg("--manifest-before");
60
+ const spacesRoot = arg("--spaces-root", false) || join(homedir(), "workspace", "ts-spaces");
61
+
62
+ if (!slug || /[/\\]/.test(slug)) fail("usage", "--slug must be a non-empty slug (no path separators).", 2);
63
+
64
+ let baseline;
65
+ try {
66
+ baseline = JSON.parse(baselineRaw);
67
+ } catch {
68
+ fail("usage", "--manifest-before must be JSON.", 2);
69
+ }
70
+ if (!baseline || typeof baseline !== "object") fail("no-baseline", "--manifest-before is not an object.", 1);
71
+ const baselineSha = typeof baseline.content_sha256 === "string" ? baseline.content_sha256 : null;
72
+ const baselineBuiltAt = typeof baseline.built_at === "string" ? baseline.built_at : null;
73
+ if (!baselineSha && !baselineBuiltAt) {
74
+ fail("no-baseline", "the baseline carries neither content_sha256 nor built_at — freshness is unprovable.", 1);
75
+ }
76
+
77
+ let currentManifest;
78
+ try {
79
+ currentManifest = JSON.parse(readFileSync(join(spacesRoot, slug, ".space-build", "manifest.json"), "utf8"));
80
+ } catch (e) {
81
+ const code = e && e.code === "ENOENT" ? "unreadable-manifest" : "unreadable-manifest";
82
+ fail(code, `cannot read the on-disk manifest: ${(e && e.message) || e}`.slice(0, 300), 1);
83
+ }
84
+ if (!currentManifest || typeof currentManifest !== "object") {
85
+ fail("unparseable-manifest", "the on-disk manifest is not a JSON object.", 1);
86
+ }
87
+ const currentSha = typeof currentManifest.content_sha256 === "string" ? currentManifest.content_sha256 : null;
88
+ const currentBuiltAt = typeof currentManifest.built_at === "string" ? currentManifest.built_at : null;
89
+
90
+ const hashAdvanced = !!(baselineSha && currentSha && currentSha !== baselineSha);
91
+ const currentBuiltAtMs = currentBuiltAt ? Date.parse(currentBuiltAt) : NaN;
92
+ const baselineBuiltAtMs = baselineBuiltAt ? Date.parse(baselineBuiltAt) : NaN;
93
+ const timeAdvanced = !!(currentBuiltAtMs > 0 && baselineBuiltAtMs > 0 && currentBuiltAtMs > baselineBuiltAtMs);
94
+ const advanced = hashAdvanced || timeAdvanced;
95
+
96
+ process.stdout.write(JSON.stringify({
97
+ ok: true,
98
+ advanced,
99
+ baseline: { built_at: baselineBuiltAt, content_sha256: baselineSha },
100
+ current: { built_at: currentBuiltAt, content_sha256: currentSha },
101
+ }) + "\n");
@@ -14,7 +14,7 @@
14
14
  // small JSON summary verbatim. The diff bytes live in a file; the LLM
15
15
  // never carries them:
16
16
  //
17
- // node compute-publish-diff.js --repo-path <path> --base <sha> --out <file>
17
+ // node compute-publish-diff.js --repo-path <path> --base <sha> --out <file> [--commit <sha>]
18
18
  //
19
19
  // --base is the previously-stamped provenance source_commit (or the
20
20
  // empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
@@ -22,6 +22,14 @@
22
22
  // script echoes the base it used so the workflow can guard against the
23
23
  // agent running the wrong command (fail-closed, as before).
24
24
  //
25
+ // --commit is the diff's head end (2026-09-20, critic-0145 F-O2): the
26
+ // script used to ignore it and always diff base..HEAD. Optional, defaults
27
+ // to HEAD; the workflow's first-prepare call passes nothing (HEAD is the
28
+ // merge commit it just created), while the tick's byte-identical
29
+ // regeneration passes the intent's commit explicitly. The ancestor check,
30
+ // the diff range, and the reported `commit` all target --commit, never an
31
+ // implicit HEAD.
32
+ //
25
33
  // Stdout contract: exactly one JSON object, always small (no diff text):
26
34
  // { ok:true, commit, base, ancestor:"yes", diff_file, sha256, bytes,
27
35
  // file_count, changed_lines, files:[<paths>], has_binary, has_rename }
@@ -43,8 +51,8 @@ import { dirname, resolve } from "node:path";
43
51
  // --help: before required-arg parsing (shebang⇔CLI contract).
44
52
  if (process.argv.slice(2).includes("--help")) {
45
53
  console.log(
46
- "usage: node compute-publish-diff.js --repo-path <path> --base <sha> --out <file>\n" +
47
- "runs git diff <base> <HEAD>, writes the raw diff bytes to --out, prints the small JSON summary on stdout"
54
+ "usage: node compute-publish-diff.js --repo-path <path> --base <sha> --out <file> [--commit <sha>]\n" +
55
+ "runs git diff <base> <commit> (default <commit> = HEAD), writes the raw diff bytes to --out, prints the small JSON summary on stdout"
48
56
  );
49
57
  process.exit(0);
50
58
  }
@@ -74,36 +82,49 @@ function git(repoPath, args) {
74
82
  const repoPath = arg("--repo-path");
75
83
  const base = arg("--base");
76
84
  const outFile = arg("--out");
85
+ const commitArg = arg("--commit", false);
77
86
 
78
87
  if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "--base must be a 40-char hex commit SHA (or the empty-tree SHA).", 2);
79
88
  let st = null;
80
89
  try { st = statSync(repoPath); } catch { /* falls through */ }
81
90
  if (!st || !st.isDirectory()) fail("usage", "--repo-path must be an existing directory.", 2);
82
91
 
83
- const head = git(repoPath, ["rev-parse", "HEAD"]).trim();
84
- if (!/^[0-9a-f]{40}$/.test(head)) fail("git-failed", "rev-parse HEAD did not return a commit SHA.", 1);
92
+ // The diff's head end: explicit --commit, or HEAD when omitted (the
93
+ // workflow's first-prepare call passes nothing — HEAD is the merge commit
94
+ // it just created). Validated as a 40-hex commit object in this repo;
95
+ // everything below (ancestor check, diff range, reported commit) targets
96
+ // it, never an implicit HEAD (2026-09-20, critic-0145 F-O2).
97
+ let commit;
98
+ if (commitArg === null) {
99
+ commit = git(repoPath, ["rev-parse", "HEAD"]).trim();
100
+ } else {
101
+ if (!/^[0-9a-f]{40}$/.test(commitArg)) fail("usage", "--commit must be a 40-char hex commit SHA.", 2);
102
+ commit = git(repoPath, ["rev-parse", "--verify", commitArg + "^{commit}"]).trim();
103
+ }
104
+ if (!/^[0-9a-f]{40}$/.test(commit)) fail("git-failed", "could not resolve the diff target to a commit SHA.", 1);
85
105
 
86
- // Ancestor check — deterministic, in code. The empty tree is not a commit:
87
- // git merge-base --is-ancestor fails on it, so a first publish hardcodes
88
- // ANCESTOR=yes here instead of executing the conditional (clean-room
89
- // 2026-09-16: an agent ran merge-base on the empty tree directly and parked).
106
+ // Ancestor check — deterministic, in code, against the diff target. The
107
+ // empty tree is not a commit: git merge-base --is-ancestor fails on it, so
108
+ // a first publish hardcodes ANCESTOR=yes here instead of executing the
109
+ // conditional (clean-room 2026-09-16: an agent ran merge-base on the empty
110
+ // tree directly and parked).
90
111
  let ancestor = "yes";
91
112
  if (base !== EMPTY_TREE) {
92
113
  try {
93
- execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, head], { stdio: "ignore" });
114
+ execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, commit], { stdio: "ignore" });
94
115
  ancestor = "yes";
95
116
  } catch {
96
117
  ancestor = "no";
97
118
  }
98
119
  }
99
120
  if (ancestor !== "yes") {
100
- process.stdout.write(JSON.stringify({ ok: false, error: "not-ancestor", commit: head, base }) + "\n");
121
+ process.stdout.write(JSON.stringify({ ok: false, error: "not-ancestor", commit, base }) + "\n");
101
122
  process.exit(0);
102
123
  }
103
124
 
104
125
  const outPath = resolve(outFile);
105
126
  mkdirSync(dirname(outPath), { recursive: true });
106
- const diffText = git(repoPath, ["diff", base, head]);
127
+ const diffText = git(repoPath, ["diff", base, commit]);
107
128
  writeFileSync(outPath, diffText, "utf8");
108
129
  const bytes = readFileSync(outPath);
109
130
  const sha256 = createHash("sha256").update(bytes).digest("hex");
@@ -140,7 +161,7 @@ for (const line of diffText.split("\n")) {
140
161
 
141
162
  process.stdout.write(JSON.stringify({
142
163
  ok: true,
143
- commit: head,
164
+ commit,
144
165
  base,
145
166
  ancestor: "yes",
146
167
  diff_file: outPath,