session-orchestrator 3.16.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 (762) hide show
  1. package/.claude-plugin/marketplace.json +29 -0
  2. package/.claude-plugin/plugin.json +18 -0
  3. package/.codex-plugin/agents/explorer.toml +14 -0
  4. package/.codex-plugin/agents/session-reviewer.toml +23 -0
  5. package/.codex-plugin/agents/wave-worker.toml +15 -0
  6. package/.codex-plugin/config.toml +20 -0
  7. package/.codex-plugin/plugin.json +37 -0
  8. package/.cursor/rules/000-session-orchestrator.mdc +73 -0
  9. package/.cursor/rules/010-session-workflow.mdc +170 -0
  10. package/.cursor/rules/020-quality-gates.mdc +128 -0
  11. package/.cursor/rules/030-wave-execution.mdc +216 -0
  12. package/.cursor/rules/040-discovery.mdc +242 -0
  13. package/.cursor/rules/050-plan.mdc +235 -0
  14. package/.cursor/rules/060-evolve.mdc +232 -0
  15. package/.cursor/rules/070-gitlab-ops.mdc +246 -0
  16. package/.cursor/rules/080-ecosystem-health.mdc +145 -0
  17. package/.mcp.json +8 -0
  18. package/CHANGELOG.md +1544 -0
  19. package/LICENSE +21 -0
  20. package/NOTICE +64 -0
  21. package/README.md +242 -0
  22. package/SECURITY.md +90 -0
  23. package/agents/AGENTS.md +136 -0
  24. package/agents/analyst.md +99 -0
  25. package/agents/architect-reviewer.md +93 -0
  26. package/agents/code-implementer.md +106 -0
  27. package/agents/db-specialist.md +104 -0
  28. package/agents/dialectic-deriver.md +139 -0
  29. package/agents/docs-writer.md +113 -0
  30. package/agents/eval-judge.md +146 -0
  31. package/agents/memory-proposal-collector.md +297 -0
  32. package/agents/qa-strategist.md +102 -0
  33. package/agents/schemas/analyst.schema.json +46 -0
  34. package/agents/schemas/architect-reviewer.schema.json +50 -0
  35. package/agents/schemas/code-implementer.schema.json +61 -0
  36. package/agents/schemas/db-specialist.schema.json +80 -0
  37. package/agents/schemas/docs-writer.schema.json +56 -0
  38. package/agents/schemas/persona-panel-sidecar.schema.json +245 -0
  39. package/agents/schemas/qa-strategist.schema.json +46 -0
  40. package/agents/schemas/security-reviewer.schema.json +86 -0
  41. package/agents/schemas/session-reviewer.schema.json +69 -0
  42. package/agents/schemas/test-writer.schema.json +69 -0
  43. package/agents/schemas/ui-developer.schema.json +90 -0
  44. package/agents/schemas/ux-evaluator.schema.json +51 -0
  45. package/agents/security-reviewer.md +236 -0
  46. package/agents/session-reviewer.md +201 -0
  47. package/agents/skill-applied-judge.md +122 -0
  48. package/agents/test-writer.md +123 -0
  49. package/agents/ui-developer.md +109 -0
  50. package/agents/ux-evaluator.md +161 -0
  51. package/assets/icon.svg +11 -0
  52. package/assets/og-card.png +0 -0
  53. package/assets/og-card.svg +47 -0
  54. package/commands/autopilot-multi.md +74 -0
  55. package/commands/autopilot.md +80 -0
  56. package/commands/bootstrap.md +56 -0
  57. package/commands/brainstorm.md +48 -0
  58. package/commands/close.md +24 -0
  59. package/commands/debug.md +36 -0
  60. package/commands/discovery.md +32 -0
  61. package/commands/dispatcher.md +59 -0
  62. package/commands/eval.md +28 -0
  63. package/commands/evolve.md +10 -0
  64. package/commands/go.md +41 -0
  65. package/commands/grill.md +45 -0
  66. package/commands/harness-audit.md +26 -0
  67. package/commands/memory-cleanup.md +25 -0
  68. package/commands/persona-panel.md +121 -0
  69. package/commands/plan.md +15 -0
  70. package/commands/portfolio.md +97 -0
  71. package/commands/reconcile.md +23 -0
  72. package/commands/repo-audit.md +24 -0
  73. package/commands/session.md +30 -0
  74. package/commands/spinout.md +15 -0
  75. package/commands/sunset-review.md +27 -0
  76. package/commands/templates-ack.md +96 -0
  77. package/commands/test.md +97 -0
  78. package/docs/README.md +105 -0
  79. package/docs/USER-GUIDE.md +1403 -0
  80. package/docs/ci-setup.md +81 -0
  81. package/docs/codex-setup.md +142 -0
  82. package/docs/components.md +74 -0
  83. package/docs/cursor-setup.md +104 -0
  84. package/docs/events-schema.md +81 -0
  85. package/docs/migration-v3.md +148 -0
  86. package/docs/owner-config-schema.md +154 -0
  87. package/docs/persona-panel.md +433 -0
  88. package/docs/pi-setup.md +115 -0
  89. package/docs/plugin-architecture-v3.md +296 -0
  90. package/docs/pm-skills-marketplace.md +114 -0
  91. package/docs/policy-cache-validation-2026-04-28.md +118 -0
  92. package/docs/rule-authoring.md +316 -0
  93. package/docs/session-config-reference.md +1439 -0
  94. package/docs/session-config-template.md +961 -0
  95. package/docs/vault-docs-architecture.md +297 -0
  96. package/hooks/_lib/lock-bootstrap.mjs +272 -0
  97. package/hooks/_lib/lock-reconcile.mjs +93 -0
  98. package/hooks/_lib/profile-gate.mjs +95 -0
  99. package/hooks/_lib/transcript-history.mjs +211 -0
  100. package/hooks/agent-teams-h3-test.sh +362 -0
  101. package/hooks/config-protection.mjs +0 -0
  102. package/hooks/cwd-change-restore.mjs +131 -0
  103. package/hooks/enforce-commands.mjs +179 -0
  104. package/hooks/enforce-scope.mjs +273 -0
  105. package/hooks/hooks-codex.json +60 -0
  106. package/hooks/hooks-cursor.json +15 -0
  107. package/hooks/hooks-pi.json +115 -0
  108. package/hooks/hooks.json +215 -0
  109. package/hooks/loop-guard.mjs +260 -0
  110. package/hooks/on-session-end.mjs +217 -0
  111. package/hooks/on-session-start.mjs +660 -0
  112. package/hooks/on-stop.mjs +294 -0
  113. package/hooks/operator-steer.mjs +64 -0
  114. package/hooks/post-edit-validate.mjs +225 -0
  115. package/hooks/post-subagent-discovery-validator.mjs +398 -0
  116. package/hooks/post-tool-batch-wave-signal.mjs +328 -0
  117. package/hooks/post-tool-failure-corrective-context.mjs +248 -0
  118. package/hooks/post-tooluse-frontend-slop.mjs +184 -0
  119. package/hooks/pre-bash-destructive-guard.mjs +515 -0
  120. package/hooks/pre-bash-memory-propose-audit.mjs +206 -0
  121. package/hooks/pre-bash-staging-fence.mjs +223 -0
  122. package/hooks/pre-bash-templates-first.mjs +404 -0
  123. package/hooks/run-node.sh +72 -0
  124. package/hooks/skill-invocation-telemetry.mjs +99 -0
  125. package/hooks/subagent-telemetry.mjs +249 -0
  126. package/hooks/wave-scope-commit-guard.mjs +191 -0
  127. package/monitors/monitors.json +14 -0
  128. package/output-styles/finding-report.md +48 -0
  129. package/output-styles/session-report.md +53 -0
  130. package/output-styles/wave-summary.md +38 -0
  131. package/package.json +94 -0
  132. package/pi/extensions/session-orchestrator.ts +25 -0
  133. package/pi/prompts/autopilot-multi.md +12 -0
  134. package/pi/prompts/autopilot.md +12 -0
  135. package/pi/prompts/bootstrap.md +12 -0
  136. package/pi/prompts/brainstorm.md +12 -0
  137. package/pi/prompts/close.md +11 -0
  138. package/pi/prompts/debug.md +12 -0
  139. package/pi/prompts/discovery.md +12 -0
  140. package/pi/prompts/dispatcher.md +12 -0
  141. package/pi/prompts/eval.md +12 -0
  142. package/pi/prompts/evolve.md +12 -0
  143. package/pi/prompts/go.md +12 -0
  144. package/pi/prompts/grill.md +12 -0
  145. package/pi/prompts/harness-audit.md +12 -0
  146. package/pi/prompts/memory-cleanup.md +12 -0
  147. package/pi/prompts/persona-panel.md +12 -0
  148. package/pi/prompts/plan.md +12 -0
  149. package/pi/prompts/portfolio.md +12 -0
  150. package/pi/prompts/reconcile.md +12 -0
  151. package/pi/prompts/repo-audit.md +12 -0
  152. package/pi/prompts/session.md +12 -0
  153. package/pi/prompts/spinout.md +12 -0
  154. package/pi/prompts/sunset-review.md +12 -0
  155. package/pi/prompts/templates-ack.md +12 -0
  156. package/pi/prompts/test.md +12 -0
  157. package/rules/_index.md +51 -0
  158. package/rules/always-on/commit-discipline.md +26 -0
  159. package/rules/always-on/npm-quality-gates.md +26 -0
  160. package/rules/always-on/parallel-sessions.md +43 -0
  161. package/rules/opt-in-domain/prompt-caching.md +270 -0
  162. package/rules/opt-in-stack/backend-data.md +188 -0
  163. package/rules/opt-in-stack/backend.md +390 -0
  164. package/rules/opt-in-stack/frontend.md +98 -0
  165. package/rules/opt-in-stack/security-web.md +194 -0
  166. package/rules/opt-in-stack/swift.md +65 -0
  167. package/scripts/archive-closed-prds.mjs +416 -0
  168. package/scripts/autopilot-multi.mjs +802 -0
  169. package/scripts/autopilot.mjs +383 -0
  170. package/scripts/backfill-abandoned-sessions.mjs +265 -0
  171. package/scripts/backfill-learnings-expires.mjs +196 -0
  172. package/scripts/backfill-learnings.mjs +203 -0
  173. package/scripts/backfill-sessions.mjs +282 -0
  174. package/scripts/check-doc-consistency.sh +279 -0
  175. package/scripts/check-package-manager.mjs +445 -0
  176. package/scripts/ci/assert-vitest-green.mjs +267 -0
  177. package/scripts/codex-install.mjs +435 -0
  178. package/scripts/compute-grounding-injection.sh +186 -0
  179. package/scripts/cursor-install.mjs +113 -0
  180. package/scripts/dialectic-deriver.mjs +573 -0
  181. package/scripts/emit-event.mjs +160 -0
  182. package/scripts/emit-session.mjs +212 -0
  183. package/scripts/eval-session.mjs +262 -0
  184. package/scripts/export-hw-learnings.mjs +437 -0
  185. package/scripts/gc-stale-worktrees.mjs +666 -0
  186. package/scripts/generate-pi-prompts.mjs +127 -0
  187. package/scripts/harness-audit.mjs +287 -0
  188. package/scripts/lib/agent-frontmatter.mjs +266 -0
  189. package/scripts/lib/agent-output-schema.mjs +166 -0
  190. package/scripts/lib/agent-status.mjs +303 -0
  191. package/scripts/lib/ajv-loader.mjs +34 -0
  192. package/scripts/lib/auto-dialectic.mjs +382 -0
  193. package/scripts/lib/auto-dream.mjs +471 -0
  194. package/scripts/lib/autonomy/suitability.mjs +212 -0
  195. package/scripts/lib/autopilot/dep-graph.mjs +417 -0
  196. package/scripts/lib/autopilot/durable-telemetry.mjs +121 -0
  197. package/scripts/lib/autopilot/flags.mjs +104 -0
  198. package/scripts/lib/autopilot/kill-switches.mjs +174 -0
  199. package/scripts/lib/autopilot/loop.mjs +320 -0
  200. package/scripts/lib/autopilot/mr-draft.mjs +520 -0
  201. package/scripts/lib/autopilot/multi-killswitch.mjs +184 -0
  202. package/scripts/lib/autopilot/recent-runs.mjs +106 -0
  203. package/scripts/lib/autopilot/stall-sampler.mjs +97 -0
  204. package/scripts/lib/autopilot/telemetry.mjs +224 -0
  205. package/scripts/lib/autopilot/worktree-pipeline.mjs +605 -0
  206. package/scripts/lib/autopilot-telemetry.mjs +11 -0
  207. package/scripts/lib/autopilot.mjs +39 -0
  208. package/scripts/lib/backlog-scan.mjs +179 -0
  209. package/scripts/lib/bootstrap-lock-freshness.mjs +260 -0
  210. package/scripts/lib/bootstrap-lock-refresh.mjs +186 -0
  211. package/scripts/lib/build-live-signals.mjs +150 -0
  212. package/scripts/lib/ci-status-banner.mjs +425 -0
  213. package/scripts/lib/claude-md-budget-lint.mjs +246 -0
  214. package/scripts/lib/cli-flags.mjs +158 -0
  215. package/scripts/lib/codex/plugin-contract.mjs +610 -0
  216. package/scripts/lib/cold-start-detector.mjs +240 -0
  217. package/scripts/lib/command-blocker.mjs +458 -0
  218. package/scripts/lib/common.mjs +333 -0
  219. package/scripts/lib/config/auto-dream.mjs +77 -0
  220. package/scripts/lib/config/block-header.mjs +94 -0
  221. package/scripts/lib/config/broken-window.mjs +114 -0
  222. package/scripts/lib/config/coercers.mjs +248 -0
  223. package/scripts/lib/config/cold-start.mjs +92 -0
  224. package/scripts/lib/config/config-protection.mjs +120 -0
  225. package/scripts/lib/config/cross-repo.mjs +104 -0
  226. package/scripts/lib/config/custom-phases.mjs +213 -0
  227. package/scripts/lib/config/dialectic.mjs +92 -0
  228. package/scripts/lib/config/discovery-validator.mjs +75 -0
  229. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +240 -0
  230. package/scripts/lib/config/dispatcher-autonomy.mjs +152 -0
  231. package/scripts/lib/config/docs-orchestrator.mjs +90 -0
  232. package/scripts/lib/config/docs-staleness.mjs +96 -0
  233. package/scripts/lib/config/drift-check.mjs +155 -0
  234. package/scripts/lib/config/eval.mjs +130 -0
  235. package/scripts/lib/config/events-rotation.mjs +74 -0
  236. package/scripts/lib/config/evolve.mjs +308 -0
  237. package/scripts/lib/config/frontend-slop-hook.mjs +104 -0
  238. package/scripts/lib/config/gitlab-portfolio.mjs +150 -0
  239. package/scripts/lib/config/handover-gate.mjs +106 -0
  240. package/scripts/lib/config/host-paths.mjs +76 -0
  241. package/scripts/lib/config/io.mjs +54 -0
  242. package/scripts/lib/config/loop-guard.mjs +117 -0
  243. package/scripts/lib/config/memory.mjs +150 -0
  244. package/scripts/lib/config/persona-gate-wave.mjs +258 -0
  245. package/scripts/lib/config/reconcile.mjs +205 -0
  246. package/scripts/lib/config/section-extractor.mjs +100 -0
  247. package/scripts/lib/config/skill-evolution.mjs +112 -0
  248. package/scripts/lib/config/slopcheck.mjs +99 -0
  249. package/scripts/lib/config/state-md-lock.mjs +83 -0
  250. package/scripts/lib/config/templates-first.mjs +94 -0
  251. package/scripts/lib/config/test.mjs +113 -0
  252. package/scripts/lib/config/vault-integration.mjs +201 -0
  253. package/scripts/lib/config/vault-mirror-quality.mjs +99 -0
  254. package/scripts/lib/config/vault-staleness.mjs +84 -0
  255. package/scripts/lib/config/vault-sync.mjs +96 -0
  256. package/scripts/lib/config/verification-auto-fix.mjs +84 -0
  257. package/scripts/lib/config/wave-reviewers.mjs +133 -0
  258. package/scripts/lib/config-schema.mjs +345 -0
  259. package/scripts/lib/config.mjs +474 -0
  260. package/scripts/lib/convergence-monitor.mjs +389 -0
  261. package/scripts/lib/coordinator-snapshot.mjs +371 -0
  262. package/scripts/lib/crypto-digest-utils.mjs +91 -0
  263. package/scripts/lib/discovery/helpers.mjs +127 -0
  264. package/scripts/lib/discovery/triage-state.mjs +279 -0
  265. package/scripts/lib/dispatcher/cli.mjs +257 -0
  266. package/scripts/lib/dispatcher/enumerate.mjs +243 -0
  267. package/scripts/lib/dispatcher/rank.mjs +363 -0
  268. package/scripts/lib/ecosystem-health.mjs +224 -0
  269. package/scripts/lib/ecosystem-wizard/ci-detector.mjs +18 -0
  270. package/scripts/lib/ecosystem-wizard/config-parser.mjs +54 -0
  271. package/scripts/lib/ecosystem-wizard/config-writer.mjs +287 -0
  272. package/scripts/lib/ecosystem-wizard/package-manager-detector.mjs +42 -0
  273. package/scripts/lib/ecosystem-wizard/wizard-prompt.mjs +246 -0
  274. package/scripts/lib/ecosystem-wizard.mjs +48 -0
  275. package/scripts/lib/env-check.mjs +89 -0
  276. package/scripts/lib/eval/engine.mjs +605 -0
  277. package/scripts/lib/eval/judge.mjs +433 -0
  278. package/scripts/lib/eval/report.mjs +367 -0
  279. package/scripts/lib/eval/schema.mjs +618 -0
  280. package/scripts/lib/eval/session-resolve.mjs +137 -0
  281. package/scripts/lib/eval/sink.mjs +77 -0
  282. package/scripts/lib/events-rotation.mjs +86 -0
  283. package/scripts/lib/events-schema.mjs +81 -0
  284. package/scripts/lib/events.mjs +80 -0
  285. package/scripts/lib/evolve/autonomy-verdict.mjs +461 -0
  286. package/scripts/lib/evolve/autopilot-effectiveness.mjs +293 -0
  287. package/scripts/lib/exclusivity-matrix.mjs +68 -0
  288. package/scripts/lib/fetch-baseline.mjs +311 -0
  289. package/scripts/lib/file-lock.mjs +512 -0
  290. package/scripts/lib/frontend-detect/detect.mjs +138 -0
  291. package/scripts/lib/frontend-detect/rules.mjs +295 -0
  292. package/scripts/lib/frontmatter-guard.mjs +241 -0
  293. package/scripts/lib/gates/echo-stub-detect.mjs +39 -0
  294. package/scripts/lib/gates/gate-baseline.mjs +42 -0
  295. package/scripts/lib/gates/gate-full.mjs +85 -0
  296. package/scripts/lib/gates/gate-helpers.mjs +231 -0
  297. package/scripts/lib/gates/gate-incremental.mjs +76 -0
  298. package/scripts/lib/gates/gate-per-file.mjs +55 -0
  299. package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +447 -0
  300. package/scripts/lib/gitlab-portfolio/aggregator.mjs +383 -0
  301. package/scripts/lib/gitlab-portfolio/cli.mjs +428 -0
  302. package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +289 -0
  303. package/scripts/lib/gitlab-portfolio/vcs-detect.mjs +182 -0
  304. package/scripts/lib/handover-gate.mjs +222 -0
  305. package/scripts/lib/hardening.mjs +43 -0
  306. package/scripts/lib/hardware-pattern-detector.mjs +238 -0
  307. package/scripts/lib/harness-audit/categories/category1.mjs +123 -0
  308. package/scripts/lib/harness-audit/categories/category2.mjs +145 -0
  309. package/scripts/lib/harness-audit/categories/category3.mjs +143 -0
  310. package/scripts/lib/harness-audit/categories/category4.mjs +202 -0
  311. package/scripts/lib/harness-audit/categories/category5.mjs +152 -0
  312. package/scripts/lib/harness-audit/categories/category6.mjs +211 -0
  313. package/scripts/lib/harness-audit/categories/category7.mjs +125 -0
  314. package/scripts/lib/harness-audit/categories/category8.mjs +328 -0
  315. package/scripts/lib/harness-audit/categories/category9.mjs +294 -0
  316. package/scripts/lib/harness-audit/categories/helpers.mjs +165 -0
  317. package/scripts/lib/harness-audit/categories.mjs +19 -0
  318. package/scripts/lib/historical-guard.mjs +15 -0
  319. package/scripts/lib/host-identity.mjs +262 -0
  320. package/scripts/lib/instruction-budget-guard.mjs +332 -0
  321. package/scripts/lib/io.mjs +304 -0
  322. package/scripts/lib/issue-close-strip-labels.mjs +161 -0
  323. package/scripts/lib/language-mappers/README.md +57 -0
  324. package/scripts/lib/language-mappers/index.mjs +165 -0
  325. package/scripts/lib/language-mappers/markdown.mjs +149 -0
  326. package/scripts/lib/language-mappers/python.mjs +249 -0
  327. package/scripts/lib/language-mappers/swift.mjs +201 -0
  328. package/scripts/lib/language-mappers/typescript.mjs +433 -0
  329. package/scripts/lib/learnings/expiry-sweep.mjs +164 -0
  330. package/scripts/lib/learnings/filters.mjs +43 -0
  331. package/scripts/lib/learnings/io.mjs +255 -0
  332. package/scripts/lib/learnings/schema.mjs +518 -0
  333. package/scripts/lib/learnings/surface.mjs +207 -0
  334. package/scripts/lib/learnings.mjs +42 -0
  335. package/scripts/lib/lock-reaper.mjs +648 -0
  336. package/scripts/lib/locks/index.mjs +31 -0
  337. package/scripts/lib/locks/lock-body.mjs +62 -0
  338. package/scripts/lib/locks/staging-fence-lock.mjs +267 -0
  339. package/scripts/lib/locks/state-md-lock.mjs +351 -0
  340. package/scripts/lib/loop-readiness-banner.mjs +144 -0
  341. package/scripts/lib/memory-banner.mjs +478 -0
  342. package/scripts/lib/memory-cleanup/worktree-sweep.mjs +108 -0
  343. package/scripts/lib/memory-cleanup-stamp.mjs +56 -0
  344. package/scripts/lib/memory-paths.mjs +31 -0
  345. package/scripts/lib/memory-proposals/collector.mjs +334 -0
  346. package/scripts/lib/memory-proposals/schema.mjs +289 -0
  347. package/scripts/lib/memory-proposals/sink.mjs +507 -0
  348. package/scripts/lib/memory-proposals/store.mjs +441 -0
  349. package/scripts/lib/mission-status-schema.mjs +114 -0
  350. package/scripts/lib/mode-selector/alternatives.mjs +64 -0
  351. package/scripts/lib/mode-selector/constants.mjs +29 -0
  352. package/scripts/lib/mode-selector/context-pressure.mjs +157 -0
  353. package/scripts/lib/mode-selector/rationale.mjs +55 -0
  354. package/scripts/lib/mode-selector/scoring.mjs +221 -0
  355. package/scripts/lib/mode-selector-accuracy.mjs +121 -0
  356. package/scripts/lib/mode-selector.mjs +160 -0
  357. package/scripts/lib/multi-provider-build/providers.mjs +64 -0
  358. package/scripts/lib/multi-provider-build/templating.mjs +130 -0
  359. package/scripts/lib/named-baseline-resolver.mjs +233 -0
  360. package/scripts/lib/named-vault-resolver.mjs +433 -0
  361. package/scripts/lib/owner-config/coerce.mjs +29 -0
  362. package/scripts/lib/owner-config/constants.mjs +21 -0
  363. package/scripts/lib/owner-config/defaults.mjs +50 -0
  364. package/scripts/lib/owner-config/error.mjs +19 -0
  365. package/scripts/lib/owner-config/index.mjs +13 -0
  366. package/scripts/lib/owner-config/merge.mjs +52 -0
  367. package/scripts/lib/owner-config/validate.mjs +259 -0
  368. package/scripts/lib/owner-config-banner.mjs +126 -0
  369. package/scripts/lib/owner-config-loader.mjs +159 -0
  370. package/scripts/lib/owner-config.example.yaml +72 -0
  371. package/scripts/lib/owner-config.mjs +28 -0
  372. package/scripts/lib/owner-interview.mjs +243 -0
  373. package/scripts/lib/owner-yaml.mjs +571 -0
  374. package/scripts/lib/package-manager.mjs +160 -0
  375. package/scripts/lib/path-utils.mjs +217 -0
  376. package/scripts/lib/peer-cards/merger.mjs +310 -0
  377. package/scripts/lib/peer-cards/reader.mjs +125 -0
  378. package/scripts/lib/peer-cards/schema.mjs +230 -0
  379. package/scripts/lib/peer-cards/staleness-banner.mjs +86 -0
  380. package/scripts/lib/peer-cards/writer.mjs +138 -0
  381. package/scripts/lib/peer-discovery.mjs +200 -0
  382. package/scripts/lib/persona-panel/catalog-loader.mjs +577 -0
  383. package/scripts/lib/persona-panel/consolidator.mjs +370 -0
  384. package/scripts/lib/persona-panel/persona-runner.mjs +375 -0
  385. package/scripts/lib/persona-panel/threshold.mjs +130 -0
  386. package/scripts/lib/pi-hook-bridge.mjs +328 -0
  387. package/scripts/lib/platform.mjs +266 -0
  388. package/scripts/lib/playwright-driver/runner.mjs +297 -0
  389. package/scripts/lib/plugin-root.mjs +210 -0
  390. package/scripts/lib/pre-dispatch-check.mjs +126 -0
  391. package/scripts/lib/product-repo-detect.mjs +121 -0
  392. package/scripts/lib/profiles/registry.mjs +176 -0
  393. package/scripts/lib/profiles/schema.mjs +209 -0
  394. package/scripts/lib/qg-command-drift-banner.mjs +88 -0
  395. package/scripts/lib/quality-gate/diagnostics.mjs +92 -0
  396. package/scripts/lib/quality-gate.mjs +536 -0
  397. package/scripts/lib/quality-gates-cache.mjs +228 -0
  398. package/scripts/lib/quality-gates-policy.mjs +95 -0
  399. package/scripts/lib/recommendations-v0.mjs +156 -0
  400. package/scripts/lib/reconcile/eligibility.mjs +203 -0
  401. package/scripts/lib/reconcile/emitter.mjs +244 -0
  402. package/scripts/lib/reconcile/engine.mjs +412 -0
  403. package/scripts/lib/reconcile/idempotency.mjs +239 -0
  404. package/scripts/lib/reconcile/renderer.mjs +211 -0
  405. package/scripts/lib/reconcile/writer.mjs +293 -0
  406. package/scripts/lib/reconcile-nudge-banner.mjs +284 -0
  407. package/scripts/lib/resource-probe/evaluate.mjs +190 -0
  408. package/scripts/lib/resource-probe/parsers.mjs +181 -0
  409. package/scripts/lib/resource-probe/probe-platform.mjs +300 -0
  410. package/scripts/lib/resource-probe.mjs +95 -0
  411. package/scripts/lib/rule-loader.mjs +552 -0
  412. package/scripts/lib/rules-sync.mjs +439 -0
  413. package/scripts/lib/scope-gate.mjs +496 -0
  414. package/scripts/lib/session-close-backfill.mjs +539 -0
  415. package/scripts/lib/session-discovery.mjs +256 -0
  416. package/scripts/lib/session-end/phase-skip.mjs +357 -0
  417. package/scripts/lib/session-end/worktree-cleanup.mjs +112 -0
  418. package/scripts/lib/session-id.mjs +362 -0
  419. package/scripts/lib/session-lock.mjs +703 -0
  420. package/scripts/lib/session-registry.mjs +355 -0
  421. package/scripts/lib/session-schema/aliases.mjs +71 -0
  422. package/scripts/lib/session-schema/constants.mjs +115 -0
  423. package/scripts/lib/session-schema/normalizer.mjs +66 -0
  424. package/scripts/lib/session-schema/timestamps.mjs +64 -0
  425. package/scripts/lib/session-schema/validator.mjs +453 -0
  426. package/scripts/lib/session-schema.mjs +71 -0
  427. package/scripts/lib/session-token-rollup.mjs +137 -0
  428. package/scripts/lib/sessions-staleness-banner.mjs +247 -0
  429. package/scripts/lib/skill-evolution/blast-radius-classifier.mjs +114 -0
  430. package/scripts/lib/skill-evolution/candidate-intake.mjs +270 -0
  431. package/scripts/lib/skill-evolution/config-validation-gate.mjs +279 -0
  432. package/scripts/lib/skill-evolution/engine.mjs +719 -0
  433. package/scripts/lib/skill-evolution/idempotency.mjs +279 -0
  434. package/scripts/lib/skill-evolution/mr-opener.mjs +507 -0
  435. package/scripts/lib/skill-health/join.mjs +181 -0
  436. package/scripts/lib/skill-health/score.mjs +123 -0
  437. package/scripts/lib/skill-invocations-schema.mjs +214 -0
  438. package/scripts/lib/skill-judge.mjs +348 -0
  439. package/scripts/lib/skill-judgments-schema.mjs +264 -0
  440. package/scripts/lib/slopcheck.mjs +501 -0
  441. package/scripts/lib/soul-resolve.mjs +118 -0
  442. package/scripts/lib/spiral-carryover.mjs +495 -0
  443. package/scripts/lib/state-md/body-sections.mjs +851 -0
  444. package/scripts/lib/state-md/frontmatter-mutators.mjs +453 -0
  445. package/scripts/lib/state-md/mission-status.mjs +247 -0
  446. package/scripts/lib/state-md/recommendations.mjs +57 -0
  447. package/scripts/lib/state-md/yaml-parser.mjs +234 -0
  448. package/scripts/lib/state-md-peer-guard.mjs +232 -0
  449. package/scripts/lib/state-md.mjs +53 -0
  450. package/scripts/lib/subagents-schema.mjs +309 -0
  451. package/scripts/lib/sunset/walker.mjs +1192 -0
  452. package/scripts/lib/test-runner/artifact-paths.mjs +94 -0
  453. package/scripts/lib/test-runner/fingerprint.mjs +33 -0
  454. package/scripts/lib/test-runner/issue-reconcile.mjs +770 -0
  455. package/scripts/lib/tmux-layout/layouts.mjs +224 -0
  456. package/scripts/lib/tmux-layout/telemetry-stats.mjs +100 -0
  457. package/scripts/lib/tmux-layout/telemetry.mjs +88 -0
  458. package/scripts/lib/tmux-layout/tmux-shell.mjs +82 -0
  459. package/scripts/lib/tmux-layout/vcs-detector.mjs +88 -0
  460. package/scripts/lib/validate/check-agents.mjs +457 -0
  461. package/scripts/lib/validate/check-codex-plugin.mjs +37 -0
  462. package/scripts/lib/validate/check-commands.mjs +148 -0
  463. package/scripts/lib/validate/check-component-paths.mjs +112 -0
  464. package/scripts/lib/validate/check-dead-bridge.mjs +180 -0
  465. package/scripts/lib/validate/check-hooks-symmetry.mjs +258 -0
  466. package/scripts/lib/validate/check-json-files.mjs +116 -0
  467. package/scripts/lib/validate/check-owner-leakage.mjs +1011 -0
  468. package/scripts/lib/validate/check-path-utils-canary.mjs +175 -0
  469. package/scripts/lib/validate/check-peekaboo-driver-canary.mjs +201 -0
  470. package/scripts/lib/validate/check-pi-package.mjs +110 -0
  471. package/scripts/lib/validate/check-pi-prompts.mjs +43 -0
  472. package/scripts/lib/validate/check-playwright-mcp-canary.mjs +154 -0
  473. package/scripts/lib/validate/check-plugin-json.mjs +96 -0
  474. package/scripts/lib/validate/check-plugin-monitors.mjs +206 -0
  475. package/scripts/lib/validate/check-plugin-schema.mjs +137 -0
  476. package/scripts/lib/validate/check-rules.mjs +143 -0
  477. package/scripts/lib/validate/check-session-plan-routing.mjs +154 -0
  478. package/scripts/lib/validate/check-test-fixture-shapes.mjs +280 -0
  479. package/scripts/lib/validate/check-unicode-safety.mjs +533 -0
  480. package/scripts/lib/validate/confidential-names.mjs +169 -0
  481. package/scripts/lib/validate/dead-bridge-corpus.mjs +141 -0
  482. package/scripts/lib/validate/dead-bridge-detectors.mjs +568 -0
  483. package/scripts/lib/validate/tier-inference.mjs +100 -0
  484. package/scripts/lib/validate-vendored-rules.mjs +519 -0
  485. package/scripts/lib/vault-archive.mjs +404 -0
  486. package/scripts/lib/vault-backfill/glab.mjs +164 -0
  487. package/scripts/lib/vault-backfill/manifest.mjs +75 -0
  488. package/scripts/lib/vault-backfill/template.mjs +130 -0
  489. package/scripts/lib/vault-consolidate-fs.mjs +331 -0
  490. package/scripts/lib/vault-migration-rules.mjs +155 -0
  491. package/scripts/lib/vault-mirror/auto-commit.mjs +203 -0
  492. package/scripts/lib/vault-mirror/namespace.mjs +152 -0
  493. package/scripts/lib/vault-mirror/process.mjs +567 -0
  494. package/scripts/lib/vault-mirror/pseudonym-map.mjs +164 -0
  495. package/scripts/lib/vault-mirror/render-learnings.mjs +201 -0
  496. package/scripts/lib/vault-mirror/render-sessions.mjs +367 -0
  497. package/scripts/lib/vault-mirror/render.mjs +8 -0
  498. package/scripts/lib/vault-mirror/utils.mjs +217 -0
  499. package/scripts/lib/vault-relocation-rules.mjs +555 -0
  500. package/scripts/lib/vault-repo-backfill.mjs +235 -0
  501. package/scripts/lib/vault-staleness-banner.mjs +142 -0
  502. package/scripts/lib/vault-status/board-writer.mjs +769 -0
  503. package/scripts/lib/vault-status/narrative-mirror.mjs +544 -0
  504. package/scripts/lib/vault-sync-baseline.mjs +152 -0
  505. package/scripts/lib/wave-context.mjs +29 -0
  506. package/scripts/lib/wave-executor/pool.mjs +248 -0
  507. package/scripts/lib/wave-resource-gate.mjs +204 -0
  508. package/scripts/lib/wave-sizing.mjs +75 -0
  509. package/scripts/lib/webhook-url.mjs +105 -0
  510. package/scripts/lib/workspace.mjs +198 -0
  511. package/scripts/lib/worktree/constants.mjs +35 -0
  512. package/scripts/lib/worktree/index.mjs +17 -0
  513. package/scripts/lib/worktree/lifecycle.mjs +287 -0
  514. package/scripts/lib/worktree/listing.mjs +118 -0
  515. package/scripts/lib/worktree/meta.mjs +64 -0
  516. package/scripts/lib/worktree-freshness.mjs +313 -0
  517. package/scripts/lib/worktree.mjs +15 -0
  518. package/scripts/lifecycle-sim-v6.mjs +347 -0
  519. package/scripts/lock-reaper.mjs +185 -0
  520. package/scripts/mcp-server.sh +241 -0
  521. package/scripts/measure-policy-cache-effectiveness.mjs +427 -0
  522. package/scripts/memory-propose.mjs +464 -0
  523. package/scripts/migrate-cold-start-seed.mjs +404 -0
  524. package/scripts/migrate-learnings-jsonl.mjs +189 -0
  525. package/scripts/migrate-legacy-learnings.sh +61 -0
  526. package/scripts/migrate-sessions-jsonl.mjs +448 -0
  527. package/scripts/migrate-subagents-jsonl.mjs +196 -0
  528. package/scripts/migrate-vault-paths.mjs +796 -0
  529. package/scripts/parse-config.mjs +149 -0
  530. package/scripts/pi-install.mjs +117 -0
  531. package/scripts/print-applicable-rules.mjs +247 -0
  532. package/scripts/promote-vault-strict.mjs +496 -0
  533. package/scripts/relocate-vault-corpus.mjs +1178 -0
  534. package/scripts/run-migrate-v2-cross-repo.mjs +385 -0
  535. package/scripts/run-quality-gate.mjs +216 -0
  536. package/scripts/spikes/h3-agent-teams/preflight.sh +53 -0
  537. package/scripts/spikes/h3-agent-teams/run-h3.sh +112 -0
  538. package/scripts/spikes/h3-agent-teams/setup.sh +137 -0
  539. package/scripts/spikes/h3-agent-teams/toggle.sh +38 -0
  540. package/scripts/sweep-expired-learnings.mjs +135 -0
  541. package/scripts/sync-vault-schema.mjs +376 -0
  542. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +8 -0
  543. package/scripts/tmux-layout.mjs +245 -0
  544. package/scripts/token-audit.sh +191 -0
  545. package/scripts/typecheck.mjs +42 -0
  546. package/scripts/upload-social-preview.mjs +316 -0
  547. package/scripts/validate-config.mjs +46 -0
  548. package/scripts/validate-plugin-manifests.mjs +163 -0
  549. package/scripts/validate-plugin.mjs +264 -0
  550. package/scripts/validate-wave-scope.mjs +289 -0
  551. package/scripts/vault-backfill.mjs +404 -0
  552. package/scripts/vault-consolidate.mjs +596 -0
  553. package/scripts/vault-integration-watcher.mjs +394 -0
  554. package/scripts/vault-mirror.mjs +430 -0
  555. package/skills/_shared/bootstrap-gate.md +111 -0
  556. package/skills/_shared/config-reading.md +226 -0
  557. package/skills/_shared/instruction-file-resolution.md +79 -0
  558. package/skills/_shared/model-selection.md +64 -0
  559. package/skills/_shared/monitor-patterns.md +300 -0
  560. package/skills/_shared/parallel-aware-auq.md +121 -0
  561. package/skills/_shared/parallel-aware-preamble.md +185 -0
  562. package/skills/_shared/platform-tools.md +96 -0
  563. package/skills/_shared/state-ownership.md +221 -0
  564. package/skills/architecture/DEEPENING.md +37 -0
  565. package/skills/architecture/INTERFACE-DESIGN.md +44 -0
  566. package/skills/architecture/LANGUAGE.md +53 -0
  567. package/skills/architecture/SKILL.md +92 -0
  568. package/skills/autopilot/SKILL.md +419 -0
  569. package/skills/bootstrap/SKILL.md +592 -0
  570. package/skills/bootstrap/STATE.md.template +24 -0
  571. package/skills/bootstrap/_shared-template.md +243 -0
  572. package/skills/bootstrap/deep-template.md +659 -0
  573. package/skills/bootstrap/fast-template.md +251 -0
  574. package/skills/bootstrap/intensity-heuristic.md +80 -0
  575. package/skills/bootstrap/public-fallback.md +342 -0
  576. package/skills/bootstrap/standard-template.md +736 -0
  577. package/skills/bootstrap/templates/agents/project-code-review.md +18 -0
  578. package/skills/bootstrap/templates/agents/project-discovery.md +18 -0
  579. package/skills/bootstrap/templates/agents/project-quality-gate.md +18 -0
  580. package/skills/brainstorm/SKILL.md +268 -0
  581. package/skills/brainstorm/soul.md +49 -0
  582. package/skills/claude-md-drift-check/SKILL.md +186 -0
  583. package/skills/claude-md-drift-check/checker.mjs +1380 -0
  584. package/skills/claude-md-drift-check/checker.sh +37 -0
  585. package/skills/claude-md-drift-check/package.json +16 -0
  586. package/skills/convergence-monitoring/README.md +39 -0
  587. package/skills/convergence-monitoring/SIGNALS.md +246 -0
  588. package/skills/convergence-monitoring/SKILL.md +285 -0
  589. package/skills/daily/SKILL.md +222 -0
  590. package/skills/daily/generate.sh +92 -0
  591. package/skills/daily/templates/daily.md.tpl +36 -0
  592. package/skills/debug/SKILL.md +188 -0
  593. package/skills/debug/soul.md +35 -0
  594. package/skills/discovery/SKILL.md +567 -0
  595. package/skills/discovery/issue-templates.md +237 -0
  596. package/skills/discovery/probes/docs-staleness.mjs +195 -0
  597. package/skills/discovery/probes/frontend-slop.mjs +186 -0
  598. package/skills/discovery/probes/ssot-code-diff.mjs +310 -0
  599. package/skills/discovery/probes/supply-chain-slopcheck.mjs +440 -0
  600. package/skills/discovery/probes/vault-narrative-staleness.mjs +355 -0
  601. package/skills/discovery/probes/vault-staleness.mjs +272 -0
  602. package/skills/discovery/probes-arch.md +252 -0
  603. package/skills/discovery/probes-audit.md +95 -0
  604. package/skills/discovery/probes-code.md +329 -0
  605. package/skills/discovery/probes-docs.md +76 -0
  606. package/skills/discovery/probes-feature.md +150 -0
  607. package/skills/discovery/probes-infra.md +138 -0
  608. package/skills/discovery/probes-intro.md +25 -0
  609. package/skills/discovery/probes-session.md +495 -0
  610. package/skills/discovery/probes-supply-chain.md +94 -0
  611. package/skills/discovery/probes-ui.md +147 -0
  612. package/skills/discovery/probes-vault.md +64 -0
  613. package/skills/discovery/slop-patterns.md +115 -0
  614. package/skills/dispatcher/SKILL.md +173 -0
  615. package/skills/docs-orchestrator/SKILL.md +362 -0
  616. package/skills/docs-orchestrator/audience-mapping.md +140 -0
  617. package/skills/domain-model/ADR-FORMAT.md +47 -0
  618. package/skills/domain-model/CONTEXT-FORMAT.md +77 -0
  619. package/skills/domain-model/SKILL.md +85 -0
  620. package/skills/ecosystem-health/SKILL.md +119 -0
  621. package/skills/ecosystem-health/wizard.md +193 -0
  622. package/skills/eval/SKILL.md +293 -0
  623. package/skills/eval/rubric-v1.md +218 -0
  624. package/skills/evolve/SKILL.md +546 -0
  625. package/skills/frontmatter-guard/SKILL.md +126 -0
  626. package/skills/gitlab-ops/SKILL.md +368 -0
  627. package/skills/gitlab-portfolio/SKILL.md +196 -0
  628. package/skills/grill/SKILL.md +185 -0
  629. package/skills/grill/soul.md +55 -0
  630. package/skills/hook-development/SKILL.md +413 -0
  631. package/skills/mcp-builder/SKILL.md +260 -0
  632. package/skills/memory-cleanup/SKILL.md +310 -0
  633. package/skills/mode-selector/SKILL.md +226 -0
  634. package/skills/peekaboo-driver/SKILL.md +237 -0
  635. package/skills/peekaboo-driver/soul.md +32 -0
  636. package/skills/persona-panel/SKILL.md +365 -0
  637. package/skills/persona-panel/persona-format.md +205 -0
  638. package/skills/persona-panel/presets/designer-lens.md +87 -0
  639. package/skills/persona-panel/presets/engineer-lens.md +88 -0
  640. package/skills/persona-panel/presets/pm-lens.md +86 -0
  641. package/skills/plan/SKILL.md +496 -0
  642. package/skills/plan/mode-feature.md +141 -0
  643. package/skills/plan/mode-new.md +297 -0
  644. package/skills/plan/mode-retro.md +271 -0
  645. package/skills/plan/prd-feature-template.md +132 -0
  646. package/skills/plan/prd-full-template.md +151 -0
  647. package/skills/plan/prd-reviewer-prompt.md +103 -0
  648. package/skills/plan/retro-template.md +75 -0
  649. package/skills/plan/soul.md +62 -0
  650. package/skills/playwright-driver/SKILL.md +226 -0
  651. package/skills/playwright-driver/soul.md +30 -0
  652. package/skills/quality-gates/SKILL.md +212 -0
  653. package/skills/reconcile/SKILL.md +324 -0
  654. package/skills/repo-audit/SKILL.md +272 -0
  655. package/skills/session-end/SKILL.md +1044 -0
  656. package/skills/session-end/discovery-scan.md +37 -0
  657. package/skills/session-end/drift-operations.md +97 -0
  658. package/skills/session-end/learning-patterns.md +78 -0
  659. package/skills/session-end/metrics-collection.md +175 -0
  660. package/skills/session-end/phase-3-2-docs-verification.md +148 -0
  661. package/skills/session-end/phase-3-6-tail.md +344 -0
  662. package/skills/session-end/phase-3-7a-recommendations.md +86 -0
  663. package/skills/session-end/plan-verification.md +288 -0
  664. package/skills/session-end/session-metrics-write.md +223 -0
  665. package/skills/session-end/vault-operations.md +50 -0
  666. package/skills/session-end/verification-checklist.md +20 -0
  667. package/skills/session-plan/SKILL.md +554 -0
  668. package/skills/session-plan/wave-template.md +37 -0
  669. package/skills/session-start/SKILL.md +1043 -0
  670. package/skills/session-start/phase-2-5-docs-planning.md +119 -0
  671. package/skills/session-start/phase-4-5-resource-health.md +49 -0
  672. package/skills/session-start/phase-7-1-premise-check.md +47 -0
  673. package/skills/session-start/phase-7-5-mode-selector.md +237 -0
  674. package/skills/session-start/phase-8-5-express-path.md +61 -0
  675. package/skills/session-start/presentation-format.md +81 -0
  676. package/skills/session-start/soul.md +57 -0
  677. package/skills/skill-creator/SKILL.md +168 -0
  678. package/skills/spinout/SKILL.md +76 -0
  679. package/skills/sunset-review/SKILL.md +96 -0
  680. package/skills/test-runner/SKILL.md +362 -0
  681. package/skills/test-runner/rubric-v1.md +388 -0
  682. package/skills/test-runner/soul.md +46 -0
  683. package/skills/tmux-layout/SKILL.md +104 -0
  684. package/skills/ubiquitous-language/SKILL.md +97 -0
  685. package/skills/using-orchestrator/SKILL.md +144 -0
  686. package/skills/vault-mirror/SKILL.md +234 -0
  687. package/skills/vault-sync/SKILL.md +319 -0
  688. package/skills/vault-sync/package-lock.json +40 -0
  689. package/skills/vault-sync/package.json +11 -0
  690. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +8 -0
  691. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  692. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +8 -0
  693. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  694. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +8 -0
  695. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +8 -0
  696. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +8 -0
  697. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +10 -0
  698. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +8 -0
  699. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +3 -0
  700. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +11 -0
  701. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  702. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +9 -0
  703. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +8 -0
  704. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  705. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  706. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +7 -0
  707. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +9 -0
  708. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  709. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +11 -0
  710. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +3 -0
  711. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +3 -0
  712. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  713. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +11 -0
  714. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  715. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +11 -0
  716. package/skills/vault-sync/tests/schema-drift.test.mjs +133 -0
  717. package/skills/vault-sync/validator.mjs +658 -0
  718. package/skills/vault-sync/validator.sh +55 -0
  719. package/skills/wave-executor/SKILL.md +496 -0
  720. package/skills/wave-executor/circuit-breaker.md +169 -0
  721. package/skills/wave-executor/wave-loop.md +1043 -0
  722. package/skills/write-executable-plan/SKILL.md +237 -0
  723. package/skills/write-executable-plan/plan-template.md +154 -0
  724. package/templates/_minimal/CLAUDE.md.tmpl +41 -0
  725. package/templates/_minimal/README.md.tmpl +15 -0
  726. package/templates/_minimal/gitignore.tmpl +47 -0
  727. package/templates/_shared/harte-regeln.md +16 -0
  728. package/templates/_shared/loop.md +90 -0
  729. package/templates/_shared/rules/parallel-sessions.md +77 -0
  730. package/templates/nextjs-minimal/README.md +30 -0
  731. package/templates/nextjs-minimal/app/layout.tsx +18 -0
  732. package/templates/nextjs-minimal/app/page.tsx +7 -0
  733. package/templates/nextjs-minimal/eslint.config.mjs +16 -0
  734. package/templates/nextjs-minimal/next.config.mjs +4 -0
  735. package/templates/nextjs-minimal/package.json +27 -0
  736. package/templates/nextjs-minimal/tsconfig.json +23 -0
  737. package/templates/node-minimal/README.md +33 -0
  738. package/templates/node-minimal/eslint.config.mjs +10 -0
  739. package/templates/node-minimal/package.json +21 -0
  740. package/templates/node-minimal/src/index.ts +1 -0
  741. package/templates/node-minimal/tests/sanity.test.ts +5 -0
  742. package/templates/node-minimal/tsconfig.json +17 -0
  743. package/templates/personas/README.md +150 -0
  744. package/templates/personas/accounting-compliance.v1.md +120 -0
  745. package/templates/personas/accounting-tax-advisor.v1.md +116 -0
  746. package/templates/personas/buyer-p1-cto.v1.md +125 -0
  747. package/templates/personas/buyer-p2-kanzlei.v1.md +134 -0
  748. package/templates/personas/buyer-p3-build.v1.md +130 -0
  749. package/templates/personas/buyer-p4-tech-veto.v1.md +130 -0
  750. package/templates/personas/buyer-p5-solo.v1.md +132 -0
  751. package/templates/personas/buyer-p6-ld.v1.md +130 -0
  752. package/templates/personas/klima-ai-expert.v1.md +114 -0
  753. package/templates/personas/klima-physicist.v1.md +117 -0
  754. package/templates/python-uv/README.md +28 -0
  755. package/templates/python-uv/pyproject.toml +32 -0
  756. package/templates/python-uv/src/__PROJECT_NAME__/__init__.py +0 -0
  757. package/templates/python-uv/src/__PROJECT_NAME__/main.py +6 -0
  758. package/templates/python-uv/tests/test_sanity.py +2 -0
  759. package/templates/static-html/README.md +19 -0
  760. package/templates/static-html/index.html +15 -0
  761. package/templates/static-html/script.js +1 -0
  762. package/templates/static-html/styles.css +26 -0
@@ -0,0 +1,1380 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * checker.mjs — CLAUDE.md narrative drift-check (claude-md-drift-check).
4
+ *
5
+ * Ten checks: path-resolver, project-count-sync, issue-reference-freshness,
6
+ * session-file-existence, surface-count (command/skill/agent/hook-event/
7
+ * hook-matcher/test families), session-config-parity, vault-dir-parity,
8
+ * generated-rule-staleness (WARN-mode only), rule-scoping (paths-presence,
9
+ * cited-but-missing rule citations, zero-match-globs, and foreign-glob
10
+ * PascalCase tokens in .claude/rules/*.md frontmatter), and docs-parity
11
+ * (count-claims in docs/components.md vs actual on-disk counts, Session-Config
12
+ * key parity between docs/session-config-template.md and
13
+ * docs/session-config-reference.md, and stale `.claude/metrics/` path
14
+ * references in the live docs tree).
15
+ * Scans CLAUDE.md + _meta/**\/*.md by default.
16
+ * Emits JSON on stdout. Exit 0 (ok/warn/skip), 1 (hard + errors), 2 (infra).
17
+ *
18
+ * The surface-count family (issue #663) generalizes the original command-count
19
+ * drift check into a parity guard over six artifact surfaces. Each surface
20
+ * derives an ACTUAL on-disk count (glob / hooks.json wiring) and compares it
21
+ * against a CLAIMED count extracted by regex from any scanned doc (README.md,
22
+ * .orchestrator/steering/structure.md, CLAUDE.md). A surface that the doc never
23
+ * claims numerically is skipped gracefully — no claim is ever invented.
24
+ *
25
+ * Reuses `_parseVaultIntegration` from scripts/lib/config/ for Check 7;
26
+ * reuses `parseGlobsFrontmatter` from scripts/lib/rule-loader.mjs for Check 9's
27
+ * glob extraction (mirrors the same picomatch-with-inline-fallback resolution
28
+ * rule-loader.mjs uses); otherwise pure Node stdlib.
29
+ */
30
+
31
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
32
+ import { join, relative, resolve } from 'node:path';
33
+ import { execFileSync } from 'node:child_process';
34
+ import { createRequire } from 'node:module';
35
+ import { resolveInstructionFile } from '../../scripts/lib/common.mjs';
36
+ import { _parseVaultIntegration } from '../../scripts/lib/config/vault-integration.mjs';
37
+ import { parseGlobsFrontmatter } from '../../scripts/lib/rule-loader.mjs';
38
+
39
+ const FORWARD_HEADING_RE =
40
+ /(?:^|\b)(what'?s?\s+next|backlog|open\s+issues?|offene\s+(?:issues?|themen)|todo|next\s+steps?|roadmap)(?:$|\b)/i;
41
+ const BACKWARD_HEADING_RE =
42
+ /(?:^|\b)(recently\s+closed|done|closed|archive|history|changelog|decisions?|status|references?)(?:$|\b)/i;
43
+
44
+ function parseArgs(argv) {
45
+ const out = {
46
+ mode: 'warn',
47
+ includePaths: [],
48
+ skipPathResolver: false,
49
+ skipProjectCount: false,
50
+ skipIssueRefs: false,
51
+ skipSessionFiles: false,
52
+ skipCommandCount: false,
53
+ skipSurfaceCount: false,
54
+ skipSessionConfigParity: false,
55
+ skipVaultDirParity: false,
56
+ skipGeneratedRuleStaleness: false,
57
+ skipRuleScoping: false,
58
+ skipDocsParity: false,
59
+ repo: null,
60
+ commandsDir: null,
61
+ configTemplate: null,
62
+ };
63
+ for (let i = 0; i < argv.length; i++) {
64
+ const a = argv[i];
65
+ if (a === '--mode') out.mode = argv[++i];
66
+ else if (a === '--include-path') out.includePaths.push(argv[++i]);
67
+ else if (a === '--repo') out.repo = argv[++i];
68
+ else if (a === '--commands-dir') out.commandsDir = argv[++i];
69
+ else if (a === '--config-template') out.configTemplate = argv[++i];
70
+ else if (a === '--skip-path-resolver') out.skipPathResolver = true;
71
+ else if (a === '--skip-project-count') out.skipProjectCount = true;
72
+ else if (a === '--skip-issue-refs') out.skipIssueRefs = true;
73
+ else if (a === '--skip-session-files') out.skipSessionFiles = true;
74
+ else if (a === '--skip-command-count') out.skipCommandCount = true;
75
+ else if (a === '--skip-surface-count') out.skipSurfaceCount = true;
76
+ else if (a === '--skip-session-config-parity') out.skipSessionConfigParity = true;
77
+ else if (a === '--skip-vault-dir-parity') out.skipVaultDirParity = true;
78
+ else if (a === '--skip-generated-rule-staleness') out.skipGeneratedRuleStaleness = true;
79
+ else if (a === '--skip-rule-scoping') out.skipRuleScoping = true;
80
+ else if (a === '--skip-docs-parity') out.skipDocsParity = true;
81
+ else if (a === '--help' || a === '-h') {
82
+ process.stdout.write('Usage: checker.mjs [--mode hard|warn|off] [--include-path GLOB]... [--repo OWNER/NAME] [--commands-dir PATH] [--config-template PATH] [--skip-surface-count] [--skip-command-count] [--skip-generated-rule-staleness] [--skip-rule-scoping] [--skip-docs-parity] [--skip-*]\n');
83
+ process.exit(0);
84
+ } else {
85
+ process.stderr.write(`{"status":"infra-error","reason":"unknown arg: ${a}"}\n`);
86
+ process.exit(2);
87
+ }
88
+ }
89
+ // Defaults for includePaths are seeded post-vaultDir resolution so the
90
+ // instruction file (CLAUDE.md or AGENTS.md alias) can be alias-resolved.
91
+ return out;
92
+ }
93
+
94
+ function resolveScopeFiles(vaultDir, patterns) {
95
+ const files = new Set();
96
+ for (const pattern of patterns) {
97
+ if (pattern.includes('**/')) {
98
+ const [prefix, suffix] = pattern.split('/**/');
99
+ const extMatch = /^\*\.(\w+)$/.exec(suffix || '');
100
+ const ext = extMatch ? `.${extMatch[1]}` : null;
101
+ const dir = join(vaultDir, prefix);
102
+ if (existsSync(dir) && statSync(dir).isDirectory()) {
103
+ walkDir(dir, (f) => {
104
+ if (!ext || f.endsWith(ext)) files.add(f);
105
+ });
106
+ }
107
+ } else {
108
+ const abs = join(vaultDir, pattern);
109
+ if (existsSync(abs) && statSync(abs).isFile()) files.add(abs);
110
+ }
111
+ }
112
+ return Array.from(files).sort();
113
+ }
114
+
115
+ function walkDir(dir, visit) {
116
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
117
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
118
+ const full = join(dir, entry.name);
119
+ if (entry.isDirectory()) walkDir(full, visit);
120
+ else if (entry.isFile()) visit(full);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Extract the YAML block under a `## Session Config` heading from a Markdown
126
+ * document. Tries fenced YAML first (```yaml ... ```) *within the matched
127
+ * heading's own body slice*, then falls back to the raw body up to the next
128
+ * `^## ` heading or EOF.
129
+ *
130
+ * `docs/session-config-template.md` carries TWO literal `## Session Config`
131
+ * lines — both live INSIDE fenced ```yaml example blocks: the "Full minimal
132
+ * baseline" (7 mandatory keys) and the "Full opt-in baseline" (the complete
133
+ * key catalog, a strict superset of the minimal block). By default this
134
+ * function matches the FIRST occurrence (correct for a local CLAUDE.md /
135
+ * AGENTS.md instruction file, which has exactly one real heading). Pass
136
+ * `{ occurrence: 'last' }` to match the LAST occurrence instead — the template
137
+ * call site uses this to reach the opt-in baseline's full key set rather than
138
+ * silently stopping at the 7-key minimal block (issue #785).
139
+ *
140
+ * Returns { body: string, headingLine: number } or null when no heading found.
141
+ *
142
+ * @param {string} content
143
+ * @param {{ occurrence?: 'first'|'last' }} [options]
144
+ */
145
+ function extractSessionConfigBlock(content, { occurrence = 'first' } = {}) {
146
+ const lines = content.split('\n');
147
+ const headingIdxs = [];
148
+ for (let i = 0; i < lines.length; i++) {
149
+ if (/^##\s+Session Config\b/.test(lines[i])) headingIdxs.push(i);
150
+ }
151
+ if (headingIdxs.length === 0) return null;
152
+
153
+ const targetIdx = occurrence === 'last' ? headingIdxs[headingIdxs.length - 1] : headingIdxs[0];
154
+ const headingLine = targetIdx + 1; // 1-based line number for error reporting
155
+
156
+ // Raw body: from the line after the heading up to the next `## ` heading or EOF.
157
+ let endIdx = lines.length;
158
+ for (let i = targetIdx + 1; i < lines.length; i++) {
159
+ if (/^##\s+/.test(lines[i])) { endIdx = i; break; }
160
+ }
161
+ const rawBody = lines.slice(targetIdx + 1, endIdx).join('\n');
162
+
163
+ // Prefer a fenced YAML block found anywhere within the raw body slice
164
+ // (tolerates a blank line between the heading and the fence); fall back to
165
+ // the raw body itself when no fence is present.
166
+ const fenced = rawBody.match(/```ya?ml\r?\n([\s\S]*?)\r?\n```/);
167
+ const body = fenced ? fenced[1] : rawBody;
168
+
169
+ return { body, headingLine };
170
+ }
171
+
172
+ /**
173
+ * Extract top-level YAML keys from a YAML body. Only column-0 keys are
174
+ * collected (indented keys are children and ignored).
175
+ */
176
+ function extractTopLevelKeys(body) {
177
+ const keys = [];
178
+ const re = /^([A-Za-z][\w-]*):/gm;
179
+ let m;
180
+ while ((m = re.exec(body)) !== null) {
181
+ keys.push(m[1]);
182
+ }
183
+ return keys;
184
+ }
185
+
186
+ /**
187
+ * Extract Session-Config-key-shaped mentions from a "reference" style doc
188
+ * (docs/session-config-reference.md for Check 10b). Unlike the template,
189
+ * which carries one big fenced Session Config block, the reference documents
190
+ * most fields as ONE MARKDOWN TABLE ROW PER KEY (`| \`key-name\` | type |
191
+ * default | description |`) rather than in a single YAML block — a plain
192
+ * "union all yaml fences" extraction (as in `extractSessionConfigBlock`)
193
+ * massively under-counts what the reference actually documents (verified:
194
+ * 53 false-positive "missing" keys against the live doc when restricted to
195
+ * fences/headings alone, vs. the true gap of 6 keys once table rows are
196
+ * counted). Three surfaces contribute to the returned key set:
197
+ * - top-level keys inside any ```yaml fenced block (any indent depth);
198
+ * - `##`/`###`/`####` heading key-tokens (optionally backtick-wrapped),
199
+ * e.g. "## Templates-First Hook" → no match (not kebab-lowercase-shaped),
200
+ * "### resource-thresholds" → `resource-thresholds`;
201
+ * - the FIRST CELL of a markdown table row when it is a backtick-wrapped
202
+ * kebab-case token, e.g. `| \`plan-prd-location\` | string | ... |`.
203
+ * Fence state is tracked for ALL code fences (not just yaml-tagged ones) so
204
+ * headings/table-rows accidentally written inside an unrelated code example
205
+ * are not mistaken for real documentation structure.
206
+ *
207
+ * @param {string} content
208
+ * @returns {Set<string>}
209
+ */
210
+ function extractDocKeyMentions(content) {
211
+ const keys = new Set();
212
+ const lines = content.split('\n');
213
+ let inFence = false;
214
+ let fenceLang = null;
215
+ let fenceBuf = [];
216
+ for (const line of lines) {
217
+ if (!inFence) {
218
+ const fenceOpen = /^```\s*(\w*)\s*$/.exec(line);
219
+ if (fenceOpen) { inFence = true; fenceLang = fenceOpen[1]; fenceBuf = []; continue; }
220
+ } else {
221
+ if (/^```\s*$/.test(line)) {
222
+ inFence = false;
223
+ if (/^ya?ml$/i.test(fenceLang || '')) {
224
+ const re = /^\s*([a-z][\w-]*):/gm;
225
+ let m;
226
+ const body = fenceBuf.join('\n');
227
+ while ((m = re.exec(body)) !== null) keys.add(m[1]);
228
+ }
229
+ continue;
230
+ }
231
+ fenceBuf.push(line);
232
+ continue;
233
+ }
234
+
235
+ const heading = /^#{2,4}\s+`?([a-z][\w-]*)`?/.exec(line);
236
+ if (heading) keys.add(heading[1]);
237
+
238
+ const tableRow = /^\|\s*`([a-z][\w-]*)`\s*\|/.exec(line);
239
+ if (tableRow) keys.add(tableRow[1]);
240
+ }
241
+ return keys;
242
+ }
243
+
244
+ /**
245
+ * Read a file's `vault-integration` settings for the vault-dir-parity check.
246
+ *
247
+ * Returns `{ present, vaultDir }`:
248
+ * - `present` is true when the file contains a `vault-integration:` block or
249
+ * inline-object literal (block-form header `vault-integration:` on its own
250
+ * line, or `vault-integration: { ... }`), matching the source shapes
251
+ * `_parseVaultIntegration` recognises.
252
+ * - `vaultDir` is the parsed `vault-integration.vault-dir` value (null when
253
+ * unset/absent), produced by reusing `_parseVaultIntegration` rather than
254
+ * hand-rolling YAML.
255
+ *
256
+ * @param {string} filePath — absolute path to CLAUDE.md / AGENTS.md
257
+ * @returns {{ present: boolean, vaultDir: string|null }}
258
+ */
259
+ function readVaultIntegration(filePath) {
260
+ const content = readFileSync(filePath, 'utf8');
261
+ // Detect block-form header (`vault-integration:` alone on a line) or inline
262
+ // object literal (`vault-integration: { ... }`), with or without a list dash.
263
+ const present =
264
+ /^(?:-\s+)?vault-integration:\s*$/m.test(content) ||
265
+ /^(?:-\s+)?vault-integration:\s*\{[^}]*\}\s*(?:#.*)?$/m.test(content);
266
+ const parsed = _parseVaultIntegration(content);
267
+ return { present, vaultDir: parsed['vault-dir'] };
268
+ }
269
+
270
+ function classifySection(heading) {
271
+ if (!heading) return null;
272
+ if (BACKWARD_HEADING_RE.test(heading)) return 'backward';
273
+ if (FORWARD_HEADING_RE.test(heading)) return 'forward';
274
+ return null;
275
+ }
276
+
277
+ // ───────────────────────────────────────────────────────────────────────────
278
+ // Surface-count family (issue #663) — generalizes the original command-count
279
+ // drift check. Each surface has:
280
+ // - id: the check id surfaced in errors[].check and checks_run.
281
+ // - actual(vaultDir): pure function returning the ACTUAL on-disk count, or
282
+ // null when the source artifact is absent (→ surface skipped).
283
+ // - claimRe: a /g/i regex whose capture group 1 is the CLAIMED integer. Only
284
+ // an explicit numeric claim triggers a comparison; surfaces with no
285
+ // matching phrase in any doc are skipped gracefully (requirement #3).
286
+ // - noun: human label used in the drift message.
287
+ // These are EXACT-count drift checks by design — the whole point is catching
288
+ // drift, so NO floor/ceiling (see .claude/rules/testing.md "Dynamic Artifact
289
+ // Counts" carve-out — this is the explicit exception).
290
+ // ───────────────────────────────────────────────────────────────────────────
291
+
292
+ /** Count `skills/<name>/SKILL.md` directories (excludes `_shared/` which has no SKILL.md). */
293
+ function countSkills(vaultDir) {
294
+ const dir = join(vaultDir, 'skills');
295
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
296
+ let n = 0;
297
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
298
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
299
+ const skillFile = join(dir, entry.name, 'SKILL.md');
300
+ if (existsSync(skillFile) && statSync(skillFile).isFile()) n++;
301
+ }
302
+ return n;
303
+ }
304
+
305
+ /** Count `agents/*.md` definitions, excluding the AGENTS.md authoring spec. */
306
+ function countAgents(vaultDir) {
307
+ const dir = join(vaultDir, 'agents');
308
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
309
+ return readdirSync(dir)
310
+ .filter((f) => f.endsWith('.md') && f !== 'AGENTS.md' && !f.startsWith('.'))
311
+ .length;
312
+ }
313
+
314
+ /**
315
+ * Parse `hooks/hooks.json` and return `{ events, matchers }`:
316
+ * - events: distinct top-level event names (keys of `.hooks`).
317
+ * - matchers: total matcher entries across all events (the authoritative
318
+ * wiring count, not a raw file count).
319
+ * Returns null when hooks.json is absent or unparseable.
320
+ */
321
+ function readHookCounts(vaultDir) {
322
+ const file = join(vaultDir, 'hooks', 'hooks.json');
323
+ if (!existsSync(file) || !statSync(file).isFile()) return null;
324
+ let json;
325
+ try {
326
+ json = JSON.parse(readFileSync(file, 'utf8'));
327
+ } catch {
328
+ return null;
329
+ }
330
+ const hooks = json && json.hooks;
331
+ if (!hooks || typeof hooks !== 'object') return null;
332
+ const events = Object.keys(hooks);
333
+ let matchers = 0;
334
+ for (const ev of events) {
335
+ if (Array.isArray(hooks[ev])) matchers += hooks[ev].length;
336
+ }
337
+ return { events: events.length, matchers };
338
+ }
339
+
340
+ /** Count test files (`*.test.mjs`) under `tests/`. */
341
+ function countTestFiles(vaultDir) {
342
+ const dir = join(vaultDir, 'tests');
343
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
344
+ let n = 0;
345
+ walkDir(dir, (f) => { if (f.endsWith('.test.mjs')) n++; });
346
+ return n;
347
+ }
348
+
349
+ /**
350
+ * Build the surface-count descriptor table for the given vault. `hookCounts`
351
+ * is computed once and shared by the two hook surfaces. Each descriptor's
352
+ * `actual` is resolved eagerly so a single drift pass can skip null surfaces.
353
+ *
354
+ * `command-count` keeps its original id + message phrasing for back-compat with
355
+ * the result.command_count field and existing tests; the four new surfaces
356
+ * follow the same shape.
357
+ */
358
+ function buildSurfaceDescriptors(vaultDir, commandsDir) {
359
+ const hookCounts = readHookCounts(vaultDir);
360
+ return [
361
+ {
362
+ id: 'command-count',
363
+ noun: 'commands',
364
+ actual: (() => {
365
+ const dir = commandsDir || join(vaultDir, 'commands');
366
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
367
+ return readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('.')).length;
368
+ })(),
369
+ // "8 commands", "8 /commands", "8 slash commands"
370
+ claimRe: /\b(\d+)\s+(?:\/)?commands?\b/gi,
371
+ skipMsg: 'command-count: no commands/ directory found (use --commands-dir to override)',
372
+ },
373
+ {
374
+ id: 'skill-count',
375
+ noun: 'skills',
376
+ actual: countSkills(vaultDir),
377
+ // "40 skills", "40 user-facing skills"
378
+ claimRe: /\b(\d+)\s+(?:user-facing\s+)?skills?\b/gi,
379
+ skipMsg: 'skill-count: no skills/ directory found',
380
+ },
381
+ {
382
+ id: 'agent-count',
383
+ noun: 'agents',
384
+ actual: countAgents(vaultDir),
385
+ // "14 sub-agent definitions" / "14 sub-agents" / "14 agent definitions"
386
+ // Deliberately NOT matched: "3 agent role definitions" (Codex-specific).
387
+ claimRe: /\b(\d+)\s+(?:sub-agents?|agent\s+definitions?)\b/gi,
388
+ skipMsg: 'agent-count: no agents/ directory found',
389
+ },
390
+ {
391
+ id: 'hook-event-count',
392
+ noun: 'distinct hook events',
393
+ actual: hookCounts ? hookCounts.events : null,
394
+ // "10 distinct events"
395
+ claimRe: /\b(\d+)\s+distinct\s+events?\b/gi,
396
+ skipMsg: 'hook-event-count: no hooks/hooks.json found or unparseable',
397
+ },
398
+ {
399
+ id: 'hook-matcher-count',
400
+ noun: 'hook matcher entries',
401
+ actual: hookCounts ? hookCounts.matchers : null,
402
+ // "14 matcher entries"
403
+ claimRe: /\b(\d+)\s+matcher\s+entries\b/gi,
404
+ skipMsg: 'hook-matcher-count: no hooks/hooks.json found or unparseable',
405
+ },
406
+ {
407
+ id: 'test-count',
408
+ noun: 'test files',
409
+ actual: countTestFiles(vaultDir),
410
+ // "N test files" — the README badge claims a runtime PASS count
411
+ // ("9303 tests"), which a static checker cannot derive, so we match only
412
+ // the explicit file-count phrasing. No doc claims this today → skipped.
413
+ claimRe: /\b(\d+)\s+test\s+files?\b/gi,
414
+ skipMsg: 'test-count: no tests/ directory found',
415
+ },
416
+ ];
417
+ }
418
+
419
+ const REPO_SHAPE_RE = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/;
420
+
421
+ function detectRepo(vaultDir) {
422
+ const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd: vaultDir, encoding: 'utf8' }).trim();
423
+ const m = /[:/]([^:/\s]+\/[^/\s]+?)(?:\.git)?$/.exec(url);
424
+ const candidate = m ? m[1] : null;
425
+ return candidate && REPO_SHAPE_RE.test(candidate) ? candidate : null;
426
+ }
427
+
428
+ function hasGlab() {
429
+ try {
430
+ execFileSync('sh', ['-c', 'command -v glab'], { stdio: 'pipe' });
431
+ return true;
432
+ } catch { return false; }
433
+ }
434
+
435
+ function lookupIssueState(iid, repo, cache) {
436
+ if (cache.has(iid)) return cache.get(iid);
437
+ let state = 'unknown';
438
+ try {
439
+ const out = execFileSync('glab', ['issue', 'view', iid, '--repo', repo], {
440
+ encoding: 'utf8',
441
+ stdio: ['ignore', 'pipe', 'pipe'],
442
+ });
443
+ const m = /^state:\s*(open|closed)/im.exec(out);
444
+ if (m) state = m[1];
445
+ } catch { /* glab returned non-zero — leave as unknown */ }
446
+ cache.set(iid, state);
447
+ return state;
448
+ }
449
+
450
+ // ───────────────────────────────────────────────────────────────────────────
451
+ // Rule-scoping family (Check 9) — validates .claude/rules/*.md frontmatter
452
+ // against the scripts/lib/rule-loader.mjs contract:
453
+ // 1. paths-presence → errors[]: a top-level `paths:` key (rule-loader.mjs
454
+ // only recognises `globs:`; `paths:` silently loads the rule ALWAYS-ON).
455
+ // 2. cited-but-missing → errors[]: (a) `.claude/rules/<name>.md` citations in
456
+ // CLAUDE.md/AGENTS.md that don't exist on disk; (b) bare `<name>.md`
457
+ // tokens in a rule's own `## See Also` footer that don't exist on disk
458
+ // (tokens carrying a path separator are cross-directory references and
459
+ // out of scope).
460
+ // 3. zero-match-globs → warnings[]: a `globs:` pattern matching 0 tracked
461
+ // files (git ls-files). WARN, not error — library/exemplar repos may
462
+ // legitimately carry dead stack rules.
463
+ // 4. foreign-glob → warnings[]: a glob pattern containing a PascalCase
464
+ // product-like token (e.g. `WalkAITalkieTests`) — a likely copy-paste
465
+ // leftover from another project's rule.
466
+ // 5. unreadable-file → warnings[]: a rule file that could not be read
467
+ // (permissions, race with a concurrent delete, etc.) — surfaced instead
468
+ // of being silently skipped, since a completeness audit that silently
469
+ // drops files defeats its purpose. WARN, not error — an unreadable file
470
+ // must not brick the gate under `mode: hard`.
471
+ // ───────────────────────────────────────────────────────────────────────────
472
+
473
+ let _picomatchRuleScoping = null;
474
+ function getPicomatchForRuleScoping() {
475
+ if (_picomatchRuleScoping !== null) return _picomatchRuleScoping;
476
+ try {
477
+ const require = createRequire(import.meta.url);
478
+ _picomatchRuleScoping = require('picomatch');
479
+ } catch {
480
+ _picomatchRuleScoping = false;
481
+ }
482
+ return _picomatchRuleScoping;
483
+ }
484
+
485
+ /** Minimal glob-to-RegExp fallback (mirrors scripts/lib/rule-loader.mjs),
486
+ * used only when picomatch is unavailable in node_modules. */
487
+ function globToRegExpFallback(pattern) {
488
+ const p = pattern.replace(/\\/g, '/');
489
+ let re = '';
490
+ let i = 0;
491
+ while (i < p.length) {
492
+ const c = p[i];
493
+ if (c === '*' && p[i + 1] === '*') {
494
+ re += '.*';
495
+ i += 2;
496
+ if (p[i] === '/') i++;
497
+ } else if (c === '*') {
498
+ re += '[^/]*';
499
+ i++;
500
+ } else if (c === '?') {
501
+ re += '[^/]';
502
+ i++;
503
+ } else if (c === '.') {
504
+ re += '\\.';
505
+ i++;
506
+ } else {
507
+ re += c.replace(/[$()+[\]^{|}]/g, '\\$&');
508
+ i++;
509
+ }
510
+ }
511
+ return new RegExp(`^${re}$`);
512
+ }
513
+
514
+ /** Returns true when `pattern` matches at least one entry in `trackedFiles`. */
515
+ function globMatchesAny(pattern, trackedFiles) {
516
+ const pm = getPicomatchForRuleScoping();
517
+ if (pm) {
518
+ return trackedFiles.some((f) => pm.isMatch(f, pattern, { dot: true }));
519
+ }
520
+ const re = globToRegExpFallback(pattern);
521
+ return trackedFiles.some((f) => re.test(f));
522
+ }
523
+
524
+ /**
525
+ * Returns the repo's tracked files (relative, forward-slash separated) via
526
+ * `git ls-files`; falls back to a manual walk (excluding dotdirs and
527
+ * node_modules — same exclusions as `walkDir`) when git is unavailable.
528
+ */
529
+ function listTrackedFiles(vaultDir) {
530
+ try {
531
+ const out = execFileSync('git', ['ls-files'], { cwd: vaultDir, encoding: 'utf8' });
532
+ return out.split('\n').filter(Boolean);
533
+ } catch {
534
+ const files = [];
535
+ walkDir(vaultDir, (f) => files.push(relative(vaultDir, f).replace(/\\/g, '/')));
536
+ return files;
537
+ }
538
+ }
539
+
540
+ const RULE_HEADER_LINE_RE = /^[ \t]*(?:<!--.*-->)?[ \t]*$/;
541
+
542
+ function stripLeadingRuleHeaderLines(content) {
543
+ const lines = content.split(/\r?\n/);
544
+ let idx = 0;
545
+ while (idx < lines.length && RULE_HEADER_LINE_RE.test(lines[idx])) idx++;
546
+ return lines.slice(idx).join('\n');
547
+ }
548
+
549
+ /** Extracts the raw frontmatter block body (text between the `---` delimiters),
550
+ * or null when the file has no frontmatter block. Leading blank/comment
551
+ * provenance lines are tolerated so Check 9 sees the same frontmatter shape
552
+ * that rule-loader.mjs accepts. */
553
+ function extractFrontmatterBlockBody(content) {
554
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(content));
555
+ return m ? m[1] : null;
556
+ }
557
+
558
+ /** PascalCase product-like token check (e.g. `WalkAITalkieTests`) — the
559
+ * foreign-glob probe's discriminator, per Check 9 spec. */
560
+ const FOREIGN_GLOB_TOKEN_RE = /[A-Z][a-z]+[A-Z]/;
561
+
562
+ /**
563
+ * Extracts bare `<name>.md` tokens from a "## See Also" footer's body lines,
564
+ * skipping any token that carries a path separator (cross-directory
565
+ * references are out of scope) or is not shaped like a kebab-case rule
566
+ * filename. Returns `[{ token, lineOffset }]`, `lineOffset` 0-based relative
567
+ * to `bodyLines[0]`.
568
+ */
569
+ function extractSeeAlsoTokens(bodyLines) {
570
+ const results = [];
571
+ for (let i = 0; i < bodyLines.length; i++) {
572
+ const rawTokens = bodyLines[i].split(/[\s·]+/).filter(Boolean);
573
+ for (const raw of rawTokens) {
574
+ const stripped = raw.replace(/^[`[(]+/, '').replace(/[`\]),.;:]+$/, '');
575
+ if (!/\.md$/.test(stripped)) continue;
576
+ if (stripped.includes('/')) continue; // cross-directory reference — out of scope
577
+ if (!/^[a-z][a-z0-9-]*\.md$/.test(stripped)) continue; // not kebab-case rule-filename shaped
578
+ results.push({ token: stripped, lineOffset: i });
579
+ }
580
+ }
581
+ return results;
582
+ }
583
+
584
+ function main() {
585
+ const args = parseArgs(process.argv.slice(2));
586
+ const vaultDir = resolve(process.env.VAULT_DIR || process.cwd());
587
+
588
+ if (!['hard', 'warn', 'off'].includes(args.mode)) {
589
+ process.stderr.write(`{"status":"infra-error","reason":"invalid --mode: ${args.mode}"}\n`);
590
+ process.exit(2);
591
+ }
592
+
593
+ // Alias-aware instruction file resolution (issue #33).
594
+ // CLAUDE.md (Claude Code / Cursor IDE) and AGENTS.md (Codex CLI) are
595
+ // transparent aliases — see skills/_shared/instruction-file-resolution.md.
596
+ const instr = resolveInstructionFile(vaultDir); // {path, kind} | null
597
+ const resolvedPath = instr ? instr.path : null;
598
+ const resolvedKind = instr ? instr.kind : null;
599
+
600
+ if (args.mode === 'off') {
601
+ process.stdout.write(JSON.stringify({
602
+ status: 'skipped-mode-off', mode: 'off', vault_dir: vaultDir,
603
+ resolved_path: resolvedPath, resolved_kind: resolvedKind,
604
+ }) + '\n');
605
+ process.exit(0);
606
+ }
607
+
608
+ if (!existsSync(vaultDir) || !statSync(vaultDir).isDirectory()) {
609
+ process.stderr.write(`{"status":"infra-error","reason":"VAULT_DIR does not exist: ${vaultDir}"}\n`);
610
+ process.exit(2);
611
+ }
612
+
613
+ // Seed default includePaths post-vaultDir resolution (alias-aware).
614
+ if (args.includePaths.length === 0) {
615
+ args.includePaths = [
616
+ ...(instr ? [relative(vaultDir, instr.path)] : []),
617
+ '_meta/**/*.md',
618
+ ];
619
+ }
620
+
621
+ const scopeFiles = resolveScopeFiles(vaultDir, args.includePaths);
622
+ const checksSkipped = [];
623
+
624
+ let actualProjectCount = null;
625
+ if (!args.skipProjectCount) {
626
+ const projectsDir = join(vaultDir, '01-projects');
627
+ if (existsSync(projectsDir) && statSync(projectsDir).isDirectory()) {
628
+ actualProjectCount = readdirSync(projectsDir, { withFileTypes: true })
629
+ .filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
630
+ .length;
631
+ } else {
632
+ checksSkipped.push('project-count-sync: no 01-projects/ directory at vault root');
633
+ }
634
+ }
635
+
636
+ // Check 5: surface-count family (command/skill/agent/hook-event/hook-matcher/
637
+ // test). Each surface derives an ACTUAL on-disk count and compares it to a
638
+ // CLAIMED count regex-extracted from any scanned doc. Surfaces whose source
639
+ // artifact is absent (actual === null) are skipped; surfaces the doc never
640
+ // claims are skipped per-line in the scan loop (no claim invented).
641
+ // `--skip-surface-count` disables the whole family; `--skip-command-count`
642
+ // remains a back-compat alias that disables only the command-count surface.
643
+ const commandsDir = args.commandsDir ? resolve(args.commandsDir) : null;
644
+ let surfaces = [];
645
+ if (!args.skipSurfaceCount) {
646
+ surfaces = buildSurfaceDescriptors(vaultDir, commandsDir);
647
+ if (args.skipCommandCount) {
648
+ surfaces = surfaces.filter((s) => s.id !== 'command-count');
649
+ }
650
+ for (const s of surfaces) {
651
+ if (s.actual === null) checksSkipped.push(s.skipMsg);
652
+ }
653
+ }
654
+ // Active surfaces = family enabled AND source artifact present on disk.
655
+ const activeSurfaces = surfaces.filter((s) => s.actual !== null);
656
+ // Back-compat: the command-count surface's actual feeds result.command_count.
657
+ const commandSurface = activeSurfaces.find((s) => s.id === 'command-count');
658
+ const actualCommandCount = commandSurface ? commandSurface.actual : null;
659
+
660
+ let repo = args.repo;
661
+ const glabPresent = !args.skipIssueRefs && hasGlab();
662
+ if (!args.skipIssueRefs && !glabPresent) {
663
+ checksSkipped.push('issue-reference-freshness: glab not found in PATH');
664
+ }
665
+ if (!args.skipIssueRefs && glabPresent && !repo) {
666
+ try { repo = detectRepo(vaultDir); } catch { /* ignore */ }
667
+ if (!repo) checksSkipped.push('issue-reference-freshness: could not detect origin repo (use --repo)');
668
+ }
669
+ const runIssueCheck = !args.skipIssueRefs && glabPresent && !!repo;
670
+
671
+ const checksRun = [];
672
+ if (!args.skipPathResolver) checksRun.push('path-resolver');
673
+ if (!args.skipProjectCount && actualProjectCount !== null) checksRun.push('project-count-sync');
674
+ if (runIssueCheck) checksRun.push('issue-reference-freshness');
675
+ if (!args.skipSessionFiles) checksRun.push('session-file-existence');
676
+ for (const s of activeSurfaces) checksRun.push(s.id);
677
+
678
+ const errors = [];
679
+ const warnings = [];
680
+ const issueCache = new Map();
681
+
682
+ // Check 6: session-config-parity (issue #30) — diff top-level keys under
683
+ // `## Session Config` between the canonical template and the local
684
+ // instruction file.
685
+ //
686
+ // Severity split (issue #785 follow-up — coordinator triage of the initial
687
+ // #785 fix): a missing MANDATORY key (present in the template's "Full
688
+ // minimal baseline" — the 7 schema-enforced keys) is an ERROR. A missing
689
+ // OPT-IN key (present only in the "Full opt-in baseline" — i.e. NOT in the
690
+ // minimal block) is a WARNING, not an error: a consumer repo that
691
+ // legitimately does not adopt an opt-in feature (e.g. no `handover-gate`)
692
+ // must not go red — this repo's own CLAUDE.md deliberately omits 37
693
+ // opt-in-baseline keys. `mode: hard` only exits non-zero on `errors[]`, so
694
+ // pure opt-in gaps never block `autonomous-gated` skill-evolution's
695
+ // `runConfigValidationGate()`.
696
+ //
697
+ // This is a deliberate, coordinator-directed DEVIATION from #785's literal
698
+ // fix-direction ("plants an opt-in key... asserts a session-config-parity
699
+ // ERROR") — the mechanism (union template keys via `{ occurrence: 'last' }`)
700
+ // is unchanged, but the missing-opt-in-key case now lands in `warnings[]`
701
+ // instead of `errors[]`. Local keys unknown to the template union were
702
+ // (and remain) never flagged in either direction — no existing code path
703
+ // inspected that direction before this change either.
704
+ let configParityRan = false;
705
+ if (!args.skipSessionConfigParity) {
706
+ const templatePath = args.configTemplate
707
+ ? resolve(args.configTemplate)
708
+ : join(vaultDir, 'docs', 'session-config-template.md');
709
+ if (!instr) {
710
+ checksSkipped.push('session-config-parity: no instruction file');
711
+ } else if (!existsSync(templatePath) || !statSync(templatePath).isFile()) {
712
+ checksSkipped.push('session-config-parity: no docs/session-config-template.md found');
713
+ } else {
714
+ const templateContent = readFileSync(templatePath, 'utf8');
715
+ const localContent = readFileSync(instr.path, 'utf8');
716
+ // 'last' occurrence reaches the "Full opt-in baseline" block (the full
717
+ // key catalog, a strict superset of the minimal block) — issue #785.
718
+ // 'first' occurrence reaches the "Full minimal baseline" (7 mandatory
719
+ // keys) — used here to classify which missing keys are mandatory vs
720
+ // opt-in. When the template has only ONE `## Session Config` heading
721
+ // (e.g. a test fixture, or a repo that never split the two baselines),
722
+ // 'first' and 'last' resolve to the SAME block, so every key is
723
+ // treated as mandatory — preserving pre-existing behaviour for
724
+ // single-block templates.
725
+ const tplBlock = extractSessionConfigBlock(templateContent, { occurrence: 'last' });
726
+ const tplMinimalBlock = extractSessionConfigBlock(templateContent, { occurrence: 'first' });
727
+ const localBlock = extractSessionConfigBlock(localContent);
728
+ if (!tplBlock) {
729
+ checksSkipped.push('session-config-parity: template has no ## Session Config block');
730
+ } else {
731
+ configParityRan = true;
732
+ const tplKeys = extractTopLevelKeys(tplBlock.body);
733
+ const tplMinimalKeys = new Set(tplMinimalBlock ? extractTopLevelKeys(tplMinimalBlock.body) : []);
734
+ const localKeys = new Set(localBlock ? extractTopLevelKeys(localBlock.body) : []);
735
+ // Absent local block: line 1 (nothing to anchor to); present local
736
+ // block: attribute to its own heading line — same as before #785.
737
+ const rel = relative(vaultDir, instr.path);
738
+ const line = localBlock ? localBlock.headingLine : 1;
739
+ const missing = tplKeys.filter((k) => !localKeys.has(k));
740
+ for (const key of missing) {
741
+ if (tplMinimalKeys.has(key)) {
742
+ errors.push({
743
+ check: 'session-config-parity', file: rel, line,
744
+ message: `Session Config missing top-level key '${key}' (present in docs/session-config-template.md)`,
745
+ extracted: key,
746
+ });
747
+ } else {
748
+ warnings.push({
749
+ check: 'session-config-parity', file: rel, line,
750
+ message: `Session Config omits opt-in top-level key '${key}' (documented in docs/session-config-template.md's opt-in baseline; not required)`,
751
+ extracted: key,
752
+ });
753
+ }
754
+ }
755
+ }
756
+ }
757
+ } else {
758
+ checksSkipped.push('session-config-parity: explicitly skipped');
759
+ }
760
+ if (configParityRan) checksRun.push('session-config-parity');
761
+
762
+ // Check 7: vault-dir-parity (issue #600) — when BOTH instruction files
763
+ // (CLAUDE.md AND AGENTS.md) exist as transparent aliases, they must agree on
764
+ // `vault-integration.vault-dir`. A sibling project carried a dead vault-dir in
765
+ // AGENTS.md for weeks while CLAUDE.md was correct; resolveInstructionFile()
766
+ // picks only one file, so the disagreement went undetected. This check reads
767
+ // both files independently and flags any divergence.
768
+ let vaultDirParityRan = false;
769
+ if (!args.skipVaultDirParity) {
770
+ const claudePath = join(vaultDir, 'CLAUDE.md');
771
+ const agentsPath = join(vaultDir, 'AGENTS.md');
772
+ const claudeExists = existsSync(claudePath) && statSync(claudePath).isFile();
773
+ const agentsExists = existsSync(agentsPath) && statSync(agentsPath).isFile();
774
+
775
+ if (!claudeExists || !agentsExists) {
776
+ checksSkipped.push('vault-dir-parity: requires both CLAUDE.md and AGENTS.md (only one present)');
777
+ } else {
778
+ const claudeVi = readVaultIntegration(claudePath);
779
+ const agentsVi = readVaultIntegration(agentsPath);
780
+ if (!claudeVi.present && !agentsVi.present) {
781
+ checksSkipped.push('vault-dir-parity: neither file has a vault-integration: block');
782
+ } else {
783
+ vaultDirParityRan = true;
784
+ const claudeDir = claudeVi.vaultDir;
785
+ const agentsDir = agentsVi.vaultDir;
786
+ if (claudeDir !== agentsDir) {
787
+ errors.push({
788
+ check: 'vault-dir-parity', file: 'AGENTS.md', line: 1,
789
+ message: `vault-integration.vault-dir disagrees between instruction files: CLAUDE.md='${claudeDir ?? '(unset)'}' vs AGENTS.md='${agentsDir ?? '(unset)'}'`,
790
+ extracted: agentsDir ?? '(unset)',
791
+ });
792
+ }
793
+ }
794
+ }
795
+ } else {
796
+ checksSkipped.push('vault-dir-parity: explicitly skipped');
797
+ }
798
+ if (vaultDirParityRan) checksRun.push('vault-dir-parity');
799
+
800
+ // Check 8: generated-rule-staleness (WARN-mode, advisory — Epic #693 FA4 #697).
801
+ // For each .claude/rules/*.md with frontmatter `auto-generated: true`, extract
802
+ // `learning-key` and verify it matches a non-expired entry in
803
+ // `.orchestrator/metrics/learnings.jsonl`.
804
+ //
805
+ // Key derivation mirrors emitter.mjs `toActivationMetadata`:
806
+ // learningKey = `${type}/${kebab(title || subject || '')}`
807
+ // kebab(s) = s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
808
+ //
809
+ // WARN (never error) when:
810
+ // - learnings.jsonl is present AND no entry's derived key matches the rule's
811
+ // `learning-key` (absent learning), OR
812
+ // - the matching entry's `expires_at` < now (expired learning).
813
+ // When learnings.jsonl is absent, every key counts as absent — warn on each
814
+ // generated rule's key.
815
+ // The check is silently skipped (no id pushed) when .claude/rules/ is absent
816
+ // or contains no .md files with auto-generated: true.
817
+ if (!args.skipGeneratedRuleStaleness) {
818
+ (function runGeneratedRuleStaleness() {
819
+ const rulesDir = join(vaultDir, '.claude', 'rules');
820
+ if (!existsSync(rulesDir) || !statSync(rulesDir).isDirectory()) return;
821
+
822
+ // Minimal inline frontmatter extractor for auto-generated + learning-key.
823
+ // Reads the opening --- ... --- block from a markdown file.
824
+ function extractFrontmatterFields(mdContent) {
825
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(stripLeadingRuleHeaderLines(mdContent));
826
+ if (!m) return { autoGenerated: false, learningKey: null, expiresAt: null };
827
+ const block = m[1];
828
+ const autoGenM = /^auto-generated:\s*(.+)$/m.exec(block);
829
+ const learningKeyM = /^learning-key:\s*(.+)$/m.exec(block);
830
+ const expiresAtM = /^expires-at:\s*(.+)$/m.exec(block);
831
+ return {
832
+ autoGenerated: autoGenM ? autoGenM[1].trim() === 'true' : false,
833
+ learningKey: learningKeyM ? learningKeyM[1].trim() : null,
834
+ expiresAt: expiresAtM ? expiresAtM[1].trim() : null,
835
+ };
836
+ }
837
+
838
+ // Collect all .md files in .claude/rules/ with auto-generated: true.
839
+ const ruleFiles = readdirSync(rulesDir)
840
+ .filter((f) => f.endsWith('.md') && !f.startsWith('.'))
841
+ .map((f) => join(rulesDir, f))
842
+ .filter((absPath) => statSync(absPath).isFile());
843
+
844
+ const generatedRules = [];
845
+ for (const absPath of ruleFiles) {
846
+ let content;
847
+ try { content = readFileSync(absPath, 'utf8'); } catch { continue; }
848
+ const fields = extractFrontmatterFields(content);
849
+ if (!fields.autoGenerated) continue;
850
+ generatedRules.push({
851
+ absPath,
852
+ relPath: relative(vaultDir, absPath),
853
+ learningKey: fields.learningKey,
854
+ expiresAt: fields.expiresAt,
855
+ });
856
+ }
857
+
858
+ // No generated rules found → silently skip (don't push the check id).
859
+ if (generatedRules.length === 0) return;
860
+
861
+ // Slugify function mirroring emitter.mjs `kebab()`.
862
+ const kebab = (s) =>
863
+ String(s)
864
+ .toLowerCase()
865
+ .replace(/[^a-z0-9]+/g, '-')
866
+ .replace(/^-+|-+$/g, '');
867
+
868
+ // Build the set of known learning keys from learnings.jsonl.
869
+ // key → expires_at (ISO string or null)
870
+ const knownKeys = new Map();
871
+ const learningsPath = join(vaultDir, '.orchestrator', 'metrics', 'learnings.jsonl');
872
+ const learningsPresent = existsSync(learningsPath) && statSync(learningsPath).isFile();
873
+
874
+ if (learningsPresent) {
875
+ let raw;
876
+ try { raw = readFileSync(learningsPath, 'utf8'); } catch { /* leave knownKeys empty */ }
877
+ if (raw) {
878
+ for (const line of raw.split('\n')) {
879
+ const trimmed = line.trim();
880
+ if (!trimmed) continue;
881
+ let entry;
882
+ try { entry = JSON.parse(trimmed); } catch { continue; }
883
+ if (!entry || typeof entry !== 'object') continue;
884
+ const type = typeof entry.type === 'string' ? entry.type : '';
885
+ const subjectOrTitle =
886
+ (typeof entry.title === 'string' && entry.title !== '' ? entry.title : '') ||
887
+ (typeof entry.subject === 'string' && entry.subject !== '' ? entry.subject : '');
888
+ if (!type || !subjectOrTitle) continue;
889
+ const derivedKey = `${type}/${kebab(subjectOrTitle)}`;
890
+ // Store the most-recent expires_at (later entries overwrite earlier ones
891
+ // with the same key — safe; duplicates are rare and same-key learnings
892
+ // share the same expiry semantics).
893
+ knownKeys.set(derivedKey, entry.expires_at ?? null);
894
+ }
895
+ }
896
+ }
897
+
898
+ const nowMs = Date.now();
899
+
900
+ for (const rule of generatedRules) {
901
+ const { relPath, learningKey, expiresAt } = rule;
902
+
903
+ // If we cannot evaluate the key (no learning-key frontmatter), skip
904
+ // silently — avoid false positives on malformed rules.
905
+ if (!learningKey) continue;
906
+
907
+ if (!learningsPresent || !knownKeys.has(learningKey)) {
908
+ // Learning is absent from learnings.jsonl (or the file is missing entirely).
909
+ warnings.push({
910
+ check: 'generated-rule-staleness',
911
+ file: relPath,
912
+ line: 1,
913
+ message: `Auto-generated rule references learning-key '${learningKey}' which is absent from .orchestrator/metrics/learnings.jsonl`,
914
+ extracted: learningKey,
915
+ });
916
+ continue;
917
+ }
918
+
919
+ // Learning exists — check expiry.
920
+ const storedExpiresAt = knownKeys.get(learningKey);
921
+ // Prefer the frontmatter expires-at on the rule file; fall back to the
922
+ // stored expires_at from the learning entry (both should agree, but the
923
+ // rule file is authoritative for its own expiry).
924
+ const expiryStr = expiresAt ?? storedExpiresAt;
925
+ if (expiryStr) {
926
+ const expiryMs = Date.parse(expiryStr);
927
+ if (Number.isFinite(expiryMs) && expiryMs < nowMs) {
928
+ warnings.push({
929
+ check: 'generated-rule-staleness',
930
+ file: relPath,
931
+ line: 1,
932
+ message: `Auto-generated rule's learning '${learningKey}' expired on ${expiryStr} (rule should be reviewed or removed)`,
933
+ extracted: learningKey,
934
+ });
935
+ }
936
+ }
937
+ }
938
+
939
+ // Push the check id only when ≥1 generated rule was scanned.
940
+ checksRun.push('generated-rule-staleness');
941
+ })();
942
+ } else {
943
+ checksSkipped.push('generated-rule-staleness: explicitly skipped');
944
+ }
945
+
946
+ // Check 9: rule-scoping — validates .claude/rules/*.md frontmatter against
947
+ // the rule-loader.mjs contract (see the doc-comment above the helper
948
+ // functions for the five probes, incl. unreadable-file). Silently skipped (no id pushed, no
949
+ // checksSkipped entry) when .claude/rules/ is absent — mirrors Check 8's
950
+ // silent-skip semantics. `--skip-rule-scoping` disables the whole check.
951
+ if (!args.skipRuleScoping) {
952
+ (function runRuleScoping() {
953
+ const rulesDir = join(vaultDir, '.claude', 'rules');
954
+ if (!existsSync(rulesDir) || !statSync(rulesDir).isDirectory()) return;
955
+
956
+ checksRun.push('rule-scoping');
957
+
958
+ const ruleFileNames = readdirSync(rulesDir)
959
+ .filter((f) => f.endsWith('.md') && !f.startsWith('.'))
960
+ .filter((f) => statSync(join(rulesDir, f)).isFile());
961
+ const ruleFileSet = new Set(ruleFileNames);
962
+
963
+ // Lazily computed — only needed once any rule declares ≥1 glob pattern.
964
+ let trackedFiles = null;
965
+
966
+ for (const fname of ruleFileNames) {
967
+ const absPath = join(rulesDir, fname);
968
+ const relPath = relative(vaultDir, absPath);
969
+ let content;
970
+ try {
971
+ content = readFileSync(absPath, 'utf8');
972
+ } catch (err) {
973
+ warnings.push({
974
+ check: 'rule-scoping', file: relPath, line: 1,
975
+ message: `rule file unreadable — skipped from rule-scoping analysis (${err.code || err.message})`,
976
+ extracted: fname,
977
+ });
978
+ continue;
979
+ }
980
+
981
+ // --- Probe 1: paths-presence → errors[] ---
982
+ const fmBody = extractFrontmatterBlockBody(content);
983
+ if (fmBody && /^paths:/m.test(fmBody)) {
984
+ errors.push({
985
+ check: 'rule-scoping', file: relPath, line: 1,
986
+ message: `Rule frontmatter declares 'paths:' which rule-loader.mjs does not recognise — the rule silently loads ALWAYS-ON regardless of file scope. Migrate to 'globs:'.`,
987
+ extracted: 'paths:',
988
+ });
989
+ }
990
+
991
+ // --- Probes 3 & 4: zero-match-globs / foreign-glob → warnings[] ---
992
+ let parsed;
993
+ try { parsed = parseGlobsFrontmatter(content); } catch { parsed = { globs: null, meta: {} }; }
994
+ const globs = parsed.globs;
995
+ if (Array.isArray(globs) && globs.length > 0) {
996
+ if (trackedFiles === null) trackedFiles = listTrackedFiles(vaultDir);
997
+ for (const pattern of globs) {
998
+ if (!globMatchesAny(pattern, trackedFiles)) {
999
+ warnings.push({
1000
+ check: 'rule-scoping', file: relPath, line: 1,
1001
+ message: `glob '${pattern}' matches 0 tracked files (library/exemplar repos may legitimately carry dead stack rules)`,
1002
+ extracted: pattern,
1003
+ });
1004
+ }
1005
+ if (FOREIGN_GLOB_TOKEN_RE.test(pattern)) {
1006
+ warnings.push({
1007
+ check: 'rule-scoping', file: relPath, line: 1,
1008
+ message: `glob '${pattern}' contains a PascalCase product-like token — verify this rule was not copy-pasted from another project`,
1009
+ extracted: pattern,
1010
+ });
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ // --- Probe 2b: cited-but-missing (this rule's own "## See Also" footer) → errors[] ---
1016
+ const lines = content.split('\n');
1017
+ let seeAlsoStart = -1;
1018
+ for (let i = 0; i < lines.length; i++) {
1019
+ if (/^##\s+See Also\s*$/i.test(lines[i])) { seeAlsoStart = i + 1; break; }
1020
+ }
1021
+ if (seeAlsoStart !== -1) {
1022
+ let seeAlsoEnd = lines.length;
1023
+ for (let i = seeAlsoStart; i < lines.length; i++) {
1024
+ if (/^#{1,6}\s+/.test(lines[i])) { seeAlsoEnd = i; break; }
1025
+ }
1026
+ const bodyLines = lines.slice(seeAlsoStart, seeAlsoEnd);
1027
+ for (const { token, lineOffset } of extractSeeAlsoTokens(bodyLines)) {
1028
+ if (!ruleFileSet.has(token)) {
1029
+ errors.push({
1030
+ check: 'rule-scoping', file: relPath, line: seeAlsoStart + lineOffset + 1,
1031
+ message: `'## See Also' cites '${token}' which does not exist in .claude/rules/`,
1032
+ extracted: token,
1033
+ });
1034
+ }
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ // --- Probe 2a: cited-but-missing (CLAUDE.md / AGENTS.md citations) → errors[] ---
1040
+ for (const instrName of ['CLAUDE.md', 'AGENTS.md']) {
1041
+ const filePath = join(vaultDir, instrName);
1042
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) continue;
1043
+ const fcontent = readFileSync(filePath, 'utf8');
1044
+ const flines = fcontent.split('\n');
1045
+ const citeRe = /\.claude\/rules\/([A-Za-z0-9_-]+\.md)\b/g;
1046
+ for (let i = 0; i < flines.length; i++) {
1047
+ citeRe.lastIndex = 0;
1048
+ let m;
1049
+ while ((m = citeRe.exec(flines[i])) !== null) {
1050
+ const cited = m[1];
1051
+ if (!ruleFileSet.has(cited)) {
1052
+ errors.push({
1053
+ check: 'rule-scoping', file: instrName, line: i + 1,
1054
+ message: `References .claude/rules/${cited} which does not exist on disk`,
1055
+ extracted: cited,
1056
+ });
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ })();
1062
+ } else {
1063
+ checksSkipped.push('rule-scoping: explicitly skipped');
1064
+ }
1065
+
1066
+ // Check 10: docs-parity (issue #780) — three sub-checks over the public docs
1067
+ // surface, silently skipped (no checksRun push, no checksSkipped entry) when
1068
+ // docs/components.md is absent (mirrors Check 8/9's silent-skip semantics):
1069
+ // (a) count-claims — docs/components.md's own heading counts ("## Skills
1070
+ // (N user-facing)", "## Commands (N)", "## Agents (N typed
1071
+ // sub-agents)", "## Hook event types (N)") vs the SAME actual on-disk
1072
+ // derivation the surface-count family (Check 5) uses (countSkills /
1073
+ // the commands-dir listing / countAgents / readHookCounts). This does
1074
+ // NOT reuse the surface-count family's `claimRe` regexes — those are
1075
+ // tuned for CLAUDE.md/README prose phrasing ("40 skills", "8 distinct
1076
+ // events") and verifiably do not match components.md's own heading
1077
+ // convention (count precedes/follows the noun differently); reusing
1078
+ // them verbatim would silently never fire. New regexes tailored to
1079
+ // the doc's actual authored structure are used instead.
1080
+ // (b) config-block-parity — top-level Session Config keys documented in
1081
+ // docs/session-config-template.md (opt-in baseline, via the Part 1
1082
+ // 'last'-occurrence extractor) vs mentioned anywhere in
1083
+ // docs/session-config-reference.md (`extractDocKeyMentions` — yaml
1084
+ // fences, headings, and markdown-table first-cell keys; see its
1085
+ // doc-comment for why a naive fence-only extraction under-counts).
1086
+ // (c) metrics-path-liveness — stale `.claude/metrics/` path references
1087
+ // (canonical path is `.orchestrator/metrics/`) in root `docs/*.md` or
1088
+ // `docs/examples/*.md`.
1089
+ // `--skip-docs-parity` disables the whole check.
1090
+ if (!args.skipDocsParity) {
1091
+ (function runDocsParity() {
1092
+ const componentsPath = join(vaultDir, 'docs', 'components.md');
1093
+ if (!existsSync(componentsPath) || !statSync(componentsPath).isFile()) return; // silent skip
1094
+
1095
+ checksRun.push('docs-parity');
1096
+ const componentsRel = relative(vaultDir, componentsPath);
1097
+ const componentsContent = readFileSync(componentsPath, 'utf8');
1098
+ const componentsLines = componentsContent.split('\n');
1099
+
1100
+ // --- Sub-check (a): count-claims ---
1101
+ const docsParitySurfaces = [
1102
+ {
1103
+ noun: 'skills',
1104
+ actual: countSkills(vaultDir),
1105
+ re: /^##\s+Skills\s+\((\d+)\s+user-facing\)/i,
1106
+ },
1107
+ {
1108
+ noun: 'commands',
1109
+ actual: (() => {
1110
+ const dir = commandsDir || join(vaultDir, 'commands');
1111
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return null;
1112
+ return readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('.')).length;
1113
+ })(),
1114
+ re: /^##\s+Commands\s+\((\d+)\)/i,
1115
+ },
1116
+ {
1117
+ noun: 'agents',
1118
+ actual: countAgents(vaultDir),
1119
+ re: /^##\s+Agents\s+\((\d+)\s+typed\s+sub-agents\)/i,
1120
+ },
1121
+ {
1122
+ noun: 'distinct hook events',
1123
+ actual: (() => {
1124
+ const hc = readHookCounts(vaultDir);
1125
+ return hc ? hc.events : null;
1126
+ })(),
1127
+ re: /^##\s+Hook\s+event\s+types\s+\((\d+)\)/i,
1128
+ },
1129
+ ];
1130
+ for (let i = 0; i < componentsLines.length; i++) {
1131
+ const line = componentsLines[i];
1132
+ for (const surface of docsParitySurfaces) {
1133
+ if (surface.actual === null) continue; // artifact absent — nothing to compare
1134
+ const m = surface.re.exec(line);
1135
+ if (m) {
1136
+ const claimed = parseInt(m[1], 10);
1137
+ if (claimed !== surface.actual) {
1138
+ errors.push({
1139
+ check: 'docs-parity', file: componentsRel, line: i + 1,
1140
+ message: `docs/components.md claims ${claimed} ${surface.noun} but actual on-disk count is ${surface.actual}`,
1141
+ extracted: m[0],
1142
+ });
1143
+ }
1144
+ }
1145
+ }
1146
+ }
1147
+
1148
+ // --- Sub-check (b): config-block-parity ---
1149
+ const templatePathForDocsParity = join(vaultDir, 'docs', 'session-config-template.md');
1150
+ const referencePath = join(vaultDir, 'docs', 'session-config-reference.md');
1151
+ if (
1152
+ existsSync(templatePathForDocsParity) && statSync(templatePathForDocsParity).isFile() &&
1153
+ existsSync(referencePath) && statSync(referencePath).isFile()
1154
+ ) {
1155
+ const templateContentForDocsParity = readFileSync(templatePathForDocsParity, 'utf8');
1156
+ const referenceContent = readFileSync(referencePath, 'utf8');
1157
+ const tplBlockForDocsParity = extractSessionConfigBlock(templateContentForDocsParity, { occurrence: 'last' });
1158
+ if (tplBlockForDocsParity) {
1159
+ const tplKeysForDocsParity = extractTopLevelKeys(tplBlockForDocsParity.body);
1160
+ const refKeys = extractDocKeyMentions(referenceContent);
1161
+ const referenceRel = relative(vaultDir, referencePath);
1162
+ for (const key of tplKeysForDocsParity) {
1163
+ if (!refKeys.has(key)) {
1164
+ errors.push({
1165
+ check: 'docs-parity', file: referenceRel, line: 1,
1166
+ message: `Session Config key '${key}' (documented in docs/session-config-template.md) is not documented in docs/session-config-reference.md`,
1167
+ extracted: key,
1168
+ });
1169
+ }
1170
+ }
1171
+ }
1172
+ }
1173
+
1174
+ // --- Sub-check (c): metrics-path-liveness ---
1175
+ const docsDir = join(vaultDir, 'docs');
1176
+ const metricsScanTargets = [];
1177
+ if (existsSync(docsDir) && statSync(docsDir).isDirectory()) {
1178
+ for (const f of readdirSync(docsDir, { withFileTypes: true })) {
1179
+ if (f.isFile() && f.name.endsWith('.md')) metricsScanTargets.push(join(docsDir, f.name));
1180
+ }
1181
+ const examplesDir = join(docsDir, 'examples');
1182
+ if (existsSync(examplesDir) && statSync(examplesDir).isDirectory()) {
1183
+ for (const f of readdirSync(examplesDir, { withFileTypes: true })) {
1184
+ if (f.isFile() && f.name.endsWith('.md')) metricsScanTargets.push(join(examplesDir, f.name));
1185
+ }
1186
+ }
1187
+ }
1188
+ for (const abs of metricsScanTargets) {
1189
+ const rel = relative(vaultDir, abs);
1190
+ let content;
1191
+ try { content = readFileSync(abs, 'utf8'); } catch { continue; }
1192
+ const metricsLines = content.split('\n');
1193
+ for (let i = 0; i < metricsLines.length; i++) {
1194
+ if (metricsLines[i].includes('.claude/metrics/')) {
1195
+ errors.push({
1196
+ check: 'docs-parity', file: rel, line: i + 1,
1197
+ message: `Stale metrics path '.claude/metrics/' referenced — canonical path is '.orchestrator/metrics/'`,
1198
+ extracted: '.claude/metrics/',
1199
+ });
1200
+ }
1201
+ }
1202
+ }
1203
+ })();
1204
+ } else {
1205
+ checksSkipped.push('docs-parity: explicitly skipped');
1206
+ }
1207
+
1208
+ if (scopeFiles.length === 0) {
1209
+ process.stdout.write(JSON.stringify({
1210
+ status: 'skipped', mode: args.mode, vault_dir: vaultDir,
1211
+ resolved_path: resolvedPath, resolved_kind: resolvedKind,
1212
+ files_scanned: 0, checks_run: checksRun, checks_skipped: checksSkipped,
1213
+ errors, warnings, reason: 'no scope files matched',
1214
+ }) + '\n');
1215
+ process.exit(errors.length > 0 && args.mode === 'hard' ? 1 : 0);
1216
+ }
1217
+
1218
+ for (const abs of scopeFiles) {
1219
+ const rel = relative(vaultDir, abs);
1220
+ const content = readFileSync(abs, 'utf8');
1221
+ const lines = content.split('\n');
1222
+
1223
+ let inFence = false;
1224
+ let currentSection = null;
1225
+ let currentSectionType = null;
1226
+
1227
+ for (let i = 0; i < lines.length; i++) {
1228
+ const line = lines[i];
1229
+ const lineNum = i + 1;
1230
+
1231
+ if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
1232
+
1233
+ const hmatch = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
1234
+ if (hmatch && !inFence) {
1235
+ currentSection = hmatch[2];
1236
+ currentSectionType = classifySection(currentSection);
1237
+ continue;
1238
+ }
1239
+
1240
+ if (!args.skipPathResolver && !inFence) {
1241
+ const pathRegex = /\/Users\/[A-Za-z0-9._/-]+/g;
1242
+ let m;
1243
+ while ((m = pathRegex.exec(line)) !== null) {
1244
+ const p = m[0].replace(/[.,;:)\]`"']+$/, '');
1245
+ if (!existsSync(p)) {
1246
+ errors.push({
1247
+ check: 'path-resolver', file: rel, line: lineNum,
1248
+ message: `Absolute path does not exist: ${p}`,
1249
+ extracted: p,
1250
+ });
1251
+ }
1252
+ }
1253
+ }
1254
+
1255
+ if (!args.skipProjectCount && actualProjectCount !== null) {
1256
+ const countRegex = /\((\d+)\s+(registered|projects?|subfolders?)\)/gi;
1257
+ let m;
1258
+ while ((m = countRegex.exec(line)) !== null) {
1259
+ const claimed = parseInt(m[1], 10);
1260
+ if (claimed !== actualProjectCount) {
1261
+ errors.push({
1262
+ check: 'project-count-sync', file: rel, line: lineNum,
1263
+ message: `Hardcoded count claims ${claimed} ${m[2]} but actual 01-projects/ count is ${actualProjectCount}`,
1264
+ extracted: m[0],
1265
+ });
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ if (runIssueCheck && currentSectionType === 'forward' && !inFence) {
1271
+ const issueRegex = /#(\d+)\b/g;
1272
+ let m;
1273
+ while ((m = issueRegex.exec(line)) !== null) {
1274
+ const iid = m[1];
1275
+ const state = lookupIssueState(iid, repo, issueCache);
1276
+ if (state === 'closed') {
1277
+ errors.push({
1278
+ check: 'issue-reference-freshness', file: rel, line: lineNum,
1279
+ message: `Issue #${iid} is closed but referenced in forward-looking section "${currentSection}"`,
1280
+ extracted: `#${iid}`,
1281
+ });
1282
+ } else if (state === 'unknown') {
1283
+ warnings.push({
1284
+ check: 'issue-reference-freshness', file: rel, line: lineNum,
1285
+ message: `Could not resolve state of issue #${iid} via glab (may not exist or auth issue)`,
1286
+ extracted: `#${iid}`,
1287
+ });
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ if (!args.skipSessionFiles) {
1293
+ // Dual-read: tolerate both flat legacy `50-sessions/<id>.md` AND
1294
+ // per-repo namespaced `50-sessions/<repo>/<id>.md` (Issue #660).
1295
+ const sessionRegex = /50-sessions\/(?:([a-z0-9][a-z0-9-]*)\/)?(\d{4}-\d{2}-\d{2}-[a-z0-9-]+)\.md/g;
1296
+ let m;
1297
+ while ((m = sessionRegex.exec(line)) !== null) {
1298
+ const repo = m[1]; // optional repo segment (undefined if flat reference)
1299
+ const id = m[2]; // session date-slug
1300
+ let found = false;
1301
+ if (repo !== undefined) {
1302
+ // Namespaced reference: only check the specific namespaced path.
1303
+ found = existsSync(join(vaultDir, '50-sessions', repo, `${id}.md`));
1304
+ } else {
1305
+ // Flat reference: accept if the flat path exists OR if ANY per-repo
1306
+ // subfolder contains a file with the same id (migration tolerance).
1307
+ const flatPath = join(vaultDir, '50-sessions', `${id}.md`);
1308
+ if (existsSync(flatPath)) {
1309
+ found = true;
1310
+ } else {
1311
+ // Scan one level of subdirectories under 50-sessions/ for <id>.md.
1312
+ const sessionsDir = join(vaultDir, '50-sessions');
1313
+ try {
1314
+ const subdirs = readdirSync(sessionsDir, { withFileTypes: true })
1315
+ .filter((e) => e.isDirectory())
1316
+ .map((e) => e.name);
1317
+ found = subdirs.some((sub) =>
1318
+ existsSync(join(sessionsDir, sub, `${id}.md`))
1319
+ );
1320
+ } catch {
1321
+ // sessionsDir does not exist or is unreadable — found stays false.
1322
+ }
1323
+ }
1324
+ }
1325
+ if (!found) {
1326
+ errors.push({
1327
+ check: 'session-file-existence', file: rel, line: lineNum,
1328
+ message: `Referenced session file does not exist: ${m[0]}`,
1329
+ extracted: m[0],
1330
+ });
1331
+ }
1332
+ }
1333
+ }
1334
+
1335
+ // Surface-count family: one EXACT-count drift check per active surface.
1336
+ // A surface only fires when the doc makes an explicit numeric claim that
1337
+ // its regex matches; an unclaimed surface produces no errors (skip).
1338
+ for (const surface of activeSurfaces) {
1339
+ const re = surface.claimRe;
1340
+ re.lastIndex = 0; // reset shared /g/ regex before each line
1341
+ let m;
1342
+ while ((m = re.exec(line)) !== null) {
1343
+ const claimed = parseInt(m[1], 10);
1344
+ if (claimed !== surface.actual) {
1345
+ const err = {
1346
+ check: surface.id, file: rel, line: lineNum,
1347
+ message: `Narrative claims ${claimed} ${surface.noun} but actual on-disk count is ${surface.actual}`,
1348
+ extracted: m[0],
1349
+ count: { surface: surface.id, actual: surface.actual, claimed },
1350
+ };
1351
+ // Back-compat: the original command-count error also carried a
1352
+ // top-level `command_count` field; preserve it.
1353
+ if (surface.id === 'command-count') {
1354
+ err.command_count = { actual: surface.actual, claimed };
1355
+ }
1356
+ errors.push(err);
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ const status = errors.length === 0 ? 'ok' : 'invalid';
1364
+ const result = {
1365
+ status, mode: args.mode, vault_dir: vaultDir,
1366
+ resolved_path: resolvedPath, resolved_kind: resolvedKind,
1367
+ files_scanned: scopeFiles.length,
1368
+ checks_run: checksRun,
1369
+ checks_skipped: checksSkipped,
1370
+ errors, warnings,
1371
+ };
1372
+ if (actualCommandCount !== null) {
1373
+ result.command_count = { actual: actualCommandCount };
1374
+ }
1375
+ process.stdout.write(JSON.stringify(result) + '\n');
1376
+
1377
+ process.exit(errors.length > 0 && args.mode === 'hard' ? 1 : 0);
1378
+ }
1379
+
1380
+ main();