@mmerterden/multi-agent-pipeline 12.7.0 → 12.9.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 (247) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/install/_common.mjs +48 -0
  3. package/install/_dev-only-files.mjs +125 -5
  4. package/install/claude.mjs +14 -8
  5. package/install/copilot.mjs +5 -8
  6. package/package.json +17 -2
  7. package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
  8. package/pipeline/lib/credential-store.sh +20 -0
  9. package/pipeline/multi-agent-refs/_account-picker.md +1 -1
  10. package/pipeline/multi-agent-refs/_dev-context.md +1 -1
  11. package/pipeline/multi-agent-refs/_repo-picker.md +1 -1
  12. package/pipeline/multi-agent-refs/features/review-multi-repo.md +65 -0
  13. package/pipeline/multi-agent-refs/features/url-enrichment.md +93 -0
  14. package/pipeline/multi-agent-refs/phases/operations.md +28 -0
  15. package/pipeline/multi-agent-refs/phases/phase-0-init.md +3 -83
  16. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
  17. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -2
  18. package/pipeline/multi-agent-refs/phases/phase-4-review.md +50 -62
  19. package/pipeline/schemas/prefs.schema.json +6 -0
  20. package/pipeline/scripts/_smoke-root.sh +61 -0
  21. package/pipeline/scripts/agent-guard.py +57 -3
  22. package/pipeline/scripts/audit-log.sh +25 -0
  23. package/pipeline/scripts/smoke-cross-cli-behavior.sh +21 -7
  24. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +0 -25
  25. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +0 -30
  26. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +0 -20
  27. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +0 -15
  28. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +0 -14
  29. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +0 -12
  30. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +0 -29
  31. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +0 -43
  32. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +0 -35
  33. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +0 -35
  34. package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +0 -14
  35. package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +0 -12
  36. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-1-analysis.json +0 -29
  37. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-2-plan.json +0 -42
  38. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-review.json +0 -20
  39. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-triage.json +0 -15
  40. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/metadata.json +0 -14
  41. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/task.json +0 -12
  42. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-1-analysis.json +0 -29
  43. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-2-plan.json +0 -40
  44. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-review.json +0 -20
  45. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-triage.json +0 -15
  46. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/metadata.json +0 -14
  47. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/task.json +0 -12
  48. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-1-analysis.json +0 -29
  49. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-2-plan.json +0 -42
  50. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-review.json +0 -28
  51. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-triage.json +0 -27
  52. package/pipeline/eval/golden-tasks/05-ios-security-keychain/metadata.json +0 -14
  53. package/pipeline/eval/golden-tasks/05-ios-security-keychain/task.json +0 -12
  54. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-1-analysis.json +0 -29
  55. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-2-plan.json +0 -41
  56. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-review.json +0 -12
  57. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-triage.json +0 -6
  58. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/metadata.json +0 -14
  59. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/task.json +0 -12
  60. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-1-analysis.json +0 -29
  61. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-2-plan.json +0 -42
  62. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-review.json +0 -28
  63. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-triage.json +0 -27
  64. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/metadata.json +0 -14
  65. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/task.json +0 -12
  66. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-1-analysis.json +0 -25
  67. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-2-plan.json +0 -31
  68. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-review.json +0 -12
  69. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-triage.json +0 -18
  70. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/metadata.json +0 -14
  71. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/task.json +0 -12
  72. package/pipeline/eval/golden-tasks/README.md +0 -65
  73. package/pipeline/eval/intent-cases.json +0 -40
  74. package/pipeline/eval/run-metrics-fixture.json +0 -93
  75. package/pipeline/eval/triage/01-empty-findings/expected.json +0 -6
  76. package/pipeline/eval/triage/01-empty-findings/input.json +0 -5
  77. package/pipeline/eval/triage/01-empty-findings/notes.md +0 -7
  78. package/pipeline/eval/triage/02-real-blocker/expected.json +0 -15
  79. package/pipeline/eval/triage/02-real-blocker/input.json +0 -14
  80. package/pipeline/eval/triage/02-real-blocker/notes.md +0 -7
  81. package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +0 -18
  82. package/pipeline/eval/triage/03-out-of-scope-defer/input.json +0 -14
  83. package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +0 -10
  84. package/pipeline/eval/triage/04-false-positive-reject/expected.json +0 -18
  85. package/pipeline/eval/triage/04-false-positive-reject/input.json +0 -14
  86. package/pipeline/eval/triage/04-false-positive-reject/notes.md +0 -10
  87. package/pipeline/eval/triage/05-mixed-classification/expected.json +0 -43
  88. package/pipeline/eval/triage/05-mixed-classification/input.json +0 -38
  89. package/pipeline/eval/triage/05-mixed-classification/notes.md +0 -17
  90. package/pipeline/eval/triage/06-severity-mismatch/expected.json +0 -15
  91. package/pipeline/eval/triage/06-severity-mismatch/input.json +0 -14
  92. package/pipeline/eval/triage/06-severity-mismatch/notes.md +0 -9
  93. package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +0 -27
  94. package/pipeline/eval/triage/07-duplicate-reviewers/input.json +0 -22
  95. package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +0 -9
  96. package/pipeline/eval/triage/08-style-misclassified/expected.json +0 -18
  97. package/pipeline/eval/triage/08-style-misclassified/input.json +0 -14
  98. package/pipeline/eval/triage/08-style-misclassified/notes.md +0 -9
  99. package/pipeline/eval/triage/09-cascading-finding/expected.json +0 -23
  100. package/pipeline/eval/triage/09-cascading-finding/input.json +0 -22
  101. package/pipeline/eval/triage/09-cascading-finding/notes.md +0 -9
  102. package/pipeline/eval/triage/10-deferred-crossref/expected.json +0 -18
  103. package/pipeline/eval/triage/10-deferred-crossref/input.json +0 -14
  104. package/pipeline/eval/triage/10-deferred-crossref/notes.md +0 -9
  105. package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +0 -27
  106. package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +0 -22
  107. package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +0 -14
  108. package/pipeline/eval/triage/README.md +0 -54
  109. package/pipeline/scripts/benchmark-phase-0.sh +0 -128
  110. package/pipeline/scripts/check-md-links.mjs +0 -88
  111. package/pipeline/scripts/eval-golden-tasks-live.mjs +0 -302
  112. package/pipeline/scripts/eval-golden-tasks.mjs +0 -224
  113. package/pipeline/scripts/eval-intent.mjs +0 -107
  114. package/pipeline/scripts/eval-mine-corpus.mjs +0 -211
  115. package/pipeline/scripts/eval-triage.mjs +0 -171
  116. package/pipeline/scripts/fixtures/diff-risk-android.diff +0 -40
  117. package/pipeline/scripts/fixtures/diff-risk-ios.diff +0 -48
  118. package/pipeline/scripts/fixtures/diff-risk-test-removal.diff +0 -40
  119. package/pipeline/scripts/fixtures/install-layout.tsv +0 -19
  120. package/pipeline/scripts/fixtures/pack-expected-count.txt +0 -1
  121. package/pipeline/scripts/fixtures/test-gap-node.diff +0 -30
  122. package/pipeline/scripts/fixtures/test-gap-python.diff +0 -32
  123. package/pipeline/scripts/lint-mcp-refs.mjs +0 -218
  124. package/pipeline/scripts/lint-skills.mjs +0 -154
  125. package/pipeline/scripts/run-smokes.mjs +0 -130
  126. package/pipeline/scripts/scorecard.mjs +0 -258
  127. package/pipeline/scripts/smoke-add-detail.sh +0 -137
  128. package/pipeline/scripts/smoke-agent-guard.sh +0 -74
  129. package/pipeline/scripts/smoke-agent-log-cost.sh +0 -262
  130. package/pipeline/scripts/smoke-agent-model-routing.sh +0 -87
  131. package/pipeline/scripts/smoke-ask-choice.sh +0 -42
  132. package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +0 -36
  133. package/pipeline/scripts/smoke-bitbucket-contract.sh +0 -255
  134. package/pipeline/scripts/smoke-changelog-version.sh +0 -47
  135. package/pipeline/scripts/smoke-channels-approval-gate.sh +0 -60
  136. package/pipeline/scripts/smoke-channels-flow.sh +0 -130
  137. package/pipeline/scripts/smoke-ci-workflows.sh +0 -88
  138. package/pipeline/scripts/smoke-clarify.sh +0 -148
  139. package/pipeline/scripts/smoke-command-inventory.sh +0 -81
  140. package/pipeline/scripts/smoke-commands-skills-parity.sh +0 -87
  141. package/pipeline/scripts/smoke-community-gates.sh +0 -75
  142. package/pipeline/scripts/smoke-compliance-skills.sh +0 -119
  143. package/pipeline/scripts/smoke-config-hygiene.sh +0 -58
  144. package/pipeline/scripts/smoke-context-budget.sh +0 -72
  145. package/pipeline/scripts/smoke-cost-budget.sh +0 -70
  146. package/pipeline/scripts/smoke-cost-summary.sh +0 -139
  147. package/pipeline/scripts/smoke-cross-phase-cohesion.sh +0 -128
  148. package/pipeline/scripts/smoke-description-tr.sh +0 -82
  149. package/pipeline/scripts/smoke-dev-critic.sh +0 -144
  150. package/pipeline/scripts/smoke-diff-explain.sh +0 -147
  151. package/pipeline/scripts/smoke-diff-risk.sh +0 -190
  152. package/pipeline/scripts/smoke-dynamic-skill-loading.sh +0 -160
  153. package/pipeline/scripts/smoke-eval-live.sh +0 -136
  154. package/pipeline/scripts/smoke-evidence-gate.sh +0 -93
  155. package/pipeline/scripts/smoke-extract-conventions.sh +0 -163
  156. package/pipeline/scripts/smoke-fetchers-offline.sh +0 -448
  157. package/pipeline/scripts/smoke-figma-dispatch.sh +0 -112
  158. package/pipeline/scripts/smoke-gate-hooks.sh +0 -74
  159. package/pipeline/scripts/smoke-gc-tmp.sh +0 -130
  160. package/pipeline/scripts/smoke-gc-worktrees.sh +0 -125
  161. package/pipeline/scripts/smoke-generate-issue.sh +0 -120
  162. package/pipeline/scripts/smoke-handoff-contract.sh +0 -92
  163. package/pipeline/scripts/smoke-identity-isolation.sh +0 -70
  164. package/pipeline/scripts/smoke-install-layout.sh +0 -248
  165. package/pipeline/scripts/smoke-intent-guard.sh +0 -86
  166. package/pipeline/scripts/smoke-issue-comment-template.sh +0 -86
  167. package/pipeline/scripts/smoke-issue-jira-triad.sh +0 -120
  168. package/pipeline/scripts/smoke-keychain.sh +0 -158
  169. package/pipeline/scripts/smoke-language-axis.sh +0 -109
  170. package/pipeline/scripts/smoke-learning-curve.sh +0 -61
  171. package/pipeline/scripts/smoke-learnings-ledger.sh +0 -86
  172. package/pipeline/scripts/smoke-lib-scripts.sh +0 -448
  173. package/pipeline/scripts/smoke-mcp-gate.sh +0 -68
  174. package/pipeline/scripts/smoke-md-links.sh +0 -8
  175. package/pipeline/scripts/smoke-md2confluence.sh +0 -126
  176. package/pipeline/scripts/smoke-metrics-cache-ratio.sh +0 -72
  177. package/pipeline/scripts/smoke-migrate-state.sh +0 -102
  178. package/pipeline/scripts/smoke-mode-dispatch-drift.sh +0 -161
  179. package/pipeline/scripts/smoke-model-fallback.sh +0 -89
  180. package/pipeline/scripts/smoke-multi-repo-integration.sh +0 -116
  181. package/pipeline/scripts/smoke-multi-repo-worktree.sh +0 -61
  182. package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +0 -194
  183. package/pipeline/scripts/smoke-no-token-prompt.sh +0 -85
  184. package/pipeline/scripts/smoke-own-punctuation.sh +0 -103
  185. package/pipeline/scripts/smoke-pack-contents.sh +0 -140
  186. package/pipeline/scripts/smoke-pat-audit.sh +0 -128
  187. package/pipeline/scripts/smoke-per-repo-memory.sh +0 -156
  188. package/pipeline/scripts/smoke-phase-0-multi-repo.sh +0 -170
  189. package/pipeline/scripts/smoke-phase-6-multi.sh +0 -79
  190. package/pipeline/scripts/smoke-phase-banner.sh +0 -101
  191. package/pipeline/scripts/smoke-phase-tracker.sh +0 -324
  192. package/pipeline/scripts/smoke-phase0-bridge-contract.sh +0 -241
  193. package/pipeline/scripts/smoke-phase4-gates.sh +0 -45
  194. package/pipeline/scripts/smoke-phase4-triage.sh +0 -229
  195. package/pipeline/scripts/smoke-plan-approval-gate.sh +0 -71
  196. package/pipeline/scripts/smoke-plan-safety.sh +0 -139
  197. package/pipeline/scripts/smoke-plan-todos.sh +0 -196
  198. package/pipeline/scripts/smoke-pr-review-actions.sh +0 -152
  199. package/pipeline/scripts/smoke-pre-commit.sh +0 -170
  200. package/pipeline/scripts/smoke-pref-migration.sh +0 -226
  201. package/pipeline/scripts/smoke-prefs-language.sh +0 -134
  202. package/pipeline/scripts/smoke-progress-contract.sh +0 -127
  203. package/pipeline/scripts/smoke-prune-logs.sh +0 -137
  204. package/pipeline/scripts/smoke-purge.sh +0 -138
  205. package/pipeline/scripts/smoke-push-retry.sh +0 -75
  206. package/pipeline/scripts/smoke-repo-map.sh +0 -300
  207. package/pipeline/scripts/smoke-review-readiness.sh +0 -92
  208. package/pipeline/scripts/smoke-review-watch.sh +0 -146
  209. package/pipeline/scripts/smoke-routines.sh +0 -84
  210. package/pipeline/scripts/smoke-run-aggregator.sh +0 -216
  211. package/pipeline/scripts/smoke-run-metrics.sh +0 -50
  212. package/pipeline/scripts/smoke-search.sh +0 -187
  213. package/pipeline/scripts/smoke-shadow-git.sh +0 -224
  214. package/pipeline/scripts/smoke-skill-authoring.sh +0 -137
  215. package/pipeline/scripts/smoke-skill-language.sh +0 -83
  216. package/pipeline/scripts/smoke-skill-manifest.sh +0 -138
  217. package/pipeline/scripts/smoke-skill-scan.sh +0 -198
  218. package/pipeline/scripts/smoke-source-parity.sh +0 -85
  219. package/pipeline/scripts/smoke-subagent-validators.sh +0 -108
  220. package/pipeline/scripts/smoke-sync-parity.sh +0 -92
  221. package/pipeline/scripts/smoke-tasklist-ordering.sh +0 -112
  222. package/pipeline/scripts/smoke-telemetry.sh +0 -147
  223. package/pipeline/scripts/smoke-test-gap.sh +0 -183
  224. package/pipeline/scripts/smoke-token-budget.sh +0 -67
  225. package/pipeline/scripts/smoke-token-preflight.sh +0 -82
  226. package/pipeline/scripts/smoke-tracker-contract.sh +0 -191
  227. package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +0 -73
  228. package/pipeline/scripts/smoke-triage-memory.sh +0 -174
  229. package/pipeline/scripts/smoke-update-check.sh +0 -135
  230. package/pipeline/scripts/smoke-url-enrichment.sh +0 -70
  231. package/pipeline/scripts/smoke-validate-analysis-doc.sh +0 -161
  232. package/pipeline/scripts/smoke-validator-contradiction.sh +0 -67
  233. package/pipeline/scripts/smoke-validator-gates.sh +0 -164
  234. package/pipeline/scripts/smoke-vercel-deploy-redact.sh +0 -129
  235. package/pipeline/scripts/smoke-verify-by-test.sh +0 -148
  236. package/pipeline/scripts/smoke-wiki-integration.sh +0 -122
  237. package/pipeline/scripts/smoke-work-summary.sh +0 -163
  238. package/pipeline/scripts/smoke-workflow-audit.sh +0 -101
  239. package/pipeline/scripts/smoke-worktree-path-convention.sh +0 -86
  240. package/pipeline/scripts/smoke-wrapper-preservation.sh +0 -68
  241. package/pipeline/scripts/smoke-write-state.sh +0 -159
  242. package/pipeline/scripts/sync-parity-check.sh +0 -135
  243. package/pipeline/scripts/test-gap-rules/android.json +0 -25
  244. package/pipeline/scripts/test-gap-rules/ios.json +0 -34
  245. package/pipeline/scripts/test-gap-rules/node.json +0 -29
  246. package/pipeline/scripts/test-gap-rules/python.json +0 -25
  247. package/pipeline/scripts/validate-schemas.mjs +0 -88
@@ -1,218 +0,0 @@
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([
49
- "node_modules",
50
- ".git",
51
- ".worktrees",
52
- "Pods",
53
- ".build",
54
- "DerivedData",
55
- ".next",
56
- ]);
57
-
58
- function walk(dir, out = []) {
59
- let entries;
60
- try {
61
- entries = readdirSync(dir, { withFileTypes: true });
62
- } catch {
63
- return out;
64
- }
65
- for (const e of entries) {
66
- if (PRUNE.has(e.name)) continue;
67
- const p = join(dir, e.name);
68
- if (e.isDirectory()) walk(p, out);
69
- else if (SCAN_EXT.test(e.name)) out.push(p);
70
- }
71
- return out;
72
- }
73
-
74
- const files = SCAN_DIRS.flatMap((d) => walk(join(repoRoot, d)));
75
- const REF = /mcp__([A-Za-z0-9_-]+?)__([a-z0-9_]+)/g;
76
-
77
- const errors = [];
78
- const refs = new Map(); // server -> Map(tool -> Set(file:line))
79
-
80
- for (const file of files) {
81
- const rel = file.slice(repoRoot.length + 1);
82
- const lines = readFileSync(file, "utf8").split("\n");
83
- lines.forEach((line, i) => {
84
- for (const m of line.matchAll(REF)) {
85
- const [, server, tool] = m;
86
- // Prose writes families as mcp__dev-toolkit__ios_* - a wildcard, not a tool.
87
- const next = line[m.index + m[0].length];
88
- if (next === "*" || tool.endsWith("_")) continue;
89
- const where = `${rel}:${i + 1}`;
90
- if (!refs.has(server)) refs.set(server, new Map());
91
- const tools = refs.get(server);
92
- if (!tools.has(tool)) tools.set(tool, new Set());
93
- tools.get(tool).add(where);
94
- }
95
- });
96
- }
97
-
98
- for (const [server, tools] of refs) {
99
- if (server in KNOWN_SERVERS) continue;
100
- const fix = KNOWN_BAD[server];
101
- const sample = [...tools.values()][0].values().next().value;
102
- errors.push(
103
- fix
104
- ? `unknown MCP server segment "${server}" (${tools.size} tool refs, e.g. ${sample}) - the registered server is "${fix}", so the prefix must be mcp__${fix}__`
105
- : `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`,
106
- );
107
- }
108
-
109
- // ---- optional deep check: do the referenced dev-toolkit tools exist? --------
110
-
111
- function resolveToolkitRepo() {
112
- const prefsPath = join(homedir(), ".claude", "multi-agent-preferences.json");
113
- if (existsSync(prefsPath)) {
114
- try {
115
- const dt = JSON.parse(readFileSync(prefsPath, "utf8"))?.global?.devToolkit;
116
- if (dt && dt.enabled === false) return null;
117
- if (dt?.localPath) {
118
- const p = dt.localPath.replace(/^~|\$HOME/, homedir());
119
- if (existsSync(join(p, "package.json"))) return p;
120
- }
121
- } catch {
122
- /* fall through to registration lookup */
123
- }
124
- }
125
- const candidates = [];
126
- const collect = (obj) => {
127
- for (const [name, cfg] of Object.entries(obj || {})) {
128
- const arg = Array.isArray(cfg?.args) ? cfg.args.find((a) => /\.(mjs|js)$/.test(a)) : null;
129
- if (name.includes("dev") && arg) candidates.push(dirname(resolve(arg)));
130
- }
131
- };
132
- for (const cfgPath of [
133
- join(homedir(), ".claude.json"),
134
- join(homedir(), ".claude", "settings.json"),
135
- ]) {
136
- if (!existsSync(cfgPath)) continue;
137
- try {
138
- const json = JSON.parse(readFileSync(cfgPath, "utf8"));
139
- collect(json.mcpServers);
140
- for (const proj of Object.values(json.projects || {})) collect(proj.mcpServers);
141
- } catch {
142
- /* ignore unreadable config */
143
- }
144
- }
145
- for (const c of candidates) {
146
- try {
147
- const pkg = JSON.parse(readFileSync(join(c, "package.json"), "utf8"));
148
- if (pkg.dependencies?.["@modelcontextprotocol/sdk"] && statSync(join(c, ".git"))) return c;
149
- } catch {
150
- /* not the toolkit */
151
- }
152
- }
153
- return null;
154
- }
155
-
156
- function listTools(repo) {
157
- const frames =
158
- [
159
- '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lint-mcp-refs","version":"1"}}}',
160
- '{"jsonrpc":"2.0","method":"notifications/initialized"}',
161
- '{"jsonrpc":"2.0","id":2,"method":"tools/list"}',
162
- ].join("\n") + "\n";
163
- const r = spawnSync(process.execPath, ["index.js"], {
164
- cwd: repo,
165
- input: frames,
166
- encoding: "utf8",
167
- timeout: 20000,
168
- maxBuffer: 32 * 1024 * 1024,
169
- });
170
- if (r.error || !r.stdout) return null;
171
- for (const line of r.stdout.split("\n")) {
172
- if (!line.trim()) continue;
173
- try {
174
- const msg = JSON.parse(line);
175
- if (msg.id === 2 && msg.result?.tools) return msg.result.tools.map((t) => t.name);
176
- } catch {
177
- /* not a frame */
178
- }
179
- }
180
- return null;
181
- }
182
-
183
- const referencedToolkitTools = refs.get("dev-toolkit");
184
- let deep = "skipped (no local dev-toolkit checkout)";
185
- if (referencedToolkitTools) {
186
- const repo = resolveToolkitRepo();
187
- const served = repo ? listTools(repo) : null;
188
- if (!served) {
189
- deep = repo ? `skipped (${repo} did not answer tools/list)` : deep;
190
- } else {
191
- const missing = [...referencedToolkitTools.keys()].filter((t) => !served.includes(t));
192
- if (missing.length === 0) {
193
- deep = `all ${referencedToolkitTools.size} referenced tools exist in the live server (${served.length} served)`;
194
- } else {
195
- const msg = `dev-toolkit tools referenced but not served (${served.length} served): ${missing
196
- .map((t) => `${t} [${[...referencedToolkitTools.get(t)][0]}]`)
197
- .join(", ")}`;
198
- deep = `${missing.length} referenced tool(s) not served`;
199
- if (strictTools) errors.push(msg);
200
- else console.warn(` WARNING - ${msg}`);
201
- }
202
- }
203
- }
204
-
205
- const totalRefs = [...refs.values()].reduce((n, tools) => n + tools.size, 0);
206
- console.log(
207
- `lint-mcp-refs: ${totalRefs} distinct tool references across ${refs.size} server(s) in ${files.length} files`,
208
- );
209
- for (const [server, tools] of refs) {
210
- console.log(` ${server in KNOWN_SERVERS ? "✓" : "✗"} mcp__${server}__* : ${tools.size} tools`);
211
- }
212
- console.log(` tool-existence check: ${deep}`);
213
-
214
- if (errors.length > 0) {
215
- console.error(`lint-mcp-refs: ${errors.length} error(s)`);
216
- for (const e of errors) console.error(" " + e);
217
- process.exit(1);
218
- }
@@ -1,154 +0,0 @@
1
- #!/usr/bin/env node
2
- // Frontmatter linter for every SKILL.md shipped by the pipeline.
3
- //
4
- // Until v10.0.0 the shipped SKILL.md files were counted by smokes but never
5
- // content-checked, so a skill could ship without the frontmatter that
6
- // Claude Code / Copilot CLI need to register it. This gate enforces the
7
- // minimal contract; it is zero-dependency (line-based YAML subset) so it
8
- // runs inside `npm test` and ci-lite without installs.
9
- //
10
- // Contract per SKILL.md (errors - fail the gate):
11
- // - frontmatter block present (--- ... ---) starting at line 1
12
- // - name: required, lowercase kebab-case (underscores tolerated for
13
- // legacy skills already published under a snake_case name)
14
- // - description: required, non-empty, single line, <= 1024 chars
15
- // - user-invocable: when present, must be true or false
16
- // - platform: when present, must be ios | android | common | all
17
- // - description: must carry a routing/trigger clause ("use when ..."), the
18
- // one thing it is for - blocking since all 193 skills have one
19
- //
20
- // Spec-conformance signals (warnings - never fail the gate):
21
- // - name should match the agent-skills spec pattern
22
- // (lowercase letters/digits/hyphens, <= 64 chars)
23
- // - a body over ~500 lines with no companion-file reference should be
24
- // split into references/ files loaded on demand
25
-
26
- import { readFileSync } from "node:fs";
27
- import { execSync } from "node:child_process";
28
- import { dirname, join } from "node:path";
29
- import { fileURLToPath } from "node:url";
30
-
31
- const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
32
- // Scope to pipeline/skills only. Slash commands under pipeline/commands also use
33
- // the <name>/SKILL.md layout now, but they are command definitions
34
- // (description + allowed-tools, name derived from the dir), NOT skills, so the
35
- // skill frontmatter contract (required `name:`) does not apply to them.
36
- const files = execSync("find pipeline/skills -name SKILL.md", { cwd: repoRoot, encoding: "utf-8" })
37
- .trim()
38
- .split("\n")
39
- .filter(Boolean)
40
- .sort();
41
-
42
- const errors = [];
43
- const fail = (file, msg) => errors.push(`${file}: ${msg}`);
44
- // Non-fatal quality signals (a terse description routes poorly). Warnings never
45
- // fail the gate; they surface skills whose frontmatter could trigger better.
46
- const warnings = [];
47
- const warn = (file, msg) => warnings.push(`${file}: ${msg}`);
48
-
49
- for (const file of files) {
50
- const raw = readFileSync(join(repoRoot, file), "utf-8");
51
-
52
- if (!raw.startsWith("---\n")) {
53
- fail(file, "missing frontmatter block at line 1");
54
- continue;
55
- }
56
- const end = raw.indexOf("\n---", 4);
57
- if (end === -1) {
58
- fail(file, "frontmatter block never closed");
59
- continue;
60
- }
61
-
62
- const fields = {};
63
- for (const line of raw.slice(4, end).split("\n")) {
64
- const m = line.match(/^([a-zA-Z][a-zA-Z0-9-]*):\s*(.*)$/);
65
- if (m) fields[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
66
- }
67
-
68
- if (!fields.name) {
69
- fail(file, "missing required field: name");
70
- } else if (!/^[a-z0-9]+([-_][a-z0-9]+)*$/.test(fields.name)) {
71
- fail(file, `name is not lowercase kebab/snake-case: "${fields.name}"`);
72
- } else {
73
- // Spec conformance (warn-only): the agent-skills spec allows lowercase
74
- // letters, digits, and hyphens, max 64 chars. Snake_case passes the
75
- // legacy error check above but is out of spec.
76
- if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(fields.name)) {
77
- warn(file, `name is outside the agent-skills spec pattern (hyphens only): "${fields.name}"`);
78
- }
79
- if (fields.name.length > 64) {
80
- warn(file, `name exceeds the spec's 64-char limit (${fields.name.length})`);
81
- }
82
- }
83
-
84
- if (!("description" in fields)) {
85
- fail(file, "missing required field: description");
86
- } else if (fields.description.length === 0) {
87
- fail(file, "description is empty");
88
- } else if (fields.description.length > 1024) {
89
- fail(file, `description exceeds 1024 chars (${fields.description.length})`);
90
- } else {
91
- // Routing quality: a description of a few words rarely says what the skill
92
- // does AND when to use it, so the model cannot decide to trigger it.
93
- // Skip YAML block scalars (`description: |` / `>`): this line-based parser
94
- // only sees the indicator, not the real multi-line text, so a word count
95
- // would be a false positive.
96
- const desc = fields.description;
97
- const isBlockScalar = /^[|>][+-]?\d*$/.test(desc);
98
- const words = desc.split(/\s+/).filter(Boolean).length;
99
- if (!isBlockScalar && words < 6) {
100
- warn(file, `description is terse (${words} words) - say what it does AND when to use it`);
101
- }
102
- // Routing quality (BLOCKING since the backlog reached zero). A description
103
- // without a trigger clause tells the model what the skill is and nothing
104
- // about when to reach for it, which is the one job the description has -
105
- // it is the routing rule, not a summary.
106
- //
107
- // This was warn-only while 93 of 193 skills were missing one, because a
108
- // gate that fires on half the tree gets ignored. All 93 were written, so
109
- // the baseline is zero and it can hold the line. Block scalars are skipped
110
- // for the same parser reason as above.
111
- const ROUTING_RE =
112
- /\b(use (this|it|when|for|whenever)|when (the|a|an|you|asked|working|building|implementing|writing|reviewing)|triggers?|invoked?( when| for)?|applies when|activated?)\b/i;
113
- if (!isBlockScalar && !ROUTING_RE.test(desc)) {
114
- fail(file, 'description has no routing/trigger clause (e.g. "use when ...")');
115
- }
116
- }
117
-
118
- // Body size (warn-only): the agent-skills guidance is to keep SKILL.md
119
- // lean and push detail into companion files loaded on demand. A ~500+
120
- // line body that references no companion file loads whole every time.
121
- const body = raw.slice(end + 4);
122
- const bodyLines = body.split("\n").length;
123
- if (bodyLines > 500) {
124
- const referencesCompanion =
125
- /\]\((?!https?:|mailto:|#)[^)]+\)/.test(body) ||
126
- /`[^`\n]*\/[^`\n]*\.(md|sh|mjs|json|py|txt)`/.test(body);
127
- if (!referencesCompanion) {
128
- warn(
129
- file,
130
- `body is ${bodyLines} lines with no companion-file reference - consider splitting into references/`,
131
- );
132
- }
133
- }
134
-
135
- if ("user-invocable" in fields && !["true", "false"].includes(fields["user-invocable"])) {
136
- fail(file, `user-invocable must be true|false, got "${fields["user-invocable"]}"`);
137
- }
138
-
139
- if ("platform" in fields && !["ios", "android", "common", "all"].includes(fields.platform)) {
140
- fail(file, `platform must be ios|android|common|all, got "${fields.platform}"`);
141
- }
142
- }
143
-
144
- if (warnings.length > 0) {
145
- console.warn(`lint-skills: ${warnings.length} warning(s) (non-fatal):`);
146
- for (const w of warnings) console.warn(" " + w);
147
- }
148
-
149
- if (errors.length > 0) {
150
- console.error(`lint-skills: ${errors.length} error(s) across ${files.length} SKILL.md files`);
151
- for (const e of errors) console.error(" " + e);
152
- process.exit(1);
153
- }
154
- console.log(`lint-skills: ${files.length} SKILL.md files OK`);
@@ -1,130 +0,0 @@
1
- #!/usr/bin/env node
2
- // Structured runner for pipeline/scripts/smoke-*.sh.
3
- //
4
- // Replaces the bare `for f in smoke-*.sh; do bash "$f" || exit 1; done`
5
- // loop with per-suite timeouts, timing, and a summary, so a hung smoke
6
- // (network wait, interactive prompt) can never block `npm test` or CI
7
- // indefinitely.
8
- //
9
- // Env:
10
- // SMOKE_TIMEOUT_SECONDS per-suite timeout (default 180)
11
- // SMOKE_NO_BAIL=1 run every suite and summarize instead of
12
- // stopping at the first failure
13
- // Args:
14
- // optional substring filters: `node run-smokes.mjs adapters keychain`
15
- // runs only suites whose filename contains one of the terms.
16
-
17
- import { spawnSync } from "node:child_process";
18
- import { readdirSync } from "node:fs";
19
- import { dirname, join } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
-
22
- const scriptsDir = dirname(fileURLToPath(import.meta.url));
23
- const timeoutSec = Number(process.env.SMOKE_TIMEOUT_SECONDS || 180);
24
- const bail = process.env.SMOKE_NO_BAIL !== "1";
25
- const argv = process.argv.slice(2);
26
- const reportCounts = argv.includes("--counts");
27
- const filters = argv.filter((a) => !a.startsWith("--"));
28
-
29
- // How many assertions did a suite actually make?
30
- //
31
- // Exit code alone cannot answer that, and the difference matters: three smokes
32
- // were exiting 0 having checked nothing. smoke-workflow-audit skipped both its
33
- // linters because neither was installed anywhere, so the GitHub Actions
34
- // security audit had never run while showing green - it was hiding a
35
- // high-severity finding. smoke-plugin-validate had no manifest to validate in
36
- // this repo at all. A gate that cannot fail is worse than no gate, because the
37
- // green tick is read as coverage.
38
- //
39
- // The four output shapes actually in use here, surveyed across all suites
40
- // rather than assumed. Anything a suite prints that matches none of them
41
- // counts as zero, which is the intended answer: an unrecognisable result is
42
- // not evidence that something was checked.
43
- //
44
- // 1. "══ name: 22 passed, 0 failed ══" most suites
45
- // 2. "check-md-links: 212 files OK" counted sweeps
46
- // 3. per-line "✓" / "ok " / "PASS:" suites with no summary line
47
- // 4. "smoke-language-axis: ok" terse single-verdict suites
48
- function countAssertions(stdout) {
49
- const passed = [...stdout.matchAll(/(\d+) passed/g)];
50
- if (passed.length > 0) return Number(passed[passed.length - 1][1]);
51
-
52
- const swept = [...stdout.matchAll(/(\d+) (?:files|patterns|checks|entries) (?:OK|clean)/gi)];
53
- if (swept.length > 0) return Number(swept[swept.length - 1][1]);
54
-
55
- const marks = stdout.match(/^[ \t]*(✓|ok |PASS[: ])/gm);
56
- if (marks) return marks.length;
57
-
58
- if (/^\S+: ok\s*$/m.test(stdout)) return 1;
59
-
60
- return 0;
61
- }
62
-
63
- const suites = readdirSync(scriptsDir)
64
- .filter((f) => f.startsWith("smoke-") && f.endsWith(".sh"))
65
- .filter((f) => filters.length === 0 || filters.some((t) => f.includes(t)))
66
- .sort();
67
-
68
- if (suites.length === 0) {
69
- console.error("run-smokes: no smoke suites matched");
70
- process.exit(1);
71
- }
72
-
73
- const failures = [];
74
- let ran = 0;
75
- const startedAt = Date.now();
76
-
77
- for (const suite of suites) {
78
- ran++;
79
- const t0 = Date.now();
80
- const res = spawnSync("bash", [join(scriptsDir, suite)], {
81
- encoding: "utf-8",
82
- timeout: timeoutSec * 1000,
83
- maxBuffer: 16 * 1024 * 1024,
84
- });
85
- const secs = ((Date.now() - t0) / 1000).toFixed(1);
86
-
87
- const timedOut = res.error && res.error.code === "ETIMEDOUT";
88
- const asserted = countAssertions(res.stdout || "");
89
- const ok = !timedOut && res.status === 0 && (reportCounts || asserted > 0);
90
-
91
- if (reportCounts) {
92
- console.log(`${String(asserted).padStart(4)} ${suite}`);
93
- continue;
94
- }
95
-
96
- if (ok) {
97
- console.log(`ok ${suite} (${secs}s, ${asserted} checks)`);
98
- continue;
99
- }
100
-
101
- if (!timedOut && res.status === 0 && asserted === 0) {
102
- console.error(
103
- `FAIL ${suite} (exited 0 having asserted nothing, ${secs}s)\n` +
104
- " A suite that checks nothing reports coverage it does not have.\n" +
105
- " Give it something to assert, or delete it.",
106
- );
107
- if (res.stdout) console.error(res.stdout.split("\n").slice(-15).join("\n"));
108
- failures.push(suite);
109
- if (bail) break;
110
- continue;
111
- }
112
-
113
- const reason = timedOut ? `timeout after ${timeoutSec}s` : `exit ${res.status}`;
114
- console.error(`FAIL ${suite} (${reason}, ${secs}s)`);
115
- if (res.stdout) console.error(res.stdout.split("\n").slice(-30).join("\n"));
116
- if (res.stderr) console.error(res.stderr.split("\n").slice(-30).join("\n"));
117
- failures.push(suite);
118
- if (bail) break;
119
- }
120
-
121
- const totalSecs = ((Date.now() - startedAt) / 1000).toFixed(1);
122
- const skipped = suites.length - ran;
123
- console.log(
124
- `run-smokes: ${ran - failures.length}/${suites.length} passed in ${totalSecs}s` +
125
- (skipped > 0 ? ` (${skipped} not run after bail)` : ""),
126
- );
127
- if (failures.length > 0) {
128
- console.error(`run-smokes: failed -> ${failures.join(", ")}`);
129
- process.exit(1);
130
- }