@mmerterden/multi-agent-pipeline 12.6.0 → 12.7.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 (271) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +18 -18
  3. package/docs/FIGMA_PIPELINE.md +34 -34
  4. package/docs/adr/0001-three-model-triage.md +12 -12
  5. package/docs/adr/0002-instruction-driven-flag.md +5 -5
  6. package/docs/adr/0003-unified-shared-skills.md +5 -5
  7. package/docs/adr/0004-zero-dependency-philosophy.md +5 -5
  8. package/docs/adr/0005-lazy-phase-docs.md +2 -2
  9. package/docs/adr/0006-skills-core-external-split.md +6 -6
  10. package/docs/adr/0007-multi-tool-adapter-framework.md +19 -19
  11. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +19 -19
  12. package/docs/adr/README.md +1 -1
  13. package/docs/best-practices.md +3 -3
  14. package/docs/features.md +28 -28
  15. package/docs/performance.md +16 -16
  16. package/docs/recovery-guide.md +39 -39
  17. package/index.js +4 -4
  18. package/install/_common.mjs +5 -11
  19. package/install/_copilot-instructions.mjs +2 -2
  20. package/install/_dev-only-files.mjs +1 -1
  21. package/install/_platform-filter.mjs +1 -1
  22. package/install/_telemetry.mjs +1 -1
  23. package/install/claude.mjs +10 -9
  24. package/install/copilot.mjs +10 -19
  25. package/install/index.mjs +7 -15
  26. package/install/templates/copilot-instructions.md +54 -54
  27. package/install.js +1 -1
  28. package/package.json +13 -10
  29. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  30. package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
  31. package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +1 -1
  32. package/pipeline/commands/multi-agent/autopilot/SKILL.md +1 -1
  33. package/pipeline/commands/multi-agent/build-optimize/SKILL.md +1 -1
  34. package/pipeline/commands/multi-agent/create-jira/SKILL.md +1 -1
  35. package/pipeline/commands/multi-agent/design-check/SKILL.md +1 -1
  36. package/pipeline/commands/multi-agent/dev/SKILL.md +1 -1
  37. package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +1 -1
  38. package/pipeline/commands/multi-agent/dev-local/SKILL.md +1 -1
  39. package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +1 -1
  40. package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -1
  41. package/pipeline/commands/multi-agent/finish/SKILL.md +6 -6
  42. package/pipeline/commands/multi-agent/forget/SKILL.md +1 -1
  43. package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
  44. package/pipeline/commands/multi-agent/help/SKILL.md +3 -3
  45. package/pipeline/commands/multi-agent/issue/SKILL.md +1 -1
  46. package/pipeline/commands/multi-agent/jira/SKILL.md +1 -1
  47. package/pipeline/commands/multi-agent/kill/SKILL.md +1 -1
  48. package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
  49. package/pipeline/commands/multi-agent/local/SKILL.md +1 -1
  50. package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +1 -1
  51. package/pipeline/commands/multi-agent/log/SKILL.md +1 -1
  52. package/pipeline/commands/multi-agent/manual-test/SKILL.md +1 -1
  53. package/pipeline/commands/multi-agent/prune-logs/SKILL.md +1 -1
  54. package/pipeline/commands/multi-agent/purge/SKILL.md +1 -1
  55. package/pipeline/commands/multi-agent/refactor/SKILL.md +16 -8
  56. package/pipeline/commands/multi-agent/resume/SKILL.md +2 -2
  57. package/pipeline/commands/multi-agent/review/SKILL.md +1 -1
  58. package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -1
  59. package/pipeline/commands/multi-agent/review-jira/SKILL.md +1 -1
  60. package/pipeline/commands/multi-agent/routines/SKILL.md +1 -1
  61. package/pipeline/commands/multi-agent/save/SKILL.md +1 -1
  62. package/pipeline/commands/multi-agent/scan/SKILL.md +1 -1
  63. package/pipeline/commands/multi-agent/search/SKILL.md +1 -1
  64. package/pipeline/commands/multi-agent/setup/SKILL.md +2 -2
  65. package/pipeline/commands/multi-agent/stack/SKILL.md +3 -3
  66. package/pipeline/commands/multi-agent/status/SKILL.md +1 -1
  67. package/pipeline/commands/multi-agent/sync/SKILL.md +5 -5
  68. package/pipeline/commands/multi-agent/test/SKILL.md +1 -1
  69. package/pipeline/commands/multi-agent/uninstall/SKILL.md +1 -1
  70. package/pipeline/commands/multi-agent/update/SKILL.md +3 -3
  71. package/pipeline/eval/run-metrics-fixture.json +60 -13
  72. package/pipeline/lib/account-resolver.sh +1 -1
  73. package/pipeline/lib/channels-multi-repo.sh +1 -1
  74. package/pipeline/lib/context-link-extractor.sh +1 -1
  75. package/pipeline/lib/credential-store.sh +1 -1
  76. package/pipeline/lib/fetch-confluence.sh +1 -1
  77. package/pipeline/lib/fetch-crashlytics.sh +1 -1
  78. package/pipeline/lib/fetch-fortify.sh +1 -1
  79. package/pipeline/lib/fetch-graylog.sh +1 -1
  80. package/pipeline/lib/fetch-swagger.sh +1 -1
  81. package/pipeline/lib/issue-fetcher.sh +1 -1
  82. package/pipeline/lib/multi-repo-pipeline.sh +1 -1
  83. package/pipeline/lib/repo-cache.sh +1 -1
  84. package/pipeline/lib/submodule-detector.sh +1 -1
  85. package/pipeline/multi-agent-refs/component-dispatch.md +1 -1
  86. package/pipeline/multi-agent-refs/component-generation.md +121 -0
  87. package/pipeline/multi-agent-refs/cross-cli-contract.md +1 -1
  88. package/pipeline/multi-agent-refs/features/model-fallback.md +2 -2
  89. package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
  90. package/pipeline/preferences-template.json +5 -11
  91. package/pipeline/schemas/agent-state.schema.json +39 -9
  92. package/pipeline/schemas/analysis-output.schema.json +18 -4
  93. package/pipeline/schemas/analysis-spec.schema.json +120 -32
  94. package/pipeline/schemas/clarify-output.schema.json +15 -5
  95. package/pipeline/schemas/design-check-config.schema.json +32 -11
  96. package/pipeline/schemas/dev-critic-output.schema.json +20 -5
  97. package/pipeline/schemas/figma-project-config.schema.json +42 -10
  98. package/pipeline/schemas/learnings-ledger.schema.json +10 -2
  99. package/pipeline/schemas/migrations/figma-config-1.0.0-to-2.0.0.mjs +1 -4
  100. package/pipeline/schemas/migrations/prefs-2.0.0-to-2.1.0.mjs +24 -7
  101. package/pipeline/schemas/plan-todos.schema.json +6 -3
  102. package/pipeline/schemas/planning-output.schema.json +5 -1
  103. package/pipeline/schemas/prefs.schema.json +91 -229
  104. package/pipeline/schemas/test-gap.schema.json +5 -5
  105. package/pipeline/schemas/token-budget.json +8 -8
  106. package/pipeline/schemas/triage-corpus.schema.json +1 -1
  107. package/pipeline/scripts/aggregate-metrics.mjs +18 -6
  108. package/pipeline/scripts/build-skills-index.mjs +6 -2
  109. package/pipeline/scripts/build-stack-plugins.mjs +142 -39
  110. package/pipeline/scripts/check-derived-drift.mjs +196 -0
  111. package/pipeline/scripts/check-md-links.mjs +6 -2
  112. package/pipeline/scripts/classify-plan-safety.mjs +20 -7
  113. package/pipeline/scripts/cost-budget-check.mjs +2 -1
  114. package/pipeline/scripts/cost-table.json +1 -1
  115. package/pipeline/scripts/diff-explain.mjs +7 -3
  116. package/pipeline/scripts/diff-risk-score.mjs +13 -3
  117. package/pipeline/scripts/eval-golden-tasks-live.mjs +8 -3
  118. package/pipeline/scripts/eval-golden-tasks.mjs +21 -9
  119. package/pipeline/scripts/eval-intent.mjs +8 -4
  120. package/pipeline/scripts/eval-mine-corpus.mjs +14 -4
  121. package/pipeline/scripts/evidence-gate.mjs +7 -2
  122. package/pipeline/scripts/fixtures/install-layout.tsv +3 -3
  123. package/pipeline/scripts/gen-mode-dispatch.mjs +38 -21
  124. package/pipeline/scripts/gen-skills-index.mjs +18 -3
  125. package/pipeline/scripts/learning-curve.mjs +13 -3
  126. package/pipeline/scripts/learnings-ledger.mjs +103 -36
  127. package/pipeline/scripts/lint-mcp-refs.mjs +13 -2
  128. package/pipeline/scripts/lint-skills.mjs +20 -9
  129. package/pipeline/scripts/localize-commands.mjs +6 -1
  130. package/pipeline/scripts/match-skills.mjs +15 -4
  131. package/pipeline/scripts/migrate-prefs.mjs +33 -16
  132. package/pipeline/scripts/phase-tracker.sh +3 -1
  133. package/pipeline/scripts/repo-map.mjs +110 -64
  134. package/pipeline/scripts/review-scope.mjs +7 -1
  135. package/pipeline/scripts/routine-registry.mjs +4 -9
  136. package/pipeline/scripts/run-aggregator.mjs +11 -5
  137. package/pipeline/scripts/run-metrics.mjs +13 -8
  138. package/pipeline/scripts/run-smokes.mjs +57 -3
  139. package/pipeline/scripts/scorecard.mjs +258 -0
  140. package/pipeline/scripts/smoke-context-budget.sh +72 -0
  141. package/pipeline/scripts/smoke-model-fallback.sh +1 -1
  142. package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +86 -7
  143. package/pipeline/scripts/smoke-own-punctuation.sh +103 -0
  144. package/pipeline/scripts/smoke-workflow-audit.sh +43 -11
  145. package/pipeline/scripts/smoke-write-state.sh +49 -5
  146. package/pipeline/scripts/test-gap-rules/android.json +11 -11
  147. package/pipeline/scripts/test-gap-rules/ios.json +16 -11
  148. package/pipeline/scripts/test-gap-rules/node.json +19 -7
  149. package/pipeline/scripts/test-gap-rules/python.json +10 -4
  150. package/pipeline/scripts/test-gap-scan.mjs +44 -12
  151. package/pipeline/scripts/test-integrity-gate.mjs +5 -1
  152. package/pipeline/scripts/token-budget-report.mjs +44 -21
  153. package/pipeline/scripts/triage-memory.mjs +126 -34
  154. package/pipeline/scripts/uninstall.mjs +74 -30
  155. package/pipeline/scripts/validate-analysis-doc.mjs +15 -5
  156. package/pipeline/scripts/validate-diff-risk.mjs +32 -18
  157. package/pipeline/scripts/validate-test-gap.mjs +17 -7
  158. package/pipeline/scripts/validate-triage.mjs +17 -5
  159. package/pipeline/scripts/write-state.mjs +32 -9
  160. package/pipeline/skills/.skills-index.json +91 -91
  161. package/pipeline/skills/shared/README.md +57 -57
  162. package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +1 -1
  163. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +1 -1
  164. package/pipeline/skills/shared/core/multi-agent/SKILL.md +26 -279
  165. package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +1 -1
  166. package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
  167. package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +1 -1
  168. package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
  169. package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
  170. package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +1 -1
  171. package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +1 -1
  172. package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +1 -1
  173. package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +1 -1
  174. package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +1 -1
  175. package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +1 -1
  176. package/pipeline/skills/shared/core/multi-agent-finish/SKILL.md +1 -1
  177. package/pipeline/skills/shared/core/multi-agent-forget/SKILL.md +1 -1
  178. package/pipeline/skills/shared/core/multi-agent-garbage-collect/SKILL.md +1 -1
  179. package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +1 -1
  180. package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +1 -1
  181. package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +1 -1
  182. package/pipeline/skills/shared/core/multi-agent-kill/SKILL.md +1 -1
  183. package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
  184. package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +1 -1
  185. package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +1 -1
  186. package/pipeline/skills/shared/core/multi-agent-log/SKILL.md +1 -1
  187. package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +1 -1
  188. package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +1 -1
  189. package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +1 -1
  190. package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +16 -8
  191. package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +1 -1
  192. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
  193. package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +1 -1
  194. package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +1 -1
  195. package/pipeline/skills/shared/core/multi-agent-routines/SKILL.md +1 -1
  196. package/pipeline/skills/shared/core/multi-agent-save/SKILL.md +1 -1
  197. package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +1 -1
  198. package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +1 -1
  199. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +1 -1
  200. package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +3 -3
  201. package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +1 -1
  202. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +1 -1
  203. package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +1 -1
  204. package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +1 -1
  205. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +1 -1
  206. package/pipeline/skills/shared/external/accessibility-compliance-accessibility-audit/SKILL.md +1 -1
  207. package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +4 -4
  208. package/pipeline/skills/shared/external/agentflow/SKILL.md +1 -1
  209. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +1 -1
  210. package/pipeline/skills/shared/external/android_ui_verification/SKILL.md +1 -1
  211. package/pipeline/skills/shared/external/api-patterns/SKILL.md +1 -1
  212. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +1 -1
  213. package/pipeline/skills/shared/external/app-store-changelog/SKILL.md +1 -1
  214. package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
  215. package/pipeline/skills/shared/external/backlog/SKILL.md +12 -12
  216. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -1
  217. package/pipeline/skills/shared/external/context-compression/SKILL.md +1 -1
  218. package/pipeline/skills/shared/external/council/SKILL.md +3 -3
  219. package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -1
  220. package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -1
  221. package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +1 -1
  222. package/pipeline/skills/shared/external/docker-expert/SKILL.md +1 -1
  223. package/pipeline/skills/shared/external/fastapi-pro/SKILL.md +1 -1
  224. package/pipeline/skills/shared/external/firebase/SKILL.md +1 -1
  225. package/pipeline/skills/shared/external/github-actions-templates/SKILL.md +1 -1
  226. package/pipeline/skills/shared/external/help-skills/SKILL.md +1 -1
  227. package/pipeline/skills/shared/external/hig-components-content/SKILL.md +1 -1
  228. package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +1 -1
  229. package/pipeline/skills/shared/external/hig-components-status/SKILL.md +1 -1
  230. package/pipeline/skills/shared/external/hig-components-system/SKILL.md +1 -1
  231. package/pipeline/skills/shared/external/hig-foundations/SKILL.md +1 -1
  232. package/pipeline/skills/shared/external/hig-inputs/SKILL.md +1 -1
  233. package/pipeline/skills/shared/external/hig-patterns/SKILL.md +1 -1
  234. package/pipeline/skills/shared/external/hig-platforms/SKILL.md +1 -1
  235. package/pipeline/skills/shared/external/hig-technologies/SKILL.md +1 -1
  236. package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -1
  237. package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -1
  238. package/pipeline/skills/shared/external/ios-debugger-agent/SKILL.md +1 -1
  239. package/pipeline/skills/shared/external/ios-developer/SKILL.md +1 -1
  240. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +1 -1
  241. package/pipeline/skills/shared/external/macos-menubar-tuist-app/SKILL.md +1 -1
  242. package/pipeline/skills/shared/external/macos-spm-app-packaging/SKILL.md +1 -1
  243. package/pipeline/skills/shared/external/monorepo-architect/SKILL.md +1 -1
  244. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -1
  245. package/pipeline/skills/shared/external/nodejs-backend-patterns/SKILL.md +1 -1
  246. package/pipeline/skills/shared/external/observability-engineer/SKILL.md +1 -1
  247. package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -1
  248. package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -1
  249. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -1
  250. package/pipeline/skills/shared/external/search-first/SKILL.md +2 -2
  251. package/pipeline/skills/shared/external/skill-creator/SKILL.md +12 -12
  252. package/pipeline/skills/shared/external/skill-creator/audit.md +21 -21
  253. package/pipeline/skills/shared/external/skill-creator/checklist.md +3 -3
  254. package/pipeline/skills/shared/external/skill-creator/examples.md +10 -10
  255. package/pipeline/skills/shared/external/skill-creator/label-check.md +17 -17
  256. package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +86 -50
  257. package/pipeline/skills/shared/external/skill-creator/template.md +9 -9
  258. package/pipeline/skills/shared/external/swift-concurrency-expert/SKILL.md +1 -1
  259. package/pipeline/skills/shared/external/swiftui-performance-audit/SKILL.md +1 -1
  260. package/pipeline/skills/shared/external/swiftui-ui-patterns/SKILL.md +1 -1
  261. package/pipeline/skills/shared/external/swiftui-view-refactor/SKILL.md +1 -1
  262. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -1
  263. package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -1
  264. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -1
  265. package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -1
  266. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -1
  267. package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -1
  268. package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -1
  269. package/pipeline/skills/shared/external/xcode-build-benchmark/schemas/build-benchmark.schema.json +9 -49
  270. package/pipeline/skills/skills-index.md +57 -57
  271. package/pipeline/scripts/smoke-plugin-validate.sh +0 -64
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ // scorecard.mjs - the measurable part of the review score, as a gate.
3
+ //
4
+ // A refactor review scores ten categories out of ten. Six of those numbers can
5
+ // be measured; four cannot, and writing a number for those is how a scorecard
6
+ // turns into self-congratulation. So this reports exactly two things:
7
+ //
8
+ // MEASURED a metric with a threshold, pass or fail
9
+ // UNMEASURED a category no script can score, named as such
10
+ //
11
+ // Nothing here re-implements a check. Each metric either reads a committed
12
+ // threshold file or shells out to the gate that already owns the question, so
13
+ // there is one source of truth per question and the scorecard cannot drift from
14
+ // the gates it summarises.
15
+ //
16
+ // Thresholds are locked at the values measured when the gate was written. They
17
+ // are a ratchet: lowering one is a deliberate, reviewable edit, and a metric
18
+ // that improves should have its floor raised.
19
+ //
20
+ // Runs on a clean clone: no $HOME lookups, no credentials, no network. The one
21
+ // metric that needs local configuration (derived-skill drift) reports
22
+ // NOT CONFIGURED rather than passing by default.
23
+ //
24
+ // Usage: node pipeline/scripts/scorecard.mjs [--json]
25
+ // Exit: 0 every measured metric within threshold, 1 otherwise
26
+
27
+ import { execFileSync } from "node:child_process";
28
+ import { existsSync, readFileSync } from "node:fs";
29
+ import { dirname, join } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
33
+ const asJson = process.argv.includes("--json");
34
+
35
+ // Locked floors. Raise when a metric improves; lowering one is the reviewable act.
36
+ const FLOORS = {
37
+ coverageLines: 72,
38
+ coverageBranches: 68,
39
+ coverageFunctions: 85,
40
+ contextBytes: 60000,
41
+ };
42
+
43
+ const results = [];
44
+
45
+ function measured(category, metric, ok, detail) {
46
+ results.push({ kind: "MEASURED", category, metric, ok, detail });
47
+ }
48
+ function unmeasured(category, why) {
49
+ results.push({ kind: "UNMEASURED", category, why });
50
+ }
51
+
52
+ // Run a gate and report whether it passed. The gate owns the assertion; this
53
+ // only records the verdict, so the two can never disagree.
54
+ function gate(category, metric, argv) {
55
+ try {
56
+ execFileSync(argv[0], argv.slice(1), { cwd: ROOT, stdio: "pipe" });
57
+ return measured(category, metric, true, "gate passed");
58
+ } catch (e) {
59
+ const out = `${e.stdout || ""}${e.stderr || ""}`.trim().split("\n").slice(-3).join(" | ");
60
+ return measured(category, metric, false, out || `exit ${e.status}`);
61
+ }
62
+ }
63
+
64
+ // --- Tests: the coverage floors must not be lowered ------------------------
65
+ // c8 enforces the numbers on every run; what nothing enforced was the numbers
66
+ // themselves. .c8rc.json reported 83% while the real figure over its own include
67
+ // set was 33%, because `all` was missing - so the floors are worth pinning.
68
+ {
69
+ const c8 = JSON.parse(readFileSync(join(ROOT, ".c8rc.json"), "utf-8"));
70
+ const checks = [
71
+ ["lines", c8.lines, FLOORS.coverageLines],
72
+ ["branches", c8.branches, FLOORS.coverageBranches],
73
+ ["functions", c8.functions, FLOORS.coverageFunctions],
74
+ ];
75
+ const low = checks.filter(([, actual, floor]) => !(actual >= floor));
76
+ measured(
77
+ "Tests",
78
+ "coverage floors held",
79
+ c8.all === true && low.length === 0,
80
+ c8.all !== true
81
+ ? "`all` is not set, so the floors only cover files the tests already load"
82
+ : low.length
83
+ ? low.map(([k, a, f]) => `${k} ${a} below the locked ${f}`).join(", ")
84
+ : checks.map(([k, a]) => `${k} ${a}`).join(", "),
85
+ );
86
+ }
87
+
88
+ // --- Dependency management: no high or critical advisories ------------------
89
+ {
90
+ let vulns;
91
+ try {
92
+ const raw = execFileSync("npm", ["audit", "--json"], { cwd: ROOT, encoding: "utf-8" });
93
+ vulns = JSON.parse(raw).metadata.vulnerabilities;
94
+ } catch (e) {
95
+ // npm audit exits non-zero when it finds something; the JSON is still on stdout.
96
+ try {
97
+ vulns = JSON.parse(e.stdout).metadata.vulnerabilities;
98
+ } catch {
99
+ vulns = null;
100
+ }
101
+ }
102
+ if (!vulns) {
103
+ measured(
104
+ "Dependency management",
105
+ "no high or critical advisories",
106
+ false,
107
+ "npm audit produced no parseable report",
108
+ );
109
+ } else {
110
+ const bad = vulns.high + vulns.critical;
111
+ measured(
112
+ "Dependency management",
113
+ "no high or critical advisories",
114
+ bad === 0,
115
+ `critical ${vulns.critical}, high ${vulns.high}, moderate ${vulns.moderate}`,
116
+ );
117
+ }
118
+ }
119
+
120
+ // --- Performance: the bytes every run pays before it starts ----------------
121
+ gate("Performance", "fixed per-run context within budget", [
122
+ "bash",
123
+ "pipeline/scripts/smoke-context-budget.sh",
124
+ ]);
125
+
126
+ // --- CI/CD: the workflow audit runs, and finds nothing ---------------------
127
+ // Strict by default, so a runner without the linters fails here rather than
128
+ // reporting a clean audit it never performed.
129
+ gate("CI/CD", "workflow audit clean (zizmor + actionlint)", [
130
+ "bash",
131
+ "pipeline/scripts/smoke-workflow-audit.sh",
132
+ ]);
133
+
134
+ // --- Code quality: our own punctuation rule, both encodings ---------------
135
+ gate("Code quality", "no banned punctuation in shipped files", [
136
+ "bash",
137
+ "pipeline/scripts/smoke-own-punctuation.sh",
138
+ ]);
139
+
140
+ // --- Documentation: every skill description routes ------------------------
141
+ gate("Documentation", "every skill description has a routing clause", [
142
+ "node",
143
+ "pipeline/scripts/lint-skills.mjs",
144
+ ]);
145
+
146
+ // --- Documentation: the licence and changelog a consumer expects ----------
147
+ {
148
+ const missing = ["LICENSE", "CHANGELOG.md", "README.md", "SECURITY.md"].filter(
149
+ (f) => !existsSync(join(ROOT, f)),
150
+ );
151
+ measured(
152
+ "Documentation",
153
+ "licence, changelog, readme, security present",
154
+ missing.length === 0,
155
+ missing.length ? `missing: ${missing.join(", ")}` : "all four present",
156
+ );
157
+ }
158
+
159
+ // --- Tests: no suite reports success without asserting anything ------------
160
+ // The runner owns this check; asking it for the counts would re-run 120 suites,
161
+ // so the scorecard asserts the capability is wired rather than re-measuring it.
162
+ {
163
+ const runner = readFileSync(join(ROOT, "pipeline", "scripts", "run-smokes.mjs"), "utf-8");
164
+ const wired = runner.includes("countAssertions") && /asserted nothing/.test(runner);
165
+ measured(
166
+ "Tests",
167
+ "runner rejects a suite that asserted nothing",
168
+ wired,
169
+ wired
170
+ ? "zero-assertion check present in run-smokes.mjs"
171
+ : "run-smokes.mjs no longer counts assertions",
172
+ );
173
+ }
174
+
175
+ // --- Drift: needs local configuration, so it reports honestly -------------
176
+ {
177
+ // The exit code alone cannot answer this. check-derived-drift exits 0 both
178
+ // when every derivation is current AND when nothing is configured at all, and
179
+ // those are different facts: one is a pass, the other is an absence of
180
+ // evidence. Reading `configured` off the JSON is what tells them apart. The
181
+ // first version of this block trusted exit 0 and reported an unconfigured
182
+ // machine as a passing metric, which is the exact failure this whole gate set
183
+ // exists to prevent.
184
+ let state;
185
+ let ok = false;
186
+ let parsed;
187
+ try {
188
+ parsed = JSON.parse(
189
+ execFileSync("node", ["pipeline/scripts/check-derived-drift.mjs", "--json"], {
190
+ cwd: ROOT,
191
+ encoding: "utf-8",
192
+ stdio: ["ignore", "pipe", "ignore"],
193
+ }),
194
+ );
195
+ } catch (e) {
196
+ // Non-zero exit still writes the report to stdout; an unparseable one means
197
+ // the check itself could not run.
198
+ try {
199
+ parsed = JSON.parse(e.stdout);
200
+ } catch {
201
+ parsed = undefined;
202
+ }
203
+ }
204
+ if (parsed && parsed.configured > 0) {
205
+ const bad = parsed.drifted + parsed.unverified + parsed.unreachable;
206
+ ok = bad === 0;
207
+ state = ok
208
+ ? `${parsed.configured} derivation(s) current with upstream`
209
+ : `${parsed.drifted} drifted, ${parsed.unverified} unverified, ${parsed.unreachable} unreachable`;
210
+ } else if (parsed) {
211
+ state = "NOT CONFIGURED - prefs carry no derivedSkillSources on this machine";
212
+ } else {
213
+ state = "NOT CONFIGURED - the drift check produced no parseable report";
214
+ }
215
+ if (state.startsWith("NOT CONFIGURED")) {
216
+ unmeasured("Derived-skill drift", state);
217
+ } else {
218
+ measured("Architecture", "derived skills current with upstream", ok, state);
219
+ }
220
+ }
221
+
222
+ // --- What no script can score --------------------------------------------
223
+ unmeasured("Architecture", "layer separation and dependency direction are judgement, not a number");
224
+ unmeasured("DevEx", "onboarding time and error-message quality need a person");
225
+ unmeasured(
226
+ "Accessibility",
227
+ "nothing in this repo renders UI; the audits it ships are scored per target app",
228
+ );
229
+ unmeasured(
230
+ "Code quality",
231
+ "naming, cohesion and altitude are judgement; only the mechanical rules above are gated",
232
+ );
233
+
234
+ const failed = results.filter((r) => r.kind === "MEASURED" && !r.ok);
235
+ const passed = results.filter((r) => r.kind === "MEASURED" && r.ok);
236
+
237
+ if (asJson) {
238
+ console.log(JSON.stringify({ passed: passed.length, failed: failed.length, results }, null, 2));
239
+ } else {
240
+ console.log("MEASURED");
241
+ for (const r of results.filter((x) => x.kind === "MEASURED")) {
242
+ console.log(` ${r.ok ? "✓" : "✗"} ${r.category}: ${r.metric}`);
243
+ console.log(` ${r.detail}`);
244
+ }
245
+ console.log("");
246
+ console.log("UNMEASURED (reported, never scored)");
247
+ for (const r of results.filter((x) => x.kind === "UNMEASURED")) {
248
+ console.log(` - ${r.category}: ${r.why}`);
249
+ }
250
+ console.log("");
251
+ console.log(`══ scorecard: ${passed.length} measured metrics passed, ${failed.length} failed ══`);
252
+ if (failed.length === 0) {
253
+ console.log("Every mechanically checkable claim holds. The four unmeasured");
254
+ console.log("categories above are not covered by that statement.");
255
+ }
256
+ }
257
+
258
+ process.exit(failed.length === 0 ? 0 : 1);
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # smoke-context-budget.sh - the bytes every pipeline run pays before it starts.
4
+ #
5
+ # core/multi-agent/SKILL.md plus multi-agent-refs/rules.md load on every run of
6
+ # every mode, whatever the task. They were 67150 bytes together, roughly 17k
7
+ # tokens spent before the first phase, because the orchestrator skill had grown
8
+ # its own inline copies of things that already existed as loadable references -
9
+ # the log-file template (whose copy had gone stale and named a path the code no
10
+ # longer uses), the help text the multi-agent-help skill owns, and a component
11
+ # generation guide that only applies to design tasks.
12
+ #
13
+ # A budget nobody measures only moves one way, so this pins it. The ceiling is
14
+ # the measured value plus a small margin: enough to add a paragraph, not enough
15
+ # to move a chapter back in. Lowering it is always fine.
16
+ #
17
+ # Exit codes: 0=within budget, 1=over, 2=a tracked file is missing
18
+
19
+ set -euo pipefail
20
+
21
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
22
+
23
+ # The files loaded unconditionally at run start.
24
+ TRACKED="
25
+ pipeline/skills/shared/core/multi-agent/SKILL.md
26
+ pipeline/multi-agent-refs/rules.md
27
+ "
28
+
29
+ # Measured 57289 after the split. 60000 leaves ~2.7 kB of headroom.
30
+ CEILING=${CONTEXT_BUDGET_CEILING:-60000}
31
+
32
+ PASS=0
33
+ FAIL=0
34
+ pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
35
+ fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
36
+
37
+ echo "→ 1. every tracked file exists"
38
+ TOTAL=0
39
+ for rel in $TRACKED; do
40
+ f="$REPO_ROOT/$rel"
41
+ if [ ! -f "$f" ]; then
42
+ echo " ✗ missing: $rel"
43
+ echo " a tracked file that vanished makes the budget meaningless"
44
+ exit 2
45
+ fi
46
+ n=$(wc -c < "$f" | tr -d ' ')
47
+ TOTAL=$((TOTAL + n))
48
+ pass "$rel ($n bytes)"
49
+ done
50
+
51
+ echo "→ 2. fixed per-run load is within budget"
52
+ if [ "$TOTAL" -le "$CEILING" ]; then
53
+ pass "fixed load $TOTAL bytes, ceiling $CEILING"
54
+ else
55
+ fail "fixed load $TOTAL bytes exceeds the $CEILING ceiling by $((TOTAL - CEILING))"
56
+ echo " Move detail into multi-agent-refs/ and point at it, rather than"
57
+ echo " raising the ceiling. Every byte here is paid by every run."
58
+ fi
59
+
60
+ echo "→ 3. the orchestrator points at the references instead of copying them"
61
+ SKILL="$REPO_ROOT/pipeline/skills/shared/core/multi-agent/SKILL.md"
62
+ REFS=$(grep -c "multi-agent-refs" "$SKILL" || true)
63
+ if [ "$REFS" -ge 3 ]; then
64
+ pass "orchestrator cites multi-agent-refs $REFS times"
65
+ else
66
+ fail "orchestrator cites multi-agent-refs only $REFS time(s)"
67
+ echo " It carried zero citations while duplicating three of those files."
68
+ fi
69
+
70
+ echo ""
71
+ echo "══ smoke-context-budget: $PASS passed, $FAIL failed ══"
72
+ [ "$FAIL" -gt 0 ] && exit 1 || exit 0
@@ -70,7 +70,7 @@ if [ "$FABLE_COUNT" -ge 4 ]; then
70
70
  else
71
71
  fail "expected >=4 fable personas (architects + Reviewer-1), found $FABLE_COUNT"
72
72
  fi
73
- # Opus stays a valid tier the first fallback below fable, and the top tier for security personas.
73
+ # Opus stays a valid tier - the first fallback below fable, and the top tier for security personas.
74
74
  OPUS_COUNT=$(grep -l "^preferredModel: opus" "$REPO_ROOT"/pipeline/agents/*.md 2>/dev/null | wc -l | tr -d ' ')
75
75
  if [ "$OPUS_COUNT" -ge 1 ]; then
76
76
  ok "opus personas intact ($OPUS_COUNT files)"
@@ -7,20 +7,30 @@
7
7
  # Phase 1; any entry in state.telemetry.mcpCalls[] with phase >= 2 is a
8
8
  # violation.
9
9
  #
10
- # Inputs
11
- # --state-file <path> Optional override (default: ~/.claude/state/run/latest.json)
10
+ # Two modes.
12
11
  #
13
- # Outcomes
14
- # - State file missing -> WARN, exit 0 (no run to check)
12
+ # CHECK MODE (--state-file <path>): audit that one state file.
15
13
  # - telemetry.mcpCalls field absent -> PASS, exit 0 (older run, no telemetry)
16
14
  # - All entries phase < 2 -> PASS, exit 0
17
15
  # - One or more violations -> FAIL summary on stdout, exit 1
16
+ # - Missing / corrupt file -> exit 2
17
+ #
18
+ # SELF-TEST MODE (no arguments): drive check mode over fixtures that cover
19
+ # every verdict, then audit the live state file if one happens to exist.
20
+ #
21
+ # Why: with no --state-file this script used to print "no run to check" and
22
+ # exit 0. On a clean clone, on CI, and on any machine that had not just
23
+ # finished a pipeline run - which is nearly always - the gate for Locked
24
+ # decision 30 therefore asserted nothing while reporting success. A gate whose
25
+ # green tick means "I had nothing to look at" is indistinguishable from one
26
+ # that passed, and this suite had three of those.
18
27
  #
19
28
  # stdout: human-readable summary only. stderr: empty in normal operation.
20
29
 
21
30
  set -euo pipefail
22
31
 
23
32
  STATE_FILE="${HOME}/.claude/state/run/latest.json"
33
+ STATE_FILE_EXPLICIT=0
24
34
 
25
35
  while [ "$#" -gt 0 ]; do
26
36
  case "$1" in
@@ -31,16 +41,19 @@ while [ "$#" -gt 0 ]; do
31
41
  exit 2
32
42
  fi
33
43
  STATE_FILE="$1"
44
+ STATE_FILE_EXPLICIT=1
34
45
  shift
35
46
  ;;
36
47
  --state-file=*)
37
48
  STATE_FILE="${1#--state-file=}"
49
+ STATE_FILE_EXPLICIT=1
38
50
  shift
39
51
  ;;
40
52
  -h|--help)
41
53
  echo "Usage: $0 [--state-file <path>]"
42
54
  echo ""
43
- echo "Default state file: ~/.claude/state/run/latest.json"
55
+ echo "No arguments: run the fixture self-test suite."
56
+ echo "Default state file when checking live: ~/.claude/state/run/latest.json"
44
57
  exit 0
45
58
  ;;
46
59
  *)
@@ -55,9 +68,75 @@ if ! command -v jq >/dev/null 2>&1; then
55
68
  exit 2
56
69
  fi
57
70
 
71
+ # ── self-test mode ─────────────────────────────────────────────────────────
72
+ if [ "$STATE_FILE_EXPLICIT" -eq 0 ]; then
73
+ SELF="$0"
74
+ TMP=$(mktemp -d)
75
+ trap 'rm -rf "$TMP"' EXIT
76
+ PASS=0
77
+ FAIL=0
78
+
79
+ expect() {
80
+ local name="$1" want="$2" file="$3"
81
+ local got=0
82
+ bash "$SELF" --state-file "$file" >/dev/null 2>&1 || got=$?
83
+ if [ "$got" -eq "$want" ]; then
84
+ PASS=$((PASS + 1)); echo " ✓ $name (exit $got)"
85
+ else
86
+ FAIL=$((FAIL + 1)); echo " ✗ $name (exit $got, expected $want)"
87
+ fi
88
+ }
89
+
90
+ echo "→ Locked decision 30 gate, fixture self-test"
91
+
92
+ printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"mcp__figma__get_design_context","phase":1}]}}' \
93
+ > "$TMP/phase1-only.json"
94
+ expect "MCP call in Phase 1 passes" 0 "$TMP/phase1-only.json"
95
+
96
+ printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"mcp__figma__get_design_context","phase":3}]}}' \
97
+ > "$TMP/phase3.json"
98
+ expect "MCP call in Phase 3 is a violation" 1 "$TMP/phase3.json"
99
+
100
+ printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"a","phase":1},{"tool":"b","phase":2}]}}' \
101
+ > "$TMP/mixed.json"
102
+ expect "one violation among clean entries still fails" 1 "$TMP/mixed.json"
103
+
104
+ printf '%s' '{"telemetry":{"mcpCalls":[]}}' > "$TMP/empty.json"
105
+ expect "empty call list passes" 0 "$TMP/empty.json"
106
+
107
+ printf '%s' '{"telemetry":{}}' > "$TMP/no-field.json"
108
+ expect "absent telemetry.mcpCalls passes (older state format)" 0 "$TMP/no-field.json"
109
+
110
+ printf '%s' '{"telemetry":{"mcpCalls":[{"tool":"a"}]}}' > "$TMP/no-phase.json"
111
+ expect "entry with no phase defaults to Phase 0 and passes" 0 "$TMP/no-phase.json"
112
+
113
+ printf '%s' 'not json at all' > "$TMP/corrupt.json"
114
+ expect "corrupt state is an error, never a silent pass" 2 "$TMP/corrupt.json"
115
+
116
+ expect "missing state file is an error in check mode" 2 "$TMP/does-not-exist.json"
117
+
118
+ # The live run, when there is one. Absence is not a failure here - the
119
+ # fixtures above already proved the gate works.
120
+ if [ -f "$STATE_FILE" ]; then
121
+ echo "→ live state file"
122
+ if bash "$SELF" --state-file "$STATE_FILE"; then
123
+ PASS=$((PASS + 1)); echo " ✓ live run clean"
124
+ else
125
+ FAIL=$((FAIL + 1)); echo " ✗ live run has violations"
126
+ fi
127
+ else
128
+ echo "→ live state file: none at $STATE_FILE (fixtures already exercised every path)"
129
+ fi
130
+
131
+ echo ""
132
+ echo "══ smoke-no-mcp-in-dev-phases: $PASS passed, $FAIL failed ══"
133
+ [ "$FAIL" -gt 0 ] && exit 1 || exit 0
134
+ fi
135
+
136
+ # ── check mode ─────────────────────────────────────────────────────────────
58
137
  if [ ! -f "$STATE_FILE" ]; then
59
- echo "WARN: state file not found at $STATE_FILE - no run to check"
60
- exit 0
138
+ echo "FAIL: state file not found at $STATE_FILE"
139
+ exit 2
61
140
  fi
62
141
 
63
142
  # Validate JSON parse first; corrupt state cannot be silently passed.
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # smoke-own-punctuation.sh - the punctuation rule applies to this repo's own
4
+ # shipped files, not only to the documents it generates.
5
+ #
6
+ # validate-analysis-doc.mjs has enforced the locked policy (no em-dash, en-dash,
7
+ # ellipsis or section sign) on generated analysis documents for a long time,
8
+ # and smoke-generate-issue.sh / smoke-review-readiness.sh enforce it on issue
9
+ # and review payloads. Nothing applied it to the pipeline's own tree, so 77
10
+ # shipped files carried 719 em-dashes, 10 en-dashes and 24 ellipses while the
11
+ # rule was described as enforced.
12
+ #
13
+ # ALLOWED holds the files that carry these characters as data rather than
14
+ # prose: the detector's own table and the grep patterns that look for them.
15
+ # Stripping those would disable the very checks this rule depends on. Every
16
+ # entry needs a reason, and the list is expected to stay this short.
17
+ #
18
+ # Exit codes: 0=clean, 1=violations
19
+
20
+ set -euo pipefail
21
+
22
+ REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
23
+ cd "$REPO_ROOT"
24
+
25
+ # file path -> why the characters are legitimately present
26
+ ALLOWED_PATHS="
27
+ pipeline/scripts/validate-analysis-doc.mjs|the detector's own table of banned characters
28
+ pipeline/scripts/smoke-generate-issue.sh|greps for the banned characters
29
+ pipeline/scripts/smoke-review-readiness.sh|greps for the banned characters
30
+ pipeline/scripts/smoke-md2confluence.sh|dirty-input fixture plus the forbidden list
31
+ pipeline/scripts/smoke-own-punctuation.sh|this file names the characters it bans
32
+ pipeline/lib/md2confluence-v3.py|the converter's codepoint-to-ASCII mapping table
33
+ "
34
+
35
+ # The same characters written as escapes. prefs.schema.json carried 50 of them
36
+ # in its descriptions and was invisible to a search for the literal character,
37
+ # so the first version of this gate passed it. A rule that only sees one
38
+ # encoding of what it bans is not enforcing the rule.
39
+ ESCAPED='\\u2014|\\u2013|\\u2026|\\u00a7'
40
+
41
+ is_allowed() {
42
+ printf '%s\n' "$ALLOWED_PATHS" | grep -q "^$1|"
43
+ }
44
+
45
+ PASS=0
46
+ FAIL=0
47
+ pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
48
+ fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
49
+
50
+ echo "→ 1. no em-dash, en-dash, ellipsis or section sign in shipped files"
51
+
52
+ # CHANGELOG history is left as written; rewriting released notes to satisfy a
53
+ # later style rule would falsify the record.
54
+ VIOLATIONS=""
55
+ while IFS= read -r f; do
56
+ case "$f" in CHANGELOG*.md) continue ;; esac
57
+ is_allowed "$f" && continue
58
+ VIOLATIONS="${VIOLATIONS}${f}\n"
59
+ done < <(git grep -lI -e $'—' -e $'–' -e $'…' -e $'§' -- . 2>/dev/null || true)
60
+
61
+ if [ -z "$VIOLATIONS" ]; then
62
+ pass "no banned punctuation outside the allowlist"
63
+ else
64
+ COUNT=$(printf '%b' "$VIOLATIONS" | grep -c . || true)
65
+ fail "$COUNT file(s) carry banned punctuation"
66
+ printf '%b' "$VIOLATIONS" | sed 's/^/ /'
67
+ echo " fix: plain ASCII - or : or parentheses; 'section 4' rather than the sign"
68
+ fi
69
+
70
+ echo "→ 2. same characters written as unicode escapes"
71
+ ESC_VIOLATIONS=""
72
+ while IFS= read -r f; do
73
+ case "$f" in CHANGELOG*.md) continue ;; esac
74
+ is_allowed "$f" && continue
75
+ ESC_VIOLATIONS="${ESC_VIOLATIONS}${f}\n"
76
+ done < <(git grep -lI -E "$ESCAPED" -- . 2>/dev/null || true)
77
+
78
+ if [ -z "$ESC_VIOLATIONS" ]; then
79
+ pass "no escaped forms outside the allowlist"
80
+ else
81
+ COUNT=$(printf '%b' "$ESC_VIOLATIONS" | grep -c . || true)
82
+ fail "$COUNT file(s) carry the banned characters as unicode escapes"
83
+ printf '%b' "$ESC_VIOLATIONS" | sed 's/^/ /'
84
+ fi
85
+
86
+ echo "→ 3. every allowlist entry still exists and still needs the exemption"
87
+ while IFS='|' read -r path reason; do
88
+ [ -z "$path" ] && continue
89
+ if [ ! -f "$path" ]; then
90
+ fail "allowlisted path no longer exists: $path"
91
+ elif ! grep -qI -e $'—' -e $'–' -e $'…' -e $'§' "$path" 2>/dev/null \
92
+ && ! grep -qIE "$ESCAPED" "$path" 2>/dev/null; then
93
+ # Either encoding justifies the entry: md2confluence-v3.py holds them as
94
+ # escapes, the detector table holds them literally.
95
+ fail "$path is allowlisted ($reason) but is now clean - drop it from the list"
96
+ else
97
+ pass "allowlist justified: ${path##*/} ($reason)"
98
+ fi
99
+ done < <(printf '%s\n' "$ALLOWED_PATHS" | grep '|')
100
+
101
+ echo ""
102
+ echo "══ smoke-own-punctuation: $PASS passed, $FAIL failed ══"
103
+ [ "$FAIL" -gt 0 ] && exit 1 || exit 0
@@ -4,20 +4,38 @@
4
4
  # `.github/workflows/` with the two dedicated external linters:
5
5
  #
6
6
  # - zizmor GitHub Actions security auditor (template injection,
7
- # credential persistence, unpinned actions, ...). Runs in
8
- # offline mode so the smoke never touches the network.
7
+ # credential persistence, cache poisoning, unpinned actions,
8
+ # excessive permissions, ...). Runs offline, no network.
9
9
  # - actionlint workflow correctness linter (expression syntax, matrix
10
10
  # typos, shellcheck of run: blocks, ...).
11
11
  #
12
- # Both tools are optional: each is skipped with a SKIP line when its binary
13
- # is not on PATH (billing-friendly CI runners install neither; run locally
14
- # via `brew install zizmor actionlint` for full coverage). Structural
15
- # baseline checks that need no external binary live in
16
- # smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
17
- #
18
12
  # Any finding from either tool fails the smoke.
19
13
  #
20
- # Exit codes: 0=pass or all tools skipped, 1=findings, 2=workflows missing
14
+ # WHY THIS IS STRICT BY DEFAULT
15
+ #
16
+ # Until now a missing binary produced a SKIP and the smoke exited 0, so it
17
+ # reported "0 passed, 0 failed, 2 skipped" on the dev machine AND on the CI
18
+ # runner - neither had the tools. ci-lite.yml justified not installing them by
19
+ # saying to run the smoke locally "where the tools exist", but they did not
20
+ # exist there either. The audit had therefore never run anywhere, while
21
+ # appearing as a green step. When it was finally run it found nine real issues,
22
+ # one of them high severity (a dependency cache restored into the job that
23
+ # signs and publishes the release tarball).
24
+ #
25
+ # So the failure mode is inverted: absent tooling is a failure, and skipping is
26
+ # something you opt into deliberately and locally.
27
+ #
28
+ # brew install zizmor actionlint # macOS
29
+ # pipx install zizmor && go install github.com/rhysd/actionlint/cmd/actionlint@latest
30
+ #
31
+ # WORKFLOW_AUDIT_ALLOW_MISSING=1 downgrades a missing binary back to a SKIP for
32
+ # a local run. It is ignored when CI is set: CI installs both, pinned, and a
33
+ # runner that cannot must fail loudly rather than pass quietly.
34
+ #
35
+ # Structural baseline checks that need no external binary live in
36
+ # smoke-ci-workflows.sh - this smoke is the deeper, tool-backed layer.
37
+ #
38
+ # Exit codes: 0=pass, 1=findings or missing tooling, 2=workflows missing
21
39
 
22
40
  set -uo pipefail
23
41
 
@@ -26,6 +44,10 @@ WF_DIR="$REPO_ROOT/.github/workflows"
26
44
 
27
45
  [ -d "$WF_DIR" ] || { echo "FAIL: .github/workflows/ missing"; exit 2; }
28
46
 
47
+ # In CI the escape hatch does not apply, whatever the environment says.
48
+ ALLOW_MISSING="${WORKFLOW_AUDIT_ALLOW_MISSING:-0}"
49
+ [ -n "${CI:-}" ] && ALLOW_MISSING=0
50
+
29
51
  PASS=0
30
52
  FAIL=0
31
53
  SKIPPED=0
@@ -33,6 +55,16 @@ pass() { PASS=$((PASS + 1)); echo " ✓ $1"; }
33
55
  fail() { FAIL=$((FAIL + 1)); echo " ✗ $1"; }
34
56
  skip() { SKIPPED=$((SKIPPED + 1)); echo " ↷ SKIP: $1"; }
35
57
 
58
+ # A tool that is not installed is a failure unless a local run opted out.
59
+ missing_tool() {
60
+ local tool="$1" install="$2"
61
+ if [ "$ALLOW_MISSING" = "1" ]; then
62
+ skip "$tool not on PATH, WORKFLOW_AUDIT_ALLOW_MISSING=1 ($install)"
63
+ else
64
+ fail "$tool not on PATH - the audit cannot run ($install)"
65
+ fi
66
+ }
67
+
36
68
  # ──────────────────────────────────────────────────────────────────────────
37
69
  echo "→ 1. zizmor (security audit, offline)"
38
70
  if command -v zizmor >/dev/null 2>&1; then
@@ -48,7 +80,7 @@ if command -v zizmor >/dev/null 2>&1; then
48
80
  echo "$OUT" | sed 's/^/ /'
49
81
  fi
50
82
  else
51
- skip "zizmor not on PATH (brew install zizmor)"
83
+ missing_tool zizmor "brew install zizmor"
52
84
  fi
53
85
 
54
86
  # ──────────────────────────────────────────────────────────────────────────
@@ -61,7 +93,7 @@ if command -v actionlint >/dev/null 2>&1; then
61
93
  echo "$OUT" | sed 's/^/ /'
62
94
  fi
63
95
  else
64
- skip "actionlint not on PATH (brew install actionlint)"
96
+ missing_tool actionlint "brew install actionlint"
65
97
  fi
66
98
 
67
99
  echo ""