continuous-improvement 3.1.0 → 3.9.1

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 (136) hide show
  1. package/.claude-plugin/marketplace.json +66 -0
  2. package/CHANGELOG.md +216 -0
  3. package/QUICKSTART.md +79 -12
  4. package/README.md +229 -349
  5. package/SKILL.md +87 -9
  6. package/action.yml +1 -1
  7. package/bin/analyze.sh +9 -1
  8. package/bin/backfill.mjs +172 -0
  9. package/bin/check-doc-runtime-claims.mjs +120 -0
  10. package/bin/check-docs-substrings.mjs +333 -0
  11. package/bin/check-everything-mirror.mjs +145 -0
  12. package/bin/check-routing-targets.mjs +151 -0
  13. package/bin/check-skill-law-tag.mjs +128 -0
  14. package/bin/check-skill-mirror.mjs +119 -0
  15. package/bin/check-skill-tiers.mjs +116 -0
  16. package/bin/check-third-party-shape.mjs +202 -0
  17. package/bin/generate-plugin-manifests.mjs +171 -0
  18. package/bin/harvest-friction.mjs +279 -0
  19. package/bin/hook-stats.mjs +258 -0
  20. package/bin/install.mjs +407 -499
  21. package/bin/lint-transcript.mjs +182 -210
  22. package/bin/mcp-server.mjs +834 -608
  23. package/bin/observe.mjs +148 -0
  24. package/bin/pre-commit-block-strays.sh +49 -0
  25. package/bin/refresh-third-party.mjs +416 -0
  26. package/bin/unified-cli.mjs +533 -0
  27. package/commands/continuous-improvement.md +43 -2
  28. package/commands/discipline.md +14 -0
  29. package/commands/harvest.md +76 -0
  30. package/commands/learn-eval.md +117 -0
  31. package/commands/planning-with-files.md +66 -0
  32. package/commands/proceed-with-the-recommendation.md +62 -0
  33. package/commands/ralph.md +103 -0
  34. package/commands/release-train.md +81 -0
  35. package/commands/seven-laws.md +16 -0
  36. package/commands/superpowers.md +180 -0
  37. package/commands/swarm.md +101 -0
  38. package/commands/workspace-surface-audit.md +77 -0
  39. package/hooks/gateguard.mjs +172 -0
  40. package/hooks/observe.sh +42 -4
  41. package/hooks/session.sh +3 -3
  42. package/hooks/three-section-close.mjs +181 -0
  43. package/instinct-packs/meta.json +16 -0
  44. package/lib/cli-anything.mjs +401 -0
  45. package/lib/compound-engineering.mjs +831 -0
  46. package/lib/gateguard-state.mjs +85 -0
  47. package/lib/observe-event.mjs +128 -0
  48. package/lib/plugin-metadata.mjs +435 -0
  49. package/lib/pm-marketplace.mjs +61 -0
  50. package/lib/pm-skills.mjs +1274 -0
  51. package/lib/resolve-home-dir.mjs +43 -0
  52. package/lib/skill-tiers.mjs +137 -0
  53. package/lib/unified-plugin.mjs +924 -0
  54. package/llms.txt +32 -7
  55. package/package.json +29 -19
  56. package/plugins/beginner.json +17 -6
  57. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +20 -0
  58. package/plugins/continuous-improvement/.claude-plugin/plugin.json +26 -0
  59. package/plugins/continuous-improvement/LICENSE +21 -0
  60. package/plugins/continuous-improvement/README.md +57 -0
  61. package/plugins/continuous-improvement/agents/README.md +120 -0
  62. package/plugins/continuous-improvement/agents/code-reviewer.md +97 -0
  63. package/plugins/continuous-improvement/agents/security-auditor.md +101 -0
  64. package/plugins/continuous-improvement/agents/test-engineer.md +95 -0
  65. package/plugins/continuous-improvement/bin/backfill.mjs +172 -0
  66. package/plugins/continuous-improvement/bin/mcp-server.mjs +889 -0
  67. package/plugins/continuous-improvement/bin/observe.mjs +148 -0
  68. package/plugins/continuous-improvement/commands/continuous-improvement.md +115 -0
  69. package/plugins/continuous-improvement/commands/dashboard.md +56 -0
  70. package/plugins/continuous-improvement/commands/discipline.md +51 -0
  71. package/plugins/continuous-improvement/commands/harvest.md +76 -0
  72. package/plugins/continuous-improvement/commands/learn-eval.md +117 -0
  73. package/plugins/continuous-improvement/commands/planning-with-files.md +66 -0
  74. package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +62 -0
  75. package/plugins/continuous-improvement/commands/ralph.md +103 -0
  76. package/plugins/continuous-improvement/commands/release-train.md +81 -0
  77. package/plugins/continuous-improvement/commands/seven-laws.md +16 -0
  78. package/plugins/continuous-improvement/commands/superpowers.md +180 -0
  79. package/plugins/continuous-improvement/commands/swarm.md +101 -0
  80. package/plugins/continuous-improvement/commands/workspace-surface-audit.md +77 -0
  81. package/plugins/continuous-improvement/hooks/gateguard.mjs +172 -0
  82. package/plugins/continuous-improvement/hooks/hooks.json +65 -0
  83. package/plugins/continuous-improvement/hooks/observe.sh +172 -0
  84. package/plugins/continuous-improvement/hooks/session.sh +106 -0
  85. package/plugins/continuous-improvement/hooks/three-section-close.mjs +181 -0
  86. package/plugins/continuous-improvement/instinct-packs/go.json +58 -0
  87. package/plugins/continuous-improvement/instinct-packs/meta.json +16 -0
  88. package/plugins/continuous-improvement/instinct-packs/python.json +58 -0
  89. package/plugins/continuous-improvement/instinct-packs/react.json +58 -0
  90. package/plugins/continuous-improvement/lib/observe-event.mjs +128 -0
  91. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +435 -0
  92. package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +43 -0
  93. package/plugins/continuous-improvement/skills/README.md +37 -0
  94. package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +249 -0
  95. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +178 -0
  96. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +163 -0
  97. package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +108 -0
  98. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +447 -0
  99. package/plugins/continuous-improvement/skills/ralph/SKILL.md +221 -0
  100. package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +73 -0
  101. package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +76 -0
  102. package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +63 -0
  103. package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +104 -0
  104. package/plugins/continuous-improvement/skills/superpowers/SKILL.md +219 -0
  105. package/plugins/continuous-improvement/skills/tdd-workflow/SKILL.md +411 -0
  106. package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +136 -0
  107. package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +224 -0
  108. package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +191 -0
  109. package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +147 -0
  110. package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +66 -0
  111. package/plugins/continuous-improvement/templates/planning-with-files/findings.md +8 -0
  112. package/plugins/continuous-improvement/templates/planning-with-files/progress.md +7 -0
  113. package/plugins/continuous-improvement/templates/planning-with-files/task_plan.md +23 -0
  114. package/plugins/expert.json +26 -5
  115. package/skills/README.md +79 -0
  116. package/skills/deploy-receipt.md +178 -0
  117. package/skills/gateguard.md +163 -0
  118. package/skills/para-memory-files.md +108 -0
  119. package/skills/proceed-with-the-recommendation.md +447 -0
  120. package/skills/ralph.md +221 -0
  121. package/skills/recovery-classification.md +73 -0
  122. package/skills/safety-guard.md +76 -0
  123. package/skills/state-reconciliation.md +63 -0
  124. package/skills/strategic-compact.md +104 -0
  125. package/skills/superpowers.md +219 -0
  126. package/skills/tdd-workflow.md +411 -0
  127. package/skills/token-budget-advisor.md +136 -0
  128. package/skills/verification-loop.md +224 -0
  129. package/skills/wild-risa-balance.md +191 -0
  130. package/skills/workspace-surface-audit.md +147 -0
  131. package/skills/worktree-safety.md +66 -0
  132. package/templates/insights-claude-md.md +91 -0
  133. package/templates/planning-with-files/findings.md +8 -0
  134. package/templates/planning-with-files/progress.md +7 -0
  135. package/templates/planning-with-files/task_plan.md +23 -0
  136. package/templates/verify-ladder.example.json +60 -0
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Docs Substring Check
4
+ *
5
+ * Verifies that every substring this repo's tests assert on inside a real
6
+ * `*.md` file is currently present in that file. Mechanical enforcement of
7
+ * the "Test/docs sync rule" in CONTRIBUTING.md: a wholesale docs rewrite
8
+ * (README.md, CHANGELOG.md, skill markdown) must preserve the substrings
9
+ * the test suite expects, or update the tests in the same PR.
10
+ *
11
+ * The full test suite (`npm test`) already runs each `assert.match` and
12
+ * fails on drift. This lint adds three things on top:
13
+ * 1. A focused, fast (<500ms) check that only verifies prose substrings.
14
+ * 2. A clean failure surface — it names (target file, expected substring,
15
+ * originating test) instead of dumping the full doc content into stderr.
16
+ * 3. A standalone CI step that fails before the broader test job has a
17
+ * chance to bury the drift in unrelated output.
18
+ *
19
+ * The manifest below is the spec. When you add or change a prose-substring
20
+ * assertion in `src/test/*.mts`, add or update the corresponding entry here
21
+ * in the same PR (per the Test/docs sync rule).
22
+ *
23
+ * Usage:
24
+ * node bin/check-docs-substrings.mjs # Check the current repo
25
+ * node bin/check-docs-substrings.mjs <repo-root> # Check a specific repo root
26
+ * node bin/check-docs-substrings.mjs --list # Print the inventory and exit
27
+ *
28
+ * Exit codes:
29
+ * 0 — every substring matches
30
+ * 1 — at least one assertion failed (target file is missing or doesn't
31
+ * contain the expected substring)
32
+ */
33
+ import { readFileSync } from "node:fs";
34
+ import { join } from "node:path";
35
+ import { argv, cwd, exit } from "node:process";
36
+ export const DOCS_ASSERTIONS = [
37
+ // README.md (src/test/community.test.mts)
38
+ { file: "README.md", pattern: /planning-with-files/i, source: "community.test.mts:88" },
39
+ { file: "README.md", pattern: /task_plan\.md/, source: "community.test.mts:89" },
40
+ { file: "README.md", pattern: /ci_plan_init/, source: "community.test.mts:90" },
41
+ // README.md install hero — locked 2026-05-05 to prevent silent rewording of
42
+ // the install decision rule. The whole install section was restructured to
43
+ // lead with this one line because the previous "pick one path" framing left
44
+ // first-time installers comparison-shopping. Drop or rewrite this and
45
+ // beginner-vs-expert ambiguity returns immediately.
46
+ { file: "README.md", pattern: "If you don't know which to pick, use Beginner.", source: "docs-substrings-manifest:readme-install-decision-rule" },
47
+ // CONTRIBUTING.md (src/test/community.test.mts)
48
+ { file: "CONTRIBUTING.md", pattern: /Contributing/, source: "community.test.mts:16" },
49
+ { file: "CONTRIBUTING.md", pattern: /npm test/, source: "community.test.mts:17" },
50
+ // CONTRIBUTING.md Law Coverage Matrix anchor — locked 2026-05-05. The README
51
+ // points to this section via #law-coverage-matrix; renaming the heading
52
+ // creates a silent 404 in the README link. Lint catches it.
53
+ { file: "CONTRIBUTING.md", pattern: "## Law Coverage Matrix", source: "docs-substrings-manifest:contributing-law-coverage-anchor" },
54
+ // CODE_OF_CONDUCT.md (src/test/community.test.mts)
55
+ { file: "CODE_OF_CONDUCT.md", pattern: /Code of Conduct/, source: "community.test.mts:24" },
56
+ { file: "CODE_OF_CONDUCT.md", pattern: /Contributor Covenant/, source: "community.test.mts:25" },
57
+ // SECURITY.md (src/test/community.test.mts)
58
+ { file: "SECURITY.md", pattern: /Security/, source: "community.test.mts:32" },
59
+ { file: "SECURITY.md", pattern: /Reporting/i, source: "community.test.mts:33" },
60
+ { file: "SECURITY.md", pattern: /Vulnerabilit/i, source: "community.test.mts:34" },
61
+ // .github/ISSUE_TEMPLATE/bug_report.md (src/test/community.test.mts)
62
+ { file: ".github/ISSUE_TEMPLATE/bug_report.md", pattern: /Bug/, source: "community.test.mts:99" },
63
+ { file: ".github/ISSUE_TEMPLATE/bug_report.md", pattern: /Steps to reproduce/, source: "community.test.mts:100" },
64
+ // .github/ISSUE_TEMPLATE/feature_request.md (src/test/community.test.mts)
65
+ { file: ".github/ISSUE_TEMPLATE/feature_request.md", pattern: /Feature/, source: "community.test.mts:107" },
66
+ { file: ".github/ISSUE_TEMPLATE/feature_request.md", pattern: /law/i, source: "community.test.mts:108" },
67
+ // SKILL.md (src/test/skill.test.mts)
68
+ { file: "SKILL.md", pattern: /^---\r?\n/, source: "skill.test.mts:19" },
69
+ { file: "SKILL.md", pattern: /name: continuous-improvement/, source: "skill.test.mts:20" },
70
+ { file: "SKILL.md", pattern: /description:/, source: "skill.test.mts:21" },
71
+ { file: "SKILL.md", pattern: /Law 1.*Research Before Executing/s, source: "skill.test.mts:25" },
72
+ { file: "SKILL.md", pattern: /Law 2.*Plan Is Sacred/s, source: "skill.test.mts:26" },
73
+ { file: "SKILL.md", pattern: /Law 3.*One Thing at a Time/s, source: "skill.test.mts:27" },
74
+ { file: "SKILL.md", pattern: /Law 4.*Verify Before Reporting/s, source: "skill.test.mts:28" },
75
+ { file: "SKILL.md", pattern: /Law 5.*Reflect After/s, source: "skill.test.mts:29" },
76
+ { file: "SKILL.md", pattern: /Law 6.*Iterate/s, source: "skill.test.mts:30" },
77
+ { file: "SKILL.md", pattern: /Law 7.*Learn From Every Session/s, source: "skill.test.mts:31" },
78
+ { file: "SKILL.md", pattern: /Research.*Plan.*Execute.*Verify.*Reflect.*Learn.*Iterate/s, source: "skill.test.mts:36" },
79
+ { file: "SKILL.md", pattern: /Mulahazah/i, source: "skill.test.mts:42" },
80
+ { file: "SKILL.md", pattern: /confidence/i, source: "skill.test.mts:43" },
81
+ { file: "SKILL.md", pattern: /Auto-Level/i, source: "skill.test.mts:44" },
82
+ { file: "SKILL.md", pattern: /id:/, source: "skill.test.mts:48" },
83
+ { file: "SKILL.md", pattern: /trigger:/, source: "skill.test.mts:49" },
84
+ { file: "SKILL.md", pattern: /confidence:/, source: "skill.test.mts:50" },
85
+ { file: "SKILL.md", pattern: /Planning-With-Files/i, source: "skill.test.mts:54" },
86
+ { file: "SKILL.md", pattern: /opt-in/i, source: "skill.test.mts:55" },
87
+ { file: "SKILL.md", pattern: /task_plan\.md/, source: "skill.test.mts:56" },
88
+ // commands/discipline.md (src/test/commands.test.mts)
89
+ { file: "commands/discipline.md", pattern: /^---\r?\n/, source: "commands.test.mts:20" },
90
+ { file: "commands/discipline.md", pattern: /name: discipline/, source: "commands.test.mts:21" },
91
+ { file: "commands/discipline.md", pattern: /description:/, source: "commands.test.mts:22" },
92
+ { file: "commands/discipline.md", pattern: /Research Before Executing/, source: "commands.test.mts:26" },
93
+ { file: "commands/discipline.md", pattern: /Plan Is Sacred/, source: "commands.test.mts:27" },
94
+ { file: "commands/discipline.md", pattern: /One Thing at a Time/, source: "commands.test.mts:28" },
95
+ { file: "commands/discipline.md", pattern: /Verify Before Reporting/, source: "commands.test.mts:29" },
96
+ { file: "commands/discipline.md", pattern: /Reflect After Sessions/, source: "commands.test.mts:30" },
97
+ { file: "commands/discipline.md", pattern: /Iterate One Change/, source: "commands.test.mts:31" },
98
+ { file: "commands/discipline.md", pattern: /Learn From Every Session/, source: "commands.test.mts:32" },
99
+ { file: "commands/discipline.md", pattern: /I'll just quickly/, source: "commands.test.mts:36" },
100
+ { file: "commands/discipline.md", pattern: /This should work/, source: "commands.test.mts:37" },
101
+ { file: "commands/discipline.md", pattern: /Code runs without errors/, source: "commands.test.mts:41" },
102
+ { file: "commands/discipline.md", pattern: /Build passes/, source: "commands.test.mts:42" },
103
+ // commands/dashboard.md (src/test/commands.test.mts)
104
+ { file: "commands/dashboard.md", pattern: /^---\r?\n/, source: "commands.test.mts:56" },
105
+ { file: "commands/dashboard.md", pattern: /name: dashboard/, source: "commands.test.mts:57" },
106
+ { file: "commands/dashboard.md", pattern: /Dashboard/, source: "commands.test.mts:61" },
107
+ { file: "commands/dashboard.md", pattern: /Observations/, source: "commands.test.mts:62" },
108
+ { file: "commands/dashboard.md", pattern: /Instincts/, source: "commands.test.mts:63" },
109
+ { file: "commands/dashboard.md", pattern: /Health/, source: "commands.test.mts:64" },
110
+ { file: "commands/dashboard.md", pattern: /CAPTURE/, source: "commands.test.mts:68" },
111
+ { file: "commands/dashboard.md", pattern: /ANALYZE/, source: "commands.test.mts:69" },
112
+ { file: "commands/dashboard.md", pattern: /beginner|expert/, source: "commands.test.mts:70" },
113
+ // commands/planning-with-files.md (src/test/commands.test.mts)
114
+ { file: "commands/planning-with-files.md", pattern: /^---\r?\n/, source: "commands.test.mts:91" },
115
+ { file: "commands/planning-with-files.md", pattern: /name: planning-with-files/, source: "commands.test.mts:92" },
116
+ { file: "commands/planning-with-files.md", pattern: /description:/, source: "commands.test.mts:93" },
117
+ { file: "commands/planning-with-files.md", pattern: /task_plan\.md/, source: "commands.test.mts:97" },
118
+ { file: "commands/planning-with-files.md", pattern: /findings\.md/, source: "commands.test.mts:98" },
119
+ { file: "commands/planning-with-files.md", pattern: /progress\.md/, source: "commands.test.mts:99" },
120
+ { file: "commands/planning-with-files.md", pattern: /init/i, source: "commands.test.mts:100" },
121
+ { file: "commands/planning-with-files.md", pattern: /status/i, source: "commands.test.mts:101" },
122
+ { file: "commands/planning-with-files.md", pattern: /checkpoint/i, source: "commands.test.mts:102" },
123
+ { file: "commands/planning-with-files.md", pattern: /recover/i, source: "commands.test.mts:103" },
124
+ // Reflection-block mirror files (src/test/reflection-iteration-field.test.mts)
125
+ // Each must contain the literal "Iteration — Next best recommendations (ranked, top 3)" string.
126
+ { file: "SKILL.md", pattern: "Iteration — Next best recommendations (ranked, top 3)", source: "reflection-iteration-field.test.mts:43" },
127
+ { file: "commands/continuous-improvement.md", pattern: "Iteration — Next best recommendations (ranked, top 3)", source: "reflection-iteration-field.test.mts:43" },
128
+ { file: "plugins/continuous-improvement/skills/continuous-improvement/SKILL.md", pattern: "Iteration — Next best recommendations (ranked, top 3)", source: "reflection-iteration-field.test.mts:43" },
129
+ { file: "plugins/continuous-improvement/commands/continuous-improvement.md", pattern: "Iteration — Next best recommendations (ranked, top 3)", source: "reflection-iteration-field.test.mts:43" },
130
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Iteration — Next best recommendations (ranked, top 3)", source: "reflection-iteration-field.test.mts:43" },
131
+ // Past Mistake Acknowledgment Gate / P-MAG (src/test/past-mistake-gate.test.mts)
132
+ // Source skill + plugin mirror must both contain the Phase 0 heading, the negative-prompt field marker,
133
+ // and the Stop Conditions clearance bullet. Dropping any of the three silently removes part of the gate.
134
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Phase 0: Acknowledge (Past Mistake Acknowledgment Gate / P-MAG)", source: "past-mistake-gate.test.mts" },
135
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "Phase 0: Acknowledge (Past Mistake Acknowledgment Gate / P-MAG)", source: "past-mistake-gate.test.mts" },
136
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Will NOT repeat:", source: "past-mistake-gate.test.mts" },
137
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "Will NOT repeat:", source: "past-mistake-gate.test.mts" },
138
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Prior-mistake residue still present", source: "past-mistake-gate.test.mts" },
139
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "Prior-mistake residue still present", source: "past-mistake-gate.test.mts" },
140
+ // P-MAG third surface (auto-memory feedback_*.md) — locked 2026-05-07 after PR #83.
141
+ // Rule 1 of Phase 0 was upgraded from scanning two surfaces to scanning three so
142
+ // the operator's named past-mistake corrections (which live in
143
+ // ~/.claude/projects/<hash>/memory/feedback_*.md) cannot be silently skipped.
144
+ // Each assertion below catches a specific class of regression:
145
+ // - "Scan three surfaces" → reverting the surface count back to two
146
+ // - "memory/feedback_*.md" → dropping the glob pattern entirely
147
+ // - "feedback_past_mistake_gate.md" → generic rewording that loses the concrete anchor
148
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Scan three surfaces", source: "past-mistake-gate.test.mts" },
149
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "Scan three surfaces", source: "past-mistake-gate.test.mts" },
150
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "memory/feedback_*.md", source: "past-mistake-gate.test.mts" },
151
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "memory/feedback_*.md", source: "past-mistake-gate.test.mts" },
152
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "feedback_past_mistake_gate.md", source: "past-mistake-gate.test.mts" },
153
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "feedback_past_mistake_gate.md", source: "past-mistake-gate.test.mts" },
154
+ // workspace-surface-audit Environment Grain — locked 2026-05-07 (PR A of second-release train).
155
+ // Phase 1 of the audit gained an Environment Grain subsection that records shell flavor,
156
+ // autocrlf state, jq availability, case-sensitive filesystem flag, CWD baseline, and a
157
+ // parallel-actor expectation flag — the per-host facts that root the report's
158
+ // "command failed / wrong approach" friction class. Each assertion catches a specific
159
+ // class of regression:
160
+ // - "#### Environment Grain" → removing the whole subsection
161
+ // - "core.autocrlf" → dropping the Windows-line-endings probe
162
+ // - "Parallel-actor expectation" → losing the cross-skill integration with gateguard PR #83
163
+ { file: "skills/workspace-surface-audit.md", pattern: "#### Environment Grain", source: "docs-substrings-manifest:workspace-surface-audit-environment-grain" },
164
+ { file: "plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md", pattern: "#### Environment Grain", source: "docs-substrings-manifest:workspace-surface-audit-environment-grain" },
165
+ { file: "skills/workspace-surface-audit.md", pattern: "core.autocrlf", source: "docs-substrings-manifest:workspace-surface-audit-environment-grain" },
166
+ { file: "plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md", pattern: "core.autocrlf", source: "docs-substrings-manifest:workspace-surface-audit-environment-grain" },
167
+ { file: "skills/workspace-surface-audit.md", pattern: "Parallel-actor expectation", source: "docs-substrings-manifest:workspace-surface-audit-environment-grain" },
168
+ { file: "plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md", pattern: "Parallel-actor expectation", source: "docs-substrings-manifest:workspace-surface-audit-environment-grain" },
169
+ // superpowers Stacked-PR Plan Precondition — locked 2026-05-07 (PR B of second-release train).
170
+ // The dispatcher gained a non-negotiable rule for ≥3-file changes: produce a stacked-PR plan
171
+ // (per-PR table, dependency graph, worktree-per-PR, out-of-scope) before the first edit. The
172
+ // rule excludes markdown-only / lockfile-only / generated-only / vendor-refresh / skill-mirror
173
+ // changes. Each assertion below catches a specific class of regression:
174
+ // - "## Stacked-PR Plan Precondition (≥3 files)" → removing the whole rule heading
175
+ // - "Per-PR table" → losing the required-output enumeration
176
+ { file: "skills/superpowers.md", pattern: "## Stacked-PR Plan Precondition (≥3 files)", source: "docs-substrings-manifest:superpowers-stacked-pr-precondition" },
177
+ { file: "plugins/continuous-improvement/skills/superpowers/SKILL.md", pattern: "## Stacked-PR Plan Precondition (≥3 files)", source: "docs-substrings-manifest:superpowers-stacked-pr-precondition" },
178
+ { file: "skills/superpowers.md", pattern: "Per-PR table", source: "docs-substrings-manifest:superpowers-stacked-pr-precondition" },
179
+ { file: "plugins/continuous-improvement/skills/superpowers/SKILL.md", pattern: "Per-PR table", source: "docs-substrings-manifest:superpowers-stacked-pr-precondition" },
180
+ // verification-loop per-project ladder — locked 2026-05-07 (PR C of second-release train).
181
+ // Phase 0 (Ladder Resolution) was added so Phases 1–6 read the project's actual build/typecheck/
182
+ // lint/test/security/deploy_receipt invocations from .claude/verify-ladder.json (or sniff /
183
+ // ask fallback) instead of hardcoded npm run X. Phase 8 wires deploy-receipt (PR #83) for
184
+ // auto-deploy projects. Each assertion below catches a specific class of regression:
185
+ // - "### Phase 0: Resolve the Ladder" → removing the whole resolution phase
186
+ // - ".claude/verify-ladder.json" → renaming or moving the manifest path
187
+ // - "verify-ladder (resolved):" → losing the resolved-ladder output contract
188
+ // - "### Phase 8: Deploy Receipt" → losing the cross-skill integration with PR #83
189
+ { file: "skills/verification-loop.md", pattern: "### Phase 0: Resolve the Ladder", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
190
+ { file: "plugins/continuous-improvement/skills/verification-loop/SKILL.md", pattern: "### Phase 0: Resolve the Ladder", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
191
+ { file: "skills/verification-loop.md", pattern: ".claude/verify-ladder.json", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
192
+ { file: "plugins/continuous-improvement/skills/verification-loop/SKILL.md", pattern: ".claude/verify-ladder.json", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
193
+ { file: "skills/verification-loop.md", pattern: "verify-ladder (resolved):", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
194
+ { file: "plugins/continuous-improvement/skills/verification-loop/SKILL.md", pattern: "verify-ladder (resolved):", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
195
+ { file: "skills/verification-loop.md", pattern: "### Phase 8: Deploy Receipt", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
196
+ { file: "plugins/continuous-improvement/skills/verification-loop/SKILL.md", pattern: "### Phase 8: Deploy Receipt", source: "docs-substrings-manifest:verification-loop-per-project-ladder" },
197
+ // /harvest slash command + Law-7 prose — locked 2026-05-07 (PR E, follow-up to PR D #88).
198
+ // The deferred follow-up landed: commands/harvest.md (+ plugin mirror) and a Friction Harvest
199
+ // Pipeline subsection inside SKILL.md Law 7 (+ plugin mirror). Each assertion below catches
200
+ // a specific class of regression:
201
+ // - "name: harvest" frontmatter → renaming the slash command
202
+ // - "Friction Harvest Pipeline" → losing the SKILL.md Law-7 subsection title
203
+ // - "dedup_key = sha1(type + tool" → losing the idempotency contract documentation
204
+ { file: "commands/harvest.md", pattern: "name: harvest", source: "docs-substrings-manifest:harvest-slash-command-and-law7-prose" },
205
+ { file: "plugins/continuous-improvement/commands/harvest.md", pattern: "name: harvest", source: "docs-substrings-manifest:harvest-slash-command-and-law7-prose" },
206
+ { file: "SKILL.md", pattern: "Friction Harvest Pipeline", source: "docs-substrings-manifest:harvest-slash-command-and-law7-prose" },
207
+ { file: "plugins/continuous-improvement/skills/continuous-improvement/SKILL.md", pattern: "Friction Harvest Pipeline", source: "docs-substrings-manifest:harvest-slash-command-and-law7-prose" },
208
+ { file: "SKILL.md", pattern: "dedup_key = sha1(type + tool", source: "docs-substrings-manifest:harvest-slash-command-and-law7-prose" },
209
+ { file: "plugins/continuous-improvement/skills/continuous-improvement/SKILL.md", pattern: "dedup_key = sha1(type + tool", source: "docs-substrings-manifest:harvest-slash-command-and-law7-prose" },
210
+ // wild-risa-balance recommendation floor (src/test/wild-risa-floor.test.mts)
211
+ // Source skill + plugin mirror must both contain the literal "2 WILD + 5 RISA = 7 items minimum".
212
+ { file: "skills/wild-risa-balance.md", pattern: "2 WILD + 5 RISA = 7 items minimum", source: "wild-risa-floor.test.mts:38" },
213
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "2 WILD + 5 RISA = 7 items minimum", source: "wild-risa-floor.test.mts:38" },
214
+ // wild-risa-balance audience tiers (src/test/wild-risa-tiers.test.mts)
215
+ // Source + plugin mirror must contain the tier headings, item-count rule, and model-version lock.
216
+ // Dropping any one downgrades the tier contract back to a flat expert-only block.
217
+ { file: "skills/wild-risa-balance.md", pattern: "## Audience Tiers (beginner vs expert)", source: "wild-risa-tiers.test.mts" },
218
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "## Audience Tiers (beginner vs expert)", source: "wild-risa-tiers.test.mts" },
219
+ { file: "skills/wild-risa-balance.md", pattern: "### Beginner tier (lite shape)", source: "wild-risa-tiers.test.mts" },
220
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "### Beginner tier (lite shape)", source: "wild-risa-tiers.test.mts" },
221
+ { file: "skills/wild-risa-balance.md", pattern: "### Expert tier", source: "wild-risa-tiers.test.mts" },
222
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "### Expert tier", source: "wild-risa-tiers.test.mts" },
223
+ { file: "skills/wild-risa-balance.md", pattern: "### Tier signal in the 3-section close", source: "wild-risa-tiers.test.mts" },
224
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "### Tier signal in the 3-section close", source: "wild-risa-tiers.test.mts" },
225
+ { file: "skills/wild-risa-balance.md", pattern: "3 minimum, 5 maximum", source: "wild-risa-tiers.test.mts" },
226
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "3 minimum, 5 maximum", source: "wild-risa-tiers.test.mts" },
227
+ { file: "skills/wild-risa-balance.md", pattern: "Opus 4.7", source: "wild-risa-tiers.test.mts" },
228
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "Opus 4.7", source: "wild-risa-tiers.test.mts" },
229
+ // wild-risa-balance "no" escape valve (src/test/wild-risa-tiers.test.mts)
230
+ // Both tiers must allow `Recommendation: no` when no real recommendation exists; padding to hit
231
+ // the floor is the failure mode. Heading + literal phrase + handoff-action clause are locked.
232
+ { file: "skills/wild-risa-balance.md", pattern: `### The "no" escape valve (both tiers)`, source: "wild-risa-tiers.test.mts" },
233
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: `### The "no" escape valve (both tiers)`, source: "wild-risa-tiers.test.mts" },
234
+ { file: "skills/wild-risa-balance.md", pattern: "Recommendation: no", source: "wild-risa-tiers.test.mts" },
235
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "Recommendation: no", source: "wild-risa-tiers.test.mts" },
236
+ { file: "skills/wild-risa-balance.md", pattern: "switch context", source: "wild-risa-tiers.test.mts" },
237
+ { file: "plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md", pattern: "switch context", source: "wild-risa-tiers.test.mts" },
238
+ // proceed-with-the-recommendation tier-aware contract (src/test/wild-risa-tiers.test.mts)
239
+ // Phase 1 must accept beginner blocks (3..5 unlabeled); Phase 7 close must render the tier suffix.
240
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Beginner tier:**", source: "wild-risa-tiers.test.mts" },
241
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "Beginner tier:**", source: "wild-risa-tiers.test.mts" },
242
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "3 ≤ n ≤ 5", source: "wild-risa-tiers.test.mts" },
243
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "3 ≤ n ≤ 5", source: "wild-risa-tiers.test.mts" },
244
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "Tier signal in the heading", source: "wild-risa-tiers.test.mts" },
245
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "Tier signal in the heading", source: "wild-risa-tiers.test.mts" },
246
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "## Recommendation (expert)", source: "wild-risa-tiers.test.mts" },
247
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "## Recommendation (expert)", source: "wild-risa-tiers.test.mts" },
248
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "## Recommendation (beginner)", source: "wild-risa-tiers.test.mts" },
249
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "## Recommendation (beginner)", source: "wild-risa-tiers.test.mts" },
250
+ // proceed-with-the-recommendation "no" escape valve in Phase 7 close (src/test/wild-risa-tiers.test.mts)
251
+ // Phase 7 must instruct renderer to ship `no` + the tier-suffixed heading when the surface is exhausted.
252
+ { file: "skills/proceed-with-the-recommendation.md", pattern: `The "no" escape valve (both tiers)`, source: "wild-risa-tiers.test.mts" },
253
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: `The "no" escape valve (both tiers)`, source: "wild-risa-tiers.test.mts" },
254
+ { file: "skills/proceed-with-the-recommendation.md", pattern: "explicit handoff signal", source: "wild-risa-tiers.test.mts" },
255
+ { file: "plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md", pattern: "explicit handoff signal", source: "wild-risa-tiers.test.mts" },
256
+ ];
257
+ export function checkAssertions(repoRoot, assertions = DOCS_ASSERTIONS) {
258
+ const fileCache = new Map();
259
+ const failures = [];
260
+ for (const a of assertions) {
261
+ const fullPath = join(repoRoot, a.file);
262
+ let content = fileCache.get(fullPath);
263
+ if (content === undefined) {
264
+ try {
265
+ content = readFileSync(fullPath, "utf8");
266
+ }
267
+ catch {
268
+ content = null;
269
+ }
270
+ fileCache.set(fullPath, content);
271
+ }
272
+ if (content === null) {
273
+ failures.push({ assertion: a, reason: "target-missing" });
274
+ continue;
275
+ }
276
+ const matched = typeof a.pattern === "string"
277
+ ? content.includes(a.pattern)
278
+ : a.pattern.test(content);
279
+ if (!matched) {
280
+ failures.push({ assertion: a, reason: "pattern-not-found" });
281
+ }
282
+ }
283
+ return failures;
284
+ }
285
+ function describePattern(p) {
286
+ return typeof p === "string" ? `"${p}"` : p.toString();
287
+ }
288
+ function printList() {
289
+ const grouped = new Map();
290
+ for (const a of DOCS_ASSERTIONS) {
291
+ const list = grouped.get(a.file) ?? [];
292
+ list.push(a);
293
+ grouped.set(a.file, list);
294
+ }
295
+ console.log(`docs-substrings inventory (${DOCS_ASSERTIONS.length} assertions across ${grouped.size} files):\n`);
296
+ const files = [...grouped.keys()].sort();
297
+ for (const f of files) {
298
+ const items = grouped.get(f);
299
+ console.log(` ${f} (${items.length})`);
300
+ for (const a of items) {
301
+ console.log(` - ${describePattern(a.pattern)} [${a.source}]`);
302
+ }
303
+ }
304
+ }
305
+ function main() {
306
+ const args = argv.slice(2);
307
+ if (args.includes("--list")) {
308
+ printList();
309
+ exit(0);
310
+ }
311
+ const repoRoot = args[0] ?? cwd();
312
+ const failures = checkAssertions(repoRoot);
313
+ if (failures.length === 0) {
314
+ console.log(`OK docs-substrings: all ${DOCS_ASSERTIONS.length} substring assertion(s) match their target files.`);
315
+ exit(0);
316
+ }
317
+ console.error(`FAIL docs-substrings: ${failures.length} of ${DOCS_ASSERTIONS.length} assertion(s) failed.\n`);
318
+ for (const f of failures) {
319
+ const a = f.assertion;
320
+ if (f.reason === "target-missing") {
321
+ console.error(` - ${a.file}: target file not found (asserted by ${a.source}).`);
322
+ }
323
+ else {
324
+ console.error(` - ${a.file}: missing expected substring ${describePattern(a.pattern)} (asserted by ${a.source}).`);
325
+ }
326
+ }
327
+ console.error("\nFix per CONTRIBUTING.md \"Test/docs sync rule\": tests asserting on prose content must ship in the same PR as the docs change adding the asserted substring. A wholesale docs rewrite must preserve every existing substring — or update the test in the same PR.");
328
+ exit(1);
329
+ }
330
+ const invokedDirectly = argv[1]?.endsWith("check-docs-substrings.mjs");
331
+ if (invokedDirectly) {
332
+ main();
333
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Everything Mirror Check
4
+ *
5
+ * Verifies that every non-skill, non-manifest file under
6
+ * plugins/continuous-improvement/ has a byte-identical sibling at the same
7
+ * relative path under the repo root. Catches drift across commands/, hooks/
8
+ * (excluding the plugin-only hooks.json), instinct-packs/, templates/, lib/,
9
+ * bin/, and the LICENSE file — surfaces that check-skill-mirror.mjs does not
10
+ * cover.
11
+ *
12
+ * Skills are intentionally excluded because their flat-vs-plugin layout is
13
+ * structurally different (skills/<name>.md vs plugins/.../skills/<name>/SKILL.md)
14
+ * and check-skill-mirror.mjs already enforces that pair. A few plugin-only
15
+ * surfaces are explicitly allowlisted: the .claude-plugin/ manifests, the
16
+ * generated bundle README.md and skills/README.md, and hooks/hooks.json.
17
+ *
18
+ * Usage:
19
+ * node bin/check-everything-mirror.mjs # Check the current repo
20
+ * node bin/check-everything-mirror.mjs <repo-root> # Check a specific repo root
21
+ *
22
+ * Exit codes:
23
+ * 0 — every mirrored file matches its flat-tree sibling
24
+ * 1 — at least one drift detected (missing flat sibling or content mismatch)
25
+ */
26
+ import { readFileSync, readdirSync, statSync } from "node:fs";
27
+ import { join, relative, sep } from "node:path";
28
+ import { argv, cwd, exit } from "node:process";
29
+ const PLUGIN_ROOT_REL = "plugins/continuous-improvement";
30
+ // Paths under PLUGIN_ROOT_REL that are intentionally plugin-only (no flat
31
+ // sibling expected) or are covered by another lint. Stored with forward
32
+ // slashes; matched against the file's relative-to-plugin-root path normalized
33
+ // the same way.
34
+ const EXCLUDE_FILES = new Set([
35
+ ".claude-plugin/plugin.json",
36
+ ".claude-plugin/marketplace.json",
37
+ "README.md",
38
+ "hooks/hooks.json",
39
+ "skills/README.md",
40
+ ]);
41
+ // Whole directories under PLUGIN_ROOT_REL that this lint never walks into.
42
+ const EXCLUDE_DIRS = new Set(["skills", ".claude-plugin"]);
43
+ function toForward(p) {
44
+ return p.split(sep).join("/");
45
+ }
46
+ export function discoverMirrorPairs(repoRoot) {
47
+ const pluginRoot = join(repoRoot, PLUGIN_ROOT_REL);
48
+ const out = [];
49
+ function walk(dir) {
50
+ let entries;
51
+ try {
52
+ entries = readdirSync(dir);
53
+ }
54
+ catch {
55
+ return;
56
+ }
57
+ for (const name of entries) {
58
+ const full = join(dir, name);
59
+ let stat;
60
+ try {
61
+ stat = statSync(full);
62
+ }
63
+ catch {
64
+ continue;
65
+ }
66
+ const relFromPlugin = toForward(relative(pluginRoot, full));
67
+ if (stat.isDirectory()) {
68
+ if (EXCLUDE_DIRS.has(relFromPlugin))
69
+ continue;
70
+ walk(full);
71
+ continue;
72
+ }
73
+ if (!stat.isFile())
74
+ continue;
75
+ if (EXCLUDE_FILES.has(relFromPlugin))
76
+ continue;
77
+ out.push({
78
+ relPath: relFromPlugin,
79
+ pluginPath: full,
80
+ flatPath: join(repoRoot, relFromPlugin),
81
+ });
82
+ }
83
+ }
84
+ walk(pluginRoot);
85
+ return out.sort((a, b) => a.relPath.localeCompare(b.relPath));
86
+ }
87
+ export function checkMirrorPairs(repoRoot) {
88
+ const pairs = discoverMirrorPairs(repoRoot);
89
+ const drifts = [];
90
+ for (const pair of pairs) {
91
+ let flatContent;
92
+ try {
93
+ flatContent = readFileSync(pair.flatPath);
94
+ }
95
+ catch {
96
+ drifts.push({
97
+ relPath: pair.relPath,
98
+ reason: "missing-flat",
99
+ pluginPath: pair.pluginPath,
100
+ flatPath: pair.flatPath,
101
+ });
102
+ continue;
103
+ }
104
+ const pluginContent = readFileSync(pair.pluginPath);
105
+ if (!pluginContent.equals(flatContent)) {
106
+ drifts.push({
107
+ relPath: pair.relPath,
108
+ reason: "content-drift",
109
+ pluginPath: pair.pluginPath,
110
+ flatPath: pair.flatPath,
111
+ pluginBytes: pluginContent.length,
112
+ flatBytes: flatContent.length,
113
+ });
114
+ }
115
+ }
116
+ return drifts;
117
+ }
118
+ function main() {
119
+ const repoRoot = argv[2] ?? cwd();
120
+ const pairs = discoverMirrorPairs(repoRoot);
121
+ const drifts = checkMirrorPairs(repoRoot);
122
+ if (drifts.length === 0) {
123
+ console.log(`OK everything-mirror: all ${pairs.length} mirrored file(s) match their flat-tree sibling.`);
124
+ exit(0);
125
+ }
126
+ console.error(`FAIL everything-mirror: ${drifts.length} drift(s) detected across ${pairs.length} mirrored file(s).\n`);
127
+ for (const d of drifts) {
128
+ if (d.reason === "missing-flat") {
129
+ console.error(` - ${d.relPath}: plugin copy exists but flat sibling is missing.`);
130
+ console.error(` plugin: ${d.pluginPath}`);
131
+ console.error(` flat: ${d.flatPath} (NOT FOUND)`);
132
+ }
133
+ else {
134
+ console.error(` - ${d.relPath}: byte drift between plugin and flat copy (${d.pluginBytes} vs ${d.flatBytes} bytes).`);
135
+ console.error(` plugin: ${d.pluginPath}`);
136
+ console.error(` flat: ${d.flatPath}`);
137
+ }
138
+ }
139
+ console.error("\nFix: every change to a mirrored file under plugins/continuous-improvement/ must be applied to its flat-tree sibling at the same relative path in the same PR. Plugin-only files (.claude-plugin/manifests, hooks/hooks.json, the bundle README.md, skills/README.md) are allowlisted; skills are covered by check-skill-mirror.mjs.");
140
+ exit(1);
141
+ }
142
+ const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
143
+ if (invokedDirectly || argv[1]?.endsWith("check-everything-mirror.mjs")) {
144
+ main();
145
+ }
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Routing-Target Invariant Check
4
+ *
5
+ * Verifies that every routing target named in the orchestrator skill at
6
+ * skills/proceed-with-the-recommendation.md § "Routing Table (with Inline
7
+ * Fallbacks)" either:
8
+ * (a) ships bundled at plugins/continuous-improvement/skills/<name>/SKILL.md, or
9
+ * (b) is declared in the root-level optional-companions.json file.
10
+ *
11
+ * Catches: a routing-table row that names a skill the bundle does not ship and
12
+ * the maintainer has not declared as an optional companion. Without this gate,
13
+ * such drift only surfaces at runtime when the orchestrator routes to a target
14
+ * that resolves to neither a bundled skill nor an inline fallback the
15
+ * maintainer was tracking.
16
+ *
17
+ * Usage:
18
+ * node bin/check-routing-targets.mjs # Check the current repo
19
+ * node bin/check-routing-targets.mjs <repo-root> # Check a specific repo root
20
+ *
21
+ * Exit codes:
22
+ * 0 — every routing target is accounted for (bundled or optional-declared)
23
+ * 1 — at least one routing target is unaccounted for
24
+ */
25
+ import { readdirSync, readFileSync, statSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { argv, cwd, exit } from "node:process";
28
+ const ORCHESTRATOR_SKILL_PATH = "skills/proceed-with-the-recommendation.md";
29
+ const OPTIONAL_COMPANIONS_PATH = "optional-companions.json";
30
+ const PLUGIN_SKILLS_DIR = "plugins/continuous-improvement/skills";
31
+ const SECTION_HEADER = "### Routing Table (with Inline Fallbacks)";
32
+ export function discoverBundledSkills(repoRoot) {
33
+ const dir = join(repoRoot, PLUGIN_SKILLS_DIR);
34
+ let entries;
35
+ try {
36
+ entries = readdirSync(dir);
37
+ }
38
+ catch {
39
+ return new Set();
40
+ }
41
+ const names = new Set();
42
+ for (const name of entries) {
43
+ const skillFile = join(dir, name, "SKILL.md");
44
+ try {
45
+ const stat = statSync(skillFile);
46
+ if (stat.isFile())
47
+ names.add(name);
48
+ }
49
+ catch {
50
+ /* not a skill dir */
51
+ }
52
+ }
53
+ return names;
54
+ }
55
+ export function loadOptionalCompanions(repoRoot) {
56
+ const path = join(repoRoot, OPTIONAL_COMPANIONS_PATH);
57
+ const raw = readFileSync(path, "utf8");
58
+ const data = JSON.parse(raw);
59
+ if (!Array.isArray(data.optional_companions)) {
60
+ throw new Error(`${OPTIONAL_COMPANIONS_PATH}: missing or non-array "optional_companions" field`);
61
+ }
62
+ return new Set(data.optional_companions);
63
+ }
64
+ export function extractRoutingTargets(orchestratorMarkdown) {
65
+ const lines = orchestratorMarkdown.split(/\r?\n/);
66
+ const headerIdx = lines.findIndex((line) => line.trim() === SECTION_HEADER);
67
+ if (headerIdx === -1) {
68
+ throw new Error(`Routing-table section header not found: "${SECTION_HEADER}"`);
69
+ }
70
+ const targets = [];
71
+ let inTable = false;
72
+ let sawHeaderRow = false;
73
+ let sawDividerRow = false;
74
+ for (let i = headerIdx + 1; i < lines.length; i += 1) {
75
+ const line = lines[i];
76
+ const trimmed = line.trim();
77
+ if (trimmed.startsWith("## ") || trimmed.startsWith("### "))
78
+ break;
79
+ if (!trimmed.startsWith("|")) {
80
+ if (inTable)
81
+ break;
82
+ continue;
83
+ }
84
+ if (!sawHeaderRow) {
85
+ sawHeaderRow = true;
86
+ inTable = true;
87
+ continue;
88
+ }
89
+ if (!sawDividerRow) {
90
+ sawDividerRow = true;
91
+ continue;
92
+ }
93
+ const cells = trimmed.split("|").map((c) => c.trim());
94
+ // cells[0] and cells[last] are empty strings from leading/trailing pipes.
95
+ // cells[1] = "Recommendation type", cells[2] = "Preferred skill", cells[3] = "Inline fallback".
96
+ const preferredCell = cells[2] ?? "";
97
+ const tokens = [...preferredCell.matchAll(/`([^`]+)`/g)].map((m) => m[1]);
98
+ for (const token of tokens) {
99
+ targets.push({
100
+ rowIndex: targets.length,
101
+ recommendationType: cells[1] ?? "",
102
+ target: token,
103
+ });
104
+ }
105
+ }
106
+ return targets;
107
+ }
108
+ export function checkRoutingTargets(repoRoot) {
109
+ const orchestratorPath = join(repoRoot, ORCHESTRATOR_SKILL_PATH);
110
+ const orchestratorContent = readFileSync(orchestratorPath, "utf8");
111
+ const targets = extractRoutingTargets(orchestratorContent);
112
+ const bundled = discoverBundledSkills(repoRoot);
113
+ const optional = loadOptionalCompanions(repoRoot);
114
+ const drifts = [];
115
+ for (const t of targets) {
116
+ if (bundled.has(t.target))
117
+ continue;
118
+ if (optional.has(t.target))
119
+ continue;
120
+ drifts.push(t);
121
+ }
122
+ return {
123
+ targets,
124
+ drifts,
125
+ bundledCount: bundled.size,
126
+ optionalCount: optional.size,
127
+ };
128
+ }
129
+ function main() {
130
+ const repoRoot = argv[2] ?? cwd();
131
+ const { targets, drifts, bundledCount, optionalCount } = checkRoutingTargets(repoRoot);
132
+ if (drifts.length === 0) {
133
+ console.log(`OK routing-targets: all ${targets.length} routing target(s) accounted for ` +
134
+ `(${bundledCount} bundled skill(s), ${optionalCount} optional companion(s) declared).`);
135
+ exit(0);
136
+ }
137
+ console.error(`FAIL routing-targets: ${drifts.length} unaccounted target(s) in ${ORCHESTRATOR_SKILL_PATH}.\n`);
138
+ for (const d of drifts) {
139
+ console.error(` - "${d.target}" — referenced by row "${d.recommendationType}"`);
140
+ console.error(` Not bundled at ${PLUGIN_SKILLS_DIR}/${d.target}/SKILL.md`);
141
+ console.error(` Not declared in ${OPTIONAL_COMPANIONS_PATH}`);
142
+ }
143
+ console.error(`\nFix: either bundle the skill (add plugins/continuous-improvement/skills/<name>/SKILL.md ` +
144
+ `via the source skill + 'npm run build'), OR declare it in ${OPTIONAL_COMPANIONS_PATH} ` +
145
+ `with a pointer note in docs/audits/.`);
146
+ exit(1);
147
+ }
148
+ const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
149
+ if (invokedDirectly || argv[1]?.endsWith("check-routing-targets.mjs")) {
150
+ main();
151
+ }