@mmerterden/multi-agent-pipeline 12.5.0 → 12.6.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +1 -1
  3. package/docs/features.md +20 -0
  4. package/index.js +7 -1
  5. package/install/_dev-only-files.mjs +1 -0
  6. package/package.json +4 -3
  7. package/pipeline/agents/security-auditor.md +1 -1
  8. package/pipeline/commands/archive-guard.md +5 -5
  9. package/pipeline/commands/multi-agent/SKILL.md +2 -0
  10. package/pipeline/commands/multi-agent/design-check/SKILL.md +287 -0
  11. package/pipeline/commands/multi-agent/help/SKILL.md +45 -5
  12. package/pipeline/commands/multi-agent/refactor/SKILL.md +92 -12
  13. package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
  14. package/pipeline/commands/multi-agent/sync/SKILL.md +119 -12
  15. package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
  16. package/pipeline/commands/sim-test.md +5 -5
  17. package/pipeline/lib/credential-store.sh +32 -0
  18. package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
  19. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +2 -2
  20. package/pipeline/multi-agent-refs/phases/phase-5-test.md +4 -4
  21. package/pipeline/preferences-template.json +18 -1
  22. package/pipeline/schemas/agent-state.schema.json +90 -0
  23. package/pipeline/schemas/design-check-config.schema.json +162 -0
  24. package/pipeline/schemas/migrations/state-2.0.0-to-2.1.0.mjs +30 -12
  25. package/pipeline/schemas/prefs.schema.json +161 -5
  26. package/pipeline/scripts/README.md +7 -5
  27. package/pipeline/scripts/classify-plan-safety.mjs +8 -3
  28. package/pipeline/scripts/cost-budget-check.mjs +9 -5
  29. package/pipeline/scripts/fixtures/install-layout.tsv +6 -6
  30. package/pipeline/scripts/learnings-ledger.mjs +18 -0
  31. package/pipeline/scripts/lint-mcp-refs.mjs +207 -0
  32. package/pipeline/scripts/memory-load.sh +5 -1
  33. package/pipeline/scripts/render-work-summary.sh +4 -1
  34. package/pipeline/scripts/smoke-command-inventory.sh +81 -0
  35. package/pipeline/scripts/smoke-commands-skills-parity.sh +1 -1
  36. package/pipeline/scripts/smoke-compliance-skills.sh +4 -4
  37. package/pipeline/scripts/smoke-cross-cli-behavior.sh +12 -2
  38. package/pipeline/scripts/smoke-generate-issue.sh +6 -5
  39. package/pipeline/scripts/smoke-per-repo-memory.sh +2 -2
  40. package/pipeline/scripts/smoke-review-readiness.sh +3 -2
  41. package/pipeline/scripts/smoke-schema-validation.sh +19 -5
  42. package/pipeline/scripts/smoke-shadow-git.sh +4 -2
  43. package/pipeline/scripts/triage-memory.mjs +18 -0
  44. package/pipeline/scripts/uninstall.mjs +1 -1
  45. package/pipeline/skills/.skill-manifest.json +24 -8
  46. package/pipeline/skills/.skills-index.json +39 -3
  47. package/pipeline/skills/shared/README.md +10 -6
  48. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +10 -8
  49. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +248 -0
  50. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +22 -0
  51. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +67 -11
  52. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
  53. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +51 -9
  54. package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +1 -1
  55. package/pipeline/skills/skills-index.md +8 -4
@@ -34,8 +34,10 @@
34
34
  // - infrastructure 20 - touches Dockerfile, CI yaml, Gradle wrapper,
35
35
  // package.json resolutions / engines, install.js
36
36
  //
37
- // recommendPause = true when score >= 50 (tuned: any two medium signals or
38
- // any one heavy signal flips it).
37
+ // recommendPause = true when any one heavy signal (weight >= 30) fires, OR two
38
+ // or more medium-and-above signals (weight >= 20) fire, OR the accumulated
39
+ // score reaches 50. A score threshold alone cannot express the first two
40
+ // clauses: the heaviest single rule is 35 and two medium signals can total 45.
39
41
  //
40
42
  // Exit codes: 0 = verdict produced, 1 = malformed input, 2 = usage error.
41
43
 
@@ -168,7 +170,10 @@ if (infraHits.length > 0) {
168
170
 
169
171
  // --- aggregate ---
170
172
  const score = reasons.reduce((s, r) => s + r.weight, 0);
171
- const recommendPause = score >= 50;
173
+ const heavyHits = reasons.filter((r) => r.weight >= 30);
174
+ const mediumOrAboveHits = reasons.filter((r) => r.weight >= 20);
175
+ const recommendPause =
176
+ heavyHits.length >= 1 || mediumOrAboveHits.length >= 2 || score >= 50;
172
177
 
173
178
  let summary;
174
179
  if (recommendPause) {
@@ -33,6 +33,7 @@
33
33
 
34
34
  import { readFileSync, existsSync } from "fs";
35
35
  import { dirname, join } from "path";
36
+ import { homedir } from "os";
36
37
  import { fileURLToPath } from "url";
37
38
  import { costUsd } from "./_cost.mjs";
38
39
 
@@ -103,10 +104,11 @@ function resolveTracker() {
103
104
  const id = flags["task-id"];
104
105
  if (!id) die("either --tracker or --task-id is required");
105
106
  const bare = String(id).replace(/^#/, "");
107
+ const logRoot = join(homedir(), ".claude", "logs", "multi-agent");
106
108
  for (const c of [
107
- join(process.cwd(), ".worktrees", String(id), "phase-tracker.json"),
108
- join(process.cwd(), ".worktrees", bare, "phase-tracker.json"),
109
- join(process.cwd(), ".worktrees", `task-${bare}`, "phase-tracker.json"),
109
+ join(logRoot, String(id), "tracker-state.json"),
110
+ join(logRoot, bare, "tracker-state.json"),
111
+ join(logRoot, `task-${bare}`, "tracker-state.json"),
110
112
  ]) {
111
113
  if (existsSync(c)) return c;
112
114
  }
@@ -134,15 +136,17 @@ if (!rate) die(`no price for model "${cfg.pricingModel}" in cost-table.json`);
134
136
 
135
137
  let tokensIn = 0;
136
138
  let tokensOut = 0;
139
+ let tokensCached = 0;
137
140
  for (const p of Object.values(tracker.phases || {})) {
138
141
  tokensIn += Number(p?.tokens_in || 0);
139
142
  tokensOut += Number(p?.tokens_out || 0);
143
+ tokensCached += Number(p?.tokens_cached || 0);
140
144
  }
141
145
 
142
- const usd = costUsd(rate, tokensIn, tokensOut);
146
+ const usd = costUsd(rate, tokensIn, tokensOut, tokensCached);
143
147
  const pctOfMax = (usd / cfg.maxUsd) * 100;
144
148
  const usdStr = `$${usd.toFixed(2)}`;
145
- const totalTok = tokensIn + tokensOut;
149
+ const totalTok = tokensIn + tokensOut + tokensCached;
146
150
 
147
151
  const base =
148
152
  `cost-budget: ${usdStr} of $${cfg.maxUsd.toFixed(2)} ` +
@@ -1,19 +1,19 @@
1
1
  .claude/.pipeline-version 1
2
2
  .claude/CLAUDE.md 1
3
3
  .claude/agents 8
4
- .claude/commands 47
4
+ .claude/commands 48
5
5
  .claude/lib 27
6
6
  .claude/multi-agent-preferences.json 1
7
7
  .claude/multi-agent-refs 52
8
8
  .claude/rules 12
9
- .claude/schemas 24
10
- .claude/scripts 199
9
+ .claude/schemas 25
10
+ .claude/scripts 201
11
11
  .claude/settings.json 1
12
12
  .claude/skills 428
13
13
  .copilot/.pipeline-version 1
14
14
  .copilot/agents 8
15
15
  .copilot/copilot-instructions.md 1
16
16
  .copilot/lib 27
17
- .copilot/schemas 24
18
- .copilot/scripts 199
19
- .copilot/skills 470
17
+ .copilot/schemas 25
18
+ .copilot/scripts 201
19
+ .copilot/skills 471
@@ -74,9 +74,27 @@ function die(msg, code = 1) {
74
74
  process.exit(code);
75
75
  }
76
76
 
77
+ // Resolves to the MAIN repository name even when called from inside a pipeline
78
+ // worktree. `--show-toplevel` returns the worktree root, whose basename is the
79
+ // task id, which would shard the per-repo ledger one directory per task and make
80
+ // the learning replay always miss. `--git-common-dir` points at the main
81
+ // repository's .git for every linked worktree.
77
82
  function repoSlug() {
78
83
  if (flags["repo-slug"]) return String(flags["repo-slug"]);
79
84
  if (process.env.MULTI_AGENT_REPO_SLUG) return process.env.MULTI_AGENT_REPO_SLUG;
85
+ try {
86
+ const commonDir = execFileSync(
87
+ "git",
88
+ ["rev-parse", "--path-format=absolute", "--git-common-dir"],
89
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
90
+ ).trim();
91
+ if (commonDir) {
92
+ const base = basename(commonDir);
93
+ const root = base === ".git" ? dirname(commonDir) : commonDir.replace(/\.git$/, "");
94
+ const slug = basename(root);
95
+ if (slug) return slug;
96
+ }
97
+ } catch { /* not a git repo, or git too old for --path-format */ }
80
98
  try {
81
99
  const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
82
100
  encoding: "utf8",
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ // lint-mcp-refs.mjs - every mcp__<server>__<tool> reference must be callable.
3
+ //
4
+ // A host composes an MCP tool name as mcp__<registered-server-name>__<tool>.
5
+ // The pipeline had 100 references to `mcp__dev_toolkit__*` while the server is
6
+ // registered as `dev-toolkit`, so the real names are `mcp__dev-toolkit__*`.
7
+ // Nothing caught it: prose is not executed, and a wrong name inside an
8
+ // `allowed-tools:` allowlist silently denies the tool instead of erroring.
9
+ //
10
+ // Checks (errors - fail the gate):
11
+ // 1. the server segment must be a known registered server name
12
+ // 2. the known-bad snake_case spellings are called out with their fix
13
+ //
14
+ // Optional deep check (advisory - never fails):
15
+ // 3. when the companion dev-toolkit repo resolves locally (via
16
+ // prefs.global.devToolkit.localPath, or the mcpServers registration in
17
+ // ~/.claude.json / ~/.claude/settings.json), handshake it over stdio and
18
+ // confirm every referenced dev-toolkit tool actually exists in tools/list.
19
+ // CI has no local checkout, so an unresolvable repo is a skip, not a fail.
20
+ //
21
+ // Zero-dependency. Runs inside `npm test` and ci-lite.
22
+ //
23
+ // Usage: node pipeline/scripts/lint-mcp-refs.mjs [--strict-tools]
24
+ // Exit: 0 clean, 1 unknown server segment (or a missing tool with --strict-tools).
25
+
26
+ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
27
+ import { homedir } from "node:os";
28
+ import { dirname, join, resolve } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+ import { spawnSync } from "node:child_process";
31
+
32
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
33
+ const strictTools = process.argv.includes("--strict-tools");
34
+
35
+ // Servers the pipeline is allowed to reference. The key is the registered
36
+ // server name as it appears in an mcpServers config, which is what the host
37
+ // interpolates into the tool name.
38
+ const KNOWN_SERVERS = {
39
+ "dev-toolkit": "companion device/browser toolkit (@mmerterden/dev-toolkit-mcp)",
40
+ claude_ai_Figma: "Figma MCP (analysis phase only)",
41
+ XcodeBuildMCP: "optional third-party Xcode build server",
42
+ };
43
+ // Spellings that look right but cannot resolve to a registered server.
44
+ const KNOWN_BAD = { dev_toolkit: "dev-toolkit", "claude-ai-Figma": "claude_ai_Figma" };
45
+
46
+ const SCAN_DIRS = ["pipeline/commands", "pipeline/skills", "pipeline/multi-agent-refs"];
47
+ const SCAN_EXT = /\.(md|sh|mjs|json)$/;
48
+ const PRUNE = new Set(["node_modules", ".git", ".worktrees", "Pods", ".build", "DerivedData", ".next"]);
49
+
50
+ function walk(dir, out = []) {
51
+ let entries;
52
+ try {
53
+ entries = readdirSync(dir, { withFileTypes: true });
54
+ } catch {
55
+ return out;
56
+ }
57
+ for (const e of entries) {
58
+ if (PRUNE.has(e.name)) continue;
59
+ const p = join(dir, e.name);
60
+ if (e.isDirectory()) walk(p, out);
61
+ else if (SCAN_EXT.test(e.name)) out.push(p);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ const files = SCAN_DIRS.flatMap((d) => walk(join(repoRoot, d)));
67
+ const REF = /mcp__([A-Za-z0-9_-]+?)__([a-z0-9_]+)/g;
68
+
69
+ const errors = [];
70
+ const refs = new Map(); // server -> Map(tool -> Set(file:line))
71
+
72
+ for (const file of files) {
73
+ const rel = file.slice(repoRoot.length + 1);
74
+ const lines = readFileSync(file, "utf8").split("\n");
75
+ lines.forEach((line, i) => {
76
+ for (const m of line.matchAll(REF)) {
77
+ const [, server, tool] = m;
78
+ // Prose writes families as mcp__dev-toolkit__ios_* - a wildcard, not a tool.
79
+ const next = line[m.index + m[0].length];
80
+ if (next === "*" || tool.endsWith("_")) continue;
81
+ const where = `${rel}:${i + 1}`;
82
+ if (!refs.has(server)) refs.set(server, new Map());
83
+ const tools = refs.get(server);
84
+ if (!tools.has(tool)) tools.set(tool, new Set());
85
+ tools.get(tool).add(where);
86
+ }
87
+ });
88
+ }
89
+
90
+ for (const [server, tools] of refs) {
91
+ if (server in KNOWN_SERVERS) continue;
92
+ const fix = KNOWN_BAD[server];
93
+ const sample = [...tools.values()][0].values().next().value;
94
+ errors.push(
95
+ fix
96
+ ? `unknown MCP server segment "${server}" (${tools.size} tool refs, e.g. ${sample}) - the registered server is "${fix}", so the prefix must be mcp__${fix}__`
97
+ : `unknown MCP server segment "${server}" (${tools.size} tool refs, e.g. ${sample}) - add it to KNOWN_SERVERS in lint-mcp-refs.mjs if this server is expected, or fix the prefix to match the registered server name`,
98
+ );
99
+ }
100
+
101
+ // ---- optional deep check: do the referenced dev-toolkit tools exist? --------
102
+
103
+ function resolveToolkitRepo() {
104
+ const prefsPath = join(homedir(), ".claude", "multi-agent-preferences.json");
105
+ if (existsSync(prefsPath)) {
106
+ try {
107
+ const dt = JSON.parse(readFileSync(prefsPath, "utf8"))?.global?.devToolkit;
108
+ if (dt && dt.enabled === false) return null;
109
+ if (dt?.localPath) {
110
+ const p = dt.localPath.replace(/^~|\$HOME/, homedir());
111
+ if (existsSync(join(p, "package.json"))) return p;
112
+ }
113
+ } catch {
114
+ /* fall through to registration lookup */
115
+ }
116
+ }
117
+ const candidates = [];
118
+ const collect = (obj) => {
119
+ for (const [name, cfg] of Object.entries(obj || {})) {
120
+ const arg = Array.isArray(cfg?.args) ? cfg.args.find((a) => /\.(mjs|js)$/.test(a)) : null;
121
+ if (name.includes("dev") && arg) candidates.push(dirname(resolve(arg)));
122
+ }
123
+ };
124
+ for (const cfgPath of [join(homedir(), ".claude.json"), join(homedir(), ".claude", "settings.json")]) {
125
+ if (!existsSync(cfgPath)) continue;
126
+ try {
127
+ const json = JSON.parse(readFileSync(cfgPath, "utf8"));
128
+ collect(json.mcpServers);
129
+ for (const proj of Object.values(json.projects || {})) collect(proj.mcpServers);
130
+ } catch {
131
+ /* ignore unreadable config */
132
+ }
133
+ }
134
+ for (const c of candidates) {
135
+ try {
136
+ const pkg = JSON.parse(readFileSync(join(c, "package.json"), "utf8"));
137
+ if (pkg.dependencies?.["@modelcontextprotocol/sdk"] && statSync(join(c, ".git"))) return c;
138
+ } catch {
139
+ /* not the toolkit */
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function listTools(repo) {
146
+ const frames =
147
+ [
148
+ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lint-mcp-refs","version":"1"}}}',
149
+ '{"jsonrpc":"2.0","method":"notifications/initialized"}',
150
+ '{"jsonrpc":"2.0","id":2,"method":"tools/list"}',
151
+ ].join("\n") + "\n";
152
+ const r = spawnSync(process.execPath, ["index.js"], {
153
+ cwd: repo,
154
+ input: frames,
155
+ encoding: "utf8",
156
+ timeout: 20000,
157
+ maxBuffer: 32 * 1024 * 1024,
158
+ });
159
+ if (r.error || !r.stdout) return null;
160
+ for (const line of r.stdout.split("\n")) {
161
+ if (!line.trim()) continue;
162
+ try {
163
+ const msg = JSON.parse(line);
164
+ if (msg.id === 2 && msg.result?.tools) return msg.result.tools.map((t) => t.name);
165
+ } catch {
166
+ /* not a frame */
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ const referencedToolkitTools = refs.get("dev-toolkit");
173
+ let deep = "skipped (no local dev-toolkit checkout)";
174
+ if (referencedToolkitTools) {
175
+ const repo = resolveToolkitRepo();
176
+ const served = repo ? listTools(repo) : null;
177
+ if (!served) {
178
+ deep = repo ? `skipped (${repo} did not answer tools/list)` : deep;
179
+ } else {
180
+ const missing = [...referencedToolkitTools.keys()].filter((t) => !served.includes(t));
181
+ if (missing.length === 0) {
182
+ deep = `all ${referencedToolkitTools.size} referenced tools exist in the live server (${served.length} served)`;
183
+ } else {
184
+ const msg = `dev-toolkit tools referenced but not served (${served.length} served): ${missing
185
+ .map((t) => `${t} [${[...referencedToolkitTools.get(t)][0]}]`)
186
+ .join(", ")}`;
187
+ deep = `${missing.length} referenced tool(s) not served`;
188
+ if (strictTools) errors.push(msg);
189
+ else console.warn(` WARNING - ${msg}`);
190
+ }
191
+ }
192
+ }
193
+
194
+ const totalRefs = [...refs.values()].reduce((n, tools) => n + tools.size, 0);
195
+ console.log(
196
+ `lint-mcp-refs: ${totalRefs} distinct tool references across ${refs.size} server(s) in ${files.length} files`,
197
+ );
198
+ for (const [server, tools] of refs) {
199
+ console.log(` ${server in KNOWN_SERVERS ? "✓" : "✗"} mcp__${server}__* : ${tools.size} tools`);
200
+ }
201
+ console.log(` tool-existence check: ${deep}`);
202
+
203
+ if (errors.length > 0) {
204
+ console.error(`lint-mcp-refs: ${errors.length} error(s)`);
205
+ for (const e of errors) console.error(" " + e);
206
+ process.exit(1);
207
+ }
@@ -16,9 +16,13 @@ PROJECT_ROOT="${1:?usage: memory-load.sh <project-root>}"
16
16
  MEM_DIR="$PROJECT_ROOT/.multi-agent/memory"
17
17
  MEM_INDEX="$MEM_DIR/MEMORY.md"
18
18
 
19
- # Pref-gate: locate prefs.json (home-dir first, then project-specific).
19
+ # Pref-gate: locate the preferences file (home-dir first, then XDG config).
20
+ # The canonical name written by the installer is multi-agent-preferences.json;
21
+ # the bare preferences.json names are kept only as legacy fallbacks.
20
22
  prefs_file=""
21
23
  for candidate in \
24
+ "$HOME/.claude/multi-agent-preferences.json" \
25
+ "$HOME/.config/multi-agent-pipeline/multi-agent-preferences.json" \
22
26
  "$HOME/.claude/preferences.json" \
23
27
  "$HOME/.config/multi-agent-pipeline/preferences.json"
24
28
  do
@@ -126,7 +126,10 @@ fi
126
126
  changed_block=""
127
127
  files_count=0
128
128
  total_add=0; total_del=0
129
- if [ -n "$WORKTREE" ] && [ -d "$WORKTREE/.git" -o -d "$WORKTREE" ]; then
129
+ # Only the directory needs to exist here; `git rev-parse HEAD` below is the real
130
+ # repository check. Testing -d "$WORKTREE/.git" would be wrong regardless: in a
131
+ # linked worktree .git is a file, not a directory.
132
+ if [ -n "$WORKTREE" ] && [ -d "$WORKTREE" ]; then
130
133
  if git -C "$WORKTREE" rev-parse HEAD >/dev/null 2>&1; then
131
134
  diffstat=$(git -C "$WORKTREE" diff --numstat "$state_base"...HEAD 2>/dev/null || true)
132
135
  if [ -n "$diffstat" ]; then
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env bash
2
+ # smoke-command-inventory.sh - the command inventory is DERIVED, never asserted.
3
+ #
4
+ # Every count that describes "how many multi-agent commands exist" used to be a
5
+ # literal in prose (contract header, sync skill, dispatcher). Adding a command
6
+ # left those literals stale while every gate stayed green, because the gates
7
+ # asserted the literal instead of the tree. This gate computes the count from
8
+ # pipeline/commands/multi-agent/*/ and requires the prose to agree.
9
+ #
10
+ # Verifies:
11
+ # 1. contract header count == number of command directories
12
+ # 2. every command directory appears in the contract inventory block
13
+ # 3. both sync surfaces (command + Copilot skill) carry the same count twice
14
+ # ("N commands are synced" + "instructions + N sub-command skills")
15
+ # 4. every command directory appears in both sync inventory blocks
16
+ # 5. every command directory has its shared/core skill counterpart
17
+ #
18
+ # Exit: 0 all green, 1 any failure.
19
+
20
+ set -euo pipefail
21
+
22
+ HERE="$(cd "$(dirname "$0")" && pwd)"
23
+ ROOT="$(cd "$HERE/../.." && pwd)"
24
+ CMD_DIR="$ROOT/pipeline/commands/multi-agent"
25
+ CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
26
+ SYNC="$ROOT/pipeline/commands/multi-agent/sync/SKILL.md"
27
+ SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
28
+ SKILLS_DIR="$ROOT/pipeline/skills/shared/core"
29
+
30
+ PASS=0
31
+ FAIL=0
32
+ pass() { PASS=$((PASS+1)); echo " ✓ $1"; }
33
+ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
34
+
35
+ echo "══ smoke-command-inventory ══"
36
+
37
+ for f in "$CONTRACT" "$SYNC" "$SYNC_SKILL"; do
38
+ [ -f "$f" ] || { echo " ✗ missing file: ${f#$ROOT/}"; exit 1; }
39
+ done
40
+
41
+ COMMANDS=$(find "$CMD_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
42
+ N=$(printf '%s\n' "$COMMANDS" | grep -c . || true)
43
+
44
+ echo "→ 1. derived count from the command tree: $N commands"
45
+ [ "$N" -gt 0 ] && pass "found $N command directories" || fail "no command directories under ${CMD_DIR#$ROOT/}"
46
+
47
+ echo "→ 2. contract header agrees"
48
+ if grep -Fq "## 1. Command Inventory ($N commands)" "$CONTRACT"; then
49
+ pass "contract header = $N"
50
+ else
51
+ fail "contract header does not say ($N commands): $(grep -o '## 1\. Command Inventory ([0-9]* commands)' "$CONTRACT" || echo 'header not found')"
52
+ fi
53
+
54
+ echo "→ 3. sync surfaces agree (2 counts x 2 files)"
55
+ for f in "$SYNC" "$SYNC_SKILL"; do
56
+ rel="${f#$ROOT/}"
57
+ grep -Fq "**$N commands are synced**" "$f" && pass "$rel: synced count = $N" || fail "$rel: 'N commands are synced' != $N"
58
+ grep -Fq "instructions + $N sub-command skills" "$f" && pass "$rel: step-2 count = $N" || fail "$rel: 'instructions + N sub-command skills' != $N"
59
+ done
60
+
61
+ echo "→ 4. every command is listed in the contract + both sync inventories"
62
+ MISSING_LIST=0
63
+ while IFS= read -r cmd; do
64
+ [ -n "$cmd" ] || continue
65
+ for f in "$CONTRACT" "$SYNC" "$SYNC_SKILL"; do
66
+ grep -Eq "(^|[ ,])$cmd([ ,]|$)" "$f" || { fail "${f#$ROOT/}: inventory missing '$cmd'"; MISSING_LIST=1; }
67
+ done
68
+ done <<< "$COMMANDS"
69
+ [ "$MISSING_LIST" -eq 0 ] && pass "all $N commands listed on all 3 surfaces"
70
+
71
+ echo "→ 5. shared/core skill counterpart exists for every command"
72
+ MISSING_SKILL=0
73
+ while IFS= read -r cmd; do
74
+ [ -n "$cmd" ] || continue
75
+ [ -f "$SKILLS_DIR/multi-agent-$cmd/SKILL.md" ] || { fail "no shared/core skill for '$cmd'"; MISSING_SKILL=1; }
76
+ done <<< "$COMMANDS"
77
+ [ "$MISSING_SKILL" -eq 0 ] && pass "all $N commands have a shared/core skill"
78
+
79
+ echo ""
80
+ echo "══ command-inventory smoke: $PASS passed, $FAIL failed ══"
81
+ [ "$FAIL" -eq 0 ] || exit 1
@@ -36,7 +36,7 @@ fail() { FAIL=$((FAIL+1)); echo " ✗ $1"; }
36
36
  # Underscore-prefixed files (e.g. _account-picker.md) are internal fragments
37
37
  # loaded by other commands via Read; they're not invocable slash commands and
38
38
  # intentionally have no matching skill.
39
- CMDS=$(find "$CMDS_DIR" -mindepth 2 -maxdepth 2 -name "SKILL.md" -exec dirname {} \; | xargs -n1 basename | sort)
39
+ CMDS=$(find "$CMDS_DIR" -mindepth 2 -maxdepth 2 -name "SKILL.md" -exec sh -c 'for f do basename "$(dirname "$f")"; done' sh {} + | sort)
40
40
 
41
41
  # help.md in commands maps to multi-agent-help skill; the main "multi-agent"
42
42
  # skill covers the behavior documented across multiple command files, so we
@@ -5,7 +5,7 @@
5
5
  # that the 4 claimed consumer surfaces actually reference them (not just promise).
6
6
  #
7
7
  # Covers:
8
- # 1. apple-archive-compliance/SKILL.md exists, has frontmatter, 17-rule catalog,
8
+ # 1. apple-archive-compliance/SKILL.md exists, has frontmatter, 18-rule catalog,
9
9
  # EN + TR humanizer blocks, severity table, prerequisite detection.
10
10
  # 2. google-play-compliance/SKILL.md exists, has frontmatter, 21-rule catalog
11
11
  # across 4 categories, EN + TR humanizer blocks, severity table, prereq detection.
@@ -43,8 +43,8 @@ grep -q "^name: apple-archive-compliance$" "$APPLE_SKILL" && pass "apple frontma
43
43
  grep -q "^user-invocable: true$" "$APPLE_SKILL" && pass "apple user-invocable" || fail "apple user-invocable"
44
44
  grep -q "argument-hint:" "$APPLE_SKILL" && pass "apple argument-hint" || fail "apple argument-hint"
45
45
  # Count rule rows in the catalog table (excluding header + separator)
46
- APPLE_RULES=$(awk '/^## 17-Rule Catalog/,/^## Severity mapping/' "$APPLE_SKILL" | grep -cE '^\| *[0-9]+ *\|' || true)
47
- [ "$APPLE_RULES" -eq 17 ] && pass "apple catalog has 17 rule rows" || fail "apple catalog has $APPLE_RULES rule rows (want 17)"
46
+ APPLE_RULES=$(awk '/^## 18-Rule Catalog/,/^## Severity mapping/' "$APPLE_SKILL" | grep -cE '^\| *[0-9]+ *\|' || true)
47
+ [ "$APPLE_RULES" -eq 18 ] && pass "apple catalog has 18 rule rows" || fail "apple catalog has $APPLE_RULES rule rows (want 18)"
48
48
 
49
49
  echo "→ 2. apple skill humanizer blocks EN + TR + severity table"
50
50
  # v8.2 split humanizer language from picker language: gating moved from
@@ -59,7 +59,7 @@ echo "→ 3. apple skill prerequisite detection + archive resolution (v8.4.0+ us
59
59
  grep -q 'ios_app_store_audit' "$APPLE_SKILL" && pass "apple references ios_app_store_audit MCP tool" || fail "apple ios_app_store_audit reference missing"
60
60
  grep -q '.xcarchive' "$APPLE_SKILL" && pass "apple references .xcarchive" || fail "apple .xcarchive missing"
61
61
  grep -q '@mmerterden/dev-toolkit-mcp' "$APPLE_SKILL" && pass "apple references @mmerterden/dev-toolkit-mcp package" || fail "apple dev-toolkit-mcp reference missing"
62
- grep -q 'mcp__dev_toolkit__ios_app_store_audit' "$APPLE_SKILL" && pass "apple shows MCP-call invocation form" || fail "apple MCP-call invocation form missing"
62
+ grep -q 'mcp__dev-toolkit__ios_app_store_audit' "$APPLE_SKILL" && pass "apple shows MCP-call invocation form" || fail "apple MCP-call invocation form missing"
63
63
 
64
64
  echo "→ 4. google-play-compliance SKILL.md frontmatter + catalog shape"
65
65
  grep -q "^name: google-play-compliance$" "$GOOGLE_SKILL" && pass "google frontmatter name" || fail "google frontmatter name"
@@ -61,6 +61,16 @@ fi
61
61
  # ──────────────────────────────────────────────────────────────────────────
62
62
  echo ""
63
63
  echo "→ 2. No deprecated placeholders in Claude source"
64
+ # Corpus guard: the scans below can only be trusted if there is something to
65
+ # scan. Without this, an empty or missing command tree reports every pattern
66
+ # clean instead of reporting that it found nothing to check.
67
+ SKILL_COUNT=$(find "$CLAUDE_CMDS" -name SKILL.md 2>/dev/null | wc -l | tr -d ' ')
68
+ if [ "$SKILL_COUNT" -gt 0 ]; then
69
+ pass "command corpus non-empty ($SKILL_COUNT SKILL.md)"
70
+ else
71
+ fail "no SKILL.md under $CLAUDE_CMDS - placeholder and hardcode scans below are vacuous"
72
+ fi
73
+
64
74
  # Per refs/cross-cli-contract.md Section 2.5
65
75
  DEPRECATED=(
66
76
  "{github-username}"
@@ -72,7 +82,7 @@ DEPRECATED=(
72
82
  )
73
83
  for p in "${DEPRECATED[@]}"; do
74
84
  # grep -l returns filenames; empty = no matches
75
- FOUND=$(grep -l -F -- "$p" $(find "$CLAUDE_CMDS" -name SKILL.md 2>/dev/null) 2>/dev/null || true)
85
+ FOUND=$(find "$CLAUDE_CMDS" -name SKILL.md -exec grep -l -F -- "$p" {} + 2>/dev/null || true)
76
86
  if [ -z "$FOUND" ]; then
77
87
  pass "no '$p' in Claude commands"
78
88
  else
@@ -88,7 +98,7 @@ PERSONAL_HARDCODES=(
88
98
  "${USER}_tkgithub"
89
99
  )
90
100
  for p in "${PERSONAL_HARDCODES[@]}"; do
91
- FOUND=$(grep -l -F -- "$p" $(find "$CLAUDE_CMDS" -name SKILL.md 2>/dev/null) 2>/dev/null || true)
101
+ FOUND=$(find "$CLAUDE_CMDS" -name SKILL.md -exec grep -l -F -- "$p" {} + 2>/dev/null || true)
92
102
  if [ -z "$FOUND" ]; then
93
103
  pass "no '$p' leaked into Claude commands"
94
104
  else
@@ -9,7 +9,7 @@
9
9
  # 5. Convention mining + createmeta + Agile sprint endpoints present.
10
10
  # 6. UTF-8 verbatim POST path (--rawfile + --data-binary) present.
11
11
  # 7. Standard template vocab: Task/Story (Scope/Acceptance/Test Scenarios), Bug (Steps/Expected/Actual/Environment), Swagger.
12
- # 8. Registration: dispatcher rows, inventory lists, count consistency (36).
12
+ # 8. Registration: dispatcher rows, inventory lists, count derived from the command tree.
13
13
  # 9. Forbidden strings absent (em-dash, section sign, the M-word, unicode_escape).
14
14
 
15
15
  set -euo pipefail
@@ -21,6 +21,7 @@ REF="$ROOT/pipeline/multi-agent-refs/generate-issue.md"
21
21
  SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md"
22
22
  DISPATCHER="$ROOT/pipeline/commands/multi-agent/SKILL.md"
23
23
  CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
24
+ CMD_COUNT=$(find "$ROOT/pipeline/commands/multi-agent" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
24
25
  SYNC="$ROOT/pipeline/commands/multi-agent/sync/SKILL.md"
25
26
  HELP="$ROOT/pipeline/commands/multi-agent/help/SKILL.md"
26
27
 
@@ -88,14 +89,14 @@ grep -Fqi "auto-sizing" "$REF" && pass "auto-sizing rule present" || fail "auto-
88
89
  grep -Fqi "swagger" "$REF" && pass "swagger context present" || fail "swagger context missing"
89
90
 
90
91
  echo ""
91
- echo "→ 8. registration + count consistency (36)"
92
+ echo "→ 8. registration + count consistency (derived)"
92
93
  grep -Fq '| `create-jira ' "$DISPATCHER" && pass "dispatcher routing: create-jira" || fail "dispatcher routing missing: generate"
93
94
  grep -Fq "create-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher loading row" || fail "dispatcher loading row missing"
94
- grep -Fq "## 1. Command Inventory (41 commands)" "$CONTRACT" && pass "contract count = 41" || fail "contract count wrong"
95
+ grep -Fq "## 1. Command Inventory ($CMD_COUNT commands)" "$CONTRACT" && pass "contract count = $CMD_COUNT (derived)" || fail "contract count wrong"
95
96
  grep -Eq '(^|[ ,])create-jira([ ,]|$)' "$CONTRACT" && pass "contract list has create-jira" || fail "contract list missing generate"
96
- grep -Fq "**41 commands are synced**" "$SYNC" && pass "sync count = 41" || fail "sync count wrong"
97
+ grep -Fq "**$CMD_COUNT commands are synced**" "$SYNC" && pass "sync count = $CMD_COUNT (derived)" || fail "sync count wrong"
97
98
  SYNC_SKILL="$ROOT/pipeline/skills/shared/core/multi-agent-sync/SKILL.md"
98
- grep -Fq "**41 commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = 41" || fail "sync SKILL count wrong (Copilot parity)"
99
+ grep -Fq "**$CMD_COUNT commands are synced**" "$SYNC_SKILL" && pass "sync SKILL count = $CMD_COUNT (derived)" || fail "sync SKILL count wrong (Copilot parity)"
99
100
  grep -Fq "create-jira" "$ROOT/pipeline/skills/shared/core/multi-agent-help/SKILL.md" && pass "help SKILL lists create-jira" || fail "help SKILL missing create-jira (Copilot parity)"
100
101
  grep -c "multi-agent:create-jira" "$HELP" | awk '{exit !($1>=2)}' && pass "help.md has EN + TR entries" || fail "help.md entries incomplete"
101
102
  # no stale split-command references anywhere in the synced surfaces
@@ -35,7 +35,7 @@ repo="$sandbox/repo"
35
35
  mkdir -p "$repo"
36
36
 
37
37
  # --- 1: pref off → silent exit 0 ---
38
- printf '{"global":{"perRepoMemory":false}}' > "$HOME/.claude/preferences.json"
38
+ printf '{"global":{"perRepoMemory":false}}' > "$HOME/.claude/multi-agent-preferences.json"
39
39
  out=$(bash "$LOAD" "$repo" 2>&1 || true)
40
40
  if [ -z "$out" ]; then
41
41
  record_pass "load exits silent when pref off"
@@ -44,7 +44,7 @@ else
44
44
  fi
45
45
 
46
46
  # --- 2: pref on + no memory → silent exit 0 ---
47
- printf '{"global":{"perRepoMemory":true}}' > "$HOME/.claude/preferences.json"
47
+ printf '{"global":{"perRepoMemory":true}}' > "$HOME/.claude/multi-agent-preferences.json"
48
48
  out=$(bash "$LOAD" "$repo" 2>&1 || true)
49
49
  if [ -z "$out" ]; then
50
50
  record_pass "load exits silent when pref on but no memory present"
@@ -21,6 +21,7 @@ SK_J="$ROOT/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md"
21
21
  SK_I="$ROOT/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md"
22
22
  DISPATCHER="$ROOT/pipeline/commands/multi-agent/SKILL.md"
23
23
  CONTRACT="$ROOT/pipeline/multi-agent-refs/cross-cli-contract.md"
24
+ CMD_COUNT=$(find "$ROOT/pipeline/commands/multi-agent" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')
24
25
  HELP="$ROOT/pipeline/commands/multi-agent/help/SKILL.md"
25
26
 
26
27
  PASS=0; FAIL=0
@@ -61,12 +62,12 @@ grep -Fqi "confirm" "$REF" && pass "ref has confirm-before-post gate" || fail "r
61
62
  grep -Fqi "rubric" "$REF" && pass "ref has readiness rubric" || fail "ref missing rubric"
62
63
 
63
64
  echo ""
64
- echo "→ 5. registration + count (38)"
65
+ echo "→ 5. registration + count (derived)"
65
66
  grep -Fq '| `review-jira ' "$DISPATCHER" && pass "dispatcher desc: review-jira" || fail "dispatcher desc missing review-jira"
66
67
  grep -Fq '| `review-issue ' "$DISPATCHER" && pass "dispatcher desc: review-issue" || fail "dispatcher desc missing review-issue"
67
68
  grep -Fq "review-jira/SKILL.md" "$DISPATCHER" && pass "dispatcher routing: review-jira" || fail "dispatcher routing missing review-jira"
68
69
  grep -Fq "review-issue/SKILL.md" "$DISPATCHER" && pass "dispatcher routing: review-issue" || fail "dispatcher routing missing review-issue"
69
- grep -Fq "## 1. Command Inventory (41 commands)" "$CONTRACT" && pass "contract count = 41" || fail "contract count wrong"
70
+ grep -Fq "## 1. Command Inventory ($CMD_COUNT commands)" "$CONTRACT" && pass "contract count = $CMD_COUNT (derived)" || fail "contract count wrong"
70
71
  grep -Eq '(^|[ ,])review-jira([ ,]|$)' "$CONTRACT" && pass "contract inventory has review-jira" || fail "contract missing review-jira"
71
72
  grep -Eq '(^|[ ,])review-issue([ ,]|$)' "$CONTRACT" && pass "contract inventory has review-issue" || fail "contract missing review-issue"
72
73
  grep -Fq "multi-agent:review-jira" "$HELP" && pass "help lists review-jira" || fail "help missing review-jira"
@@ -87,17 +87,31 @@ done
87
87
 
88
88
  # ──────────────────────────────────────────────────────────────────────────
89
89
  echo ""
90
- echo "→ 4. prefs.schema.json - keychainMapping has 12 services"
90
+ echo "→ 4. prefs.schema.json - keychainMapping covers every resolved service"
91
+ # A floor, not a pin: new credentials get added over time, and pinning the count
92
+ # made every addition a false failure. What matters is that the keys shipped
93
+ # flows resolve are all declared.
91
94
  KM_COUNT=$(node -e "
92
95
  const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
93
96
  const km = s.properties.global.properties.keychainMapping.properties;
94
97
  console.log(Object.keys(km).length);
95
98
  " 2>/dev/null)
96
- EXPECTED_SERVICES=13 # 12 + legacy "bitbucket" alias
97
- if [ "$KM_COUNT" = "$EXPECTED_SERVICES" ] || [ "$KM_COUNT" = "12" ]; then
98
- pass "keychainMapping has $KM_COUNT services (≥12 expected)"
99
+ if [ "${KM_COUNT:-0}" -ge 12 ] 2>/dev/null; then
100
+ pass "keychainMapping declares $KM_COUNT services (≥12 expected)"
99
101
  else
100
- fail "keychainMapping has $KM_COUNT services, expected ≥12"
102
+ fail "keychainMapping declares ${KM_COUNT:-0} services, expected ≥12"
103
+ fi
104
+
105
+ MISSING_KEYS=$(node -e "
106
+ const s = JSON.parse(require('fs').readFileSync('$PREFS_SCHEMA', 'utf8'));
107
+ const km = Object.keys(s.properties.global.properties.keychainMapping.properties);
108
+ const required = ['jira','bitbucket_token','bitbucket_user','github','confluence','figma','figma_pat','figma_mcp','firebase','npm'];
109
+ console.log(required.filter((k) => !km.includes(k)).join(','));
110
+ " 2>/dev/null)
111
+ if [ -z "$MISSING_KEYS" ]; then
112
+ pass "every credential the pipeline resolves is declared"
113
+ else
114
+ fail "keychainMapping missing declared-and-used key(s): $MISSING_KEYS"
101
115
  fi
102
116
 
103
117
  # ──────────────────────────────────────────────────────────────────────────