forge-workflow 0.0.10 → 0.1.0-beta.2

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 (454) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +461 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +17 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +0 -1
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +838 -972
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +382 -11
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,2089 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+ const babelParser = require('@babel/parser');
7
+
8
+ const SUPPORTED_TARGET = '0.1.0';
9
+ const AUDIT_ARTIFACT = 'docs/work/2026-06-06-kernel-backlog-memory-roadmap/bd-call-site-kill-list.md';
10
+
11
+ const GROUPS = ['command', 'runtime', 'docs', 'skills', 'hooks'];
12
+
13
+ const STATIC_ROOT_DOC_FILES = [
14
+ 'AGENTS.md',
15
+ 'README.md',
16
+ ];
17
+
18
+ // Instruction/rules roots always scanned for lingering `bd` references, independent
19
+ // of plugin manifests. `.claude/rules` was dropped from claude.plugin.json (so setup
20
+ // no longer creates an empty, never-populated dir), but a project may still carry
21
+ // `.claude/rules/*.md`, so the bd-retirement audit must keep scanning it.
22
+ const STATIC_INSTRUCTION_ROOTS = [
23
+ '.claude/rules',
24
+ ];
25
+
26
+ const FORGE_SKILLS_PACK_ROOTS = [
27
+ 'packages/skills/forge-plugin',
28
+ 'packages/skills/forge-workflow',
29
+ 'packages/skills/forge',
30
+ ];
31
+
32
+ const STATIC_SCAN_ROOTS = [
33
+ 'lefthook.yml',
34
+ 'bin',
35
+ 'lib',
36
+ 'scripts',
37
+ '.forge',
38
+ '.github/workflows',
39
+ 'packages/skills/README.md',
40
+ ...FORGE_SKILLS_PACK_ROOTS,
41
+ 'docs/PROJECT_DESIGN.md',
42
+ 'docs/guides',
43
+ 'docs/reference',
44
+ 'docs/work/2026-06-06-kernel-backlog-memory-roadmap',
45
+ ];
46
+
47
+ const SKIP_DIR_NAMES = new Set([
48
+ '.git',
49
+ 'node_modules',
50
+ 'coverage',
51
+ 'dist',
52
+ 'build',
53
+ '.worktrees',
54
+ 'test',
55
+ 'tests',
56
+ 'test-env',
57
+ 'test-results',
58
+ ]);
59
+
60
+ const SKIP_RELATIVE_PREFIXES = [
61
+ '.claude/worktrees/',
62
+ 'packages/skills/node_modules/',
63
+ 'packages/skills/test/',
64
+ ];
65
+
66
+ const SKIP_RELATIVE_FILES = new Set([
67
+ AUDIT_ARTIFACT,
68
+ '.forge/pr-body.md',
69
+ 'lib/release-readiness.js',
70
+ ]);
71
+
72
+ const TEXT_EXTENSIONS = new Set([
73
+ '.cjs',
74
+ '.js',
75
+ '.json',
76
+ '.md',
77
+ '.mdc',
78
+ '.mjs',
79
+ '.sh',
80
+ '.toml',
81
+ '.txt',
82
+ '.yaml',
83
+ '.yml',
84
+ ]);
85
+
86
+ const D20_HOT_PATH_FILES = new Set([
87
+ 'lib/commands/_issue.js',
88
+ 'lib/commands/prime.js',
89
+ 'lib/commands/orient.js',
90
+ 'lib/commands/recap.js',
91
+ 'lib/commands/remember.js',
92
+ 'lib/commands/recall.js',
93
+ 'lib/commands/sync.js',
94
+ 'lib/commands/worktree.js',
95
+ 'lib/commands/setup.js',
96
+ 'lib/project-memory.js',
97
+ 'lib/workflow/state-manager.js',
98
+ 'scripts/preflight.sh',
99
+ 'scripts/smart-status.sh',
100
+ 'scripts/smart-status-score.js',
101
+ 'scripts/smart-status-sessions.js',
102
+ 'AGENTS.md',
103
+ 'lefthook.yml',
104
+ ]);
105
+
106
+ const D20_HOT_PATH_PREFIXES = [
107
+ 'scripts/forge-team/',
108
+ '.forge/hooks/',
109
+ '.github/workflows/',
110
+ ...FORGE_SKILLS_PACK_ROOTS.map(root => `${root}/`),
111
+ ];
112
+
113
+ const REQUIRED_ISSUE_SUBCOMMANDS = [
114
+ 'ready',
115
+ 'list',
116
+ 'show',
117
+ 'search',
118
+ 'stats',
119
+ 'create',
120
+ 'update',
121
+ 'close',
122
+ 'comment',
123
+ 'dep',
124
+ ];
125
+
126
+ const REQUIRED_ISSUE_DEP_ACTIONS = ['add', 'remove'];
127
+
128
+ const REQUIRED_FORGE_SKILLS = [
129
+ { name: 'ready', commandPatterns: [/forge ready/, /forge issue ready/] },
130
+ { name: 'show', commandPatterns: [/forge show/, /forge issue show/] },
131
+ { name: 'claim', commandPatterns: [/forge claim/] },
132
+ { name: 'comment', commandPatterns: [/forge comment/, /forge issue comment/] },
133
+ { name: 'close', commandPatterns: [/forge close/, /forge issue close/] },
134
+ { name: 'recap', commandPatterns: [/forge recap/] },
135
+ ];
136
+
137
+ const REQUIRED_CLAIM_RELEASE_COMMANDS = [
138
+ {
139
+ name: 'claim',
140
+ path: 'lib/commands/claim.js',
141
+ // claim delegates to the SHARED issue dispatch (runIssueSubcommand) so the
142
+ // check-after-write verify loop (gate.issue_verify) covers it — that
143
+ // delegation is accepted as kernel evidence alongside a direct
144
+ // runIssueOperation('claim') dispatch.
145
+ contractPatterns: [
146
+ /forge claim <id>/,
147
+ /runIssueSubcommand\(['"]claim['"]|runIssueOperation\(['"]claim['"]|operation:\s*['"]claim['"]/,
148
+ ],
149
+ },
150
+ {
151
+ name: 'release',
152
+ path: 'lib/commands/release.js',
153
+ contractPatterns: [
154
+ /forge release <id>/,
155
+ /runIssueOperation\(['"]release['"]|operation:\s*['"]release['"]/,
156
+ ],
157
+ },
158
+ ];
159
+
160
+ const FORGE_SKILLS_PACK_CANDIDATES = FORGE_SKILLS_PACK_ROOTS.map(root => ({
161
+ manifest: `${root}/manifest.json`,
162
+ root,
163
+ }));
164
+
165
+ const FRESH_CLONE_ACCEPTANCE_CANDIDATES = [
166
+ 'test/e2e/fresh-clone-no-beads.test.js',
167
+ 'test/e2e/fresh-clone-no-beads-acceptance.test.js',
168
+ 'test/release/fresh-clone-no-beads.test.js',
169
+ ];
170
+
171
+ const FRESH_CLONE_ACCEPTANCE_REQUIREMENTS = [
172
+ {
173
+ name: 'enabled acceptance test',
174
+ matches: hasEnabledAcceptanceTest,
175
+ },
176
+ {
177
+ name: 'fresh clone setup',
178
+ patterns: [/fresh[-\s]?clone/i, /git\s+clone/i, /git\s*\(\s*\[\s*['"]clone['"]/i],
179
+ },
180
+ {
181
+ name: 'no Beads/Dolt environment',
182
+ patterns: [/no[-\s]?beads/i, /without\s+beads/i, /withoutTools\s*\([^)]*['"]bd['"][^)]*['"]dolt['"]/i],
183
+ },
184
+ {
185
+ name: 'forge prime',
186
+ patterns: [/forge\s+prime/i, /forge\s*\(\s*\[\s*['"]prime['"]/i],
187
+ },
188
+ {
189
+ name: 'forge issue ready --json',
190
+ patterns: [
191
+ /forge\s+issue\s+ready\b[^\r\n]*--json/i,
192
+ /forge\s*\(\s*\[\s*['"]issue['"]\s*,\s*['"]ready['"][^\]]*['"]--json['"]/i,
193
+ ],
194
+ },
195
+ {
196
+ name: 'forge claim',
197
+ patterns: [/forge\s+claim/i, /forge\s*\(\s*\[\s*['"]claim['"]/i],
198
+ },
199
+ {
200
+ name: 'forge comment',
201
+ patterns: [/forge\s+(?:issue\s+)?comment/i, /forge\s*\(\s*\[\s*['"]comment['"]/i],
202
+ },
203
+ {
204
+ name: 'forge close',
205
+ patterns: [/forge\s+(?:issue\s+)?close/i, /forge\s*\(\s*\[\s*['"]close['"]/i],
206
+ },
207
+ {
208
+ name: 'forge recap',
209
+ patterns: [/forge\s+recap/i, /forge\s*\(\s*\[\s*['"]recap['"]/i],
210
+ },
211
+ {
212
+ name: 'zero bd invocations',
213
+ patterns: [
214
+ /expect\s*\(\s*bdInvocations\s*\)\s*\.\s*(?:toEqual|toStrictEqual)\s*\(\s*\[\s*\]\s*\)/,
215
+ /expect\s*\(\s*bdInvocations\s*\)\s*\.\s*toHaveLength\s*\(\s*0\s*\)/,
216
+ /expect\s*\(\s*bdInvocations\s*\.\s*length\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(\s*0\s*\)/,
217
+ /assert\.(?:equal|strictEqual)\s*\(\s*bdInvocations\.length\s*,\s*0\s*\)/,
218
+ /assert\.deepStrictEqual\s*\(\s*bdInvocations\s*,\s*\[\s*\]\s*\)/,
219
+ ],
220
+ },
221
+ ];
222
+
223
+ const REQUIRED_KERNEL_ADAPTER_OPERATIONS = [
224
+ ...REQUIRED_ISSUE_SUBCOMMANDS,
225
+ ...REQUIRED_CLAIM_RELEASE_COMMANDS.map(command => command.name),
226
+ ];
227
+
228
+ function toRepoPath(filePath) {
229
+ return filePath.replaceAll(path.sep, '/');
230
+ }
231
+
232
+ function normalizeRepoPath(filePath) {
233
+ return toRepoPath(path.normalize(filePath)).replace(/^\.\//, '');
234
+ }
235
+
236
+ function isSafeRepoPath(projectRoot, relativePath) {
237
+ return (
238
+ typeof relativePath === 'string' &&
239
+ relativePath.length > 0 &&
240
+ !path.isAbsolute(relativePath) &&
241
+ isWithinProject(projectRoot, relativePath)
242
+ );
243
+ }
244
+
245
+ function absolutePath(projectRoot, relativePath) {
246
+ return path.join(projectRoot, ...relativePath.split('/'));
247
+ }
248
+
249
+ function isWithinProject(projectRoot, relativePath) {
250
+ const root = path.resolve(projectRoot);
251
+ const resolved = path.resolve(projectRoot, relativePath);
252
+ const relative = path.relative(root, resolved);
253
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
254
+ }
255
+
256
+ function shouldSkipRelativePath(relativePath) {
257
+ if (SKIP_RELATIVE_FILES.has(relativePath)) {
258
+ return true;
259
+ }
260
+
261
+ return SKIP_RELATIVE_PREFIXES.some(prefix => relativePath.startsWith(prefix));
262
+ }
263
+
264
+ function safeNormalizeRepoPath(projectRoot, filePath) {
265
+ if (typeof filePath !== 'string' || filePath.length === 0) {
266
+ return null;
267
+ }
268
+ const relativePath = normalizeRepoPath(filePath);
269
+ return isSafeRepoPath(projectRoot, relativePath) ? relativePath : null;
270
+ }
271
+
272
+ function readAgentPluginManifests(projectRoot) {
273
+ const agentsDir = absolutePath(projectRoot, 'lib/agents');
274
+ if (!fs.existsSync(agentsDir)) {
275
+ return [];
276
+ }
277
+
278
+ return fs.readdirSync(agentsDir, { withFileTypes: true })
279
+ .filter(entry => entry.isFile() && entry.name.endsWith('.plugin.json'))
280
+ .flatMap(entry => {
281
+ try {
282
+ return [JSON.parse(fs.readFileSync(path.join(agentsDir, entry.name), 'utf8'))];
283
+ } catch (_error) {
284
+ return [];
285
+ }
286
+ });
287
+ }
288
+
289
+ function collectAgentInstructionSurfaces(projectRoot) {
290
+ const rootDocFiles = [];
291
+ const instructionRoots = [];
292
+ const skillRoots = [];
293
+
294
+ for (const manifest of readAgentPluginManifests(projectRoot)) {
295
+ const rootConfig = safeNormalizeRepoPath(projectRoot, manifest.files?.rootConfig);
296
+ if (rootConfig) {
297
+ rootDocFiles.push(rootConfig);
298
+ }
299
+
300
+ for (const [name, directory] of Object.entries(manifest.directories || {})) {
301
+ const normalized = safeNormalizeRepoPath(projectRoot, directory);
302
+ if (!normalized) {
303
+ continue;
304
+ }
305
+
306
+ if (name === 'skills') {
307
+ skillRoots.push(normalized);
308
+ } else {
309
+ instructionRoots.push(normalized);
310
+ }
311
+ }
312
+ }
313
+
314
+ return {
315
+ instructionRoots: [...new Set([...STATIC_INSTRUCTION_ROOTS, ...instructionRoots])],
316
+ rootDocFiles: [...new Set([...STATIC_ROOT_DOC_FILES, ...rootDocFiles])],
317
+ skillRoots: [...new Set(skillRoots)],
318
+ };
319
+ }
320
+
321
+ function getRootDocFiles(projectRoot) {
322
+ return new Set(collectAgentInstructionSurfaces(projectRoot).rootDocFiles);
323
+ }
324
+
325
+ function getInstructionPrefixes(projectRoot) {
326
+ return collectAgentInstructionSurfaces(projectRoot).instructionRoots.map(root => `${root}/`);
327
+ }
328
+
329
+ function getSkillPrefixes(projectRoot) {
330
+ return collectAgentInstructionSurfaces(projectRoot).skillRoots.map(root => `${root}/`);
331
+ }
332
+
333
+ function getDefaultScanRoots(projectRoot) {
334
+ const surfaces = collectAgentInstructionSurfaces(projectRoot);
335
+ return [
336
+ ...new Set([
337
+ ...surfaces.rootDocFiles,
338
+ ...surfaces.skillRoots,
339
+ ...surfaces.instructionRoots,
340
+ ...STATIC_SCAN_ROOTS,
341
+ ]),
342
+ ];
343
+ }
344
+
345
+ function isTextFile(relativePath, projectRoot) {
346
+ const basename = path.basename(relativePath);
347
+ if (basename.endsWith('.test.js')) {
348
+ return false;
349
+ }
350
+ if (getRootDocFiles(projectRoot).has(relativePath) || basename === 'lefthook.yml') {
351
+ return true;
352
+ }
353
+ return TEXT_EXTENSIONS.has(path.extname(relativePath));
354
+ }
355
+
356
+ function walkFiles(projectRoot, relativeRoot) {
357
+ const rootPath = absolutePath(projectRoot, relativeRoot);
358
+ if (!fs.existsSync(rootPath)) {
359
+ return [];
360
+ }
361
+
362
+ const stat = fs.statSync(rootPath);
363
+ if (stat.isFile()) {
364
+ const normalized = toRepoPath(relativeRoot);
365
+ return isTextFile(normalized, projectRoot) && !shouldSkipRelativePath(normalized) ? [normalized] : [];
366
+ }
367
+
368
+ const files = [];
369
+ const entries = fs.readdirSync(rootPath, { withFileTypes: true })
370
+ .sort((left, right) => left.name.localeCompare(right.name));
371
+
372
+ for (const entry of entries) {
373
+ if (entry.isDirectory() && SKIP_DIR_NAMES.has(entry.name)) {
374
+ continue;
375
+ }
376
+
377
+ const relativePath = toRepoPath(path.join(relativeRoot, entry.name));
378
+ if (shouldSkipRelativePath(relativePath)) {
379
+ continue;
380
+ }
381
+
382
+ if (entry.isDirectory()) {
383
+ files.push(...walkFiles(projectRoot, relativePath));
384
+ } else if (entry.isFile() && isTextFile(relativePath, projectRoot)) {
385
+ files.push(relativePath);
386
+ }
387
+ }
388
+
389
+ return files;
390
+ }
391
+
392
+ function readSyncManifestScanRoots(projectRoot) {
393
+ const manifestPath = absolutePath(projectRoot, '.forge/sync-manifest.json');
394
+ if (!fs.existsSync(manifestPath)) {
395
+ return [];
396
+ }
397
+
398
+ try {
399
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
400
+ if (!Array.isArray(manifest.files)) {
401
+ return [];
402
+ }
403
+ return manifest.files
404
+ .filter(file => typeof file === 'string')
405
+ .map(normalizeRepoPath)
406
+ .filter(file => (
407
+ file.length > 0 &&
408
+ isSafeRepoPath(projectRoot, file) &&
409
+ isTextFile(file, projectRoot)
410
+ ));
411
+ } catch (_error) {
412
+ return [];
413
+ }
414
+ }
415
+
416
+ function getScanRoots(projectRoot, options = {}) {
417
+ const roots = options.scanRoots || getDefaultScanRoots(projectRoot);
418
+ return [
419
+ ...new Set([
420
+ ...roots,
421
+ ...readSyncManifestScanRoots(projectRoot),
422
+ ]),
423
+ ];
424
+ }
425
+
426
+ function isInstructionPath(relativePath, projectRoot) {
427
+ return getInstructionPrefixes(projectRoot).some(prefix => relativePath.startsWith(prefix));
428
+ }
429
+
430
+ function classifyGroup(relativePath, projectRoot) {
431
+ if (
432
+ relativePath === 'lefthook.yml' ||
433
+ relativePath.startsWith('.forge/hooks/') ||
434
+ relativePath.startsWith('.github/workflows/')
435
+ ) {
436
+ return 'hooks';
437
+ }
438
+
439
+ if (
440
+ getSkillPrefixes(projectRoot).some(prefix => relativePath.startsWith(prefix)) ||
441
+ relativePath.startsWith('packages/skills/')
442
+ ) {
443
+ return 'skills';
444
+ }
445
+
446
+ if (relativePath.startsWith('lib/commands/') || relativePath.startsWith('bin/')) {
447
+ return 'command';
448
+ }
449
+
450
+ if (
451
+ relativePath.startsWith('docs/') ||
452
+ getRootDocFiles(projectRoot).has(relativePath) ||
453
+ isInstructionPath(relativePath, projectRoot)
454
+ ) {
455
+ return 'docs';
456
+ }
457
+
458
+ return 'runtime';
459
+ }
460
+
461
+ function findBdTerms(line) {
462
+ const terms = [];
463
+ if (/\bbd\b/i.test(line)) terms.push('bd');
464
+ if (/\.beads\b/i.test(line)) terms.push('.beads');
465
+ if (/\bdolt\b/i.test(line)) terms.push('dolt');
466
+ return [...new Set(terms)];
467
+ }
468
+
469
+ function newGroupedAudit() {
470
+ const groups = {};
471
+ for (const group of GROUPS) {
472
+ groups[group] = { count: 0, fileCount: 0, files: [] };
473
+ }
474
+ return groups;
475
+ }
476
+
477
+ function addCallSites(groups, group, relativePath, lineEntries) {
478
+ if (lineEntries.length === 0) {
479
+ return;
480
+ }
481
+
482
+ const target = groups[group];
483
+ target.count += lineEntries.length;
484
+ target.fileCount += 1;
485
+ target.files.push({
486
+ path: relativePath,
487
+ count: lineEntries.length,
488
+ lines: lineEntries,
489
+ });
490
+ }
491
+
492
+ // Git-tracked repo files (repo-relative, forward-slash), or null when this isn't a git
493
+ // repo (then callers fall back to the raw walk). Restricting the bd-audit to tracked files
494
+ // makes the census deterministic under a dirty/parallel working tree: an untracked temp
495
+ // file a concurrent test drops under a scan root no longer perturbs it (issue 1d4077d8).
496
+ function trackedRepoFiles(projectRoot) {
497
+ let output;
498
+ try {
499
+ // NOSONAR S4036 — `git` is resolved from PATH by design (cross-platform CLI; the repo
500
+ // spawns git this way throughout and cannot hardcode a portable absolute path).
501
+ output = execFileSync('git', ['-C', projectRoot, 'ls-files', '-z'], {
502
+ encoding: 'utf8',
503
+ maxBuffer: 64 * 1024 * 1024,
504
+ });
505
+ } catch (error) {
506
+ // Fall back to the raw walk ONLY for a genuine non-git tree (which legitimately has no
507
+ // tracked set). Any OTHER git/exec failure must NOT silently reintroduce the untracked-
508
+ // scan nondeterminism this fix removes — surface it instead of masking it.
509
+ if (/not a git repository/i.test(String(error && error.stderr))) {
510
+ return null;
511
+ }
512
+ throw error;
513
+ }
514
+ const tracked = new Set();
515
+ for (const entry of output.split('\0')) {
516
+ if (entry) {
517
+ tracked.add(normalizeRepoPath(entry));
518
+ }
519
+ }
520
+ return tracked;
521
+ }
522
+
523
+ function auditBdCallSites(projectRoot, options = {}) {
524
+ const roots = getScanRoots(projectRoot, options);
525
+ // Only audit git-tracked files, so untracked working-tree pollution (e.g. a concurrent
526
+ // test's temp files under a scan root) can't perturb the census (issue 1d4077d8).
527
+ const tracked = trackedRepoFiles(projectRoot);
528
+ const files = [...new Set(roots.flatMap(root => walkFiles(projectRoot, root)))]
529
+ .filter(relativePath => !tracked || tracked.has(normalizeRepoPath(relativePath)))
530
+ .sort((left, right) => left.localeCompare(right));
531
+ const groups = newGroupedAudit();
532
+
533
+ for (const relativePath of files) {
534
+ const fullPath = absolutePath(projectRoot, relativePath);
535
+ const content = fs.readFileSync(fullPath, 'utf8');
536
+ const lineEntries = [];
537
+ const lines = content.split(/\r?\n/);
538
+
539
+ for (let index = 0; index < lines.length; index++) {
540
+ const terms = findBdTerms(lines[index]);
541
+ if (terms.length > 0) {
542
+ lineEntries.push({
543
+ line: index + 1,
544
+ terms,
545
+ });
546
+ }
547
+ }
548
+
549
+ addCallSites(groups, classifyGroup(relativePath, projectRoot), relativePath, lineEntries);
550
+ }
551
+
552
+ return {
553
+ artifact: AUDIT_ARTIFACT,
554
+ groups,
555
+ totalCount: GROUPS.reduce((total, group) => total + groups[group].count, 0),
556
+ totalFiles: GROUPS.reduce((total, group) => total + groups[group].fileCount, 0),
557
+ };
558
+ }
559
+
560
+ function readRepoFile(projectRoot, relativePath) {
561
+ const fullPath = absolutePath(projectRoot, relativePath);
562
+ return fs.existsSync(fullPath) ? fs.readFileSync(fullPath, 'utf8') : '';
563
+ }
564
+
565
+ function parseScriptAst(source) {
566
+ try {
567
+ return babelParser.parse(source, { sourceType: 'script' });
568
+ } catch (_error) {
569
+ return null;
570
+ }
571
+ }
572
+
573
+ function isFunctionLikeNode(node) {
574
+ return node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
575
+ }
576
+
577
+ function collectTopLevelCommandBindings(ast) {
578
+ const bindings = {
579
+ functions: new Set(),
580
+ strings: new Map(),
581
+ };
582
+
583
+ for (const node of ast?.program?.body || []) {
584
+ if (node.type === 'FunctionDeclaration' && node.id?.name) {
585
+ bindings.functions.add(node.id.name);
586
+ continue;
587
+ }
588
+
589
+ if (node.type !== 'VariableDeclaration') {
590
+ continue;
591
+ }
592
+
593
+ for (const declaration of node.declarations) {
594
+ if (declaration.id?.type !== 'Identifier') {
595
+ continue;
596
+ }
597
+
598
+ if (isFunctionLikeNode(declaration.init)) {
599
+ bindings.functions.add(declaration.id.name);
600
+ }
601
+
602
+ const stringValue = stringLiteralValue(declaration.init);
603
+ if (typeof stringValue === 'string') {
604
+ bindings.strings.set(declaration.id.name, stringValue);
605
+ }
606
+ }
607
+ }
608
+
609
+ return bindings;
610
+ }
611
+
612
+ function isModuleExportsMember(node) {
613
+ return node?.type === 'MemberExpression' &&
614
+ node.object?.type === 'Identifier' &&
615
+ node.object.name === 'module' &&
616
+ !node.computed &&
617
+ node.property?.type === 'Identifier' &&
618
+ node.property.name === 'exports';
619
+ }
620
+
621
+ function findModuleExportsObject(ast) {
622
+ for (const node of ast?.program?.body || []) {
623
+ if (
624
+ node.type === 'ExpressionStatement' &&
625
+ node.expression?.type === 'AssignmentExpression' &&
626
+ isModuleExportsMember(node.expression.left) &&
627
+ node.expression.right?.type === 'ObjectExpression'
628
+ ) {
629
+ return node.expression.right;
630
+ }
631
+ }
632
+
633
+ return null;
634
+ }
635
+
636
+ function findObjectMember(node, name) {
637
+ if (node?.type !== 'ObjectExpression') {
638
+ return null;
639
+ }
640
+
641
+ return node.properties.find(property => objectPropertyName(property) === name) || null;
642
+ }
643
+
644
+ function resolvedStringValue(node, stringBindings) {
645
+ const literalValue = stringLiteralValue(node);
646
+ if (typeof literalValue === 'string') {
647
+ return literalValue;
648
+ }
649
+
650
+ if (node?.type === 'Identifier') {
651
+ return stringBindings.get(node.name) || null;
652
+ }
653
+
654
+ return null;
655
+ }
656
+
657
+ function hasRegistryCommandHandler(property, functionBindings) {
658
+ if (!property) {
659
+ return false;
660
+ }
661
+
662
+ if (property.type === 'ObjectMethod') {
663
+ return true;
664
+ }
665
+
666
+ if (isFunctionLikeNode(property.value)) {
667
+ return true;
668
+ }
669
+
670
+ return property.value?.type === 'Identifier' && functionBindings.has(property.value.name);
671
+ }
672
+
673
+ // Mirror the CLI registry contract without executing future command modules during the gate.
674
+ function commandModuleHasRegistryContract(projectRoot, name) {
675
+ const source = readRepoFile(projectRoot, `lib/commands/${name}.js`);
676
+ if (!source) {
677
+ return false;
678
+ }
679
+
680
+ const ast = parseScriptAst(source);
681
+ if (!ast) {
682
+ return false;
683
+ }
684
+
685
+ const bindings = collectTopLevelCommandBindings(ast);
686
+ const exportedObject = findModuleExportsObject(ast);
687
+ const exportedName = resolvedStringValue(findObjectMember(exportedObject, 'name')?.value, bindings.strings);
688
+ const description = resolvedStringValue(findObjectMember(exportedObject, 'description')?.value, bindings.strings);
689
+
690
+ return exportedName === name &&
691
+ typeof description === 'string' &&
692
+ description.trim().length > 0 &&
693
+ hasRegistryCommandHandler(findObjectMember(exportedObject, 'handler'), bindings.functions);
694
+ }
695
+
696
+ function isD20HotPath(relativePath, projectRoot) {
697
+ const surfaces = collectAgentInstructionSurfaces(projectRoot);
698
+ const hotPathFiles = new Set([
699
+ ...D20_HOT_PATH_FILES,
700
+ ...surfaces.rootDocFiles,
701
+ ]);
702
+ const hotPathPrefixes = [
703
+ ...D20_HOT_PATH_PREFIXES,
704
+ ...surfaces.instructionRoots.map(root => `${root}/`),
705
+ ...surfaces.skillRoots.map(root => `${root}/`),
706
+ ];
707
+
708
+ return hotPathFiles.has(relativePath) ||
709
+ hotPathPrefixes.some(prefix => relativePath.startsWith(prefix));
710
+ }
711
+
712
+ function flattenHotPathEvidence(audit, projectRoot) {
713
+ const files = [];
714
+ for (const group of GROUPS) {
715
+ files.push(...audit.groups[group].files);
716
+ }
717
+
718
+ return files
719
+ .filter(file => isD20HotPath(file.path, projectRoot))
720
+ .filter(Boolean)
721
+ .map(file => ({
722
+ path: file.path,
723
+ count: file.count,
724
+ lines: file.lines.map(line => line.line),
725
+ }));
726
+ }
727
+
728
+ function objectPropertyName(property) {
729
+ return property.key?.name || property.key?.value;
730
+ }
731
+
732
+ function memberPropertyName(memberExpression) {
733
+ return memberExpression.property?.name || memberExpression.property?.value;
734
+ }
735
+
736
+ function memberObjectName(memberExpression) {
737
+ return memberExpression.object?.name || memberExpression.object?.value;
738
+ }
739
+
740
+ function calleeInfo(callee) {
741
+ if (callee?.type === 'Identifier') {
742
+ return { name: callee.name, objectName: null, propertyName: null };
743
+ }
744
+
745
+ if (callee?.type === 'MemberExpression') {
746
+ return {
747
+ name: null,
748
+ objectName: memberObjectName(callee),
749
+ propertyName: memberPropertyName(callee),
750
+ };
751
+ }
752
+
753
+ return { name: null, objectName: null, propertyName: null };
754
+ }
755
+
756
+ function hasEnabledAcceptanceTest(source) {
757
+ return extractEnabledTestSources(source).length > 0;
758
+ }
759
+
760
+ function extractEnabledTestSources(source) {
761
+ const ast = parseScriptAst(source);
762
+ if (!ast) {
763
+ return /\b(?:test|it)\s*\(/.test(source) &&
764
+ !/\b(?:describe|test|it)\s*\.\s*(?:skip|todo)\s*\(/.test(source)
765
+ ? [source]
766
+ : [];
767
+ }
768
+
769
+ const tests = [];
770
+
771
+ const visit = (node, disabled) => {
772
+ if (!node || typeof node !== 'object') {
773
+ return;
774
+ }
775
+
776
+ let nextDisabled = disabled;
777
+ if (node.type === 'CallExpression') {
778
+ const { name, objectName, propertyName } = calleeInfo(node.callee);
779
+ const skippedCall = ['describe', 'test', 'it'].includes(objectName) &&
780
+ ['skip', 'todo'].includes(propertyName);
781
+ nextDisabled = disabled || skippedCall;
782
+
783
+ const directEnabledTest = ['test', 'it'].includes(name);
784
+ const memberEnabledTest = ['test', 'it'].includes(objectName) &&
785
+ propertyName &&
786
+ !['skip', 'todo'].includes(propertyName);
787
+
788
+ if (!nextDisabled && (directEnabledTest || memberEnabledTest)) {
789
+ tests.push(source.slice(node.start, node.end));
790
+ }
791
+ }
792
+
793
+ for (const value of Object.values(node)) {
794
+ if (Array.isArray(value)) {
795
+ for (const child of value) {
796
+ visit(child, nextDisabled);
797
+ }
798
+ } else if (value?.type) {
799
+ visit(value, nextDisabled);
800
+ }
801
+ }
802
+ };
803
+
804
+ visit(ast, false);
805
+ return tests;
806
+ }
807
+
808
+ function parseSubcommandsObject(source) {
809
+ try {
810
+ const ast = babelParser.parse(source, { sourceType: 'script' });
811
+ const declaration = ast.program.body
812
+ .filter(node => node.type === 'VariableDeclaration')
813
+ .flatMap(node => node.declarations)
814
+ .find(node => node.id?.name === 'SUBCOMMANDS' && node.init?.type === 'ObjectExpression');
815
+ return declaration?.init || null;
816
+ } catch (_error) {
817
+ return null;
818
+ }
819
+ }
820
+
821
+ function extractIssueSubcommands(source) {
822
+ const subcommands = parseSubcommandsObject(source);
823
+ if (!subcommands) {
824
+ return [];
825
+ }
826
+
827
+ return subcommands.properties
828
+ .map(objectPropertyName)
829
+ .filter(name => typeof name === 'string');
830
+ }
831
+
832
+ function extractIssueDepActions(source) {
833
+ const subcommands = parseSubcommandsObject(source);
834
+ const depProperty = subcommands?.properties.find(property => objectPropertyName(property) === 'dep');
835
+ const actionsProperty = findObjectMember(depProperty?.value, 'actions');
836
+ const actionNames = actionsProperty?.value?.type === 'ObjectExpression'
837
+ ? actionsProperty.value.properties.map(objectPropertyName)
838
+ : [];
839
+ return REQUIRED_ISSUE_DEP_ACTIONS.filter(action => actionNames.includes(action));
840
+ }
841
+
842
+ function walkAst(node, visitor) {
843
+ if (!node || typeof node !== 'object') {
844
+ return;
845
+ }
846
+
847
+ visitor(node);
848
+
849
+ for (const value of Object.values(node)) {
850
+ if (Array.isArray(value)) {
851
+ value.forEach(child => walkAst(child, visitor));
852
+ } else if (value && typeof value === 'object' && typeof value.type === 'string') {
853
+ walkAst(value, visitor);
854
+ }
855
+ }
856
+ }
857
+
858
+ function stringLiteralValue(node) {
859
+ if (node?.type === 'StringLiteral') {
860
+ return node.value;
861
+ }
862
+ return null;
863
+ }
864
+
865
+ function booleanLiteralValue(node) {
866
+ if (node?.type === 'BooleanLiteral') {
867
+ return node.value;
868
+ }
869
+ return null;
870
+ }
871
+
872
+ function canProvideKernelBroker(node) {
873
+ if (!node) {
874
+ return false;
875
+ }
876
+ if (node.type === 'NullLiteral') {
877
+ return false;
878
+ }
879
+ if (node.type === 'Identifier') {
880
+ return node.name !== 'undefined';
881
+ }
882
+ if (
883
+ node.type === 'BooleanLiteral' ||
884
+ node.type === 'StringLiteral' ||
885
+ node.type === 'NumericLiteral'
886
+ ) {
887
+ return false;
888
+ }
889
+ return true;
890
+ }
891
+
892
+ function objectHasKernelIssueOption(node) {
893
+ if (node?.type !== 'ObjectExpression') {
894
+ return false;
895
+ }
896
+
897
+ return node.properties.some(property => {
898
+ const name = objectPropertyName(property);
899
+ return (
900
+ (name === 'issueBackend' && stringLiteralValue(property.value) === 'kernel') ||
901
+ (name === 'useKernelBroker' && booleanLiteralValue(property.value) === true) ||
902
+ (name === 'kernelBroker' && canProvideKernelBroker(property.value))
903
+ );
904
+ });
905
+ }
906
+
907
+ function callHasKernelIssueOptions(node) {
908
+ return node.arguments.slice(3).some(objectHasKernelIssueOption);
909
+ }
910
+
911
+ function isRunIssueOperationCall(node) {
912
+ return node?.type === 'CallExpression' &&
913
+ node.callee?.type === 'Identifier' &&
914
+ node.callee.name === 'runIssueOperation';
915
+ }
916
+
917
+ function collectIssueOperationCoverage(source) {
918
+ const coverage = {
919
+ kernelDynamic: false,
920
+ kernelOperations: new Set(),
921
+ nonKernelDynamic: false,
922
+ nonKernelOperations: new Set(),
923
+ };
924
+
925
+ try {
926
+ const ast = babelParser.parse(source, { sourceType: 'script' });
927
+ walkAst(ast, node => {
928
+ if (!isRunIssueOperationCall(node)) {
929
+ return;
930
+ }
931
+
932
+ const operation = stringLiteralValue(node.arguments[0]);
933
+ const target = callHasKernelIssueOptions(node)
934
+ ? { dynamic: 'kernelDynamic', operations: coverage.kernelOperations }
935
+ : { dynamic: 'nonKernelDynamic', operations: coverage.nonKernelOperations };
936
+ if (operation) {
937
+ target.operations.add(operation);
938
+ } else {
939
+ coverage[target.dynamic] = true;
940
+ }
941
+ });
942
+ } catch (_error) {
943
+ // Leave coverage empty on parse failures.
944
+ }
945
+
946
+ return coverage;
947
+ }
948
+
949
+ function sourceHasOnlyKernelBackedIssueOperations(source, operations) {
950
+ try {
951
+ const ast = babelParser.parse(source, { sourceType: 'script' });
952
+ const reachableRoots = collectHandlerReachableRoots(ast);
953
+ if (reachableRoots.length === 0) {
954
+ return false;
955
+ }
956
+
957
+ let found = false;
958
+ let hasNonKernelMatch = false;
959
+ for (const root of reachableRoots) {
960
+ walkExecutableNodes(root, node => {
961
+ if (!isRunIssueOperationCall(node)) {
962
+ return;
963
+ }
964
+
965
+ const operation = stringLiteralValue(node.arguments[0]);
966
+ const matchesOperation = operation ? operations.has(operation) : true;
967
+ if (!matchesOperation) {
968
+ return;
969
+ }
970
+
971
+ if (callHasKernelIssueOptions(node)) {
972
+ found = true;
973
+ } else {
974
+ hasNonKernelMatch = true;
975
+ }
976
+ });
977
+ }
978
+
979
+ return found && !hasNonKernelMatch;
980
+ } catch (_error) {
981
+ return false;
982
+ }
983
+ }
984
+
985
+ function sourceHasIssueSurfaceKernelOptions(source) {
986
+ const coverage = collectIssueOperationCoverage(source);
987
+ if (coverage.nonKernelDynamic) {
988
+ return false;
989
+ }
990
+
991
+ if (REQUIRED_ISSUE_SUBCOMMANDS.some(command => coverage.nonKernelOperations.has(command))) {
992
+ return false;
993
+ }
994
+
995
+ return coverage.kernelDynamic ||
996
+ REQUIRED_ISSUE_SUBCOMMANDS.every(command => coverage.kernelOperations.has(command));
997
+ }
998
+
999
+ function findModuleExportsAssignment(ast) {
1000
+ for (const node of ast?.program?.body || []) {
1001
+ if (
1002
+ node.type === 'ExpressionStatement' &&
1003
+ node.expression?.type === 'AssignmentExpression' &&
1004
+ isModuleExportsMember(node.expression.left)
1005
+ ) {
1006
+ return node.expression.right;
1007
+ }
1008
+ }
1009
+
1010
+ return null;
1011
+ }
1012
+
1013
+ function isRequireCallOf(node, requestPaths) {
1014
+ return node?.type === 'CallExpression' &&
1015
+ node.callee?.type === 'Identifier' &&
1016
+ node.callee.name === 'require' &&
1017
+ requestPaths.includes(stringLiteralValue(node.arguments?.[0]));
1018
+ }
1019
+
1020
+ function extractRequireBindingNames(declaration, requestPaths) {
1021
+ if (!isRequireCallOf(declaration.init, requestPaths)) {
1022
+ return [];
1023
+ }
1024
+
1025
+ if (declaration.id?.type === 'Identifier') {
1026
+ return [declaration.id.name];
1027
+ }
1028
+
1029
+ if (declaration.id?.type === 'ObjectPattern') {
1030
+ return declaration.id.properties
1031
+ .filter(property => property.type === 'ObjectProperty' && property.value?.type === 'Identifier')
1032
+ .map(property => property.value.name);
1033
+ }
1034
+
1035
+ return [];
1036
+ }
1037
+
1038
+ function requireLocalNames(ast, requestPaths) {
1039
+ const names = new Set();
1040
+ for (const node of ast?.program?.body || []) {
1041
+ if (node.type !== 'VariableDeclaration') {
1042
+ continue;
1043
+ }
1044
+
1045
+ for (const declaration of node.declarations) {
1046
+ for (const name of extractRequireBindingNames(declaration, requestPaths)) {
1047
+ names.add(name);
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ return names;
1053
+ }
1054
+
1055
+ function collectTopLevelFunctions(ast) {
1056
+ const functions = new Map();
1057
+ for (const node of ast?.program?.body || []) {
1058
+ if (node.type === 'FunctionDeclaration' && node.id?.name) {
1059
+ functions.set(node.id.name, node);
1060
+ continue;
1061
+ }
1062
+
1063
+ if (node.type === 'VariableDeclaration') {
1064
+ for (const declaration of node.declarations) {
1065
+ if (declaration.id?.type === 'Identifier' && isFunctionLikeNode(declaration.init)) {
1066
+ functions.set(declaration.id.name, declaration.init);
1067
+ }
1068
+ }
1069
+ }
1070
+ }
1071
+
1072
+ return functions;
1073
+ }
1074
+
1075
+ function exportedHandlerNode(ast) {
1076
+ const exportsObject = findModuleExportsObject(ast);
1077
+ const handlerProperty = findObjectMember(exportsObject, 'handler');
1078
+ if (!handlerProperty) {
1079
+ return { node: null, identifierName: null };
1080
+ }
1081
+
1082
+ if (handlerProperty.type === 'ObjectMethod') {
1083
+ return { node: handlerProperty, identifierName: null };
1084
+ }
1085
+
1086
+ if (isFunctionLikeNode(handlerProperty.value)) {
1087
+ return { node: handlerProperty.value, identifierName: null };
1088
+ }
1089
+
1090
+ if (handlerProperty.value?.type === 'Identifier') {
1091
+ return { node: null, identifierName: handlerProperty.value.name };
1092
+ }
1093
+
1094
+ return { node: null, identifierName: null };
1095
+ }
1096
+
1097
+ function isFunctionNode(node) {
1098
+ return node?.type === 'FunctionDeclaration' ||
1099
+ node?.type === 'FunctionExpression' ||
1100
+ node?.type === 'ArrowFunctionExpression' ||
1101
+ node?.type === 'ObjectMethod' ||
1102
+ node?.type === 'ClassMethod';
1103
+ }
1104
+
1105
+ // Walk a root node visiting its own executable body but NOT descending into
1106
+ // nested function definitions. Dead/unused inner closures therefore contribute
1107
+ // no evidence; code in another function only counts when that function is itself
1108
+ // reached through a real call (see collectHandlerReachableRoots).
1109
+ function walkExecutableNodes(root, visitor) {
1110
+ const visit = node => {
1111
+ if (!node || typeof node !== 'object' || typeof node.type !== 'string') {
1112
+ return;
1113
+ }
1114
+ visitor(node);
1115
+ if (isFunctionNode(node) && node !== root) {
1116
+ return;
1117
+ }
1118
+ for (const value of Object.values(node)) {
1119
+ if (Array.isArray(value)) {
1120
+ value.forEach(visit);
1121
+ } else {
1122
+ visit(value);
1123
+ }
1124
+ }
1125
+ };
1126
+ visit(root);
1127
+ }
1128
+
1129
+ // Collect the function bodies actually reachable from the exported `handler`, so evidence
1130
+ // scans ignore unused helpers that never run when `forge <command>` dispatches.
1131
+ function collectHandlerReachableRoots(ast) {
1132
+ const functions = collectTopLevelFunctions(ast);
1133
+ const { node, identifierName } = exportedHandlerNode(ast);
1134
+
1135
+ const visited = new Set();
1136
+ const roots = [];
1137
+ const queue = [];
1138
+ if (node) {
1139
+ queue.push(node);
1140
+ } else if (identifierName && functions.has(identifierName)) {
1141
+ queue.push(functions.get(identifierName));
1142
+ }
1143
+
1144
+ while (queue.length > 0) {
1145
+ const current = queue.shift();
1146
+ if (!current || visited.has(current)) {
1147
+ continue;
1148
+ }
1149
+ visited.add(current);
1150
+ roots.push(current);
1151
+
1152
+ walkExecutableNodes(current, inner => {
1153
+ if (inner.type === 'CallExpression' && inner.callee?.type === 'Identifier') {
1154
+ const target = functions.get(inner.callee.name);
1155
+ if (target && !visited.has(target)) {
1156
+ queue.push(target);
1157
+ }
1158
+ }
1159
+ });
1160
+ }
1161
+
1162
+ return roots;
1163
+ }
1164
+
1165
+ function publicIssueHandlerDelegatesToSurface(source) {
1166
+ if (!source) {
1167
+ return false;
1168
+ }
1169
+
1170
+ let ast;
1171
+ try {
1172
+ ast = babelParser.parse(source, { sourceType: 'script' });
1173
+ } catch (_error) {
1174
+ return false;
1175
+ }
1176
+
1177
+ const surfaceBindings = requireLocalNames(ast, ['./_issue', './_issue.js']);
1178
+ if (surfaceBindings.size === 0) {
1179
+ return false;
1180
+ }
1181
+
1182
+ const exportsValue = findModuleExportsAssignment(ast);
1183
+ if (!exportsValue) {
1184
+ return false;
1185
+ }
1186
+
1187
+ const roots = exportsValue.type === 'ObjectExpression'
1188
+ ? collectHandlerReachableRoots(ast)
1189
+ : [exportsValue];
1190
+ if (roots.length === 0) {
1191
+ return false;
1192
+ }
1193
+
1194
+ let delegates = false;
1195
+ for (const root of roots) {
1196
+ walkExecutableNodes(root, node => {
1197
+ if (node.type !== 'CallExpression') {
1198
+ return;
1199
+ }
1200
+ // Delegation requires actually CALLING a binding from ./_issue (e.g.
1201
+ // dispatch(...) or createIssueCommand()), not merely referencing it —
1202
+ // `void dispatch` / `typeof dispatch` must not count as delegation.
1203
+ const callee = node.callee;
1204
+ if (callee?.type === 'Identifier' && surfaceBindings.has(callee.name)) {
1205
+ delegates = true;
1206
+ } else if (
1207
+ callee?.type === 'MemberExpression' &&
1208
+ callee.object?.type === 'Identifier' &&
1209
+ surfaceBindings.has(callee.object.name)
1210
+ ) {
1211
+ delegates = true;
1212
+ }
1213
+ });
1214
+ }
1215
+
1216
+ return delegates;
1217
+ }
1218
+
1219
+ function stripJsComments(code) {
1220
+ let ast;
1221
+ try {
1222
+ ast = babelParser.parse(code, { sourceType: 'script' });
1223
+ } catch (_error) {
1224
+ return code;
1225
+ }
1226
+
1227
+ const comments = ast.comments || [];
1228
+ if (comments.length === 0) {
1229
+ return code;
1230
+ }
1231
+
1232
+ const chars = [...code];
1233
+ for (const comment of comments) {
1234
+ for (let index = comment.start; index < comment.end && index < chars.length; index += 1) {
1235
+ if (chars[index] !== '\n' && chars[index] !== '\r') {
1236
+ chars[index] = ' ';
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ return chars.join('');
1242
+ }
1243
+
1244
+ function extractKernelAdapterOperations(projectRoot) {
1245
+ const source = readRepoFile(projectRoot, 'lib/adapters/kernel-issue-adapter.js');
1246
+ if (!source) {
1247
+ return [];
1248
+ }
1249
+
1250
+ const operations = new Set();
1251
+ try {
1252
+ const ast = babelParser.parse(source, { sourceType: 'script' });
1253
+ walkAst(ast, node => {
1254
+ if (
1255
+ node.type === 'VariableDeclarator' &&
1256
+ node.id?.name === 'KERNEL_ISSUE_OPERATIONS' &&
1257
+ node.init?.type === 'CallExpression' &&
1258
+ node.init.arguments[0]?.type === 'ObjectExpression'
1259
+ ) {
1260
+ for (const property of node.init.arguments[0].properties) {
1261
+ const name = objectPropertyName(property);
1262
+ const value = stringLiteralValue(property.value);
1263
+ if (typeof name === 'string') operations.add(name);
1264
+ if (typeof value === 'string') operations.add(value);
1265
+ }
1266
+ }
1267
+
1268
+ if (node.type === 'ClassMethod' && node.key) {
1269
+ const name = objectPropertyName(node);
1270
+ if (typeof name === 'string') {
1271
+ operations.add(name);
1272
+ }
1273
+ }
1274
+ });
1275
+ } catch (_error) {
1276
+ return [];
1277
+ }
1278
+
1279
+ return [...operations];
1280
+ }
1281
+
1282
+ function kernelAdapterSupportsOperation(operations, operation) {
1283
+ return operations.has(operation) || (operation === 'show' && operations.has('read'));
1284
+ }
1285
+
1286
+ function missingKernelAdapterOperations(projectRoot) {
1287
+ const operations = new Set(extractKernelAdapterOperations(projectRoot));
1288
+ return REQUIRED_KERNEL_ADAPTER_OPERATIONS.filter(operation =>
1289
+ !kernelAdapterSupportsOperation(operations, operation)
1290
+ );
1291
+ }
1292
+
1293
+ function hasKernelBackedIssueSurface(projectRoot) {
1294
+ const source = readRepoFile(projectRoot, 'lib/commands/_issue.js');
1295
+ const issueCommandSource = readRepoFile(projectRoot, 'lib/commands/issue.js');
1296
+ const registryValid = commandModuleHasRegistryContract(projectRoot, 'issue');
1297
+ const delegatesToSurface = publicIssueHandlerDelegatesToSurface(issueCommandSource);
1298
+ const missingAdapterOperations = missingKernelAdapterOperations(projectRoot);
1299
+ if (!source) {
1300
+ return {
1301
+ ok: false,
1302
+ missingSubcommands: REQUIRED_ISSUE_SUBCOMMANDS,
1303
+ missingDepActions: REQUIRED_ISSUE_DEP_ACTIONS,
1304
+ missingAdapterOperations,
1305
+ missingKernelEvidence: true,
1306
+ beadsBacked: true,
1307
+ registryValid,
1308
+ delegatesToSurface,
1309
+ };
1310
+ }
1311
+
1312
+ const subcommands = extractIssueSubcommands(source);
1313
+ const missingSubcommands = REQUIRED_ISSUE_SUBCOMMANDS.filter(command => !subcommands.includes(command));
1314
+ const depActions = extractIssueDepActions(source);
1315
+ const missingDepActions = REQUIRED_ISSUE_DEP_ACTIONS.filter(action => !depActions.includes(action));
1316
+ const missingKernelEvidence = !defaultIssueBackendIsKernel(projectRoot) &&
1317
+ !sourceHasIssueSurfaceKernelOptions(source);
1318
+ const beadsBacked = isBeadsBackedCommandSource(source) ||
1319
+ isBeadsBackedCommandSource(issueCommandSource);
1320
+
1321
+ return {
1322
+ ok: missingSubcommands.length === 0 &&
1323
+ missingDepActions.length === 0 &&
1324
+ missingAdapterOperations.length === 0 &&
1325
+ !missingKernelEvidence &&
1326
+ !beadsBacked &&
1327
+ registryValid &&
1328
+ delegatesToSurface,
1329
+ missingSubcommands,
1330
+ missingDepActions,
1331
+ missingAdapterOperations,
1332
+ missingKernelEvidence,
1333
+ beadsBacked,
1334
+ registryValid,
1335
+ delegatesToSurface,
1336
+ };
1337
+ }
1338
+
1339
+ function isBeadsBackedCommandSource(source) {
1340
+ return /makeAliasCommand|buildBdArgs|Beads issue|bd-|\.beads/.test(source) ||
1341
+ sourceHasDirectBdInvocation(source);
1342
+ }
1343
+
1344
+ function callCalleeName(callee) {
1345
+ if (callee?.type === 'Identifier') {
1346
+ return callee.name;
1347
+ }
1348
+
1349
+ if (callee?.type === 'MemberExpression') {
1350
+ return memberPropertyName(callee);
1351
+ }
1352
+
1353
+ return null;
1354
+ }
1355
+
1356
+ function shellCommandStartsWithBdText(value) {
1357
+ return typeof value === 'string' && /^\s*bd(?:\s|$|\.exe\b|\.cmd\b)/i.test(value);
1358
+ }
1359
+
1360
+ function staticStringPrefix(node) {
1361
+ const literal = stringLiteralValue(node);
1362
+ if (typeof literal === 'string') {
1363
+ return literal;
1364
+ }
1365
+
1366
+ if (node?.type === 'TemplateLiteral') {
1367
+ return node.quasis[0]?.value?.cooked ?? node.quasis[0]?.value?.raw ?? null;
1368
+ }
1369
+
1370
+ if (node?.type === 'BinaryExpression' && node.operator === '+') {
1371
+ return staticStringPrefix(node.left);
1372
+ }
1373
+
1374
+ return null;
1375
+ }
1376
+
1377
+ function collectBdCommandAliases(ast) {
1378
+ const exact = new Set();
1379
+ const shell = new Set();
1380
+
1381
+ walkAst(ast, node => {
1382
+ if (
1383
+ node.type === 'VariableDeclarator' &&
1384
+ node.id?.type === 'Identifier'
1385
+ ) {
1386
+ if (stringLiteralValue(node.init) === 'bd') {
1387
+ exact.add(node.id.name);
1388
+ shell.add(node.id.name);
1389
+ } else if (shellCommandStartsWithBdText(staticStringPrefix(node.init))) {
1390
+ shell.add(node.id.name);
1391
+ }
1392
+ }
1393
+ });
1394
+
1395
+ return { exact, shell };
1396
+ }
1397
+
1398
+ function isBdCommandArgument(node, aliases) {
1399
+ return stringLiteralValue(node) === 'bd' ||
1400
+ (node?.type === 'Identifier' && aliases.has(node.name));
1401
+ }
1402
+
1403
+ function isBdShellCommandArgument(node, aliases) {
1404
+ if (shellCommandStartsWithBdText(staticStringPrefix(node))) {
1405
+ return true;
1406
+ }
1407
+
1408
+ if (node?.type === 'Identifier' && aliases.has(node.name)) {
1409
+ return true;
1410
+ }
1411
+
1412
+ if (node?.type === 'BinaryExpression' && node.operator === '+') {
1413
+ return isBdShellCommandArgument(node.left, aliases);
1414
+ }
1415
+
1416
+ return false;
1417
+ }
1418
+
1419
+ function sourceHasDirectBdInvocation(source) {
1420
+ const ast = parseScriptAst(source);
1421
+ if (!ast) {
1422
+ return /(?:exec|execSync|execFile|execFileSync|spawn|spawnSync)\s*\(\s*['"]\s*bd(?:\s|['"]|\.exe\b|\.cmd\b)/.test(source);
1423
+ }
1424
+
1425
+ const aliases = collectBdCommandAliases(ast);
1426
+ let found = false;
1427
+ walkAst(ast, node => {
1428
+ if (found || node.type !== 'CallExpression') {
1429
+ return;
1430
+ }
1431
+
1432
+ const calleeName = callCalleeName(node.callee);
1433
+ if (
1434
+ ['exec', 'execSync'].includes(calleeName) &&
1435
+ isBdShellCommandArgument(node.arguments[0], aliases.shell)
1436
+ ) {
1437
+ found = true;
1438
+ } else if (
1439
+ ['execFile', 'execFileSync', 'spawn', 'spawnSync'].includes(calleeName) &&
1440
+ isBdCommandArgument(node.arguments[0], aliases.exact)
1441
+ ) {
1442
+ found = true;
1443
+ }
1444
+ });
1445
+
1446
+ return found;
1447
+ }
1448
+
1449
+ function defaultIssueBackendIsKernel(projectRoot) {
1450
+ const source = readRepoFile(projectRoot, 'lib/forge-issues.js');
1451
+ const ast = parseScriptAst(source);
1452
+ if (!ast) {
1453
+ return false;
1454
+ }
1455
+
1456
+ let foundKernelDefault = false;
1457
+ walkAst(ast, node => {
1458
+ if (foundKernelDefault) {
1459
+ return;
1460
+ }
1461
+
1462
+ const body = createIssueServiceBody(node);
1463
+ if (!body) {
1464
+ return;
1465
+ }
1466
+
1467
+ walkAst(body, child => {
1468
+ if (
1469
+ !foundKernelDefault &&
1470
+ child.type === 'VariableDeclarator' &&
1471
+ child.id?.type === 'Identifier' &&
1472
+ child.id.name === 'resolvedBackend'
1473
+ ) {
1474
+ foundKernelDefault = expressionDefaultsToKernelBackend(child.init);
1475
+ }
1476
+ });
1477
+ });
1478
+
1479
+ return foundKernelDefault;
1480
+ }
1481
+
1482
+ function createIssueServiceBody(node) {
1483
+ if (node?.type === 'FunctionDeclaration' && node.id?.name === 'createIssueService') {
1484
+ return node.body;
1485
+ }
1486
+
1487
+ if (
1488
+ node?.type === 'VariableDeclarator' &&
1489
+ node.id?.type === 'Identifier' &&
1490
+ node.id.name === 'createIssueService' &&
1491
+ isFunctionLikeNode(node.init)
1492
+ ) {
1493
+ return node.init.body;
1494
+ }
1495
+
1496
+ return null;
1497
+ }
1498
+
1499
+ function expressionDefaultsToKernelBackend(node) {
1500
+ for (const operand of flattenLogicalFallbackOperands(node)) {
1501
+ if (isCreateBeadsIssueBackendCall(operand)) {
1502
+ return false;
1503
+ }
1504
+ if (isCreateKernelIssueBackendCall(operand)) {
1505
+ return true;
1506
+ }
1507
+ }
1508
+
1509
+ return false;
1510
+ }
1511
+
1512
+ function flattenLogicalFallbackOperands(node) {
1513
+ if (node?.type === 'LogicalExpression' && ['||', '??'].includes(node.operator)) {
1514
+ return [
1515
+ ...flattenLogicalFallbackOperands(node.left),
1516
+ ...flattenLogicalFallbackOperands(node.right),
1517
+ ];
1518
+ }
1519
+
1520
+ return [node];
1521
+ }
1522
+
1523
+ function isCreateKernelIssueBackendCall(node) {
1524
+ return node?.type === 'CallExpression' &&
1525
+ node.callee?.type === 'Identifier' &&
1526
+ node.callee.name === 'createKernelIssueBackend';
1527
+ }
1528
+
1529
+ function isCreateBeadsIssueBackendCall(node) {
1530
+ return node?.type === 'CallExpression' &&
1531
+ node.callee?.type === 'Identifier' &&
1532
+ node.callee.name === 'createBeadsIssueBackend';
1533
+ }
1534
+
1535
+ function hasKernelBackedClaimReleaseSurface(projectRoot) {
1536
+ const missingCommands = [];
1537
+ const beadsBackedCommands = [];
1538
+ const nonKernelCommands = [];
1539
+ const defaultKernelBackend = defaultIssueBackendIsKernel(projectRoot);
1540
+
1541
+ for (const command of REQUIRED_CLAIM_RELEASE_COMMANDS) {
1542
+ const source = readRepoFile(projectRoot, command.path);
1543
+ if (!source || !command.contractPatterns.every(pattern => pattern.test(source))) {
1544
+ missingCommands.push(command.name);
1545
+ continue;
1546
+ }
1547
+
1548
+ if (!commandModuleHasRegistryContract(projectRoot, command.name)) {
1549
+ missingCommands.push(command.name);
1550
+ }
1551
+
1552
+ if (isBeadsBackedCommandSource(source)) {
1553
+ beadsBackedCommands.push(command.name);
1554
+ continue;
1555
+ }
1556
+
1557
+ if (!defaultKernelBackend && !sourceHasOnlyKernelBackedIssueOperations(source, new Set([command.name]))) {
1558
+ nonKernelCommands.push(command.name);
1559
+ }
1560
+ }
1561
+
1562
+ return {
1563
+ ok: missingCommands.length === 0 && beadsBackedCommands.length === 0 && nonKernelCommands.length === 0,
1564
+ missingCommands,
1565
+ beadsBackedCommands,
1566
+ nonKernelCommands,
1567
+ };
1568
+ }
1569
+
1570
+ function hasIssueScopedRecap(projectRoot) {
1571
+ const source = readRepoFile(projectRoot, 'lib/commands/recap.js');
1572
+ return commandModuleHasRegistryContract(projectRoot, 'recap') &&
1573
+ /forge recap <issue>/.test(source) &&
1574
+ /issue/i.test(source) &&
1575
+ !/\.beads|buildRecap\(projectRoot/.test(source);
1576
+ }
1577
+
1578
+ function hasForgeSkillsPack(projectRoot) {
1579
+ return FORGE_SKILLS_PACK_CANDIDATES.some(candidate =>
1580
+ fs.existsSync(absolutePath(projectRoot, candidate.manifest)) &&
1581
+ manifestDeclaresSkills(projectRoot, candidate.manifest) &&
1582
+ REQUIRED_FORGE_SKILLS.every(skill => hasCliWrapperSkill(projectRoot, candidate.root, skill))
1583
+ );
1584
+ }
1585
+
1586
+ function collectJsonStringsAndKeys(value) {
1587
+ if (Array.isArray(value)) {
1588
+ return value.flatMap(collectJsonStringsAndKeys);
1589
+ }
1590
+ if (value && typeof value === 'object') {
1591
+ return Object.entries(value).flatMap(([key, child]) => [
1592
+ key,
1593
+ ...collectJsonStringsAndKeys(child),
1594
+ ]);
1595
+ }
1596
+ return typeof value === 'string' ? [value] : [];
1597
+ }
1598
+
1599
+ function manifestDeclaresSkills(projectRoot, manifestPath) {
1600
+ try {
1601
+ const manifest = JSON.parse(fs.readFileSync(absolutePath(projectRoot, manifestPath), 'utf8'));
1602
+ const manifestTerms = collectJsonStringsAndKeys(manifest);
1603
+ return REQUIRED_FORGE_SKILLS.every(skill =>
1604
+ manifestTerms.some(term => term === skill.name || term.endsWith(`/${skill.name}`))
1605
+ );
1606
+ } catch (_error) {
1607
+ return false;
1608
+ }
1609
+ }
1610
+
1611
+ function hasCliWrapperSkill(projectRoot, skillRoot, skill) {
1612
+ const candidates = [
1613
+ `${skillRoot}/skills/${skill.name}/SKILL.md`,
1614
+ `${skillRoot}/${skill.name}/SKILL.md`,
1615
+ `${skillRoot}/skills/${skill.name}.md`,
1616
+ ];
1617
+
1618
+ return candidates.some(relativePath => {
1619
+ const fullPath = absolutePath(projectRoot, relativePath);
1620
+ if (!fs.existsSync(fullPath)) {
1621
+ return false;
1622
+ }
1623
+
1624
+ const source = fs.readFileSync(fullPath, 'utf8');
1625
+ return skill.commandPatterns.some(pattern => pattern.test(source));
1626
+ });
1627
+ }
1628
+
1629
+ function acceptanceRequirementSatisfied(requirement, source) {
1630
+ if (typeof requirement.matches === 'function') {
1631
+ return requirement.matches(source);
1632
+ }
1633
+
1634
+ return requirement.patterns.some(pattern => pattern.test(source));
1635
+ }
1636
+
1637
+ function missingFreshCloneAcceptanceCoverage(source) {
1638
+ const enabledTestSources = extractEnabledTestSources(source);
1639
+ if (enabledTestSources.length === 0) {
1640
+ return FRESH_CLONE_ACCEPTANCE_REQUIREMENTS.map(requirement => requirement.name);
1641
+ }
1642
+
1643
+ const workflowRequirements = FRESH_CLONE_ACCEPTANCE_REQUIREMENTS
1644
+ .filter(requirement => requirement.name !== 'enabled acceptance test');
1645
+ const candidateMissing = enabledTestSources
1646
+ .map(testSource => stripJsComments(testSource))
1647
+ .map(executableSource => workflowRequirements
1648
+ .filter(requirement => !acceptanceRequirementSatisfied(requirement, executableSource))
1649
+ .map(requirement => requirement.name))
1650
+ .sort((left, right) => left.length - right.length);
1651
+
1652
+ return candidateMissing[0] || [];
1653
+ }
1654
+
1655
+ function freshCloneAcceptanceStatus(projectRoot) {
1656
+ const inspected = [];
1657
+
1658
+ for (const relativePath of FRESH_CLONE_ACCEPTANCE_CANDIDATES) {
1659
+ const fullPath = absolutePath(projectRoot, relativePath);
1660
+ if (!fs.existsSync(fullPath)) {
1661
+ continue;
1662
+ }
1663
+
1664
+ const missing = missingFreshCloneAcceptanceCoverage(fs.readFileSync(fullPath, 'utf8'));
1665
+ if (missing.length === 0) {
1666
+ return {
1667
+ ok: true,
1668
+ path: relativePath,
1669
+ missing,
1670
+ };
1671
+ }
1672
+ inspected.push({ path: relativePath, missing });
1673
+ }
1674
+
1675
+ if (inspected.length === 0) {
1676
+ return {
1677
+ ok: false,
1678
+ path: FRESH_CLONE_ACCEPTANCE_CANDIDATES[0],
1679
+ missing: FRESH_CLONE_ACCEPTANCE_REQUIREMENTS.map(requirement => requirement.name),
1680
+ reason: 'missing',
1681
+ };
1682
+ }
1683
+
1684
+ inspected.sort((left, right) => left.missing.length - right.missing.length);
1685
+ return {
1686
+ ok: false,
1687
+ path: inspected[0].path,
1688
+ missing: inspected[0].missing,
1689
+ reason: 'incomplete',
1690
+ };
1691
+ }
1692
+
1693
+ // Canonicalize the kill-list for the currency comparison: drop the per-file
1694
+ // `- lines: …` detail so the check is line-number-INSENSITIVE. The rendered
1695
+ // artifact keeps those line numbers as an informational aid, but they are
1696
+ // volatile — any PR that edits a scanned file shifts them, which would
1697
+ // otherwise mark the artifact "stale" and red the release-readiness gate on
1698
+ // every unrelated PR. The comparison therefore keeps only the stable signal:
1699
+ // the summary table plus each file's path and bd-term COUNT. A real change in
1700
+ // call-site counts (adding/removing bd usage) still trips staleness; a pure
1701
+ // line shift no longer does.
1702
+ function canonicalizeAuditArtifact(markdown) {
1703
+ return markdown
1704
+ .replace(/\r\n/g, '\n')
1705
+ .split('\n')
1706
+ .filter(line => !/^\s*- lines:/.test(line))
1707
+ .join('\n');
1708
+ }
1709
+
1710
+ function isAuditArtifactCurrent(projectRoot, audit) {
1711
+ const artifactPath = absolutePath(projectRoot, AUDIT_ARTIFACT);
1712
+ if (!fs.existsSync(artifactPath)) {
1713
+ return {
1714
+ ok: false,
1715
+ reason: 'missing',
1716
+ evidence: missingArtifactEvidence(AUDIT_ARTIFACT),
1717
+ };
1718
+ }
1719
+
1720
+ const expected = canonicalizeAuditArtifact(renderBdCallSiteAuditMarkdown(audit));
1721
+ const actual = canonicalizeAuditArtifact(fs.readFileSync(artifactPath, 'utf8'));
1722
+ if (actual !== expected) {
1723
+ return {
1724
+ ok: false,
1725
+ reason: 'stale',
1726
+ evidence: [{ path: AUDIT_ARTIFACT }],
1727
+ };
1728
+ }
1729
+
1730
+ return { ok: true, reason: 'current', evidence: [] };
1731
+ }
1732
+
1733
+ function missingArtifactEvidence(relativePath) {
1734
+ return [{ path: relativePath, missing: true }];
1735
+ }
1736
+
1737
+ function hotPathBlocker(audit, projectRoot) {
1738
+ const hotPathEvidence = flattenHotPathEvidence(audit, projectRoot);
1739
+ if (hotPathEvidence.length > 0) {
1740
+ return {
1741
+ id: 'bd-hot-path-issue-commands',
1742
+ title: 'bd in D20 hot-path surfaces',
1743
+ detail: 'Remove direct bd, .beads, and Dolt access from issue, sync, worktree, setup, preflight, smart-status, forge-team, hook, and agent-instruction hot paths before claiming Beads retirement.',
1744
+ evidence: hotPathEvidence,
1745
+ };
1746
+ }
1747
+
1748
+ return null;
1749
+ }
1750
+
1751
+ function auditArtifactBlocker(projectRoot, audit) {
1752
+ const auditArtifact = isAuditArtifactCurrent(projectRoot, audit);
1753
+ if (!auditArtifact.ok) {
1754
+ return {
1755
+ id: 'd20-audit-artifact-current',
1756
+ title: 'D20 bd call-site kill-list artifact is not current',
1757
+ detail: `Regenerate ${AUDIT_ARTIFACT}; current status: ${auditArtifact.reason}.`,
1758
+ evidence: auditArtifact.evidence,
1759
+ };
1760
+ }
1761
+
1762
+ return null;
1763
+ }
1764
+
1765
+ function formatIssueSurfaceDetail(issueSurface, claimReleaseSurface) {
1766
+ const issueParts = [
1767
+ `Required issue subcommands: ${REQUIRED_ISSUE_SUBCOMMANDS.join(', ')}. Missing today: ${issueSurface.missingSubcommands.join(', ') || 'none'}`,
1768
+ `missing issue dep actions: ${issueSurface.missingDepActions.map(action => `dep ${action}`).join(', ') || 'none'}`,
1769
+ `missing Kernel adapter operations: ${issueSurface.missingAdapterOperations.join(', ') || 'none'}`,
1770
+ `issue command registry-valid: ${issueSurface.registryValid ? 'yes' : 'no'}`,
1771
+ `issue command delegates to issue surface: ${issueSurface.delegatesToSurface ? 'yes' : 'no'}`,
1772
+ `Kernel evidence missing for issue surface: ${issueSurface.missingKernelEvidence ? 'yes' : 'no'}`,
1773
+ `Beads-backed issue surface: ${issueSurface.beadsBacked ? 'yes' : 'no'}`,
1774
+ ];
1775
+ const claimReleaseParts = [
1776
+ `Required claim/release commands: claim, release. Missing claim/release today: ${claimReleaseSurface.missingCommands.join(', ') || 'none'}`,
1777
+ `Beads-backed claim/release: ${claimReleaseSurface.beadsBackedCommands.join(', ') || 'none'}`,
1778
+ `Kernel evidence missing for claim/release: ${claimReleaseSurface.nonKernelCommands.join(', ') || 'none'}`,
1779
+ ];
1780
+ return `${issueParts.join('; ')}. ${claimReleaseParts.join('; ')}.`;
1781
+ }
1782
+
1783
+ function issueSurfaceBlocker(projectRoot) {
1784
+ const issueSurface = hasKernelBackedIssueSurface(projectRoot);
1785
+ const claimReleaseSurface = hasKernelBackedClaimReleaseSurface(projectRoot);
1786
+ if (!issueSurface.ok || !claimReleaseSurface.ok) {
1787
+ return {
1788
+ id: 'kernel-backed-forge-issue',
1789
+ title: 'missing Kernel-backed forge issue command set',
1790
+ detail: formatIssueSurfaceDetail(issueSurface, claimReleaseSurface),
1791
+ evidence: [
1792
+ { path: 'lib/commands/issue.js' },
1793
+ { path: 'lib/commands/_issue.js' },
1794
+ { path: 'lib/adapters/kernel-issue-adapter.js' },
1795
+ ...REQUIRED_CLAIM_RELEASE_COMMANDS.map(command => ({ path: command.path })),
1796
+ ],
1797
+ };
1798
+ }
1799
+
1800
+ return null;
1801
+ }
1802
+
1803
+ function primeBlocker(projectRoot) {
1804
+ if (!commandModuleHasRegistryContract(projectRoot, 'prime')) {
1805
+ return {
1806
+ id: 'forge-prime',
1807
+ title: 'missing forge prime',
1808
+ detail: 'D22 requires a registered forge prime command that replaces bd prime and emits bounded orientation.',
1809
+ evidence: missingArtifactEvidence('lib/commands/prime.js'),
1810
+ };
1811
+ }
1812
+
1813
+ return null;
1814
+ }
1815
+
1816
+ function orientRecapBlocker(projectRoot) {
1817
+ const hasOrient = commandModuleHasRegistryContract(projectRoot, 'orient');
1818
+ const recapIssueScoped = hasIssueScopedRecap(projectRoot);
1819
+ if (!hasOrient || !recapIssueScoped) {
1820
+ return {
1821
+ id: 'forge-orient-issue-recap',
1822
+ title: 'missing forge orient / issue-scoped forge recap',
1823
+ detail: `forge orient registered: ${hasOrient ? 'yes' : 'no'}; issue-scoped forge recap present: ${recapIssueScoped ? 'yes' : 'no'}.`,
1824
+ evidence: [
1825
+ ...(hasOrient ? [] : missingArtifactEvidence('lib/commands/orient.js')),
1826
+ ...(recapIssueScoped ? [] : [{ path: 'lib/commands/recap.js' }]),
1827
+ ],
1828
+ };
1829
+ }
1830
+
1831
+ return null;
1832
+ }
1833
+
1834
+ function skillsPackBlocker(projectRoot) {
1835
+ if (!hasForgeSkillsPack(projectRoot)) {
1836
+ return {
1837
+ id: 'forge-skills-pack',
1838
+ title: 'missing Forge skills pack',
1839
+ detail: `D22 requires a Forge plugin skills pack with CLI-wrapper skills: ${REQUIRED_FORGE_SKILLS.map(skill => skill.name).join(', ')}.`,
1840
+ evidence: missingArtifactEvidence('packages/skills/forge-plugin/manifest.json'),
1841
+ };
1842
+ }
1843
+
1844
+ return null;
1845
+ }
1846
+
1847
+ function rememberRecallBlocker(projectRoot) {
1848
+ const hasRemember = commandModuleHasRegistryContract(projectRoot, 'remember');
1849
+ const hasRecall = commandModuleHasRegistryContract(projectRoot, 'recall');
1850
+ if (!hasRemember || !hasRecall) {
1851
+ return {
1852
+ id: 'forge-remember-recall',
1853
+ title: 'missing forge remember / forge recall',
1854
+ detail: `forge remember registered: ${hasRemember ? 'yes' : 'no'}; forge recall registered: ${hasRecall ? 'yes' : 'no'}.`,
1855
+ evidence: [
1856
+ ...(hasRemember ? [] : missingArtifactEvidence('lib/commands/remember.js')),
1857
+ ...(hasRecall ? [] : missingArtifactEvidence('lib/commands/recall.js')),
1858
+ ],
1859
+ };
1860
+ }
1861
+
1862
+ return null;
1863
+ }
1864
+
1865
+ // Surfaces that could re-model pre-merge as a universal stage/command, each paired
1866
+ // with the pattern that flags real stage/command residue WITHOUT tripping on the
1867
+ // legacy read-path name maps we intentionally keep (so historical
1868
+ // `currentStage='premerge'` state still round-trips):
1869
+ // - stage/command emitters (the AGENTS.md generator, status.js) are matched on the
1870
+ // SLASH form `/premerge`, so the legacy `premerge:` map keys (no slash) are tolerated;
1871
+ // - the pure stage taxonomies (plugin-catalog, recommend, capability matrix) are
1872
+ // matched on any `premerge`, since none of them carry a legacy read-path.
1873
+ function premergeEmbeddedGateStatus(projectRoot) {
1874
+ const slashCommand = /\/premerge/;
1875
+ const anyPremerge = /premerge/i;
1876
+ const checks = [
1877
+ ['lib/workflow/stages.js', /\bpremerge\b/],
1878
+ ['lib/workflow-profiles.js', /['"]\/premerge['"]/],
1879
+ ['AGENTS.md', anyPremerge],
1880
+ ['lib/agents-config.js', slashCommand],
1881
+ ['lib/plugin-catalog.js', anyPremerge],
1882
+ ['lib/commands/recommend.js', anyPremerge],
1883
+ ['lib/harness-capability-matrix.js', anyPremerge],
1884
+ ['lib/commands/status.js', slashCommand],
1885
+ ];
1886
+ return checks.map(([relPath, pattern]) => ({
1887
+ path: relPath,
1888
+ present: pattern.test(readRepoFile(projectRoot, relPath)),
1889
+ }));
1890
+ }
1891
+
1892
+ function premergeEmbeddedGateBlocker(projectRoot) {
1893
+ const evidence = premergeEmbeddedGateStatus(projectRoot)
1894
+ .filter(item => item.present)
1895
+ .map(item => ({ path: item.path }));
1896
+
1897
+ if (evidence.length === 0) {
1898
+ return null;
1899
+ }
1900
+
1901
+ return {
1902
+ id: 'premerge-embedded-gate',
1903
+ title: 'pre-merge still modeled as a universal stage',
1904
+ detail: 'The roadmap requires pre-merge behavior to become a task-type gate/checkpoint embedded in existing stages, not a universal standalone workflow stage.',
1905
+ evidence,
1906
+ };
1907
+ }
1908
+
1909
+ function freshCloneBlocker(projectRoot) {
1910
+ const status = freshCloneAcceptanceStatus(projectRoot);
1911
+ if (!status.ok) {
1912
+ const presentDetail = status.reason === 'missing' ? 'candidate test present: no' : `candidate test present: yes (${status.path})`;
1913
+ return {
1914
+ id: 'fresh-clone-no-beads-acceptance',
1915
+ title: 'missing fresh-clone no-Beads acceptance test',
1916
+ detail: `D22 acceptance requires a fresh clone with no Beads/Dolt installed to prime, run forge issue ready --json, claim, comment, close, and recap with zero bd invocations. ${presentDetail}; missing acceptance coverage: ${status.missing.join(', ')}.`,
1917
+ evidence: status.reason === 'missing' ? missingArtifactEvidence(status.path) : [{ path: status.path }],
1918
+ };
1919
+ }
1920
+
1921
+ return null;
1922
+ }
1923
+
1924
+ function buildBlockers(projectRoot, audit) {
1925
+ return [
1926
+ hotPathBlocker(audit, projectRoot),
1927
+ auditArtifactBlocker(projectRoot, audit),
1928
+ issueSurfaceBlocker(projectRoot),
1929
+ primeBlocker(projectRoot),
1930
+ orientRecapBlocker(projectRoot),
1931
+ skillsPackBlocker(projectRoot),
1932
+ rememberRecallBlocker(projectRoot),
1933
+ premergeEmbeddedGateBlocker(projectRoot),
1934
+ freshCloneBlocker(projectRoot),
1935
+ ].filter(Boolean);
1936
+ }
1937
+
1938
+ function buildReadinessReport(projectRoot, options = {}) {
1939
+ const target = options.target || SUPPORTED_TARGET;
1940
+ // Accept prerelease variants (0.1.0-beta.1, 0.1.0-rc.1, …) against their base version's
1941
+ // readiness profile — a beta's readiness content is identical to its release.
1942
+ const baseTarget = String(target).split('-')[0];
1943
+ if (baseTarget !== SUPPORTED_TARGET) {
1944
+ return {
1945
+ success: false,
1946
+ target,
1947
+ blockers: [{
1948
+ id: 'unsupported-target',
1949
+ title: 'unsupported release readiness target',
1950
+ detail: `Unsupported release readiness target: ${target}. Supported target: ${SUPPORTED_TARGET} (prerelease suffixes like -beta.1 are accepted).`,
1951
+ evidence: [],
1952
+ }],
1953
+ audit: auditBdCallSites(projectRoot, options),
1954
+ };
1955
+ }
1956
+
1957
+ const audit = auditBdCallSites(projectRoot, options);
1958
+ const blockers = buildBlockers(projectRoot, audit);
1959
+
1960
+ return {
1961
+ success: blockers.length === 0,
1962
+ target,
1963
+ blockers,
1964
+ audit,
1965
+ };
1966
+ }
1967
+
1968
+ function formatEvidenceItem(item) {
1969
+ if (item.missing) {
1970
+ return ` - missing ${item.path}`;
1971
+ }
1972
+ if (Array.isArray(item.lines)) {
1973
+ return ` - ${item.path}: ${item.count} call sites on lines ${item.lines.join(', ')}`;
1974
+ }
1975
+ return ` - ${item.path}`;
1976
+ }
1977
+
1978
+ function formatEvidence(evidence = []) {
1979
+ if (evidence.length === 0) {
1980
+ return [];
1981
+ }
1982
+ return [' Evidence:', ...evidence.map(formatEvidenceItem)];
1983
+ }
1984
+
1985
+ function renderBlocker(blocker) {
1986
+ return [
1987
+ `- [FAIL] ${blocker.title}`,
1988
+ ` id: ${blocker.id}`,
1989
+ ` ${blocker.detail}`,
1990
+ ...formatEvidence(blocker.evidence),
1991
+ ];
1992
+ }
1993
+
1994
+ function renderBlockerSection(blockers) {
1995
+ if (blockers.length === 0) {
1996
+ return ['No blockers found.'];
1997
+ }
1998
+ return [
1999
+ `Blockers (${blockers.length}):`,
2000
+ ...blockers.flatMap(renderBlocker),
2001
+ ];
2002
+ }
2003
+
2004
+ function renderAuditSummary(report) {
2005
+ return [
2006
+ '',
2007
+ 'D20 bd call-site audit:',
2008
+ ...GROUPS.map(group => {
2009
+ const summary = report.audit.groups[group];
2010
+ return `- ${group}: ${summary.count} call sites in ${summary.fileCount} files`;
2011
+ }),
2012
+ `- artifact: ${report.audit.artifact}`,
2013
+ ];
2014
+ }
2015
+
2016
+ function renderReadinessReport(report) {
2017
+ const lines = [
2018
+ `Forge release readiness check: ${report.target}`,
2019
+ `Result: ${report.success ? 'PASS' : 'FAIL'}`,
2020
+ '',
2021
+ ...renderBlockerSection(report.blockers),
2022
+ ...renderAuditSummary(report),
2023
+ ];
2024
+
2025
+ return `${lines.join('\n')}\n`;
2026
+ }
2027
+
2028
+ function renderLineList(lines) {
2029
+ return lines
2030
+ .map(entry => `${entry.line} (${entry.terms.join(', ')})`)
2031
+ .join(', ');
2032
+ }
2033
+
2034
+ function renderBdCallSiteAuditMarkdown(audit) {
2035
+ const header = [
2036
+ '# D20 bd Call-Site Kill List',
2037
+ '',
2038
+ 'Generated by `forge release check --target 0.1.0`.',
2039
+ 'Purpose: tracked migration artifact for D20 so later PRs can remove Beads/Dolt hot-path usage against a concrete checklist.',
2040
+ '',
2041
+ '## Summary',
2042
+ '',
2043
+ '| Group | Call sites | Files |',
2044
+ '| --- | ---: | ---: |',
2045
+ ];
2046
+
2047
+ const summaryRows = GROUPS.map(group => {
2048
+ const summary = audit.groups[group];
2049
+ return `| ${group} | ${summary.count} | ${summary.fileCount} |`;
2050
+ });
2051
+
2052
+ return `${[
2053
+ ...header,
2054
+ ...summaryRows,
2055
+ ...GROUPS.flatMap(group => renderAuditGroup(audit, group)),
2056
+ '',
2057
+ ].join('\n')}`;
2058
+ }
2059
+
2060
+ function renderAuditGroup(audit, group) {
2061
+ const files = audit.groups[group].files;
2062
+ if (files.length === 0) {
2063
+ return ['', `## ${group}`, '', 'No current call sites in this group.'];
2064
+ }
2065
+
2066
+ return [
2067
+ '',
2068
+ `## ${group}`,
2069
+ '',
2070
+ ...files.flatMap(file => [
2071
+ `- [ ] ${file.path} (${file.count})`,
2072
+ ` - lines: ${renderLineList(file.lines)}`,
2073
+ ]),
2074
+ ];
2075
+ }
2076
+
2077
+ module.exports = {
2078
+ AUDIT_ARTIFACT,
2079
+ GROUPS,
2080
+ SUPPORTED_TARGET,
2081
+ auditBdCallSites,
2082
+ buildReadinessReport,
2083
+ canonicalizeAuditArtifact,
2084
+ renderBdCallSiteAuditMarkdown,
2085
+ renderReadinessReport,
2086
+ // Exposed for the premerge-de-stage certification tests.
2087
+ premergeEmbeddedGateStatus,
2088
+ premergeEmbeddedGateBlocker,
2089
+ };