@mmerterden/multi-agent-pipeline 16.13.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 +11 -0
- package/package.json +1 -1
- package/pipeline/multi-agent-refs/analysis/evidence.md +21 -0
- package/pipeline/multi-agent-refs/features/code-graph.md +8 -1
- package/pipeline/schemas/code-graph.schema.json +1 -1
- package/pipeline/scripts/_code-graph.mjs +28 -1
- package/pipeline/scripts/code-graph-rules/go.json +124 -0
- package/pipeline/scripts/test-gap-rules/go.json +24 -0
- package/pipeline/scripts/validate-code-graph.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,17 @@ Internal file-layout changes that don't affect the slash-command surface are sti
|
|
|
16
16
|
|
|
17
17
|
## [Unreleased]
|
|
18
18
|
|
|
19
|
+
## [16.14.0] - 2026-08-28
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Go joins the code graph, and `web` / `frontend` resolve to the node rules.** Phase 1 has detected Go since the stack table existed, while both rule layers rejected it: there was no `test-gap-rules/go.json` either, so `/multi-agent:test "test-gap"` was equally blind to a Go repo. Both files now exist. The alias is the other half of the same gap: the rule files are named after the toolchain (`node`) and the rest of the pipeline names the product surface (`web`), so a caller typing the word they were last shown got "unsupported". `backend` is deliberately NOT an alias - it could be node, python or go, and the error names all three rather than guessing one.
|
|
24
|
+
- **Analysis ranks its repo-evidence candidates with the graph (opt-in).** The reuse-first scan (Locked 11) caps its candidate set at 200 files and warns when a generic feature name blows past it. `head -200` truncates in grep's walk order, so the cap was dropping an arbitrary file, not the least relevant one. With `prefs.global.codeGraph.enabled`, a graph query over the feature name sorts the set first. It is a sort key, never a filter: a file grep found and the graph did not rank is still scanned, because a regex extractor's silence is not evidence of absence. The 13 buckets, the cap and the no-graph behaviour are unchanged.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **The code-graph ref did not say to build when no graph exists.** It said "build only when `baseCommit` no longer matches HEAD", which has no answer when there is nothing to compare. That is the case that matters: the knowledge base's prose files are written in Phase 7, the phase a run is least likely to reach, so a repo can carry a long task history and an empty knowledge directory. The graph must not inherit that dependency, and `--status` exiting 1 on a missing file now explicitly means build.
|
|
29
|
+
|
|
19
30
|
## [16.13.0] - 2026-08-28
|
|
20
31
|
|
|
21
32
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-pipeline",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.14.0",
|
|
4
4
|
"description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -75,6 +75,27 @@ grep -rilE "$slugs" $WHITELIST --include="*.swift" --include="*.kt" --include="*
|
|
|
75
75
|
|
|
76
76
|
If `wc -l < $CANDIDATES` exceeds 200, record `fetchWarnings += "feature-name too generic; candidate cap hit"` in `repoEvidence[<repo>]`.
|
|
77
77
|
|
|
78
|
+
**Ranking the candidates (opt-in, `prefs.global.codeGraph.enabled`).** `head -200`
|
|
79
|
+
truncates in grep's walk order, so the cap drops an arbitrary file rather than the
|
|
80
|
+
least relevant one - the failure the warning above names and cannot fix. When a
|
|
81
|
+
graph exists for the repo, rank the set first:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
GRAPH_PATH="$HOME/.claude/knowledge/$(basename "$REPO_ROOT")/code-graph.json"
|
|
85
|
+
node $HOME/.claude/scripts/graph-report.mjs --graph "$GRAPH_PATH" --status \
|
|
86
|
+
|| node $HOME/.claude/scripts/graph-build.mjs --root "$REPO_ROOT" --stack "$STACK" --json
|
|
87
|
+
node $HOME/.claude/scripts/graph-query.mjs "$featureName" --graph "$GRAPH_PATH" --json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Ranked paths go to the front of `$CANDIDATES`, grep-only hits keep their order
|
|
91
|
+
behind them, everything else is unchanged, and a repo with no graph, an
|
|
92
|
+
unsupported stack or a failing validator gets exactly today's order. It is a sort
|
|
93
|
+
key, never a filter: a file grep found and the graph did not rank is still
|
|
94
|
+
scanned, because a regex extractor's silence is not evidence of absence. Sorting
|
|
95
|
+
rather than detecting is where each tool measured better - the graph beats grep on
|
|
96
|
+
domain words, a feature name is domain words, and it loses on the exact symbols
|
|
97
|
+
the bucket patterns use (`docs/adr/0010-own-code-graph.md`).
|
|
98
|
+
|
|
78
99
|
**13 buckets** (each scanned in parallel with a 30 s timeout per bucket; timeout marks the bucket as `partial`, never blocks the others):
|
|
79
100
|
|
|
80
101
|
| Bucket | Detection (iOS Swift sketch) | Cross-platform variant |
|
|
@@ -29,7 +29,14 @@ report scripts default it from the CWD's basename, and a run happens in a
|
|
|
29
29
|
worktree whose basename need not equal the repo's, so a defaulted path can point
|
|
30
30
|
at a graph that was never built. Pass it.
|
|
31
31
|
|
|
32
|
-
Build
|
|
32
|
+
Build when there is no graph at all, and when `baseCommit` no longer matches
|
|
33
|
+
HEAD. The missing case is the one that matters in practice: `architecture.md`
|
|
34
|
+
and its siblings are written in Phase 7, which is the phase a run is least
|
|
35
|
+
likely to reach, so a repo can have a long history of tasks and an empty
|
|
36
|
+
knowledge directory. The graph must not inherit that. `--status` exits 1 on a
|
|
37
|
+
missing file, and that exit means build, not skip.
|
|
38
|
+
|
|
39
|
+
A build is not usable until
|
|
33
40
|
`validate-code-graph.mjs` exits 0: a graph whose edges point at missing nodes
|
|
34
41
|
truncates traversals silently, so a non-zero exit means skip the injection and
|
|
35
42
|
run Explore as if no graph existed.
|
|
@@ -37,6 +37,27 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
37
37
|
|
|
38
38
|
export const GRAPH_SCHEMA_VERSION = "1.0.0";
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Stack words the pipeline uses elsewhere, mapped to the rule-file id.
|
|
42
|
+
*
|
|
43
|
+
* The rule files are named after the toolchain (`node`), but the rest of the
|
|
44
|
+
* pipeline names the product surface (`web`), and a caller reasonably types
|
|
45
|
+
* whichever one they were last shown. Aliasing is safe only where the mapping
|
|
46
|
+
* is one-to-one: `backend` is deliberately absent, because it could be node,
|
|
47
|
+
* python or go and picking one would be a guess dressed as support.
|
|
48
|
+
*/
|
|
49
|
+
export const STACK_ALIASES = { web: "node", frontend: "node" };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Canonical rule-file id for a stack word.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} stack
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function resolveStack(stack) {
|
|
58
|
+
return STACK_ALIASES[stack] || stack;
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* Load and merge the two rule files for a stack.
|
|
42
63
|
*
|
|
@@ -48,9 +69,15 @@ export const GRAPH_SCHEMA_VERSION = "1.0.0";
|
|
|
48
69
|
* @param {string} [baseDir] - directory holding the two rule dirs
|
|
49
70
|
* @returns {object} merged rules
|
|
50
71
|
*/
|
|
51
|
-
export function loadRules(
|
|
72
|
+
export function loadRules(requested, baseDir = __dirname) {
|
|
73
|
+
const stack = resolveStack(requested);
|
|
52
74
|
const gapPath = join(baseDir, "test-gap-rules", `${stack}.json`);
|
|
53
75
|
const graphPath = join(baseDir, "code-graph-rules", `${stack}.json`);
|
|
76
|
+
if (requested === "backend") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"'backend' names a role, not a toolchain: pass node, python or go so the right rules load",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
54
81
|
if (!existsSync(gapPath)) throw new Error(`no test-gap rules for stack '${stack}': ${gapPath}`);
|
|
55
82
|
if (!existsSync(graphPath))
|
|
56
83
|
throw new Error(`no code-graph rules for stack '${stack}': ${graphPath}`);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"stack": "go",
|
|
4
|
+
"description": "Go code-graph rules. Adds what the graph needs on top of test-gap-rules/go.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants unexported declarations too, because Phase 1 narrows scope on the whole package, not just its exported surface.",
|
|
5
|
+
"comments": {
|
|
6
|
+
"line": [
|
|
7
|
+
"//"
|
|
8
|
+
],
|
|
9
|
+
"block": [
|
|
10
|
+
[
|
|
11
|
+
"/*",
|
|
12
|
+
"*/"
|
|
13
|
+
]
|
|
14
|
+
],
|
|
15
|
+
"string": [
|
|
16
|
+
"`",
|
|
17
|
+
"\"",
|
|
18
|
+
"'"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"importSpecifiersAreStrings": true,
|
|
22
|
+
"definitionPatterns": [
|
|
23
|
+
{
|
|
24
|
+
"id": "struct",
|
|
25
|
+
"kind": "struct",
|
|
26
|
+
"regex": "\\btype\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+struct\\b"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "interface",
|
|
30
|
+
"kind": "interface",
|
|
31
|
+
"regex": "\\btype\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+interface\\b"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "type",
|
|
35
|
+
"kind": "type",
|
|
36
|
+
"regex": "\\btype\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+(?!struct\\b|interface\\b)[A-Za-z_*\\[]"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "func",
|
|
40
|
+
"kind": "func",
|
|
41
|
+
"regex": "^func\\s+(?:\\([^)]*\\)\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\s*[(\\[]"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"importPatterns": [
|
|
45
|
+
{
|
|
46
|
+
"id": "specifier",
|
|
47
|
+
"regex": "^[ \\t]*(?:import[ \\t]+)?(?:[A-Za-z_.][A-Za-z0-9_]*[ \\t]+)?\"(?:[^\"]*/)?([A-Za-z_][A-Za-z0-9_.-]*)\"[ \\t]*$"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"referenceKinds": [
|
|
51
|
+
"struct",
|
|
52
|
+
"interface",
|
|
53
|
+
"type"
|
|
54
|
+
],
|
|
55
|
+
"referenceKindsNote": "func is deliberately absent, for the reason the Swift, Kotlin, Node and Python rules all record: a bare name matched across files is almost never a call to that exact declaration, and Go's short-name convention (New, Run, Get, Close, Error) makes it worse than most. Functions still reach the graph through their defines edge, so they stay findable by name.",
|
|
56
|
+
"ignoredIdentifiers": [
|
|
57
|
+
"package",
|
|
58
|
+
"import",
|
|
59
|
+
"func",
|
|
60
|
+
"type",
|
|
61
|
+
"struct",
|
|
62
|
+
"interface",
|
|
63
|
+
"map",
|
|
64
|
+
"chan",
|
|
65
|
+
"var",
|
|
66
|
+
"const",
|
|
67
|
+
"return",
|
|
68
|
+
"if",
|
|
69
|
+
"else",
|
|
70
|
+
"for",
|
|
71
|
+
"range",
|
|
72
|
+
"switch",
|
|
73
|
+
"case",
|
|
74
|
+
"default",
|
|
75
|
+
"select",
|
|
76
|
+
"go",
|
|
77
|
+
"defer",
|
|
78
|
+
"break",
|
|
79
|
+
"continue",
|
|
80
|
+
"fallthrough",
|
|
81
|
+
"goto",
|
|
82
|
+
"nil",
|
|
83
|
+
"true",
|
|
84
|
+
"false",
|
|
85
|
+
"iota",
|
|
86
|
+
"make",
|
|
87
|
+
"new",
|
|
88
|
+
"len",
|
|
89
|
+
"cap",
|
|
90
|
+
"append",
|
|
91
|
+
"copy",
|
|
92
|
+
"delete",
|
|
93
|
+
"panic",
|
|
94
|
+
"recover",
|
|
95
|
+
"print",
|
|
96
|
+
"println",
|
|
97
|
+
"error",
|
|
98
|
+
"string",
|
|
99
|
+
"bool",
|
|
100
|
+
"byte",
|
|
101
|
+
"rune",
|
|
102
|
+
"int",
|
|
103
|
+
"int8",
|
|
104
|
+
"int16",
|
|
105
|
+
"int32",
|
|
106
|
+
"int64",
|
|
107
|
+
"uint",
|
|
108
|
+
"uint8",
|
|
109
|
+
"uint16",
|
|
110
|
+
"uint32",
|
|
111
|
+
"uint64",
|
|
112
|
+
"float32",
|
|
113
|
+
"float64",
|
|
114
|
+
"complex64",
|
|
115
|
+
"complex128",
|
|
116
|
+
"uintptr",
|
|
117
|
+
"any",
|
|
118
|
+
"comparable",
|
|
119
|
+
"err",
|
|
120
|
+
"ctx",
|
|
121
|
+
"ok"
|
|
122
|
+
],
|
|
123
|
+
"note": "A Go import path is a string literal, so `importSpecifiersAreStrings` applies exactly as it does for JavaScript: the import pass reads a text with comments stripped and strings intact, and the pattern captures the last path segment, which is the package name in the overwhelmingly common case where the two agree. The pattern is anchored to a whole line holding nothing but an optional `import`, an optional alias and the quoted path, because Go has no import keyword on the lines inside a parenthesized block. Without that anchor it matched EVERY double-quoted string in the file and turned each one into a module node. A named import (`sql \"database/sql\"`) and the handful of packages whose name differs from their directory both resolve to the directory name, which is what the engine matches against a file basename anyway. The three type patterns are ordered so `struct` and `interface` claim their shapes before the general `type` alias pattern, which excludes them by lookahead rather than by order alone - order and lookahead together, because a rule file that relies on order silently changes meaning when someone sorts it. `func` is anchored to column 0, which is where every Go function and method declaration lives; the engine's nesting rule then never has to fire for this stack.\n\nUNMEASURED: unlike ios, android, node and python, these rules were never run against a real Go codebase. There was no Go repo and no toolchain on the machine that wrote them. The smoke exercises them on a synthetic fixture, which proves they parse and produce a schema-valid graph, not that they produce a USEFUL one. Treat the first real build as the measurement, and check the hub list before trusting it."
|
|
124
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
|
|
18
|
-
const ALLOWED_STACK = new Set(["ios", "android", "node", "python"]);
|
|
18
|
+
const ALLOWED_STACK = new Set(["ios", "android", "node", "python", "go"]);
|
|
19
19
|
const ALLOWED_NODE_KIND = new Set(["file", "symbol", "module"]);
|
|
20
20
|
const ALLOWED_EDGE_KIND = new Set(["defines", "imports", "references"]);
|
|
21
21
|
const MAX_REPORTED_DANGLING = 5;
|