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.
- package/AGENTS.md +1 -1
- package/docs/decisions/publish-path.md +53 -0
- package/docs/guide.md +2 -2
- package/docs/publish-unknown-recovery.md +10 -3
- package/docs/publish-verification.md +117 -61
- package/docs/release-integrity.md +5 -3
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +2 -1
- package/lib/check-intent-freshness.js +101 -0
- package/lib/compute-publish-diff.js +34 -13
- package/lib/crew-api.js +437 -18
- package/lib/publish-note-vocabulary.js +20 -0
- package/lib/retry-publish.js +45 -33
- package/lib/verify-publish.js +11 -9
- package/package.json +1 -1
- package/seed/cron-body-template.md +50 -16
- package/workflows/bugfix.js +96 -627
- package/workflows/chore.js +96 -627
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/standard.js +104 -636
|
@@ -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
|
|
47
|
-
"runs git diff <base> <HEAD
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
87
|
-
// git merge-base --is-ancestor fails on it, so
|
|
88
|
-
// ANCESTOR=yes here instead of executing the
|
|
89
|
-
// 2026-09-16: an agent ran merge-base on the empty
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
164
|
+
commit,
|
|
144
165
|
base,
|
|
145
166
|
ancestor: "yes",
|
|
146
167
|
diff_file: outPath,
|