@mmerterden/multi-agent-pipeline 17.3.0 → 17.5.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.
- package/CHANGELOG.md +203 -0
- package/README.md +23 -5
- package/README.tr.md +23 -5
- package/docs/adr/0013-lsp-code-intelligence.md +102 -0
- package/docs/adr/README.md +1 -0
- package/docs/token-budget-history.md +1 -1
- package/install/templates/copilot-instructions.md +9 -3
- package/package.json +1 -1
- package/pipeline/agents/code-reviewer.md +35 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +5 -3
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +17 -6
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +3 -3
- package/pipeline/lib/multi-repo-pipeline.sh +26 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +4 -4
- package/pipeline/multi-agent-refs/analysis/render.md +2 -1
- package/pipeline/multi-agent-refs/channels/pr.md +26 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +22 -0
- package/pipeline/multi-agent-refs/features/base-branch-evidence.md +222 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +40 -0
- package/pipeline/multi-agent-refs/features/code-intelligence.md +80 -0
- package/pipeline/multi-agent-refs/features/design-conformance.md +14 -0
- package/pipeline/multi-agent-refs/features/review-file-set.md +132 -0
- package/pipeline/multi-agent-refs/phases/modes.md +23 -3
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +96 -71
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +31 -23
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +1 -1
- package/pipeline/multi-agent-refs/phases.md +7 -2
- package/pipeline/multi-agent-refs/picker-contract.md +37 -5
- package/pipeline/multi-agent-refs/tracker-contract.md +25 -14
- package/pipeline/schemas/agent-state.schema.json +88 -4
- package/pipeline/schemas/prefs.schema.json +22 -0
- package/pipeline/schemas/review-file-exclusions.json +137 -0
- package/pipeline/schemas/reviewer-output.schema.json +27 -1
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/autopilot-runner.mjs +292 -45
- package/pipeline/scripts/base-branch-candidates.mjs +599 -0
- package/pipeline/scripts/diff-risk-score.mjs +1 -36
- package/pipeline/scripts/gc-abandoned.sh +5 -3
- package/pipeline/scripts/gen-mode-dispatch.mjs +39 -16
- package/pipeline/scripts/git-path.mjs +63 -0
- package/pipeline/scripts/glob-match.mjs +62 -0
- package/pipeline/scripts/graph-mermaid.mjs +251 -0
- package/pipeline/scripts/phase-tracker.sh +39 -2
- package/pipeline/scripts/phase0-exit-gate.mjs +128 -0
- package/pipeline/scripts/review-file-filter.mjs +180 -0
- package/pipeline/scripts/skill-conformance.mjs +1 -31
- package/pipeline/scripts/validate-analysis-doc.mjs +53 -0
- package/pipeline/scripts/validate-reviewer.mjs +90 -1
- package/pipeline/scripts/verify-citations.mjs +428 -0
- package/pipeline/skills/.skill-manifest.json +2 -2
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file verify-citations.mjs - a `file:line` that nothing resolves is a guess.
|
|
5
|
+
*
|
|
6
|
+
* Two surfaces in this pipeline require a citation and neither has ever checked
|
|
7
|
+
* one. `reviewer-output.schema.json` makes `file` and `line` required on every
|
|
8
|
+
* finding; `analysis-spec.schema.json` carries `repoEvidence` entries of
|
|
9
|
+
* `{name, file, line, tag}` and `analysis/locked.md` (Locked 37) asks for
|
|
10
|
+
* `repo/file:line` on every current-behaviour row. The only check that existed
|
|
11
|
+
* was `validate-analysis-doc.mjs`'s `/:\d+\b/` - a test that the row LOOKS like
|
|
12
|
+
* a citation, satisfied by `Foo.swift:9999` in a repo with no Foo.swift.
|
|
13
|
+
*
|
|
14
|
+
* The judgement is moved out of the model and into git. A path is resolved at a
|
|
15
|
+
* commit, not on the working tree, so a run that cites a file deleted three
|
|
16
|
+
* commits ago fails instead of passing on whatever happens to be checked out.
|
|
17
|
+
*
|
|
18
|
+
* Inputs:
|
|
19
|
+
* <file> JSON or markdown to read. `-` reads stdin.
|
|
20
|
+
* --repo <path> Repository root. Default: cwd
|
|
21
|
+
* --rev <sha> Commit to resolve against. Default: HEAD
|
|
22
|
+
* --worktree Resolve against the CHECKOUT instead of a commit. For
|
|
23
|
+
* Phase 4, where a reviewer cites lines the dev has not
|
|
24
|
+
* committed yet and a commit-pinned check would call
|
|
25
|
+
* every one of them invented.
|
|
26
|
+
* --json Machine-readable report
|
|
27
|
+
* --quiet Exit code only
|
|
28
|
+
*
|
|
29
|
+
* Accepted shapes, detected rather than declared, so one script serves both
|
|
30
|
+
* callers and neither has to say which it is:
|
|
31
|
+
* - reviewer output `{findings: [{file, line, ...}]}`
|
|
32
|
+
* - triage output `{accepted|deferred|rejected: [{file, line, ...}]}`
|
|
33
|
+
* - analysis spec `{evidence: {repoEvidence: {<repo>: {buckets: ...}}}}`
|
|
34
|
+
* - markdown every `path/to/file.ext:123` in the prose
|
|
35
|
+
*
|
|
36
|
+
* Exit codes:
|
|
37
|
+
* 0 - every citation resolved, or there were none to resolve
|
|
38
|
+
* 1 - at least one citation did not resolve
|
|
39
|
+
* 2 - the input could not be read or is not a repository
|
|
40
|
+
* 64 - usage error
|
|
41
|
+
*
|
|
42
|
+
* @module pipeline/scripts/verify-citations
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
46
|
+
import { resolve as resolvePath, sep } from "node:path";
|
|
47
|
+
import { spawnSync } from "node:child_process";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reject a path before it reaches git.
|
|
51
|
+
*
|
|
52
|
+
* `git show <rev>:<path>` happily resolves `../` out of the tree and an
|
|
53
|
+
* absolute path against the filesystem, so a citation could name something the
|
|
54
|
+
* repository does not contain and still "resolve". The guard is here rather
|
|
55
|
+
* than in the caller because every caller would otherwise need it.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} p
|
|
58
|
+
* @returns {string|null} the reason it is unsafe, or null
|
|
59
|
+
*/
|
|
60
|
+
export function unsafePath(p) {
|
|
61
|
+
if (!p) return "empty path";
|
|
62
|
+
if (p.startsWith("/")) return "absolute path";
|
|
63
|
+
if (p.includes("\\")) return "backslash in path";
|
|
64
|
+
const parts = p.split("/");
|
|
65
|
+
if (parts.includes("..")) return "path escapes the repository";
|
|
66
|
+
if (parts.includes(".git")) return "path reaches into .git";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extensions a markdown citation may carry.
|
|
72
|
+
*
|
|
73
|
+
* A list, not a pattern, and that direction is deliberate: the cost of a
|
|
74
|
+
* missing extension is one citation going unchecked, while the cost of an open
|
|
75
|
+
* pattern is the gate failing a correct document over a hostname. Add to it
|
|
76
|
+
* when a real repository file is missed - never widen it to a wildcard.
|
|
77
|
+
*
|
|
78
|
+
* `com`, `net`, `io`, `dev`, `tr` are absent on purpose.
|
|
79
|
+
*/
|
|
80
|
+
export const SOURCE_EXTENSIONS = new Set([
|
|
81
|
+
"swift",
|
|
82
|
+
"kt",
|
|
83
|
+
"kts",
|
|
84
|
+
"java",
|
|
85
|
+
"m",
|
|
86
|
+
"mm",
|
|
87
|
+
"h",
|
|
88
|
+
"hpp",
|
|
89
|
+
"c",
|
|
90
|
+
"cc",
|
|
91
|
+
"cpp",
|
|
92
|
+
"js",
|
|
93
|
+
"mjs",
|
|
94
|
+
"cjs",
|
|
95
|
+
"jsx",
|
|
96
|
+
"ts",
|
|
97
|
+
"tsx",
|
|
98
|
+
"py",
|
|
99
|
+
"rb",
|
|
100
|
+
"go",
|
|
101
|
+
"rs",
|
|
102
|
+
"php",
|
|
103
|
+
"sh",
|
|
104
|
+
"bash",
|
|
105
|
+
"zsh",
|
|
106
|
+
"pl",
|
|
107
|
+
"sql",
|
|
108
|
+
"json",
|
|
109
|
+
"yml",
|
|
110
|
+
"yaml",
|
|
111
|
+
"toml",
|
|
112
|
+
"xml",
|
|
113
|
+
"plist",
|
|
114
|
+
"xcconfig",
|
|
115
|
+
"pbxproj",
|
|
116
|
+
"gradle",
|
|
117
|
+
"properties",
|
|
118
|
+
"cfg",
|
|
119
|
+
"ini",
|
|
120
|
+
"env",
|
|
121
|
+
"md",
|
|
122
|
+
"mdx",
|
|
123
|
+
"txt",
|
|
124
|
+
"csv",
|
|
125
|
+
"tsv",
|
|
126
|
+
"strings",
|
|
127
|
+
"stringsdict",
|
|
128
|
+
"html",
|
|
129
|
+
"css",
|
|
130
|
+
"scss",
|
|
131
|
+
"vue",
|
|
132
|
+
"svelte",
|
|
133
|
+
"graphql",
|
|
134
|
+
"proto",
|
|
135
|
+
"entitlements",
|
|
136
|
+
"storyboard",
|
|
137
|
+
"xib",
|
|
138
|
+
"lock",
|
|
139
|
+
"gemspec",
|
|
140
|
+
"podspec",
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pull `{file, line}` pairs out of whatever was handed over.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} raw
|
|
147
|
+
* @returns {{file: string, line: number|null, where: string}[]}
|
|
148
|
+
*/
|
|
149
|
+
export function extract(raw) {
|
|
150
|
+
const out = [];
|
|
151
|
+
const push = (file, line, where) => {
|
|
152
|
+
if (typeof file === "string" && file.trim()) {
|
|
153
|
+
out.push({ file: file.trim(), line: Number.isFinite(line) ? line : null, where });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let doc;
|
|
158
|
+
try {
|
|
159
|
+
doc = JSON.parse(raw);
|
|
160
|
+
} catch {
|
|
161
|
+
doc = null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (doc && typeof doc === "object") {
|
|
165
|
+
for (const key of ["findings", "accepted", "deferred", "rejected"]) {
|
|
166
|
+
for (const [i, f] of (doc[key] || []).entries()) {
|
|
167
|
+
if (f && typeof f === "object") push(f.file, Number(f.line), `${key}[${i}]`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const repoEvidence = doc?.evidence?.repoEvidence || doc?.analysisSpec?.evidence?.repoEvidence;
|
|
171
|
+
if (repoEvidence && typeof repoEvidence === "object") {
|
|
172
|
+
for (const [repo, body] of Object.entries(repoEvidence)) {
|
|
173
|
+
for (const [bucket, rows] of Object.entries(body?.buckets || {})) {
|
|
174
|
+
for (const [i, r] of (rows || []).entries()) {
|
|
175
|
+
if (r && typeof r === "object") {
|
|
176
|
+
push(r.file, Number(r.line), `repoEvidence.${repo}.${bucket}[${i}]`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Markdown. A citation is a path with an extension followed by a line number.
|
|
186
|
+
//
|
|
187
|
+
// Two things that look exactly like one and are not:
|
|
188
|
+
//
|
|
189
|
+
// - a URL with a port. `https://wiki.example.com:8443/x` hands
|
|
190
|
+
// `wiki.example.com:8443` to the matcher, and an analysis document is
|
|
191
|
+
// full of Confluence, Jira and Figma links. Left in, this gate blocked
|
|
192
|
+
// dispatch on every real document and reported "absolute path" as the
|
|
193
|
+
// reason, which is not even the right complaint. URLs are removed first.
|
|
194
|
+
// - a dotted number. `v1.2:30` has an "extension" of `2`, so the extension
|
|
195
|
+
// must START with a letter. No source file is named `.2`.
|
|
196
|
+
//
|
|
197
|
+
// Fenced blocks are skipped for the same class of reason: a code sample that
|
|
198
|
+
// happens to contain `foo.js:12` is an illustration, not a claim about this
|
|
199
|
+
// repository.
|
|
200
|
+
//
|
|
201
|
+
// The URL strip below only removes what carries a scheme. A host written
|
|
202
|
+
// bare - `wiki.example.com:8443/x`, and an analysis document is full of
|
|
203
|
+
// them - still looks exactly like a path with an extension and a line
|
|
204
|
+
// number, and this gate would fail the document over `.com`. So the
|
|
205
|
+
// extension has to be one a repository actually contains. This is the same
|
|
206
|
+
// defect that was already fixed once here for scheme-carrying URLs; the bare
|
|
207
|
+
// form survived it.
|
|
208
|
+
let fenced = false;
|
|
209
|
+
raw.split("\n").forEach((line, i) => {
|
|
210
|
+
if (/^\s*```/.test(line)) {
|
|
211
|
+
fenced = !fenced;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (fenced) return;
|
|
215
|
+
const cleaned = line.replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, " ");
|
|
216
|
+
for (const m of cleaned.matchAll(/([A-Za-z0-9_./-]+\.([A-Za-z][A-Za-z0-9]*)):(\d+)/g)) {
|
|
217
|
+
if (!SOURCE_EXTENSIONS.has(m[2].toLowerCase())) continue;
|
|
218
|
+
push(m[1], Number(m[3]), `line ${i + 1}`);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Resolve one citation at a commit, or in the checkout when `rev` is null.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} repo
|
|
228
|
+
* @param {string|null} rev
|
|
229
|
+
* @param {{file: string, line: number|null}} c
|
|
230
|
+
* @param {Map<string, number|null>} lineCache
|
|
231
|
+
* @returns {string|null} the reason it failed, or null
|
|
232
|
+
*/
|
|
233
|
+
export function resolveOne(repo, rev, c, lineCache) {
|
|
234
|
+
const unsafe = unsafePath(c.file);
|
|
235
|
+
if (unsafe) return unsafe;
|
|
236
|
+
|
|
237
|
+
if (!lineCache.has(c.file)) {
|
|
238
|
+
lineCache.set(c.file, rev === null ? worktreeLines(repo, c.file) : revLines(repo, rev, c.file));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const lines = lineCache.get(c.file);
|
|
242
|
+
const at = rev === null ? "in the working tree" : `at ${rev.slice(0, 12)}`;
|
|
243
|
+
if (lines === null) return `no such file ${at}`;
|
|
244
|
+
if (c.line === null) return null;
|
|
245
|
+
if (c.line < 1) return `line ${c.line} is not a line number`;
|
|
246
|
+
if (c.line > lines) return `line ${c.line} is past the end of the file (${lines} lines)`;
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Line count of a blob at a commit, or null when the path is not a file there.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} repo
|
|
254
|
+
* @param {string} rev
|
|
255
|
+
* @param {string} file
|
|
256
|
+
* @returns {number|null}
|
|
257
|
+
*/
|
|
258
|
+
function revLines(repo, rev, file) {
|
|
259
|
+
const t = spawnSync("git", ["-C", repo, "cat-file", "-t", `${rev}:${file}`], {
|
|
260
|
+
encoding: "utf-8",
|
|
261
|
+
});
|
|
262
|
+
if (t.status !== 0 || t.stdout.trim() !== "blob") return null;
|
|
263
|
+
const show = spawnSync("git", ["-C", repo, "show", `${rev}:${file}`], {
|
|
264
|
+
encoding: "utf-8",
|
|
265
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
266
|
+
});
|
|
267
|
+
return countLines(show.status === 0 ? show.stdout : "");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Line count in the checkout, or null when there is no such file.
|
|
272
|
+
*
|
|
273
|
+
* `unsafePath` cannot see a symlink: `docs/out -> /etc` is a relative path with
|
|
274
|
+
* no `..` in it, and git never followed it because git reads blobs rather than
|
|
275
|
+
* the filesystem. In worktree mode the filesystem IS the source, so containment
|
|
276
|
+
* is re-checked after the path is resolved.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} repo
|
|
279
|
+
* @param {string} file
|
|
280
|
+
* @returns {number|null}
|
|
281
|
+
*/
|
|
282
|
+
function worktreeLines(repo, file) {
|
|
283
|
+
let real;
|
|
284
|
+
let root;
|
|
285
|
+
try {
|
|
286
|
+
root = realpathSync(repo);
|
|
287
|
+
real = realpathSync(resolvePath(root, file));
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
if (real !== root && !real.startsWith(root + sep)) return null;
|
|
292
|
+
try {
|
|
293
|
+
if (!statSync(real).isFile()) return null;
|
|
294
|
+
return countLines(readFileSync(real, "utf-8"));
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* A file with no trailing newline still has its last line.
|
|
302
|
+
*
|
|
303
|
+
* @param {string} text
|
|
304
|
+
* @returns {number}
|
|
305
|
+
*/
|
|
306
|
+
function countLines(text) {
|
|
307
|
+
if (text.length === 0) return 0;
|
|
308
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @param {object} params
|
|
313
|
+
* @returns {{checked: number, unresolved: object[], rev: string}}
|
|
314
|
+
*/
|
|
315
|
+
export function verify({ raw, repo, rev }) {
|
|
316
|
+
const citations = extract(raw);
|
|
317
|
+
const lineCache = new Map();
|
|
318
|
+
const unresolved = [];
|
|
319
|
+
for (const c of citations) {
|
|
320
|
+
const reason = resolveOne(repo, rev, c, lineCache);
|
|
321
|
+
if (reason) unresolved.push({ ...c, reason });
|
|
322
|
+
}
|
|
323
|
+
return { checked: citations.length, unresolved, rev };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const VALUE_FLAGS = new Set(["--repo", "--rev"]);
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* One pass, positionally. `argv.indexOf(a)` finds the FIRST occurrence of a
|
|
330
|
+
* string, so `--repo . file .` would have mis-identified which `.` was the
|
|
331
|
+
* target; walking the list is both simpler and correct.
|
|
332
|
+
*
|
|
333
|
+
* @param {string[]} argv
|
|
334
|
+
* @returns {{target: string|null, repo: string|null, rev: string|null}}
|
|
335
|
+
*/
|
|
336
|
+
export function parseArgs(argv) {
|
|
337
|
+
let target = null;
|
|
338
|
+
let repo = null;
|
|
339
|
+
let rev = null;
|
|
340
|
+
for (let i = 0; i < argv.length; i++) {
|
|
341
|
+
const a = argv[i];
|
|
342
|
+
if (VALUE_FLAGS.has(a)) {
|
|
343
|
+
const v = argv[i + 1];
|
|
344
|
+
if (v !== undefined && !v.startsWith("--")) {
|
|
345
|
+
if (a === "--repo") repo = v;
|
|
346
|
+
else rev = v;
|
|
347
|
+
i++;
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (a.startsWith("--")) continue;
|
|
352
|
+
if (target === null) target = a;
|
|
353
|
+
}
|
|
354
|
+
return { target, repo, rev };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function main() {
|
|
358
|
+
const argv = process.argv.slice(2);
|
|
359
|
+
const parsed = parseArgs(argv);
|
|
360
|
+
const target = parsed.target;
|
|
361
|
+
if (!target) {
|
|
362
|
+
console.error(
|
|
363
|
+
"usage: verify-citations.mjs <file|-> [--repo <path>] [--rev <sha>|--worktree] [--json] [--quiet]",
|
|
364
|
+
);
|
|
365
|
+
process.exit(64);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const repo = parsed.repo || process.cwd();
|
|
369
|
+
const worktree = argv.includes("--worktree");
|
|
370
|
+
if (worktree && parsed.rev) {
|
|
371
|
+
console.error("verify-citations: --worktree and --rev name two different trees; pick one");
|
|
372
|
+
process.exit(64);
|
|
373
|
+
}
|
|
374
|
+
const rev = parsed.rev || "HEAD";
|
|
375
|
+
const json = argv.includes("--json");
|
|
376
|
+
const quiet = argv.includes("--quiet");
|
|
377
|
+
|
|
378
|
+
let raw;
|
|
379
|
+
try {
|
|
380
|
+
raw = target === "-" ? readFileSync(0, "utf-8") : readFileSync(target, "utf-8");
|
|
381
|
+
} catch (e) {
|
|
382
|
+
console.error(`verify-citations: cannot read ${target}: ${e.message}`);
|
|
383
|
+
process.exit(2);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const top = spawnSync("git", ["-C", repo, "rev-parse", "--show-toplevel"], { encoding: "utf-8" });
|
|
387
|
+
if (top.status !== 0) {
|
|
388
|
+
console.error(`verify-citations: ${repo} is not a git repository`);
|
|
389
|
+
process.exit(2);
|
|
390
|
+
}
|
|
391
|
+
let sha = null;
|
|
392
|
+
if (!worktree) {
|
|
393
|
+
const head = spawnSync("git", ["-C", repo, "rev-parse", rev], { encoding: "utf-8" });
|
|
394
|
+
if (head.status !== 0) {
|
|
395
|
+
console.error(`verify-citations: ${rev} is not a commit in ${repo}`);
|
|
396
|
+
process.exit(2);
|
|
397
|
+
}
|
|
398
|
+
sha = head.stdout.trim();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const report = verify({ raw, repo, rev: sha });
|
|
402
|
+
const where = sha === null ? "in the working tree" : `at ${sha.slice(0, 12)}`;
|
|
403
|
+
|
|
404
|
+
if (json) {
|
|
405
|
+
console.log(JSON.stringify(report, null, 2));
|
|
406
|
+
} else if (!quiet) {
|
|
407
|
+
if (report.unresolved.length === 0) {
|
|
408
|
+
console.log(`verify-citations: ${report.checked} citation(s) resolve ${where}`);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(
|
|
411
|
+
`verify-citations: ${report.unresolved.length} of ${report.checked} citation(s) do not resolve ${where}:`,
|
|
412
|
+
);
|
|
413
|
+
for (const u of report.unresolved) {
|
|
414
|
+
console.log(` ${u.where}: ${u.file}${u.line === null ? "" : `:${u.line}`} - ${u.reason}`);
|
|
415
|
+
}
|
|
416
|
+
// The cheap way to make this exit 0 is to delete the line number, which
|
|
417
|
+
// turns a wrong claim into an unfalsifiable one. Said here because this
|
|
418
|
+
// is the moment someone is deciding what to do about it.
|
|
419
|
+
console.log(" Re-search and correct the citation; never drop the line number to pass.");
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
process.exit(report.unresolved.length === 0 ? 0 : 1);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
427
|
+
main();
|
|
428
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-15T12:49:28Z",
|
|
4
4
|
"skillCount": 212,
|
|
5
5
|
"entries": [
|
|
6
6
|
{
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
239
|
"path": "shared/core/multi-agent/SKILL.md",
|
|
240
|
-
"sha256": "
|
|
240
|
+
"sha256": "faac7ceb535d18778efe0ea91c19534c8e076bc709e7b1dbc567f5e71692bbbd"
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
243
|
"path": "shared/external/accessibility-compliance-accessibility-audit/SKILL.md",
|
|
@@ -316,7 +316,7 @@ All TaskCreate calls fire in strict phase-number order BEFORE any TaskUpdate is
|
|
|
316
316
|
- `multi-agent-local`, `multi-agent-autopilot`, `multi-agent-local-autopilot`: 0 → 1 → 2 → 3 → 4 → 6 → 7 (the interactive Phase 5 gate needs a worktree checkout and an attended run)
|
|
317
317
|
- `multi-agent-analysis`: 0 → 1 → 2 → 4 → 6 → 7 (no code, so no Dev and no Test)
|
|
318
318
|
|
|
319
|
-
Depth
|
|
319
|
+
Depth decides WHICH of the command's set is registered. The tracker boots at Step -1, long before the depth question can be asked, so it registers Phase 0 alone and the remaining tiles are created at Step 7.5 once the answer is known - a Short run never draws an Analysis or Planning tile.
|
|
320
320
|
|
|
321
321
|
Full contract: `refs/tracker-contract.md` section "TaskCreate ordering (strict)".
|
|
322
322
|
|