@mmerterden/multi-agent-pipeline 16.12.0 → 16.14.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 +31 -0
- package/README.md +4 -4
- package/README.tr.md +4 -4
- package/docs/adr/0010-own-code-graph.md +129 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +2 -2
- package/docs/ecosystem.md +5 -5
- package/docs/features.md +8 -0
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/graph/SKILL.md +105 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +8 -8
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -12
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +9 -7
- package/pipeline/multi-agent-refs/analysis/evidence.md +21 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -10
- package/pipeline/multi-agent-refs/features/code-graph.md +69 -0
- package/pipeline/multi-agent-refs/features/model-fallback.md +44 -2
- package/pipeline/multi-agent-refs/knowledge.md +6 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +5 -0
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +2 -0
- package/pipeline/preferences-template.json +2 -0
- package/pipeline/schemas/code-graph.schema.json +91 -0
- package/pipeline/schemas/prefs.schema.json +45 -0
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_code-graph.mjs +545 -0
- package/pipeline/scripts/_path-match.mjs +87 -0
- package/pipeline/scripts/code-graph-rules/android.json +130 -0
- package/pipeline/scripts/code-graph-rules/go.json +124 -0
- package/pipeline/scripts/code-graph-rules/ios.json +95 -0
- package/pipeline/scripts/code-graph-rules/node.json +151 -0
- package/pipeline/scripts/code-graph-rules/python.json +91 -0
- package/pipeline/scripts/graph-affected.mjs +161 -0
- package/pipeline/scripts/graph-build.mjs +157 -0
- package/pipeline/scripts/graph-query.mjs +191 -0
- package/pipeline/scripts/graph-report.mjs +237 -0
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +7 -4
- package/pipeline/scripts/test-gap-rules/go.json +24 -0
- package/pipeline/scripts/test-gap-rules/ios.json +38 -10
- package/pipeline/scripts/test-gap-scan.mjs +2 -21
- package/pipeline/scripts/uninstall.mjs +11 -2
- package/pipeline/scripts/validate-code-graph.mjs +174 -0
- package/pipeline/skills/.skills-index.json +14 -3
- package/pipeline/skills/shared/README.md +6 -5
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +106 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +11 -11
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +4 -4
- package/pipeline/skills/skills-index.md +4 -3
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file graph-report.mjs - GRAPH_REPORT.md, the human-and-model readable summary.
|
|
5
|
+
*
|
|
6
|
+
* Phase 1 reads this before deciding how wide to explore, and Phase 7 ships it
|
|
7
|
+
* next to the knowledge base. Sections are ordered by how much they narrow a
|
|
8
|
+
* search: hubs first (the types everything touches), then the module map, then
|
|
9
|
+
* the long tail that is usually safe to ignore.
|
|
10
|
+
*
|
|
11
|
+
* God-nodes exclude test files. A test base class is a hub of the test target,
|
|
12
|
+
* not of the architecture, and letting it rank pushes the real hubs off a list
|
|
13
|
+
* whose whole value is its first ten rows.
|
|
14
|
+
*
|
|
15
|
+
* Inputs:
|
|
16
|
+
* --graph <path> Graph file. Default: ~/.claude/knowledge/<cwd name>/code-graph.json
|
|
17
|
+
* --out <path> Report path. Default: GRAPH_REPORT.md beside the graph
|
|
18
|
+
* --top N Rows per ranked section. Default: 15
|
|
19
|
+
* --stdout Print instead of writing
|
|
20
|
+
* --status Print one header line and exit, writing nothing. This is
|
|
21
|
+
* what `/multi-agent:graph status` runs: the graph is a
|
|
22
|
+
* 22MB JSON on a large repo, so "read the header yourself"
|
|
23
|
+
* would mean pulling the whole file into a model's context.
|
|
24
|
+
*
|
|
25
|
+
* Exit codes:
|
|
26
|
+
* 0 - written
|
|
27
|
+
* 1 - graph missing or unreadable
|
|
28
|
+
*
|
|
29
|
+
* @module pipeline/scripts/graph-report
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { writeFileSync } from "node:fs";
|
|
33
|
+
import { execFileSync } from "node:child_process";
|
|
34
|
+
import { join, dirname, resolve } from "node:path";
|
|
35
|
+
import { parseFlags } from "./graph-build.mjs";
|
|
36
|
+
import { loadGraph, defaultGraphPath } from "./graph-query.mjs";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Top-level source directory of a path, used as the module bucket.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} path
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function moduleOf(path) {
|
|
45
|
+
if (!path) return "-";
|
|
46
|
+
const parts = path.split("/");
|
|
47
|
+
return parts.length > 1 ? parts[0] : ".";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Section inputs, derived once so the renderer stays declarative.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} graph
|
|
54
|
+
* @param {number} top
|
|
55
|
+
* @returns {object}
|
|
56
|
+
*/
|
|
57
|
+
export function summarize(graph, top) {
|
|
58
|
+
const symbols = graph.nodes.filter((n) => n.kind === "symbol");
|
|
59
|
+
const files = graph.nodes.filter((n) => n.kind === "file");
|
|
60
|
+
|
|
61
|
+
const hubs = symbols
|
|
62
|
+
.filter((n) => !n.isTest)
|
|
63
|
+
.sort((a, b) => b.degree - a.degree)
|
|
64
|
+
.slice(0, top);
|
|
65
|
+
|
|
66
|
+
const modules = new Map();
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
const key = moduleOf(f.path);
|
|
69
|
+
if (!modules.has(key)) modules.set(key, { files: 0, symbols: 0, tests: 0 });
|
|
70
|
+
const bucket = modules.get(key);
|
|
71
|
+
bucket.files++;
|
|
72
|
+
if (f.isTest) bucket.tests++;
|
|
73
|
+
}
|
|
74
|
+
for (const s of symbols) {
|
|
75
|
+
const key = moduleOf(s.path);
|
|
76
|
+
if (modules.has(key)) modules.get(key).symbols++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const externals = graph.nodes
|
|
80
|
+
.filter((n) => n.kind === "module")
|
|
81
|
+
.sort((a, b) => b.degree - a.degree)
|
|
82
|
+
.slice(0, top);
|
|
83
|
+
|
|
84
|
+
const orphans = files.filter((f) => f.degree === 0);
|
|
85
|
+
|
|
86
|
+
const kinds = {};
|
|
87
|
+
for (const s of symbols) kinds[s.symbolKind] = (kinds[s.symbolKind] || 0) + 1;
|
|
88
|
+
|
|
89
|
+
return { hubs, modules, externals, orphans, kinds, fileCount: files.length };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {object} graph
|
|
94
|
+
* @param {number} top
|
|
95
|
+
* @returns {string} markdown
|
|
96
|
+
*/
|
|
97
|
+
export function render(graph, top) {
|
|
98
|
+
const s = summarize(graph, top);
|
|
99
|
+
const L = [];
|
|
100
|
+
|
|
101
|
+
L.push(`# Code graph report`);
|
|
102
|
+
L.push("");
|
|
103
|
+
L.push(`- Root: \`${graph.root}\``);
|
|
104
|
+
L.push(`- Stack: ${graph.stack}`);
|
|
105
|
+
L.push(`- Built: ${graph.generatedAt}`);
|
|
106
|
+
if (graph.baseCommit) L.push(`- Commit: \`${graph.baseCommit.slice(0, 12)}\``);
|
|
107
|
+
L.push(
|
|
108
|
+
`- Scale: ${graph.stats.files} files, ${graph.stats.nodes} nodes, ${graph.stats.edges} edges`,
|
|
109
|
+
);
|
|
110
|
+
L.push("");
|
|
111
|
+
|
|
112
|
+
L.push(`## Architectural hubs`);
|
|
113
|
+
L.push("");
|
|
114
|
+
L.push(`The most connected non-test symbols. A change here reaches the widest surface.`);
|
|
115
|
+
L.push("");
|
|
116
|
+
L.push(`| Degree | Symbol | Kind | Path |`);
|
|
117
|
+
L.push(`|---|---|---|---|`);
|
|
118
|
+
for (const h of s.hubs) {
|
|
119
|
+
L.push(`| ${h.degree} | \`${h.name}\` | ${h.symbolKind} | \`${h.path}:${h.line || 1}\` |`);
|
|
120
|
+
}
|
|
121
|
+
L.push("");
|
|
122
|
+
|
|
123
|
+
L.push(`## Modules`);
|
|
124
|
+
L.push("");
|
|
125
|
+
L.push(`| Module | Files | Symbols | Test files |`);
|
|
126
|
+
L.push(`|---|---|---|---|`);
|
|
127
|
+
const sortedModules = [...s.modules.entries()].sort((a, b) => b[1].files - a[1].files);
|
|
128
|
+
for (const [name, m] of sortedModules) {
|
|
129
|
+
L.push(`| \`${name}\` | ${m.files} | ${m.symbols} | ${m.tests} |`);
|
|
130
|
+
}
|
|
131
|
+
L.push("");
|
|
132
|
+
|
|
133
|
+
L.push(`## External dependencies`);
|
|
134
|
+
L.push("");
|
|
135
|
+
L.push(`Imported names with no declaring file in this repo.`);
|
|
136
|
+
L.push("");
|
|
137
|
+
L.push(`| Imports | Module |`);
|
|
138
|
+
L.push(`|---|---|`);
|
|
139
|
+
for (const e of s.externals) L.push(`| ${e.degree} | \`${e.name}\` |`);
|
|
140
|
+
L.push("");
|
|
141
|
+
|
|
142
|
+
L.push(`## Symbol kinds`);
|
|
143
|
+
L.push("");
|
|
144
|
+
const kindRows = Object.entries(s.kinds).sort((a, b) => b[1] - a[1]);
|
|
145
|
+
L.push(kindRows.map(([k, n]) => `${k}: ${n}`).join(" · "));
|
|
146
|
+
L.push("");
|
|
147
|
+
|
|
148
|
+
L.push(`## Unconnected files`);
|
|
149
|
+
L.push("");
|
|
150
|
+
if (s.orphans.length === 0) {
|
|
151
|
+
L.push(`None: every file has at least one edge.`);
|
|
152
|
+
} else {
|
|
153
|
+
L.push(
|
|
154
|
+
`${s.orphans.length} of ${s.fileCount} files have no edge at all. ` +
|
|
155
|
+
`Either they declare nothing this stack's rules recognise, or nothing references them.`,
|
|
156
|
+
);
|
|
157
|
+
L.push("");
|
|
158
|
+
for (const o of s.orphans.slice(0, top)) L.push(`- \`${o.path}\``);
|
|
159
|
+
if (s.orphans.length > top) L.push(`- ... ${s.orphans.length - top} more`);
|
|
160
|
+
}
|
|
161
|
+
L.push("");
|
|
162
|
+
|
|
163
|
+
return L.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* HEAD of a repo, or null when it is not a git checkout.
|
|
168
|
+
*
|
|
169
|
+
* @param {string} root
|
|
170
|
+
* @returns {string|null}
|
|
171
|
+
*/
|
|
172
|
+
function headCommit(root) {
|
|
173
|
+
try {
|
|
174
|
+
return execFileSync("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
175
|
+
encoding: "utf8",
|
|
176
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
177
|
+
}).trim();
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One line: what the graph covers and whether it still matches the tree.
|
|
185
|
+
*
|
|
186
|
+
* @param {object} graph
|
|
187
|
+
* @param {string} path
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
export function statusLine(graph, path) {
|
|
191
|
+
const head = headCommit(graph.root);
|
|
192
|
+
let freshness = "freshness unknown (no git HEAD)";
|
|
193
|
+
if (graph.baseCommit && head) {
|
|
194
|
+
freshness =
|
|
195
|
+
graph.baseCommit === head
|
|
196
|
+
? `current (${head.slice(0, 12)})`
|
|
197
|
+
: `STALE: built at ${graph.baseCommit.slice(0, 12)}, HEAD is ${head.slice(0, 12)}`;
|
|
198
|
+
} else if (!graph.baseCommit) {
|
|
199
|
+
freshness = "freshness unknown (graph carries no baseCommit)";
|
|
200
|
+
}
|
|
201
|
+
return (
|
|
202
|
+
`graph-status: ${graph.stack} | ${graph.stats.files} files, ${graph.stats.nodes} nodes, ` +
|
|
203
|
+
`${graph.stats.edges} edges | built ${graph.generatedAt} | ${freshness} | ${path}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function main() {
|
|
208
|
+
const { flags } = parseFlags(process.argv.slice(2));
|
|
209
|
+
const path = flags.graph && flags.graph !== true ? resolve(flags.graph) : defaultGraphPath();
|
|
210
|
+
|
|
211
|
+
let graph;
|
|
212
|
+
try {
|
|
213
|
+
graph = loadGraph(path);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
process.stderr.write(`graph-report: ${e.message}\n`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (flags.status === true) {
|
|
220
|
+
process.stdout.write(statusLine(graph, path) + "\n");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const top = Number(flags.top) > 0 ? Number(flags.top) : 15;
|
|
225
|
+
const markdown = render(graph, top);
|
|
226
|
+
|
|
227
|
+
if (flags.stdout === true) {
|
|
228
|
+
process.stdout.write(markdown);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const out =
|
|
232
|
+
flags.out && flags.out !== true ? resolve(flags.out) : join(dirname(path), "GRAPH_REPORT.md");
|
|
233
|
+
writeFileSync(out, markdown, "utf8");
|
|
234
|
+
process.stdout.write(`graph-report: ${out}\n`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (process.argv[1] && process.argv[1].endsWith("graph-report.mjs")) main();
|
|
@@ -203,10 +203,13 @@ TRSCHEMA="${MA_SCHEMAS:+$MA_SCHEMAS/triage-output.schema.json}"
|
|
|
203
203
|
|
|
204
204
|
if [ -z "$P4" ] || [ ! -f "$P4" ]; then
|
|
205
205
|
echo " ↷ SKIP: phase-4-review.md not present in this $MA_LAYOUT layout"
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
# Two independent statements, because they can drift apart: the count sentence is
|
|
207
|
+
# the contract, and the matrix is what a reader dispatches from. Three regexes over
|
|
208
|
+
# overlapping prose used to stand in for this and let the count sentence 300 lines
|
|
209
|
+
# further down go stale for a whole release without failing.
|
|
210
|
+
elif grep -qF "Claude Code 3, Copilot CLI 3, Codex CLI 3" "$P4" \
|
|
211
|
+
&& grep -qE '^\| Reviewer 3 .*\|.*\|.*\|.*\|' "$P4"; then
|
|
212
|
+
pass "phase-4-review declares Claude=3 / Copilot=3 / Codex=3 reviewers, and the matrix has all three host columns"
|
|
210
213
|
else
|
|
211
214
|
fail "phase-4-review does not declare the CLI-aware reviewer count for all three hosts"
|
|
212
215
|
fi
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"stack": "go",
|
|
4
|
+
"description": "Go test gap rules. Sources in *.go; tests are *_test.go beside the code they cover, which is why there is no test directory convention here.",
|
|
5
|
+
"sourceExtensions": [".go"],
|
|
6
|
+
"testPathSuffixes": ["_test.go"],
|
|
7
|
+
"testPathContains": [],
|
|
8
|
+
"excludePathGlobs": ["vendor/", "third_party/", "*.pb.go", "*_generated.go", "*_mock.go"],
|
|
9
|
+
"publicApiPatterns": [
|
|
10
|
+
{
|
|
11
|
+
"id": "exported_func",
|
|
12
|
+
"regex": "^func\\s+([A-Z][A-Za-z0-9_]*)\\s*\\("
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "exported_method",
|
|
16
|
+
"regex": "^func\\s+\\([^)]*\\)\\s+([A-Z][A-Za-z0-9_]*)\\s*\\("
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "exported_type",
|
|
20
|
+
"regex": "^type\\s+([A-Z][A-Za-z0-9_]*)\\s+(?:struct|interface|func|map|\\[|chan|\\*|[A-Za-z])"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"hint": "Cover exported identifiers with a table-driven Test<Name> in the same package's <file>_test.go. An unexported identifier is covered through the exported one that calls it."
|
|
24
|
+
}
|
|
@@ -13,20 +13,48 @@
|
|
|
13
13
|
"Generated/",
|
|
14
14
|
"Sourcery/",
|
|
15
15
|
"_Generated.swift",
|
|
16
|
-
"Tests/Fixtures/"
|
|
16
|
+
"Tests/Fixtures/",
|
|
17
|
+
".xctemplate/"
|
|
17
18
|
],
|
|
18
19
|
"publicApiPatterns": [
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
{
|
|
21
|
+
"id": "public_func",
|
|
22
|
+
"regex": "\\bpublic\\s+func\\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "public_class",
|
|
26
|
+
"regex": "\\bpublic\\s+(?:final\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "public_struct",
|
|
30
|
+
"regex": "\\bpublic\\s+struct\\s+([A-Z][A-Za-z0-9_]*)"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "public_enum",
|
|
34
|
+
"regex": "\\bpublic\\s+enum\\s+([A-Z][A-Za-z0-9_]*)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "public_proto",
|
|
38
|
+
"regex": "\\bpublic\\s+protocol\\s+([A-Z][A-Za-z0-9_]*)"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "public_init",
|
|
42
|
+
"regex": "\\bpublic\\s+init\\b"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "objc_export",
|
|
46
|
+
"regex": "@objc(?:Members)?\\s+(?:public\\s+)?(?:func|class|var)"
|
|
47
|
+
}
|
|
26
48
|
],
|
|
27
49
|
"swiftUiViewPatterns": [
|
|
28
|
-
{
|
|
29
|
-
|
|
50
|
+
{
|
|
51
|
+
"id": "view_struct",
|
|
52
|
+
"regex": "struct\\s+([A-Z][A-Za-z0-9_]*)\\s*:\\s*View\\b"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "config_struct",
|
|
56
|
+
"regex": "struct\\s+([A-Z][A-Za-z0-9_]*Configuration)\\b"
|
|
57
|
+
}
|
|
30
58
|
],
|
|
31
59
|
"expectedTestKindsForView": ["snapshot", "viewinspector", "unit"],
|
|
32
60
|
"snapshotPathHints": ["SnapshotTests", "__Snapshots__"],
|
|
@@ -42,6 +42,7 @@ import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
|
42
42
|
import { execFileSync } from "node:child_process";
|
|
43
43
|
import { join, dirname, extname, basename } from "node:path";
|
|
44
44
|
import { fileURLToPath } from "node:url";
|
|
45
|
+
import { isExcludedBy } from "./_path-match.mjs";
|
|
45
46
|
|
|
46
47
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
47
48
|
|
|
@@ -118,27 +119,7 @@ function loadDiff() {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
function isExcluded(path) {
|
|
121
|
-
|
|
122
|
-
for (const pat of rules.excludePathGlobs) {
|
|
123
|
-
if (pat.endsWith("/")) {
|
|
124
|
-
if (path.includes(pat) || path.startsWith(pat)) return true;
|
|
125
|
-
} else if (pat.includes("**")) {
|
|
126
|
-
// Tokenize ** before the single-* pass so the * rewrite cannot mangle
|
|
127
|
-
// the already-translated .* (which broke cross-directory matches).
|
|
128
|
-
const DOUBLE_STAR = "\u0000";
|
|
129
|
-
const re = new RegExp(
|
|
130
|
-
"^" +
|
|
131
|
-
pat
|
|
132
|
-
.replace(/\./g, "\\.")
|
|
133
|
-
.replace(/\*\*/g, DOUBLE_STAR)
|
|
134
|
-
.replace(/\*/g, "[^/]*")
|
|
135
|
-
.replace(new RegExp(DOUBLE_STAR, "g"), ".*") +
|
|
136
|
-
"$",
|
|
137
|
-
);
|
|
138
|
-
if (re.test(path)) return true;
|
|
139
|
-
} else if (path.endsWith(pat)) return true;
|
|
140
|
-
}
|
|
141
|
-
return false;
|
|
122
|
+
return isExcludedBy(path, rules.excludePathGlobs);
|
|
142
123
|
}
|
|
143
124
|
|
|
144
125
|
// git quotes a `diff --git` path (core.quotePath, on by default) whenever it
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* - $HOME/.claude/multi-agent-preferences.json (pipeline settings)
|
|
33
33
|
* - $HOME/.claude/logs/multi-agent/ (task logs, state, metrics, audit trail)
|
|
34
34
|
* - $HOME/.claude/memory/multi-agent/ (per-repo learnings ledger + triage corpus)
|
|
35
|
+
* - $HOME/.claude/knowledge/ (per-repo knowledge base + code graph)
|
|
35
36
|
*
|
|
36
37
|
* @module pipeline/scripts/runtime/uninstall
|
|
37
38
|
*/
|
|
@@ -726,6 +727,7 @@ export async function main() {
|
|
|
726
727
|
console.log(" - ~/.claude/multi-agent-preferences.json (pipeline settings)");
|
|
727
728
|
console.log(" - ~/.claude/logs/multi-agent/ (task logs, state, metrics, audit trail)");
|
|
728
729
|
console.log(" - ~/.claude/memory/multi-agent/ (per-repo learnings ledger + triage corpus)");
|
|
730
|
+
console.log(" - ~/.claude/knowledge/ (per-repo knowledge base + code graph)");
|
|
729
731
|
console.log("");
|
|
730
732
|
}
|
|
731
733
|
console.log(" PRESERVED (never touched):");
|
|
@@ -834,12 +836,19 @@ export async function main() {
|
|
|
834
836
|
// architectural facts and past review findings written in prose about the
|
|
835
837
|
// repos they were collected from. On a corporate checkout that is concrete
|
|
836
838
|
// information about the codebase, and it survived even --all-data.
|
|
839
|
+
//
|
|
840
|
+
// The knowledge root is the same class of data and was missing for the same
|
|
841
|
+
// reason: architecture.md, patterns.md, gotchas.md and decisions.md are prose
|
|
842
|
+
// about a private codebase, and code-graph.json is every symbol name in it.
|
|
843
|
+
// Leaving that behind after "remove everything" is the worst version of this
|
|
844
|
+
// bug, because the graph is the most complete of the three.
|
|
837
845
|
if (allData && HOME) {
|
|
838
846
|
console.log("");
|
|
839
|
-
console.log(" [Full cleanup] Removing pipeline settings, logs +
|
|
847
|
+
console.log(" [Full cleanup] Removing pipeline settings, logs, memory + knowledge...");
|
|
840
848
|
preservePrefsCredentialMapping(join(HOME, ".claude", "multi-agent-preferences.json"));
|
|
841
849
|
rmIfExists(join(HOME, ".claude", "logs", "multi-agent"));
|
|
842
850
|
rmIfExists(join(HOME, ".claude", "memory", "multi-agent"));
|
|
851
|
+
rmIfExists(join(HOME, ".claude", "knowledge"));
|
|
843
852
|
}
|
|
844
853
|
|
|
845
854
|
// Legacy adapter-file cleanup (adapters removed in v10.7.0). These blocks
|
|
@@ -917,7 +926,7 @@ export async function main() {
|
|
|
917
926
|
if (!allData) {
|
|
918
927
|
console.log("");
|
|
919
928
|
console.log(
|
|
920
|
-
" Kept your pipeline settings, logs
|
|
929
|
+
" Kept your pipeline settings, logs, per-repo memory + knowledge base. To remove those too, re-run with --all-data.",
|
|
921
930
|
);
|
|
922
931
|
} else {
|
|
923
932
|
console.log("");
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// validate-code-graph.mjs - schema validator for graph-build.mjs output.
|
|
3
|
+
//
|
|
4
|
+
// Zero deps (ADR-0004). Reads JSON from path or STDIN, validates against
|
|
5
|
+
// pipeline/schemas/code-graph.schema.json.
|
|
6
|
+
//
|
|
7
|
+
// Edge endpoints are checked against the node set, not just for type: a graph
|
|
8
|
+
// whose edges point at ids no node declares still parses as valid JSON, but
|
|
9
|
+
// every traversal built on it silently drops those hops.
|
|
10
|
+
//
|
|
11
|
+
// Exit codes:
|
|
12
|
+
// 0 - valid
|
|
13
|
+
// 1 - invalid
|
|
14
|
+
// 64 - usage error
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
|
|
18
|
+
const ALLOWED_STACK = new Set(["ios", "android", "node", "python", "go"]);
|
|
19
|
+
const ALLOWED_NODE_KIND = new Set(["file", "symbol", "module"]);
|
|
20
|
+
const ALLOWED_EDGE_KIND = new Set(["defines", "imports", "references"]);
|
|
21
|
+
const MAX_REPORTED_DANGLING = 5;
|
|
22
|
+
|
|
23
|
+
function readInput() {
|
|
24
|
+
const arg = process.argv[2];
|
|
25
|
+
if (!arg) {
|
|
26
|
+
console.error("error: provide a path or '-' for STDIN");
|
|
27
|
+
process.exit(64);
|
|
28
|
+
}
|
|
29
|
+
if (arg === "-") {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
process.stdin.setEncoding("utf-8");
|
|
33
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
34
|
+
process.stdin.on("end", () => resolve(chunks.join("")));
|
|
35
|
+
process.stdin.on("error", reject);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return Promise.resolve(readFileSync(arg, "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateStats(obj, errors) {
|
|
42
|
+
if (typeof obj.stats !== "object" || obj.stats === null || Array.isArray(obj.stats)) {
|
|
43
|
+
errors.push("stats must be an object");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const key of ["files", "nodes", "edges"]) {
|
|
47
|
+
if (!Number.isInteger(obj.stats[key]) || obj.stats[key] < 0) {
|
|
48
|
+
errors.push(`stats.${key} must be a non-negative integer`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(obj.nodes) && obj.stats.nodes !== obj.nodes.length) {
|
|
52
|
+
errors.push(
|
|
53
|
+
`stats.nodes (${obj.stats.nodes}) does not match nodes.length (${obj.nodes.length})`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(obj.edges) && obj.stats.edges !== obj.edges.length) {
|
|
57
|
+
errors.push(
|
|
58
|
+
`stats.edges (${obj.stats.edges}) does not match edges.length (${obj.edges.length})`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateNodes(obj, errors) {
|
|
64
|
+
if (!Array.isArray(obj.nodes)) {
|
|
65
|
+
errors.push("nodes must be an array");
|
|
66
|
+
return new Set();
|
|
67
|
+
}
|
|
68
|
+
const ids = new Set();
|
|
69
|
+
obj.nodes.forEach((n, i) => {
|
|
70
|
+
const p = `nodes[${i}]`;
|
|
71
|
+
if (typeof n !== "object" || n === null) {
|
|
72
|
+
errors.push(`${p} must be an object`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (typeof n.id !== "string" || n.id.length === 0) errors.push(`${p}.id must be a string`);
|
|
76
|
+
else if (ids.has(n.id)) errors.push(`${p}.id is duplicated: ${n.id}`);
|
|
77
|
+
else ids.add(n.id);
|
|
78
|
+
if (!ALLOWED_NODE_KIND.has(n.kind))
|
|
79
|
+
errors.push(`${p}.kind must be one of ${[...ALLOWED_NODE_KIND].join(",")}`);
|
|
80
|
+
if (typeof n.name !== "string") errors.push(`${p}.name must be a string`);
|
|
81
|
+
if (!Number.isInteger(n.degree) || n.degree < 0)
|
|
82
|
+
errors.push(`${p}.degree must be a non-negative integer`);
|
|
83
|
+
});
|
|
84
|
+
return ids;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateEdges(obj, ids, errors) {
|
|
88
|
+
if (!Array.isArray(obj.edges)) {
|
|
89
|
+
errors.push("edges must be an array");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const dangling = [];
|
|
93
|
+
obj.edges.forEach((e, i) => {
|
|
94
|
+
const p = `edges[${i}]`;
|
|
95
|
+
if (typeof e !== "object" || e === null) {
|
|
96
|
+
errors.push(`${p} must be an object`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (typeof e.from !== "string") errors.push(`${p}.from must be a string`);
|
|
100
|
+
if (typeof e.to !== "string") errors.push(`${p}.to must be a string`);
|
|
101
|
+
if (!ALLOWED_EDGE_KIND.has(e.kind))
|
|
102
|
+
errors.push(`${p}.kind must be one of ${[...ALLOWED_EDGE_KIND].join(",")}`);
|
|
103
|
+
if (ids.size) {
|
|
104
|
+
if (typeof e.from === "string" && !ids.has(e.from)) dangling.push(`${p}.from -> ${e.from}`);
|
|
105
|
+
if (typeof e.to === "string" && !ids.has(e.to)) dangling.push(`${p}.to -> ${e.to}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (dangling.length) {
|
|
109
|
+
errors.push(
|
|
110
|
+
`${dangling.length} edge endpoint(s) reference no node: ` +
|
|
111
|
+
dangling.slice(0, MAX_REPORTED_DANGLING).join("; ") +
|
|
112
|
+
(dangling.length > MAX_REPORTED_DANGLING ? "; ..." : ""),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validateManifest(obj, errors) {
|
|
118
|
+
if (obj.manifest === undefined) return;
|
|
119
|
+
if (typeof obj.manifest !== "object" || obj.manifest === null || Array.isArray(obj.manifest)) {
|
|
120
|
+
errors.push("manifest must be an object");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const [path, entry] of Object.entries(obj.manifest)) {
|
|
124
|
+
if (typeof entry !== "object" || entry === null) {
|
|
125
|
+
errors.push(`manifest["${path}"] must be an object`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (!Number.isInteger(entry.size) || entry.size < 0)
|
|
129
|
+
errors.push(`manifest["${path}"].size must be a non-negative integer`);
|
|
130
|
+
if (!Number.isInteger(entry.mtimeMs) || entry.mtimeMs < 0)
|
|
131
|
+
errors.push(`manifest["${path}"].mtimeMs must be a non-negative integer`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function validate(obj) {
|
|
136
|
+
const errors = [];
|
|
137
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj))
|
|
138
|
+
return ["root must be an object"];
|
|
139
|
+
|
|
140
|
+
if (obj.schemaVersion !== "1.0.0") errors.push(`schemaVersion must be "1.0.0"`);
|
|
141
|
+
if (!ALLOWED_STACK.has(obj.stack))
|
|
142
|
+
errors.push(`stack must be one of ${[...ALLOWED_STACK].join(",")}`);
|
|
143
|
+
if (typeof obj.root !== "string" || obj.root.length === 0)
|
|
144
|
+
errors.push("root must be a non-empty string");
|
|
145
|
+
if (typeof obj.generatedAt !== "string" || Number.isNaN(Date.parse(obj.generatedAt)))
|
|
146
|
+
errors.push("generatedAt must be an ISO 8601 timestamp");
|
|
147
|
+
if (obj.baseCommit !== undefined && obj.baseCommit !== null && typeof obj.baseCommit !== "string")
|
|
148
|
+
errors.push("baseCommit must be a string or null");
|
|
149
|
+
|
|
150
|
+
validateStats(obj, errors);
|
|
151
|
+
const ids = validateNodes(obj, errors);
|
|
152
|
+
validateEdges(obj, ids, errors);
|
|
153
|
+
validateManifest(obj, errors);
|
|
154
|
+
|
|
155
|
+
return errors;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
(async () => {
|
|
159
|
+
const raw = await readInput();
|
|
160
|
+
let obj;
|
|
161
|
+
try {
|
|
162
|
+
obj = JSON.parse(raw);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.log(JSON.stringify({ ok: false, errors: [`JSON parse error: ${e.message}`] }));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const errors = validate(obj);
|
|
168
|
+
if (errors.length === 0) {
|
|
169
|
+
console.log(JSON.stringify({ ok: true, errors: [] }));
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
console.log(JSON.stringify({ ok: false, errors }));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
})();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"skillCount":
|
|
3
|
+
"skillCount": 211,
|
|
4
4
|
"entries": [
|
|
5
5
|
{
|
|
6
6
|
"name": "accessibility-compliance-accessibility-audit",
|
|
@@ -950,7 +950,7 @@
|
|
|
950
950
|
},
|
|
951
951
|
{
|
|
952
952
|
"name": "multi-agent",
|
|
953
|
-
"description": "Task orchestrator: runs the full pipeline from a Jira ID or GitHub Issue URL - analysis → plan → TDD development → parallel review (Fable + Opus + Sonnet on Claude Code, GPT + Opus + Sonnet on Copilot CLI) → commit → log. Every step is written to agent-log.md. Use when given a Jira ID, a GitHub
|
|
953
|
+
"description": "Task orchestrator: runs the full pipeline from a Jira ID or GitHub Issue URL - analysis → plan → TDD development → parallel review (Fable + Opus + Sonnet on Claude Code, GPT + Opus + Sonnet on Copilot CLI) → commit → log. Every step is written to agent-log.md. Use when given a Jira ID, a GitHub is",
|
|
954
954
|
"platform": null,
|
|
955
955
|
"group": "core",
|
|
956
956
|
"plugin": null,
|
|
@@ -1135,6 +1135,17 @@
|
|
|
1135
1135
|
"triggerPaths": [],
|
|
1136
1136
|
"relativePath": "shared/core/multi-agent-garbage-collect/SKILL.md"
|
|
1137
1137
|
},
|
|
1138
|
+
{
|
|
1139
|
+
"name": "multi-agent-graph",
|
|
1140
|
+
"description": "Build and query this repo's code graph: a deterministic, LLM-free map of symbols, imports and references used to narrow Phase 1's Explore scope and to keep the knowledge base current. Read-only on code, costs no API tokens. Use when a task needs to know where something lives or what depends on it be",
|
|
1141
|
+
"platform": null,
|
|
1142
|
+
"group": "core",
|
|
1143
|
+
"plugin": null,
|
|
1144
|
+
"invokeAs": "multi-agent-graph",
|
|
1145
|
+
"triggerKeywords": [],
|
|
1146
|
+
"triggerPaths": [],
|
|
1147
|
+
"relativePath": "shared/core/multi-agent-graph/SKILL.md"
|
|
1148
|
+
},
|
|
1138
1149
|
{
|
|
1139
1150
|
"name": "multi-agent-help",
|
|
1140
1151
|
"description": "Multi-agent pipeline usage guide - renders in EN or TR per prefs.global.outputLanguage (falls back to promptLanguage for backward compatibility). Use when asked what the pipeline can do, which mode to pick, or what a command means.",
|
|
@@ -1313,7 +1324,7 @@
|
|
|
1313
1324
|
},
|
|
1314
1325
|
{
|
|
1315
1326
|
"name": "multi-agent-review",
|
|
1316
|
-
"description": "Run parallel review on a branch diff or a Pull Request: 3 models on Claude Code (Fable + Opus + Sonnet), 3 models on Copilot CLI (GPT + Opus + Sonnet). On PR input, posts per-finding inline comments + approve/needs-work. With no input (interactive), lists open GitHub + Bitbucket PRs to multi-select.
|
|
1327
|
+
"description": "Run parallel review on a branch diff or a Pull Request: 3 models on Claude Code (Fable + Opus + Sonnet), 3 models on Copilot CLI (GPT + Opus + Sonnet). On PR input, posts per-finding inline comments + approve/needs-work. With no input (interactive), lists open GitHub + Bitbucket PRs to multi-select.",
|
|
1317
1328
|
"platform": null,
|
|
1318
1329
|
"group": "core",
|
|
1319
1330
|
"plugin": null,
|