session-orchestrator 3.23.0 → 4.0.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 (393) hide show
  1. package/.agents/skills/architecture/SKILL.md +18 -0
  2. package/.agents/skills/autopilot/SKILL.md +17 -0
  3. package/.agents/skills/bootstrap/SKILL.md +20 -0
  4. package/.agents/skills/brainstorm/SKILL.md +22 -0
  5. package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
  6. package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
  7. package/.agents/skills/debug/SKILL.md +22 -0
  8. package/.agents/skills/discovery/SKILL.md +20 -0
  9. package/.agents/skills/dispatcher/SKILL.md +15 -0
  10. package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
  11. package/.agents/skills/ecosystem-health/SKILL.md +20 -0
  12. package/.agents/skills/eli5/SKILL.md +20 -0
  13. package/.agents/skills/eval/SKILL.md +21 -0
  14. package/.agents/skills/evolve/SKILL.md +21 -0
  15. package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
  16. package/.agents/skills/gitlab-ops/SKILL.md +20 -0
  17. package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
  18. package/.agents/skills/grill/SKILL.md +22 -0
  19. package/.agents/skills/hook-development/SKILL.md +15 -0
  20. package/.agents/skills/mcp-builder/SKILL.md +15 -0
  21. package/.agents/skills/memory-cleanup/SKILL.md +21 -0
  22. package/.agents/skills/mode-selector/SKILL.md +17 -0
  23. package/.agents/skills/npm-publish/SKILL.md +16 -0
  24. package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
  25. package/.agents/skills/persona-panel/SKILL.md +17 -0
  26. package/.agents/skills/plan/SKILL.md +20 -0
  27. package/.agents/skills/playwright-driver/SKILL.md +20 -0
  28. package/.agents/skills/quality-gates/SKILL.md +20 -0
  29. package/.agents/skills/reconcile/SKILL.md +21 -0
  30. package/.agents/skills/remote-offload/SKILL.md +20 -0
  31. package/.agents/skills/repo-audit/SKILL.md +16 -0
  32. package/.agents/skills/session-end/SKILL.md +20 -0
  33. package/.agents/skills/session-plan/SKILL.md +20 -0
  34. package/.agents/skills/session-start/SKILL.md +20 -0
  35. package/.agents/skills/spinout/SKILL.md +16 -0
  36. package/.agents/skills/sunset-review/SKILL.md +16 -0
  37. package/.agents/skills/test-runner/SKILL.md +20 -0
  38. package/.agents/skills/tmux-layout/SKILL.md +21 -0
  39. package/.agents/skills/using-orchestrator/SKILL.md +17 -0
  40. package/.agents/skills/vault-mirror/SKILL.md +15 -0
  41. package/.agents/skills/vault-sync/SKILL.md +15 -0
  42. package/.agents/skills/wave-executor/SKILL.md +20 -0
  43. package/.agents/skills/write-executable-plan/SKILL.md +22 -0
  44. package/.claude-plugin/marketplace.json +1 -1
  45. package/.claude-plugin/plugin.json +1 -1
  46. package/.codex-plugin/plugin.json +1 -1
  47. package/.cursor/commands/autopilot.md +2 -2
  48. package/.cursor/commands/bootstrap.md +1 -1
  49. package/.cursor/commands/brainstorm.md +1 -1
  50. package/.cursor/commands/debug.md +1 -1
  51. package/.cursor/commands/discovery.md +1 -1
  52. package/.cursor/commands/dispatcher.md +2 -2
  53. package/.cursor/commands/eli5.md +2 -2
  54. package/.cursor/commands/eval.md +2 -2
  55. package/.cursor/commands/evolve.md +1 -1
  56. package/.cursor/commands/go.md +1 -1
  57. package/.cursor/commands/grill.md +2 -2
  58. package/.cursor/commands/memory-cleanup.md +2 -2
  59. package/.cursor/commands/persona-panel.md +1 -1
  60. package/.cursor/commands/plan.md +1 -1
  61. package/.cursor/commands/portfolio.md +1 -1
  62. package/.cursor/commands/reconcile.md +2 -2
  63. package/.cursor/commands/release.md +2 -2
  64. package/.cursor/commands/session.md +2 -2
  65. package/.cursor/commands/spinout.md +2 -2
  66. package/.cursor/commands/sunset-review.md +2 -2
  67. package/.cursor/commands/templates-ack.md +2 -2
  68. package/.cursor/commands/test.md +2 -2
  69. package/.cursor/skills/brainstorm/SKILL.md +1 -1
  70. package/.cursor/skills/eval/SKILL.md +1 -1
  71. package/.cursor/skills/quality-gates/SKILL.md +1 -1
  72. package/.cursor/skills/remote-offload/SKILL.md +13 -0
  73. package/.orchestrator/policy/blocked-commands.json +121 -0
  74. package/.orchestrator/policy/ecosystem.schema.json +66 -0
  75. package/.orchestrator/policy/quality-gates.example.json +16 -0
  76. package/.orchestrator/policy/quality-gates.schema.json +38 -0
  77. package/.orchestrator/policy/templates-policy.json +27 -0
  78. package/.orchestrator/policy/test-profiles.json +47 -0
  79. package/AGENTS.md +225 -0
  80. package/CHANGELOG.md +1401 -0
  81. package/NOTICE +11 -6
  82. package/README.md +127 -92
  83. package/agents/db-specialist.md +0 -1
  84. package/agents/eval-judge.md +1 -1
  85. package/agents/skill-applied-judge.md +1 -1
  86. package/assets/wave-lifecycle.svg +98 -0
  87. package/commands/release.md +6 -3
  88. package/commands/session.md +18 -3
  89. package/docs/README.md +4 -0
  90. package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
  91. package/docs/baseline.md +67 -0
  92. package/docs/ci-setup.md +249 -48
  93. package/docs/codex-setup.md +66 -22
  94. package/docs/components.md +37 -16
  95. package/docs/cursor-setup.md +6 -2
  96. package/docs/events-schema.md +51 -10
  97. package/docs/instruction-delivery.md +62 -0
  98. package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
  99. package/docs/migration-v4.md +341 -0
  100. package/docs/pi-setup.md +6 -1
  101. package/docs/plugin-architecture-v3.md +1 -1
  102. package/docs/rule-authoring.md +85 -19
  103. package/docs/scope-collision-guard.md +8 -8
  104. package/docs/session-config-reference.md +120 -61
  105. package/docs/session-config-template.md +40 -33
  106. package/docs/telemetry/telemetry-claims.md +11 -10
  107. package/docs/telemetry.md +187 -4
  108. package/docs/vault-docs-architecture.md +50 -11
  109. package/hooks/_lib/atomic-json.mjs +111 -0
  110. package/hooks/_lib/hook-import-set.json +1487 -0
  111. package/hooks/_lib/subagent-paths.mjs +143 -0
  112. package/hooks/_lib/subagent-transcript.mjs +562 -0
  113. package/hooks/config-protection.mjs +2 -2
  114. package/hooks/cwd-change-restore.mjs +11 -31
  115. package/hooks/enforce-commands.mjs +69 -0
  116. package/hooks/enforce-scope.mjs +35 -6
  117. package/hooks/hooks-codex.json +1 -1
  118. package/hooks/hooks-cursor.json +10 -0
  119. package/hooks/hooks-pi.json +5 -0
  120. package/hooks/hooks.json +6 -1
  121. package/hooks/loop-guard.mjs +3 -3
  122. package/hooks/on-session-end.mjs +280 -14
  123. package/hooks/on-session-start.mjs +153 -4
  124. package/hooks/on-stop.mjs +371 -17
  125. package/hooks/operator-steer.mjs +2 -2
  126. package/hooks/post-bash-write-verify.mjs +189 -4
  127. package/hooks/post-edit-import-probe.mjs +344 -0
  128. package/hooks/post-subagent-discovery-validator.mjs +278 -392
  129. package/hooks/post-tool-batch-wave-signal.mjs +272 -44
  130. package/hooks/post-tool-failure-corrective-context.mjs +11 -34
  131. package/hooks/post-tooluse-frontend-slop.mjs +3 -3
  132. package/hooks/pre-bash-destructive-guard.mjs +39 -13
  133. package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
  134. package/hooks/skill-invocation-telemetry.mjs +17 -5
  135. package/hooks/subagent-telemetry.mjs +24 -30
  136. package/monitors/monitors.json +3 -3
  137. package/package.json +9 -1
  138. package/pi/prompts/session.md +2 -2
  139. package/plugin.json +27 -0
  140. package/scripts/autopilot.mjs +26 -12
  141. package/scripts/backfill-abandoned-sessions.mjs +130 -15
  142. package/scripts/backfill-learnings-from-vault.mjs +9 -3
  143. package/scripts/dialectic-deriver.mjs +73 -8
  144. package/scripts/emit-event.mjs +10 -2
  145. package/scripts/export-hw-learnings.mjs +113 -1
  146. package/scripts/generate-agents-skills.mjs +378 -0
  147. package/scripts/generate-cursor-adapter.mjs +45 -8
  148. package/scripts/generate-hook-import-set.mjs +249 -0
  149. package/scripts/lib/agent-status.mjs +13 -2
  150. package/scripts/lib/auq/parse.mjs +5 -29
  151. package/scripts/lib/auto-dialectic.mjs +68 -0
  152. package/scripts/lib/auto-dream.mjs +38 -36
  153. package/scripts/lib/autonomy/suitability.mjs +6 -0
  154. package/scripts/lib/autopilot/loop.mjs +2 -2
  155. package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
  156. package/scripts/lib/build-live-signals.mjs +25 -22
  157. package/scripts/lib/ci-status-banner.mjs +220 -75
  158. package/scripts/lib/codex/plugin-contract.mjs +82 -6
  159. package/scripts/lib/cold-start-detector.mjs +23 -14
  160. package/scripts/lib/config/auto-dream.mjs +2 -1
  161. package/scripts/lib/config/block-header.mjs +63 -0
  162. package/scripts/lib/config/block-preprocess.mjs +177 -0
  163. package/scripts/lib/config/broken-window.mjs +2 -1
  164. package/scripts/lib/config/cold-start.mjs +2 -1
  165. package/scripts/lib/config/config-protection.mjs +22 -2
  166. package/scripts/lib/config/context-coverage.mjs +2 -1
  167. package/scripts/lib/config/cross-repo.mjs +2 -1
  168. package/scripts/lib/config/custom-phases.mjs +2 -1
  169. package/scripts/lib/config/dialectic.mjs +2 -1
  170. package/scripts/lib/config/discovery-validator.mjs +9 -3
  171. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
  172. package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
  173. package/scripts/lib/config/docs-orchestrator.mjs +2 -1
  174. package/scripts/lib/config/docs-staleness.mjs +2 -1
  175. package/scripts/lib/config/drift-check.mjs +2 -1
  176. package/scripts/lib/config/eval.mjs +2 -1
  177. package/scripts/lib/config/events-rotation.mjs +2 -1
  178. package/scripts/lib/config/evolve.mjs +8 -2
  179. package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
  180. package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
  181. package/scripts/lib/config/handover-gate.mjs +2 -1
  182. package/scripts/lib/config/health-endpoints.mjs +388 -0
  183. package/scripts/lib/config/issue-budget.mjs +2 -1
  184. package/scripts/lib/config/loop-guard.mjs +2 -1
  185. package/scripts/lib/config/memory.mjs +2 -1
  186. package/scripts/lib/config/moc-staleness.mjs +2 -1
  187. package/scripts/lib/config/persona-gate-wave.mjs +2 -1
  188. package/scripts/lib/config/private-config-dir.mjs +67 -0
  189. package/scripts/lib/config/reconcile.mjs +2 -1
  190. package/scripts/lib/config/remote-hosts.mjs +234 -0
  191. package/scripts/lib/config/section-extractor.mjs +7 -1
  192. package/scripts/lib/config/skill-evolution.mjs +2 -1
  193. package/scripts/lib/config/slopcheck.mjs +2 -1
  194. package/scripts/lib/config/state-md-lock.mjs +2 -1
  195. package/scripts/lib/config/templates-first.mjs +2 -1
  196. package/scripts/lib/config/test.mjs +2 -1
  197. package/scripts/lib/config/vault-integration.mjs +7 -1
  198. package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
  199. package/scripts/lib/config/vault-staleness.mjs +2 -1
  200. package/scripts/lib/config/vault-sync.mjs +2 -1
  201. package/scripts/lib/config/verification-auto-fix.mjs +2 -1
  202. package/scripts/lib/config/wave-reviewers.mjs +2 -1
  203. package/scripts/lib/config/worktree-orphans.mjs +2 -1
  204. package/scripts/lib/config.mjs +31 -3
  205. package/scripts/lib/convergence-monitor.mjs +82 -16
  206. package/scripts/lib/dispatcher/enumerate.mjs +2 -17
  207. package/scripts/lib/dispatcher/rank.mjs +124 -48
  208. package/scripts/lib/ecosystem-health.mjs +16 -2
  209. package/scripts/lib/eval/engine.mjs +9 -1
  210. package/scripts/lib/eval/session-resolve.mjs +23 -4
  211. package/scripts/lib/events-schema.mjs +48 -0
  212. package/scripts/lib/events.mjs +256 -7
  213. package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
  214. package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
  215. package/scripts/lib/frontmatter-guard.mjs +131 -13
  216. package/scripts/lib/gates/gate-full.mjs +26 -0
  217. package/scripts/lib/gates/gate-helpers.mjs +76 -0
  218. package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
  219. package/scripts/lib/hardware-pattern-detector.mjs +18 -1
  220. package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
  221. package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
  222. package/scripts/lib/host-identity.mjs +50 -11
  223. package/scripts/lib/instruction-budget-guard.mjs +171 -5
  224. package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
  225. package/scripts/lib/learnings/io.mjs +60 -6
  226. package/scripts/lib/memory-banner.mjs +20 -8
  227. package/scripts/lib/memory-proposals/store.mjs +30 -22
  228. package/scripts/lib/owner-config-banner.mjs +43 -6
  229. package/scripts/lib/owner-config-loader.mjs +21 -10
  230. package/scripts/lib/owner-interview.mjs +3 -3
  231. package/scripts/lib/owner-yaml.mjs +207 -14
  232. package/scripts/lib/peer-discovery.mjs +20 -2
  233. package/scripts/lib/platform.mjs +108 -15
  234. package/scripts/lib/plugin-update-banner.mjs +406 -0
  235. package/scripts/lib/project-hygiene.mjs +38 -2
  236. package/scripts/lib/qg-command-drift-banner.mjs +50 -12
  237. package/scripts/lib/quality-gate.mjs +133 -44
  238. package/scripts/lib/reconcile/emitter.mjs +68 -6
  239. package/scripts/lib/reconcile/engine.mjs +249 -9
  240. package/scripts/lib/reconcile/idempotency.mjs +37 -4
  241. package/scripts/lib/reconcile/writer.mjs +40 -18
  242. package/scripts/lib/scope-gate.mjs +36 -0
  243. package/scripts/lib/session-close-backfill.mjs +125 -18
  244. package/scripts/lib/session-discovery.mjs +57 -3
  245. package/scripts/lib/session-end/phase-skip.mjs +2 -2
  246. package/scripts/lib/session-id.mjs +12 -23
  247. package/scripts/lib/session-identity/own-session.mjs +187 -11
  248. package/scripts/lib/session-lock-shape.mjs +43 -0
  249. package/scripts/lib/session-lock.mjs +5 -10
  250. package/scripts/lib/session-registry.mjs +25 -9
  251. package/scripts/lib/session-schema/constants.mjs +36 -2
  252. package/scripts/lib/session-schema/validator.mjs +38 -4
  253. package/scripts/lib/session-start-probes.mjs +18 -1
  254. package/scripts/lib/session-transition.mjs +1 -1
  255. package/scripts/lib/sessions-canonical.mjs +446 -0
  256. package/scripts/lib/sessions-staleness-banner.mjs +18 -11
  257. package/scripts/lib/skill-health/join.mjs +17 -4
  258. package/scripts/lib/state-md.mjs +78 -0
  259. package/scripts/lib/sunset/walker.mjs +6 -0
  260. package/scripts/lib/telemetry/schema.mjs +255 -17
  261. package/scripts/lib/telemetry/sync.mjs +417 -24
  262. package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
  263. package/scripts/lib/validate/check-agents-skills.mjs +327 -0
  264. package/scripts/lib/validate/check-agents.mjs +3 -3
  265. package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
  266. package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
  267. package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
  268. package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
  269. package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
  270. package/scripts/lib/validate/check-skill-links.mjs +163 -0
  271. package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
  272. package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
  273. package/scripts/lib/validate/check-unwired-features.mjs +0 -9
  274. package/scripts/lib/validate/check-validator-registration.mjs +254 -0
  275. package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
  276. package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
  277. package/scripts/lib/validate/markdown-fences.mjs +196 -0
  278. package/scripts/lib/vault-backfill/template.mjs +63 -6
  279. package/scripts/lib/vault-mirror/process.mjs +165 -42
  280. package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
  281. package/scripts/lib/vault-status/board-lock.mjs +185 -0
  282. package/scripts/lib/vault-status/board-writer.mjs +174 -135
  283. package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
  284. package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
  285. package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
  286. package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
  287. package/scripts/lib/wave-resource-gate.mjs +133 -7
  288. package/scripts/lib/wave-sizing.mjs +4 -1
  289. package/scripts/lib/wave-transcript-tail.mjs +142 -8
  290. package/scripts/materialize-wave-scope.mjs +32 -9
  291. package/scripts/memory-propose.mjs +146 -8
  292. package/scripts/migrate-cold-start-seed.mjs +4 -1
  293. package/scripts/parse-config.mjs +60 -3
  294. package/scripts/promote-vault-strict.mjs +4 -15
  295. package/scripts/release.mjs +337 -29
  296. package/scripts/repair-invalid-sessions.mjs +3 -3
  297. package/scripts/run-quality-gate.mjs +128 -11
  298. package/scripts/site-numbers.mjs +36 -4
  299. package/scripts/sweep-expired-learnings.mjs +90 -0
  300. package/scripts/sync-vault-schema.mjs +3 -1
  301. package/scripts/telemetry.mjs +2 -2
  302. package/scripts/validate-plugin.mjs +187 -0
  303. package/scripts/validate-wave-scope.mjs +28 -8
  304. package/scripts/vault-consolidate.mjs +3 -11
  305. package/scripts/vault-integration-watcher.mjs +2 -4
  306. package/scripts/vault-mirror.mjs +111 -26
  307. package/scripts/wave-scope-binding.mjs +215 -0
  308. package/skills/_shared/instruction-file-resolution.md +10 -0
  309. package/skills/_shared/parallel-aware-auq.md +31 -2
  310. package/skills/_shared/parallel-aware-preamble.md +18 -4
  311. package/skills/_shared/platform-tools.md +1 -1
  312. package/skills/_shared/state-ownership.md +1 -1
  313. package/skills/architecture/SKILL.md +7 -5
  314. package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
  315. package/skills/autopilot/SKILL.md +4 -18
  316. package/skills/claude-md-drift-check/SKILL.md +5 -1
  317. package/skills/claude-md-drift-check/checker.mjs +62 -2
  318. package/skills/convergence-monitoring/SIGNALS.md +55 -0
  319. package/skills/discovery/probes/vault-staleness.mjs +37 -13
  320. package/skills/discovery/probes-arch.md +20 -18
  321. package/skills/dispatcher/SKILL.md +3 -2
  322. package/skills/ecosystem-health/SKILL.md +4 -1
  323. package/skills/ecosystem-health/wizard.md +5 -0
  324. package/skills/evolve/SKILL.md +87 -11
  325. package/skills/frontmatter-guard/SKILL.md +11 -5
  326. package/skills/npm-publish/SKILL.md +1 -1
  327. package/skills/reconcile/SKILL.md +38 -2
  328. package/skills/remote-offload/SKILL.md +89 -0
  329. package/skills/session-end/SKILL.md +18 -905
  330. package/skills/session-end/phase-3-6-tail.md +19 -9
  331. package/skills/session-end/plan-verification.md +221 -155
  332. package/skills/session-end/references/phase-2-quality-gate.md +93 -0
  333. package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
  334. package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
  335. package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
  336. package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
  337. package/skills/session-end/references/session-summary-template.md +62 -0
  338. package/skills/session-plan/SKILL.md +49 -0
  339. package/skills/session-start/SKILL.md +41 -900
  340. package/skills/session-start/phase-8-5-express-path.md +1 -1
  341. package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
  342. package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
  343. package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
  344. package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
  345. package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
  346. package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
  347. package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
  348. package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
  349. package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
  350. package/skills/vault-sync/validator.mjs +21 -27
  351. package/skills/wave-executor/SKILL.md +16 -2
  352. package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
  353. package/skills/wave-executor/references/wave-loop-review.md +570 -0
  354. package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
  355. package/skills/wave-executor/wave-loop.md +14 -1271
  356. package/templates/_shared/journey-manifest.md +10 -6
  357. package/.cursor/commands/autopilot-multi.md +0 -14
  358. package/.cursor/commands/contract-version-bump.md +0 -14
  359. package/.cursor/commands/journey-audit.md +0 -14
  360. package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
  361. package/.cursor/skills/daily/SKILL.md +0 -12
  362. package/.cursor/skills/domain-model/SKILL.md +0 -13
  363. package/.cursor/skills/journey-audit/SKILL.md +0 -13
  364. package/.cursor/skills/skill-creator/SKILL.md +0 -13
  365. package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
  366. package/commands/autopilot-multi.md +0 -74
  367. package/commands/contract-version-bump.md +0 -28
  368. package/commands/journey-audit.md +0 -43
  369. package/pi/prompts/autopilot-multi.md +0 -12
  370. package/pi/prompts/contract-version-bump.md +0 -12
  371. package/pi/prompts/journey-audit.md +0 -12
  372. package/scripts/autopilot-multi.mjs +0 -885
  373. package/scripts/backfill-learnings-expires.mjs +0 -196
  374. package/scripts/backfill-learnings.mjs +0 -203
  375. package/scripts/fleet-instruction-scan.mjs +0 -141
  376. package/scripts/lib/autopilot/dep-graph.mjs +0 -417
  377. package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
  378. package/scripts/lib/webhook-url.mjs +0 -105
  379. package/scripts/lifecycle-sim-v6.mjs +0 -347
  380. package/scripts/migrate-learnings-jsonl.mjs +0 -189
  381. package/scripts/migrate-subagents-jsonl.mjs +0 -196
  382. package/scripts/upload-social-preview.mjs +0 -316
  383. package/skills/_shared/model-selection.md +0 -64
  384. package/skills/contract-version-bump/SKILL.md +0 -219
  385. package/skills/daily/SKILL.md +0 -222
  386. package/skills/daily/generate.sh +0 -92
  387. package/skills/daily/templates/daily.md.tpl +0 -36
  388. package/skills/journey-audit/SKILL.md +0 -269
  389. package/skills/skill-creator/SKILL.md +0 -168
  390. package/skills/ubiquitous-language/SKILL.md +0 -97
  391. package/skills/vault-sync/package-lock.json +0 -40
  392. /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
  393. /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * check-validator-registration.mjs — every `scripts/lib/validate/check-*.mjs`
4
+ * must be referenced by basename from at least one of the three surfaces
5
+ * that actually RUN a validator (`scripts/validate-plugin.mjs`,
6
+ * `.husky/pre-commit`, `.gitlab-ci.yml`), or declare itself deliberately
7
+ * standalone. #1184.
8
+ *
9
+ * THE CLASS. A checker with real detection logic and zero callers is built
10
+ * work nobody ever runs — this repo's own `check-unwired-features.mjs`
11
+ * census exists for a sibling shape of the same problem (config keys nobody
12
+ * reads); this checker is that same discipline applied to the checkers
13
+ * directory itself. `scripts/validate-plugin.mjs` is the canonical local
14
+ * runner (`runCheck('check-foo.mjs')`); `.husky/pre-commit` and
15
+ * `.gitlab-ci.yml` are the two OTHER surfaces some checkers wire into
16
+ * directly instead of (or in addition to) the orchestrator —
17
+ * `check-owner-leakage.mjs`, `check-test-fixture-shapes.mjs` and
18
+ * `check-test-value-bans.mjs` all do this (verified live at HEAD 2ccea0f2).
19
+ *
20
+ * OPT-OUT. A checker that is deliberately CLI-only (invoked by a human or a
21
+ * different tool, never by these three surfaces) declares itself with a
22
+ * header-comment marker: `// registration: standalone <reason>`. The marker
23
+ * makes the checker report PASS as "standalone", not merely silence a
24
+ * warning — the same shape of inline self-declared exemption
25
+ * `check-untracked-test-deps.mjs`'s `IGNORE_MARKER` and
26
+ * `check-dead-bridge.mjs`'s `:ignore` marker already use in this directory.
27
+ *
28
+ * ORACLE. A plain substring match of the checker's own basename (e.g.
29
+ * `"check-foo.mjs"`) against the COMMENT-STRIPPED text of the three surface
30
+ * files — the same granularity `runCheck('check-foo.mjs')` calls and
31
+ * `.husky`/CI script lines already use to name a checker. MEASURED (HEAD
32
+ * 2ccea0f2, all 33 live `check-*.mjs` basenames): zero basenames are a
33
+ * substring of another, so this match cannot cross-attribute one checker's
34
+ * registration to a different one.
35
+ *
36
+ * COMMENT-STRIPPING (HIGH, qa review, #1184 FX-C). A basename referenced
37
+ * ONLY inside a `//`/`#` line comment or a `/* *\/` block comment is NOT a
38
+ * real registration — the surface text is stripped of comments (quote-aware,
39
+ * so a `#`/`//` INSIDE a string — a URL fragment, a shell parameter
40
+ * expansion `${VAR#pattern}` — is never mistaken for a comment start) before
41
+ * matching. MEASURED before this fix: a fixture whose only reference to
42
+ * `check-ghost.mjs` sat in `// runCheck('check-ghost.mjs'); // DISABLED`
43
+ * reported `registered: true` (exit 0) — commenting a checker OUT silently
44
+ * kept it PASSing. `scripts/validate-plugin.mjs` uses `//`+`/* *\/` (js);
45
+ * `.husky/pre-commit` and `.gitlab-ci.yml` use `#` (sh/yaml) — see
46
+ * {@link commentStyleForSurface}. The checker's OWN
47
+ * `// registration: standalone` header marker is read from the CHECKER file
48
+ * directly (never from a surface text) and is unaffected by this stripping.
49
+ *
50
+ * NAMED CEILING (BV-004): a checker referenced only in prose (a `.md` doc, a
51
+ * skill body) and nowhere in the three RUN surfaces above still reports
52
+ * UNREGISTERED — being documented is not being run. REVISIT if a fourth run
53
+ * surface (a new CI job file, a different git hook) is ever added: extend
54
+ * `RUN_SURFACES`, do not special-case it here. The comment stripper's own
55
+ * quote-tracking is a single flat state — an escaped quote (`\"`) inside a
56
+ * double-quoted string is not honoured, and a template-literal's `${...}`
57
+ * interpolation is not walked separately. Both failure directions lean
58
+ * toward treating MORE text as "inside a string" than a real parser would,
59
+ * which can only make the stripper MISS a comment (false "still
60
+ * registered"), never manufacture a false UNREGISTERED — the direction this
61
+ * checker's own false-positive history (the paragraph above) already
62
+ * measured as the live hazard.
63
+ *
64
+ * Usage: check-validator-registration.mjs <repo-root>
65
+ * Output: ` PASS: …` / ` FAIL: …` lines (two leading spaces), then
66
+ * `Results: N passed, M failed`. Exit 0 = every checker registered or
67
+ * standalone, 1 = at least one unregistered checker, 2 = tool error.
68
+ *
69
+ * Import-safety: importing this module MUST NOT execute anything — the
70
+ * isMain guard at the bottom is the only side-effecting path.
71
+ */
72
+
73
+ import { existsSync, readFileSync } from 'node:fs';
74
+ import path from 'node:path';
75
+ import { pathToFileURL } from 'node:url';
76
+ import { enumerateRepoFiles } from './enumerate-repo-files.mjs';
77
+
78
+ /** Marker line inside a checker's own header — declares deliberate CLI-only status. */
79
+ export const STANDALONE_MARKER = /^\s*\/\/\s*registration:\s*standalone\b(?:\s+(.*))?$/m;
80
+
81
+ /** The three surfaces that actually RUN a validator (not merely mention it). */
82
+ const RUN_SURFACES = Object.freeze([
83
+ path.join('scripts', 'validate-plugin.mjs'),
84
+ path.join('.husky', 'pre-commit'),
85
+ path.join('.gitlab-ci.yml'),
86
+ ]);
87
+
88
+ const VALIDATE_DIR_REL = path.join('scripts', 'lib', 'validate');
89
+
90
+ /**
91
+ * Comment style for a RUN_SURFACES path, keyed on extension rather than
92
+ * position — avoids a silent index-drift if `RUN_SURFACES` is ever
93
+ * reordered. `.mjs` gets JS-shaped comments (`//`, `/* *\/`, quotes
94
+ * `'`/`"`/`` ` ``); everything else (`.husky/pre-commit` has no extension,
95
+ * `.gitlab-ci.yml`) gets shell/YAML-shaped comments (`#` only, quotes
96
+ * `'`/`"`).
97
+ *
98
+ * @param {string} rel repo-relative surface path
99
+ * @returns {{lineComment: string, blockComment: boolean, quoteChars: string[]}}
100
+ */
101
+ function commentStyleForSurface(rel) {
102
+ return rel.endsWith('.mjs')
103
+ ? { lineComment: '//', blockComment: true, quoteChars: ['"', "'", '`'] }
104
+ : { lineComment: '#', blockComment: false, quoteChars: ['"', "'"] };
105
+ }
106
+
107
+ /**
108
+ * Strip comments from `text` so a checker basename mentioned only inside a
109
+ * comment is never read as a real registration. Quote-aware: walks `'`/`"`
110
+ * (and, for the js style, `` ` ``) spans without inspecting their contents,
111
+ * so a comment marker INSIDE a string (a URL fragment `#frag`, a shell
112
+ * parameter expansion `${VAR#pattern}`) is left untouched rather than
113
+ * truncating the line early. See the header NAMED CEILING for what this
114
+ * quote-tracking deliberately does not attempt.
115
+ *
116
+ * @param {string} text
117
+ * @param {{lineComment: string, blockComment: boolean, quoteChars: string[]}} style
118
+ * @returns {string}
119
+ */
120
+ export function stripComments(text, { lineComment, blockComment, quoteChars }) {
121
+ let out = '';
122
+ let i = 0;
123
+ let inQuote = null;
124
+ while (i < text.length) {
125
+ const ch = text[i];
126
+ if (inQuote) {
127
+ out += ch;
128
+ if (ch === '\\' && i + 1 < text.length) {
129
+ out += text[i + 1];
130
+ i += 2;
131
+ continue;
132
+ }
133
+ if (ch === inQuote) inQuote = null;
134
+ i += 1;
135
+ continue;
136
+ }
137
+ if (quoteChars.includes(ch)) {
138
+ inQuote = ch;
139
+ out += ch;
140
+ i += 1;
141
+ continue;
142
+ }
143
+ if (blockComment && ch === '/' && text[i + 1] === '*') {
144
+ const end = text.indexOf('*/', i + 2);
145
+ i = end === -1 ? text.length : end + 2;
146
+ continue;
147
+ }
148
+ if (text.startsWith(lineComment, i)) {
149
+ const nl = text.indexOf('\n', i);
150
+ i = nl === -1 ? text.length : nl; // keep the newline itself, drop the comment text
151
+ continue;
152
+ }
153
+ out += ch;
154
+ i += 1;
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /**
160
+ * @typedef {{basename: string, registered: boolean, standalone: boolean, surfaces: string[]}} RegistrationResult
161
+ */
162
+
163
+ /**
164
+ * @param {string} repoRoot
165
+ * @returns {RegistrationResult[]} sorted by basename
166
+ */
167
+ export function scanValidatorRegistration(repoRoot) {
168
+ // "Exists in this directory, tracked or not" (#1248) — a checker written but
169
+ // not yet staged is precisely the one most likely to be unregistered, and the
170
+ // bare git index cannot see it. `enumerateRepoFiles` still honours
171
+ // `.gitignore`, so no ignored tree enters the census (#1143).
172
+ const checkerFiles = enumerateRepoFiles({
173
+ repoRoot,
174
+ dirs: [VALIDATE_DIR_REL],
175
+ exts: ['mjs'],
176
+ }).filter((f) => /^check-.*\.mjs$/.test(path.basename(f)));
177
+
178
+ // Comment-stripped before matching (HIGH, #1184 FX-C): a basename
179
+ // referenced only inside a `//`/`#`/`/* *\/` comment is NOT a real
180
+ // registration — see the header's COMMENT-STRIPPING paragraph.
181
+ const surfaceTexts = RUN_SURFACES.map((rel) => {
182
+ const abs = path.join(repoRoot, rel);
183
+ try {
184
+ const raw = existsSync(abs) ? readFileSync(abs, 'utf8') : '';
185
+ return stripComments(raw, commentStyleForSurface(rel));
186
+ } catch {
187
+ return '';
188
+ }
189
+ });
190
+
191
+ /** @type {RegistrationResult[]} */
192
+ const results = [];
193
+ for (const abs of checkerFiles) {
194
+ const basename = path.basename(abs);
195
+ let source = '';
196
+ try {
197
+ source = readFileSync(abs, 'utf8');
198
+ } catch {
199
+ /* unreadable — no marker can be found, no surface reference can save it either */
200
+ }
201
+ const standalone = STANDALONE_MARKER.test(source);
202
+ const surfaces = RUN_SURFACES.filter((_, i) => surfaceTexts[i].includes(basename));
203
+ results.push({ basename, registered: surfaces.length > 0, standalone, surfaces });
204
+ }
205
+ return results.sort((a, b) => a.basename.localeCompare(b.basename));
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // CLI
210
+ // ---------------------------------------------------------------------------
211
+
212
+ /**
213
+ * Run the check against a repo root, printing the validate-plugin line
214
+ * vocabulary.
215
+ *
216
+ * @param {string} repoRoot
217
+ * @returns {number} 0 = every checker registered or standalone, 1 = finding(s), 2 = tool error
218
+ */
219
+ export function runCheckValidatorRegistration(repoRoot) {
220
+ console.log('--- Check: validator registration (check-*.mjs must be wired or standalone) ---');
221
+
222
+ const results = scanValidatorRegistration(repoRoot);
223
+ let pass = 0;
224
+ let fail = 0;
225
+
226
+ for (const r of results) {
227
+ if (r.standalone) {
228
+ console.log(` PASS: ${r.basename} — declared standalone (registration: standalone)`);
229
+ pass += 1;
230
+ } else if (r.registered) {
231
+ console.log(` PASS: ${r.basename} — referenced in ${r.surfaces.join(', ')}`);
232
+ pass += 1;
233
+ } else {
234
+ console.log(
235
+ ` FAIL: ${r.basename} — referenced by NEITHER scripts/validate-plugin.mjs, .husky/pre-commit, NOR .gitlab-ci.yml, and carries no "registration: standalone" marker`,
236
+ );
237
+ fail += 1;
238
+ }
239
+ }
240
+
241
+ console.log('');
242
+ console.log(`Results: ${pass} passed, ${fail} failed`);
243
+ return fail > 0 ? 1 : 0;
244
+ }
245
+
246
+ const isMain = import.meta.url === pathToFileURL(process.argv[1] || '').href;
247
+ if (isMain) {
248
+ const root = process.argv[2];
249
+ if (!root) {
250
+ console.error('Usage: check-validator-registration.mjs <repo-root>');
251
+ process.exit(2);
252
+ }
253
+ process.exit(runCheckValidatorRegistration(path.resolve(root)));
254
+ }
@@ -194,6 +194,7 @@
194
194
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
195
195
  import path from 'node:path';
196
196
  import { pathToFileURL } from 'node:url';
197
+ import { SHELL_LANGS, forEachLine } from './markdown-fences.mjs';
197
198
 
198
199
  /** Directories whose content is scanned. Root-level `*.md` is added separately. */
199
200
  const SCAN_DIRS = Object.freeze([
@@ -224,9 +225,6 @@ const DOC_EXTENSIONS = Object.freeze(['.md']);
224
225
  /** Code extensions (comment + string-literal rules apply). */
225
226
  const CODE_EXTENSIONS = Object.freeze(['.mjs', '.js', '.cjs']);
226
227
 
227
- /** Fence languages whose body is shell. Anything else is prose. */
228
- const SHELL_LANGS = Object.freeze(new Set(['bash', 'sh', 'shell', 'console', 'zsh']));
229
-
230
228
  /**
231
229
  * Real top-level subcommands, harvested from `gh --help` / `glab --help`
232
230
  * (gh 2.86.0 / glab 1.91.0, 2026-08-14). Filter 4: a token that is not in this
@@ -576,33 +574,13 @@ export function scanMarkdown(relative, body, tally) {
576
574
  const findings = [];
577
575
  /** @type {{line: number, text: string}[]} */
578
576
  const shellLines = [];
579
- /** @type {{marker: string, length: number, shell: boolean} | null} */
580
- let fence = null;
581
577
 
582
- const lines = body.split('\n');
583
- for (let index = 0; index < lines.length; index += 1) {
584
- const raw = lines[index];
585
- const fenceMatch = raw.match(/^\s*(`{3,}|~{3,})\s*([A-Za-z0-9_+-]*)/);
586
- if (fenceMatch) {
587
- const marker = fenceMatch[1][0];
588
- const length = fenceMatch[1].length;
589
- const lang = fenceMatch[2].toLowerCase();
590
- if (fence === null) {
591
- fence = { marker, length, shell: SHELL_LANGS.has(lang) };
592
- continue;
593
- }
594
- // A closing fence uses the same char, is at least as long, and has no info string.
595
- if (marker === fence.marker && length >= fence.length && lang === '') {
596
- fence = null;
597
- continue;
598
- }
599
- // Otherwise it is fence content (a nested fence inside a wider one).
600
- }
601
- if (fence === null || !fence.shell) continue;
578
+ forEachLine(body, (raw, { lineNumber, inFence, lang }) => {
579
+ if (!inFence || !SHELL_LANGS.has(lang)) return;
602
580
  const stripped = raw.replace(/^\s*[$❯>]\s+/, '');
603
- if (/^\s*#/.test(stripped)) continue;
604
- shellLines.push({ line: index + 1, text: stripped });
605
- }
581
+ if (/^\s*#/.test(stripped)) return;
582
+ shellLines.push({ line: lineNumber, text: stripped });
583
+ });
606
584
 
607
585
  for (const entry of joinContinuations(shellLines)) {
608
586
  for (const hit of extractBareInvocations(entry.text, tally)) {
@@ -0,0 +1,317 @@
1
+ /**
2
+ * scripts/lib/validate/enumerate-repo-files.mjs
3
+ *
4
+ * ONE enumerator for the scanners whose question is **"does this file exist in
5
+ * the repository right now?"** — GitLab #1248.
6
+ *
7
+ * ## The three populations, and why this is a third one
8
+ *
9
+ * `./repo-files.mjs` already answers two questions, and this module answers a
10
+ * third rather than duplicating either:
11
+ *
12
+ * | function | population |
13
+ * |-----------------------------|-----------------------------------------------------|
14
+ * | `listRepoFiles()` | what git TRACKS — "will ship / is versioned" |
15
+ * | `listOnDiskFiles()` | what the filesystem HOLDS minus a name list |
16
+ * | `enumerateRepoFiles()` here | **exists under these roots, tracked or not, minus what `.gitignore` excludes** |
17
+ *
18
+ * Pick deliberately. A scanner asking "is this file versioned / will it ship?"
19
+ * (a packaging check, a tarball manifest) MUST keep `listRepoFiles()`. A
20
+ * scanner asking "is this file PRESENT, so is the citation pointing at it
21
+ * dead?" belongs here — for it, the index is the wrong oracle.
22
+ *
23
+ * ## Why the index is the wrong oracle for an existence check (#1248)
24
+ *
25
+ * MEASURED in a clone (Wave-1 D6, 2026-09-06): an UNTRACKED
26
+ * `skills/zz-probe/SKILL.md` citing `scripts/does-not-exist.mjs` made
27
+ * `check-skill-script-paths.mjs` report `1 passed, 0 failed` BEFORE
28
+ * `git add -A` and `0 passed, 1 failed` AFTER — same working tree, same
29
+ * defect, no edit in between. The moment a defect is most likely to exist (a
30
+ * brand-new, not-yet-staged skill or checker) is exactly the moment the index
31
+ * cannot see it, so the check reports clean on the tree that carries the bug.
32
+ * `.claude/rules/measurement-discipline.md` § "A `git grep` drift sweep cannot
33
+ * see untracked files" is the same incident class on the release sweep.
34
+ *
35
+ * ## Why NOT a bare `readdirSync` walk
36
+ *
37
+ * The naive repair — swap `git ls-files` for a filesystem walk with a fixed
38
+ * prune list — reintroduces the MEASURED regression #1143 that put
39
+ * `repo-files.mjs` there in the first place: a walk cannot see `.gitignore`,
40
+ * so a gitignored worktree under `.claude/worktrees/<name>` drops a COMPLETE
41
+ * second checkout into the census (measured with one peer worktree present:
42
+ * +755 `.md`, +1209 `.mjs`, 133 MB, and a peer's copy of a rule file counted
43
+ * as an independent document). MEASURED here 2026-09-06 @ `befdda47` on a
44
+ * clean tree with `.claude/worktrees/` empty:
45
+ *
46
+ * git ls-files -- skills commands agents docs → 287 .md
47
+ * git ls-files --cached --others
48
+ * --exclude-standard -- (same dirs) → 287 .md
49
+ * listOnDiskFiles() walk, EXCLUDED_DIRS -- (same dirs) → 290 .md
50
+ *
51
+ * The 3-file surplus of the walk is entirely gitignored content
52
+ * (`docs/specs/2026-04-04-plan-skill-design.md`,
53
+ * `docs/specs/2026-04-16-bootstrap-gate-design.md`,
54
+ * `docs/specs/2026-05-26-parallel-aware-sessions-design.md`) — private design
55
+ * notes that would enter the census as if they were repository documentation.
56
+ * So the oracle is `--cached --others --exclude-standard`: it sees the
57
+ * untracked file #1248 is about AND honours `.gitignore`, which no prune list
58
+ * can approximate.
59
+ *
60
+ * ## The failure contract: absent is not unreadable (#1248 follow-up)
61
+ *
62
+ * The population above is "exists under these roots". Deciding whether a path
63
+ * EXISTS costs one `statSync`, and that `statSync` can fail for two
64
+ * fundamentally different reasons which an earlier version of this module
65
+ * collapsed into one bare `catch { continue }`:
66
+ *
67
+ * | stat error | what it means | this module |
68
+ * |-----------------------------------|-------------------------------------------------|-------------|
69
+ * | `ENOENT` | git lists it, the checkout lacks it — sparse checkout, a deletion staged elsewhere | SKIP (contributes zero files) |
70
+ * | `ENOTDIR` | a parent component is a file, so the path cannot exist either | SKIP (same class) |
71
+ * | `EACCES` / `EPERM` | the file EXISTS; this process may not look at it | THROW {@link RepoEnumerationError} |
72
+ * | `ELOOP` / `EIO` / `ENAMETOOLONG` / anything else | the answer is unknown | THROW {@link RepoEnumerationError} |
73
+ *
74
+ * The skip set is deliberately the same two codes `listRepoFiles()` documents
75
+ * in its own filter comment ("a tracked path can be absent from the working
76
+ * tree"), narrowed from a catch-all to exactly the codes that mean ABSENT.
77
+ * Every other code means the enumerator does not know whether the file is
78
+ * there, and a census that silently omits a file it could not look at reports
79
+ * a SMALLER population than the truth — with no signal that it did.
80
+ *
81
+ * That silence had a measured consequence. `collectDriftHits()` in
82
+ * `scripts/release.mjs` sweeps every enumerated file for the previous release
83
+ * literal; with `README.md` unreadable, the swallow made the sweep read ZERO
84
+ * files, return `status 1` ("no match"), and `evaluateDriftSweep()` reported
85
+ * `ok: true` — a release gate passing on a file it could not open. Fail-closed
86
+ * is one line there, and it was already written:
87
+ *
88
+ * } catch (err) {
89
+ * return { status: 128, stdout: '', stderr: `enumerateRepoFiles failed: ${err && err.message}` };
90
+ *
91
+ * (`scripts/release.mjs:576-578`; `evaluateDriftSweep` maps any status outside
92
+ * {0,1} to `ok: false` — "sweep is inconclusive".) So THROWING is what turns
93
+ * an unreadable file into an inconclusive sweep instead of a clean one.
94
+ *
95
+ * NAMED CEILING (BV-004): only the git path fails closed. The non-git fallback
96
+ * walks through `listOnDiskFiles()`, whose `readdirSync` walk skips an
97
+ * unreadable sub-tree by design and cannot report it — see
98
+ * {@link enumerateRepoFiles}. Revisit trigger: the first consumer that needs
99
+ * fail-closed enumeration on a NON-git root (a tarball export, a vendored
100
+ * copy); the answer then is an error-collecting walk in `repo-files.mjs`, not
101
+ * a second stat pass here.
102
+ *
103
+ * ## NAMED CEILING (BV-004)
104
+ *
105
+ * The `prune` list is a fixed set of path segments / repo-relative prefixes,
106
+ * NOT a `.gitignore` parser — it is only load-bearing on the FALLBACK path
107
+ * (a root that is not a git top level: a tarball export, a vendored copy, a
108
+ * tmpdir fixture). On the primary path git already applies the real ignore
109
+ * rules and the prune list is a cheap second filter. REVISIT if a scanner
110
+ * ever needs this on a non-git root whose ignored trees are not covered by
111
+ * the default prune set — then reach for a real ignore parser, do not grow
112
+ * this list a sixth time.
113
+ *
114
+ * @module scripts/lib/validate/enumerate-repo-files
115
+ */
116
+
117
+ import { execFileSync } from 'node:child_process';
118
+ import { statSync } from 'node:fs';
119
+ import path from 'node:path';
120
+
121
+ import { EXCLUDED_DIRS, isGitToplevel, listOnDiskFiles } from './repo-files.mjs';
122
+
123
+ /**
124
+ * Environment handed to `git`. An allowlist rather than `process.env`: an
125
+ * inherited `GIT_DIR` / `GIT_WORK_TREE` (set by any hook that spawned us)
126
+ * would silently re-point `ls-files` at a DIFFERENT repository, and the result
127
+ * would look like a plausible file list. Same allowlist as
128
+ * `repo-files.mjs`, which is not exported there.
129
+ */
130
+ const GIT_ENV_ALLOWLIST = Object.freeze(['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'TZ']);
131
+
132
+ /**
133
+ * Path segments (or repo-relative path prefixes) never enumerated — reused
134
+ * from `repo-files.mjs` rather than retyped, so an addition there reaches this
135
+ * module too.
136
+ *
137
+ * Deliberately NOT extended with this repo's own ignored trees
138
+ * (`.orchestrator/tmp/`, `.claude/worktrees/`): on the primary path `git` has
139
+ * already applied the real ignore rules — measured, `.orchestrator/tmp/` is
140
+ * `.gitignore:122` — so such an entry would be dead weight there, and naming
141
+ * an untracked path in a module a test imports is itself a finding
142
+ * (`check-untracked-test-deps.mjs` R2). A caller scanning a NON-git root whose
143
+ * ignored subtree a basename cannot express passes it via `prune` instead —
144
+ * that is what the prefix form is for.
145
+ */
146
+ export const DEFAULT_PRUNE = EXCLUDED_DIRS;
147
+
148
+ /**
149
+ * `stat` error codes that mean the path is ABSENT from the working tree, and
150
+ * are therefore skipped rather than raised. See the module header's failure
151
+ * table for why the set is exactly these two and not a catch-all.
152
+ */
153
+ const ABSENT_STAT_CODES = Object.freeze(new Set(['ENOENT', 'ENOTDIR']));
154
+
155
+ /**
156
+ * Raised when a path git listed could not be RESOLVED — the file may well be
157
+ * there and this process could not look at it (`EACCES`, `EPERM`, `ELOOP`,
158
+ * `EIO`, …). Callers are expected to fail closed on it: an enumeration that
159
+ * threw describes no population at all.
160
+ *
161
+ * @property {string} code the underlying `stat` error code (`EACCES`, …)
162
+ * @property {string} path the absolute path that could not be resolved
163
+ */
164
+ export class RepoEnumerationError extends Error {
165
+ /**
166
+ * @param {string} message
167
+ * @param {{code?: string, path?: string, cause?: unknown}} details
168
+ */
169
+ constructor(message, { code, path: target, cause } = {}) {
170
+ super(message, cause === undefined ? undefined : { cause });
171
+ this.name = 'RepoEnumerationError';
172
+ this.code = code;
173
+ this.path = target;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * True when `relative` (POSIX, repo-relative) is inside a pruned tree.
179
+ * A prune entry matches either a whole path SEGMENT (`node_modules`) or a
180
+ * repo-relative path PREFIX (`.orchestrator/tmp`).
181
+ *
182
+ * @param {string} relative repo-relative POSIX path
183
+ * @param {string[]} prune
184
+ * @returns {boolean}
185
+ */
186
+ function isPruned(relative, prune) {
187
+ const segments = relative.split('/');
188
+ for (const entry of prune) {
189
+ if (entry.includes('/')) {
190
+ if (relative === entry || relative.startsWith(`${entry}/`)) return true;
191
+ } else if (segments.includes(entry)) {
192
+ return true;
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+
198
+ /** @returns {NodeJS.ProcessEnv} the allowlisted git environment */
199
+ function gitEnv() {
200
+ /** @type {NodeJS.ProcessEnv} */
201
+ const env = {};
202
+ for (const key of GIT_ENV_ALLOWLIST) {
203
+ if (process.env[key] !== undefined) env[key] = process.env[key];
204
+ }
205
+ return env;
206
+ }
207
+
208
+ /**
209
+ * Normalise `exts` into a predicate over an absolute path.
210
+ * @param {string[] | null | undefined} exts extensions, with or without the dot
211
+ * @returns {(absolute: string) => boolean}
212
+ */
213
+ function extFilter(exts) {
214
+ if (!exts || exts.length === 0) return () => true;
215
+ const set = new Set(exts.map((e) => (e.startsWith('.') ? e : `.${e}`)));
216
+ return (absolute) => set.has(path.extname(absolute));
217
+ }
218
+
219
+ /**
220
+ * Files that EXIST under `dirs` — tracked or not — minus gitignored content
221
+ * and minus `prune`.
222
+ *
223
+ * Drop-in shaped like `listRepoFiles()`: absolute paths, sorted, deduplicated,
224
+ * never throwing for a missing directory (it contributes zero files).
225
+ *
226
+ * @param {object} options
227
+ * @param {string} options.repoRoot absolute repository root
228
+ * @param {string[]} [options.dirs] repo-relative directories to scan (default: the whole root)
229
+ * @param {string[] | null} [options.exts] extensions to keep (default: every file)
230
+ * @param {string[]} [options.prune] path segments / repo-relative prefixes to skip
231
+ * (default: {@link DEFAULT_PRUNE}; REPLACES the default when given, so pass
232
+ * `[...DEFAULT_PRUNE, 'extra']` to extend it rather than to swap it)
233
+ * @param {(target: string) => import('node:fs').Stats} [options.stat] injection
234
+ * seam for tests (default: `statSync`) — the same shape `collectDriftHits()`
235
+ * uses for its `enumerate`/`read` seams
236
+ * @returns {string[]} absolute paths, sorted
237
+ * @throws {RepoEnumerationError} when a listed path can neither be resolved
238
+ * nor proven absent (`EACCES`, `EPERM`, `ELOOP`, …) — see the module header's
239
+ * failure table. Only the git path raises; the non-git fallback walk cannot.
240
+ */
241
+ export function enumerateRepoFiles({
242
+ repoRoot,
243
+ dirs,
244
+ exts = null,
245
+ prune = DEFAULT_PRUNE,
246
+ stat = statSync,
247
+ } = {}) {
248
+ const matches = extFilter(exts);
249
+ const pruneList = [...prune];
250
+ const env = gitEnv();
251
+
252
+ if (isGitToplevel(repoRoot, env)) {
253
+ const pathspecs = dirs && dirs.length > 0 ? dirs.filter((d) => d !== '.') : [];
254
+ try {
255
+ // `--cached --others --exclude-standard` = tracked PLUS untracked, minus
256
+ // everything `.gitignore`/`.git/info/exclude` excludes. `--deduplicate`
257
+ // is deliberately not used: it needs git >= 2.31 and the Set below is
258
+ // free. See the module header for why this beats both a bare
259
+ // `ls-files` (#1248) and a bare walk (#1143).
260
+ const out = execFileSync(
261
+ 'git',
262
+ ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', ...pathspecs],
263
+ {
264
+ cwd: repoRoot,
265
+ encoding: 'utf8',
266
+ stdio: ['ignore', 'pipe', 'ignore'],
267
+ maxBuffer: 64 * 1024 * 1024,
268
+ env,
269
+ },
270
+ );
271
+ const found = new Set();
272
+ for (const rel of out.split('\0')) {
273
+ if (!rel) continue;
274
+ if (isPruned(rel, pruneList)) continue;
275
+ const absolute = path.join(repoRoot, rel);
276
+ if (!matches(absolute)) continue;
277
+ // A tracked path can be absent from the working tree (sparse checkout,
278
+ // a deletion staged elsewhere). A scanner that then read it would
279
+ // report a tool-error for a file nobody removed — so ABSENT is skipped.
280
+ // Anything else means the file may be there and we could not look:
281
+ // fail closed rather than shrink the census in silence (header table).
282
+ let entry;
283
+ try {
284
+ entry = stat(absolute);
285
+ } catch (err) {
286
+ const code = err && err.code;
287
+ if (ABSENT_STAT_CODES.has(code)) continue;
288
+ throw new RepoEnumerationError(
289
+ `cannot stat ${absolute} (${code || 'unknown error'}): the file may exist and could not be read — enumeration is inconclusive`,
290
+ { code, path: absolute, cause: err },
291
+ );
292
+ }
293
+ if (!entry.isFile()) continue;
294
+ found.add(absolute);
295
+ }
296
+ return [...found].sort();
297
+ } catch (err) {
298
+ // A resolution failure is NOT a reason to retry with a weaker oracle:
299
+ // the walk would skip the same unreadable entry silently and hand back a
300
+ // census that looks complete. Only a git/`ls-files` failure falls
301
+ // through — there we have no index to trust in the first place.
302
+ if (err instanceof RepoEnumerationError) throw err;
303
+ // fall through to the walk — a git that answered rev-parse but failed
304
+ // ls-files leaves us with no index to trust.
305
+ }
306
+ }
307
+
308
+ // Non-git root (tarball export, vendored copy, tmpdir fixture): the walk in
309
+ // `repo-files.mjs` is the reuse — it already skips symlinks and unreadable
310
+ // sub-trees. It excludes by directory BASENAME only, so prefix-shaped prune
311
+ // entries are re-applied here.
312
+ const basenamePrune = pruneList.filter((entry) => !entry.includes('/'));
313
+ return listOnDiskFiles(repoRoot, { dirs, exts, exclude: basenamePrune }).filter((absolute) => {
314
+ const rel = path.relative(repoRoot, absolute).split(path.sep).join('/');
315
+ return !isPruned(rel, pruneList);
316
+ });
317
+ }