muse-crew 0.14.4 → 0.14.6
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/API.md +61 -7
- package/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +244 -1
- package/docs/guide.md +42 -6
- package/docs/publish-unknown-recovery.md +28 -113
- package/docs/publish-verification.md +148 -445
- package/docs/release-integrity.md +5 -3
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +4 -8
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +113 -15
- package/lib/crew-api.js +855 -725
- package/lib/publish-note-vocabulary.js +137 -24
- package/lib/schema.sql +19 -0
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-ack-scan.md +44 -0
- package/seed/cron-body-template.md +65 -65
- package/seed/crons.json +12 -0
- package/workflows/bugfix.js +146 -740
- package/workflows/chore.js +146 -740
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/standard.js +146 -763
- package/lib/build-readback-request.js +0 -140
- package/lib/classify-publish-absence.js +0 -462
- package/lib/publish-content.js +0 -154
- package/lib/readback-disk.js +0 -195
- package/lib/retry-publish.js +0 -405
- package/lib/verify-publish.js +0 -414
package/lib/publish-content.js
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// publish-content.js — shared deterministic content-judgment primitives.
|
|
2
|
-
//
|
|
3
|
-
// Parent publish verification (lib/verify-publish.js) and unknown-recovery
|
|
4
|
-
// classification (lib/classify-publish-absence.js) both judge the same
|
|
5
|
-
// question — "does the on-disk source contain the publish commit's diff?" —
|
|
6
|
-
// against the same diff parser, findings parser, and collision-exemption
|
|
7
|
-
// rules. Two copies of the collision logic already diverged once (2026-09-15,
|
|
8
|
-
// task 00bca4b8); this module is the single source of truth.
|
|
9
|
-
//
|
|
10
|
-
// ESM (imported by verify-publish.js and classify-publish-absence.js).
|
|
11
|
-
// Pure and deterministic: no wall-clock reads, no randomness, no I/O except
|
|
12
|
-
// the git subprocesses in makeOldCounter (read-only).
|
|
13
|
-
|
|
14
|
-
import { execFileSync } from "node:child_process";
|
|
15
|
-
|
|
16
|
-
export const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
17
|
-
|
|
18
|
-
// Parse `git diff base commit` into per-file added/removed lines.
|
|
19
|
-
// Byte-identical split to build-readback-request.js and the old
|
|
20
|
-
// verify-publish.js inline parser (the verifier looks raw lines up
|
|
21
|
-
// verbatim, so all consumers must agree on the split).
|
|
22
|
-
export function parseDiff(diffText) {
|
|
23
|
-
const expected = new Map(); // path -> { added: [], removed: [], isBinary: bool }
|
|
24
|
-
let curFile = null;
|
|
25
|
-
for (const line of diffText.split("\n")) {
|
|
26
|
-
if (line.startsWith("diff --git")) {
|
|
27
|
-
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
28
|
-
curFile = m ? m[2] : "unknown";
|
|
29
|
-
expected.set(curFile, { added: [], removed: [], isBinary: false });
|
|
30
|
-
} else if (curFile && line.startsWith("Binary files ")) {
|
|
31
|
-
expected.get(curFile).isBinary = true;
|
|
32
|
-
} else if (curFile && line.startsWith("+") && !line.startsWith("+++")) {
|
|
33
|
-
expected.get(curFile).added.push(line.slice(1));
|
|
34
|
-
} else if (curFile && line.startsWith("-") && !line.startsWith("---")) {
|
|
35
|
-
expected.get(curFile).removed.push(line.slice(1));
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return expected;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Parse the machine-readable findings block:
|
|
42
|
-
// FILE: <path>
|
|
43
|
-
// ADDED: <line> :: PRESENT|ABSENT
|
|
44
|
-
// REMOVED: <line> :: PRESENT|ABSENT
|
|
45
|
-
// END_FILE
|
|
46
|
-
export function parseFindings(text) {
|
|
47
|
-
const findings = new Map(); // path -> { added: Map(line->verdict), removed: Map(line->verdict) }
|
|
48
|
-
let cur = null;
|
|
49
|
-
let malformed = null;
|
|
50
|
-
for (const rawLine of text.split("\n")) {
|
|
51
|
-
const line = rawLine.trimEnd();
|
|
52
|
-
if (line.startsWith("FILE: ")) {
|
|
53
|
-
cur = { added: new Map(), removed: new Map() };
|
|
54
|
-
findings.set(line.slice(6).trim(), cur);
|
|
55
|
-
} else if (line === "END_FILE") {
|
|
56
|
-
cur = null;
|
|
57
|
-
} else if (cur && (line.startsWith("ADDED: ") || line.startsWith("REMOVED: "))) {
|
|
58
|
-
const kind = line.startsWith("ADDED: ") ? "added" : "removed";
|
|
59
|
-
const rest = line.slice(kind === "added" ? 7 : 9);
|
|
60
|
-
const sep = rest.lastIndexOf(" :: ");
|
|
61
|
-
if (sep < 0) { malformed = `malformed finding line: ${line.slice(0, 80)}`; break; }
|
|
62
|
-
const content = rest.slice(0, sep);
|
|
63
|
-
const verdict = rest.slice(sep + 4).trim();
|
|
64
|
-
if (verdict !== "PRESENT" && verdict !== "ABSENT") {
|
|
65
|
-
malformed = `bad verdict: ${verdict}`;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
cur[kind].set(content, verdict);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return { findings, malformed };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Whole-line occurrence counts of a file's content at <base>.
|
|
75
|
-
// [] when the base is the empty tree or the file did not exist there.
|
|
76
|
-
function oldTreeLines(repoPath, base, path) {
|
|
77
|
-
if (base === EMPTY_TREE) return [];
|
|
78
|
-
let text;
|
|
79
|
-
try {
|
|
80
|
-
text = execFileSync("git", ["-C", repoPath, "show", `${base}:${path}`], {
|
|
81
|
-
encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
|
|
82
|
-
});
|
|
83
|
-
} catch {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
if (text === "") return [];
|
|
87
|
-
const lines = text.split("\n");
|
|
88
|
-
if (lines[lines.length - 1] === "") lines.pop(); // drop the trailing-newline artifact
|
|
89
|
-
return lines;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Returns (path, line) => occurrences of line in path's old tree.
|
|
93
|
-
// Cached per path. Read-only git subprocesses.
|
|
94
|
-
export function makeOldCounter(repoPath, base) {
|
|
95
|
-
const cache = new Map(); // path -> Map(line -> count)
|
|
96
|
-
return (path, line) => {
|
|
97
|
-
let counts = cache.get(path);
|
|
98
|
-
if (counts === undefined) {
|
|
99
|
-
counts = new Map();
|
|
100
|
-
for (const l of oldTreeLines(repoPath, base, path)) {
|
|
101
|
-
counts.set(l, (counts.get(l) || 0) + 1);
|
|
102
|
-
}
|
|
103
|
-
cache.set(path, counts);
|
|
104
|
-
}
|
|
105
|
-
return counts.get(line) || 0;
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Split each file's diff lines into discriminating vs non-discriminating.
|
|
110
|
-
//
|
|
111
|
-
// A line has zero discriminating power when its presence/absence cannot
|
|
112
|
-
// tell "hunk landed" from "hunk dropped":
|
|
113
|
-
// - added line already in the old tree (colliding pre-existing line):
|
|
114
|
-
// PRESENT whether or not the hunk landed.
|
|
115
|
-
// - removed line occurring in the old tree strictly more times than the
|
|
116
|
-
// diff removes it: PRESENT whether or not the removal happened.
|
|
117
|
-
//
|
|
118
|
-
// Returns Map(path -> {
|
|
119
|
-
// addedDisc: [...], removedDisc: [...], // discriminating lines
|
|
120
|
-
// unjudgeable: null | "binary-or-mode-or-rename" | "fully-colliding-added"
|
|
121
|
-
// }).
|
|
122
|
-
// unjudgeable marks files the line-based judge cannot certify at all:
|
|
123
|
-
// a diff with no content lines (binary, mode-only, rename), or added
|
|
124
|
-
// lines that ALL collide (their PRESENT findings prove nothing).
|
|
125
|
-
export function discriminatingLines(diffMap, oldCount) {
|
|
126
|
-
const out = new Map();
|
|
127
|
-
for (const [path, exp] of diffMap) {
|
|
128
|
-
const entry = { addedDisc: [], removedDisc: [], unjudgeable: null };
|
|
129
|
-
if (exp.added.length === 0 && exp.removed.length === 0) {
|
|
130
|
-
entry.unjudgeable = "binary-or-mode-or-rename";
|
|
131
|
-
out.set(path, entry);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
for (const line of exp.added) {
|
|
135
|
-
if (oldCount(path, line) === 0) entry.addedDisc.push(line);
|
|
136
|
-
}
|
|
137
|
-
// The diff may remove the same line more than once; the old tree must
|
|
138
|
-
// account for every removal before a line counts as colliding.
|
|
139
|
-
const removedBudget = new Map();
|
|
140
|
-
for (const line of exp.removed) {
|
|
141
|
-
removedBudget.set(line, (removedBudget.get(line) || 0) + 1);
|
|
142
|
-
}
|
|
143
|
-
for (const line of exp.removed) {
|
|
144
|
-
if (oldCount(path, line) <= removedBudget.get(line)) {
|
|
145
|
-
entry.removedDisc.push(line);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (exp.added.length > 0 && entry.addedDisc.length === 0) {
|
|
149
|
-
entry.unjudgeable = "fully-colliding-added";
|
|
150
|
-
}
|
|
151
|
-
out.set(path, entry);
|
|
152
|
-
}
|
|
153
|
-
return out;
|
|
154
|
-
}
|
package/lib/readback-disk.js
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// readback-disk.js — deterministic disk read-back sensor for parent publish
|
|
3
|
-
// verification (docs/publish-verification.md).
|
|
4
|
-
//
|
|
5
|
-
// The read-back gap (2026-09-14): the platform removed `artifact_inspect`
|
|
6
|
-
// and no agent-callable content read-back existed, so every artifact publish
|
|
7
|
-
// parked at `publish: verification-requested` permanently. The platform's
|
|
8
|
-
// artifact edits land in its on-disk working copy of the artifact source
|
|
9
|
-
// (~/workspace/ts-spaces/<slug>/ — verified empirically 2026-09-15: added
|
|
10
|
-
// lines present, removed lines absent across real platform commits), so a
|
|
11
|
-
// deterministic local sensor can replace the removed inspection tool: no
|
|
12
|
-
// LLM, no async handoff, no prose to parse.
|
|
13
|
-
//
|
|
14
|
-
// This script is the SENSOR; lib/verify-publish.js stays the judge. It
|
|
15
|
-
// emits the exact machine-readable findings block the verifier already
|
|
16
|
-
// parses:
|
|
17
|
-
//
|
|
18
|
-
// FILE: <path>
|
|
19
|
-
// ADDED: <exact added line from the diff> :: PRESENT|ABSENT
|
|
20
|
-
// REMOVED: <exact removed line from the diff> :: PRESENT|ABSENT
|
|
21
|
-
// END_FILE
|
|
22
|
-
//
|
|
23
|
-
// one ADDED line per (+) diff line and one REMOVED line per (-) diff line,
|
|
24
|
-
// in diff order. The verdict is PRESENT iff that exact line occurs in the
|
|
25
|
-
// file's CURRENT on-disk source (whole-line match), ABSENT otherwise.
|
|
26
|
-
//
|
|
27
|
-
// Authority boundary: the sensor reads the platform's working copy of the
|
|
28
|
-
// artifact source — the tree the hosted artifact is built/served from. If
|
|
29
|
-
// the working copy is stale relative to a just-applied edit, the findings
|
|
30
|
-
// honestly report ABSENT and the verifier fails CLOSED (parked, never a
|
|
31
|
-
// false stamp). Staleness can only park, never certify.
|
|
32
|
-
//
|
|
33
|
-
// Usage:
|
|
34
|
-
// node readback-disk.js --repo-path <path> --commit <sha> --base <sha>
|
|
35
|
-
// --slug <artifact-slug> --task-id <uuid> [--spaces-root <dir>]
|
|
36
|
-
//
|
|
37
|
-
// --base is the previously-stamped provenance source_commit (or the
|
|
38
|
-
// empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
|
|
39
|
-
// publish) — the SAME base the verifier uses. --spaces-root defaults to
|
|
40
|
-
// ~/workspace/ts-spaces.
|
|
41
|
-
//
|
|
42
|
-
// Exit codes: 0 ok (findings block on stdout) · 2 usage/validation ·
|
|
43
|
-
// 1 git/diff/read failure. On non-zero exit NOTHING is printed to stdout,
|
|
44
|
-
// so a caller must never save stdout as a findings file.
|
|
45
|
-
//
|
|
46
|
-
// Security: the slug is charset-validated and the resolved space directory
|
|
47
|
-
// must stay under the spaces root (realpath containment); every file path
|
|
48
|
-
// from the diff is likewise contained before reading. A path that escapes
|
|
49
|
-
// is a validation failure (exit 2), never a read.
|
|
50
|
-
|
|
51
|
-
import { execFileSync } from "node:child_process";
|
|
52
|
-
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
53
|
-
import { homedir } from "node:os";
|
|
54
|
-
import { join, resolve } from "node:path";
|
|
55
|
-
|
|
56
|
-
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
57
|
-
if (process.argv.slice(2).includes("--help")) {
|
|
58
|
-
console.log(
|
|
59
|
-
"usage: node readback-disk.js --repo-path <path> --commit <sha> --base <sha> --task-id <uuid> --slug <artifact-slug> [--spaces-root <dir>]\n" +
|
|
60
|
-
"the deterministic disk read-back SENSOR: emits the machine-readable findings block (FILE:/ADDED:/REMOVED:/END_FILE)"
|
|
61
|
-
);
|
|
62
|
-
process.exit(0);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
66
|
-
|
|
67
|
-
function arg(name) {
|
|
68
|
-
const i = process.argv.indexOf(name);
|
|
69
|
-
if (i < 0 || i + 1 >= process.argv.length) {
|
|
70
|
-
fail("usage", `${name} is required.`, 2);
|
|
71
|
-
}
|
|
72
|
-
return process.argv[i + 1];
|
|
73
|
-
}
|
|
74
|
-
function fail(code, message, exitCode) {
|
|
75
|
-
// stderr only — stdout must stay empty on failure.
|
|
76
|
-
process.stderr.write(JSON.stringify({ ok: false, error: code, message }) + "\n");
|
|
77
|
-
process.exit(exitCode);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const repoPath = arg("--repo-path");
|
|
81
|
-
const commit = arg("--commit");
|
|
82
|
-
const base = arg("--base");
|
|
83
|
-
const slug = arg("--slug");
|
|
84
|
-
const taskId = arg("--task-id");
|
|
85
|
-
const spacesRoot = process.argv.includes("--spaces-root")
|
|
86
|
-
? arg("--spaces-root")
|
|
87
|
-
: join(homedir(), "workspace", "ts-spaces");
|
|
88
|
-
|
|
89
|
-
if (!/^[0-9a-f]{40}$/.test(commit)) fail("usage", "commit must be a 40-char hex sha.", 2);
|
|
90
|
-
if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "base must be a 40-char hex sha.", 2);
|
|
91
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(slug)) {
|
|
92
|
-
fail("usage", `slug ${JSON.stringify(slug)} is not a safe artifact slug.`, 2);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Resolve the space directory with realpath containment: the slug must not
|
|
96
|
-
// escape the spaces root (no "..", no symlinks pointing out).
|
|
97
|
-
let spaceDir;
|
|
98
|
-
try {
|
|
99
|
-
const rootReal = realpathSync(spacesRoot);
|
|
100
|
-
const candReal = realpathSync(join(spacesRoot, slug));
|
|
101
|
-
if (candReal !== rootReal && !candReal.startsWith(rootReal + "/")) {
|
|
102
|
-
fail("usage", `slug ${JSON.stringify(slug)} resolves outside the spaces root.`, 2);
|
|
103
|
-
}
|
|
104
|
-
spaceDir = candReal;
|
|
105
|
-
} catch (e) {
|
|
106
|
-
fail("usage", `cannot resolve space dir for slug ${JSON.stringify(slug)}: ${e.message}`, 2);
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
if (!statSync(spaceDir).isDirectory()) fail("usage", `space dir ${spaceDir} is not a directory.`, 2);
|
|
110
|
-
} catch (e) {
|
|
111
|
-
fail("usage", `space dir ${spaceDir} unreadable: ${e.message}`, 2);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// The publish delta is base..commit — the same diff the verifier checks the
|
|
115
|
-
// findings against. Refuse a base that is not an ancestor: certifying a
|
|
116
|
-
// diff against an unrelated tree is worse than no read-back.
|
|
117
|
-
if (base !== EMPTY_TREE) {
|
|
118
|
-
try {
|
|
119
|
-
execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, commit], { stdio: "ignore" });
|
|
120
|
-
} catch (e) {
|
|
121
|
-
fail("usage", `base ${base} is not an ancestor of commit ${commit} — refusing to read back an unrelated tree.`, 2);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
let diff;
|
|
126
|
-
try {
|
|
127
|
-
diff = execFileSync("git", ["-C", repoPath, "diff", base, commit, "--"], {
|
|
128
|
-
encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
|
|
129
|
-
});
|
|
130
|
-
} catch (e) {
|
|
131
|
-
fail("git", `git diff failed: ${e.message}`, 1);
|
|
132
|
-
}
|
|
133
|
-
if (!diff.trim()) {
|
|
134
|
-
fail("git", "empty diff for commit — nothing to read back.", 1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Parse the diff into per-file added/removed lines (byte-identical parser
|
|
138
|
-
// to build-readback-request.js and verify-publish.js: the verifier looks
|
|
139
|
-
// the raw lines up verbatim, so all three must agree on the split).
|
|
140
|
-
const files = [];
|
|
141
|
-
let current = null;
|
|
142
|
-
for (const line of diff.split("\n")) {
|
|
143
|
-
if (line.startsWith("diff --git")) {
|
|
144
|
-
const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
|
|
145
|
-
current = { path: m ? m[2] : "unknown", added: [], removed: [] };
|
|
146
|
-
files.push(current);
|
|
147
|
-
} else if (current && line.startsWith("+") && !line.startsWith("+++")) {
|
|
148
|
-
current.added.push(line.slice(1));
|
|
149
|
-
} else if (current && line.startsWith("-") && !line.startsWith("---")) {
|
|
150
|
-
current.removed.push(line.slice(1));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Whole-line content of a disk file, normalized for the membership test.
|
|
155
|
-
// Trailing-newline artifact popped (same convention as verify-publish.js
|
|
156
|
-
// oldTreeLines); one trailing CR stripped per line so CRLF working copies
|
|
157
|
-
// compare honestly against LF diffs. The RAW diff lines are still what get
|
|
158
|
-
// emitted — the verifier looks those up verbatim.
|
|
159
|
-
const norm = (s) => (s.endsWith("\r") ? s.slice(0, -1) : s);
|
|
160
|
-
function diskLines(absPath) {
|
|
161
|
-
if (!existsSync(absPath)) return new Set(); // missing file: every line ABSENT
|
|
162
|
-
let text;
|
|
163
|
-
try {
|
|
164
|
-
text = readFileSync(absPath, "utf8");
|
|
165
|
-
} catch (e) {
|
|
166
|
-
fail("read", `cannot read ${absPath}: ${e.message}`, 1);
|
|
167
|
-
}
|
|
168
|
-
const lines = text.split("\n");
|
|
169
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
170
|
-
return new Set(lines.map(norm));
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const out = [];
|
|
174
|
-
for (const f of files) {
|
|
175
|
-
// Contain every diff path inside the space dir before reading.
|
|
176
|
-
const abs = resolve(spaceDir, f.path);
|
|
177
|
-
if (abs !== spaceDir && !abs.startsWith(spaceDir + "/")) {
|
|
178
|
-
fail("usage", `diff path ${JSON.stringify(f.path)} escapes the space dir.`, 2);
|
|
179
|
-
}
|
|
180
|
-
const present = diskLines(abs);
|
|
181
|
-
out.push(`FILE: ${f.path}`);
|
|
182
|
-
for (const line of f.added) {
|
|
183
|
-
out.push(`ADDED: ${line} :: ${present.has(norm(line)) ? "PRESENT" : "ABSENT"}`);
|
|
184
|
-
}
|
|
185
|
-
for (const line of f.removed) {
|
|
186
|
-
out.push(`REMOVED: ${line} :: ${present.has(norm(line)) ? "PRESENT" : "ABSENT"}`);
|
|
187
|
-
}
|
|
188
|
-
out.push("END_FILE");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
process.stdout.write(out.join("\n") + "\n");
|
|
192
|
-
process.stderr.write(JSON.stringify({
|
|
193
|
-
ok: true, slug, task_id: taskId, commit, base,
|
|
194
|
-
files: files.map((f) => f.path),
|
|
195
|
-
} ) + "\n");
|