@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.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/docs/features.md +20 -0
- package/index.js +7 -1
- package/install/_dev-only-files.mjs +1 -0
- package/package.json +4 -3
- package/pipeline/agents/security-auditor.md +1 -1
- package/pipeline/commands/archive-guard.md +5 -5
- package/pipeline/commands/multi-agent/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/design-check/SKILL.md +287 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +45 -5
- package/pipeline/commands/multi-agent/refactor/SKILL.md +92 -12
- package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/sync/SKILL.md +119 -12
- package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
- package/pipeline/commands/sim-test.md +5 -5
- package/pipeline/lib/credential-store.sh +32 -0
- package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +4 -4
- package/pipeline/preferences-template.json +18 -1
- package/pipeline/schemas/agent-state.schema.json +90 -0
- package/pipeline/schemas/design-check-config.schema.json +162 -0
- package/pipeline/schemas/migrations/state-2.0.0-to-2.1.0.mjs +30 -12
- package/pipeline/schemas/prefs.schema.json +161 -5
- package/pipeline/scripts/README.md +7 -5
- package/pipeline/scripts/classify-plan-safety.mjs +8 -3
- package/pipeline/scripts/cost-budget-check.mjs +9 -5
- package/pipeline/scripts/fixtures/install-layout.tsv +6 -6
- package/pipeline/scripts/learnings-ledger.mjs +18 -0
- package/pipeline/scripts/lint-mcp-refs.mjs +207 -0
- package/pipeline/scripts/memory-load.sh +5 -1
- package/pipeline/scripts/render-work-summary.sh +4 -1
- package/pipeline/scripts/smoke-command-inventory.sh +81 -0
- package/pipeline/scripts/smoke-commands-skills-parity.sh +1 -1
- package/pipeline/scripts/smoke-compliance-skills.sh +4 -4
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +12 -2
- package/pipeline/scripts/smoke-generate-issue.sh +6 -5
- package/pipeline/scripts/smoke-per-repo-memory.sh +2 -2
- package/pipeline/scripts/smoke-review-readiness.sh +3 -2
- package/pipeline/scripts/smoke-schema-validation.sh +19 -5
- package/pipeline/scripts/smoke-shadow-git.sh +4 -2
- package/pipeline/scripts/triage-memory.mjs +18 -0
- package/pipeline/scripts/uninstall.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +24 -8
- package/pipeline/skills/.skills-index.json +39 -3
- package/pipeline/skills/shared/README.md +10 -6
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +10 -8
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +248 -0
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +22 -0
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +67 -11
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +51 -9
- package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +1 -1
- 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
|
|
38
|
-
//
|
|
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
|
|
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(
|
|
108
|
-
join(
|
|
109
|
-
join(
|
|
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
|
|
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
|
|
10
|
-
.claude/scripts
|
|
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
|
|
18
|
-
.copilot/scripts
|
|
19
|
-
.copilot/skills
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 '/^##
|
|
47
|
-
[ "$APPLE_RULES" -eq
|
|
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 '
|
|
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"
|
|
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"
|
|
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
|
|
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 (
|
|
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 (
|
|
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 "
|
|
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 "
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
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
|
# ──────────────────────────────────────────────────────────────────────────
|