forge-workflow 0.0.9 → 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 (479) 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 +151 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -106
  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 +466 -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/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
  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/reference/TOOLCHAIN.md +658 -0
  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 +225 -28
  81. package/lib/beads-sync-scaffold.js +36 -107
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +744 -70
  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 +66 -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 +22 -2
  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 +851 -979
  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 +329 -11
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +15 -2
  142. package/lib/commands/test.js +58 -7
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +56 -25
  146. package/lib/commands/worktree.js +308 -128
  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 +184 -0
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +42 -17
  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 +697 -0
  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/issue-sync/authority.js +100 -0
  174. package/lib/issue-sync/github-pull.js +184 -0
  175. package/lib/issue-sync/import-primitives.js +98 -0
  176. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  177. package/lib/issue-sync/link-store.js +292 -0
  178. package/lib/issue-sync/project-github.js +123 -0
  179. package/lib/issue-sync/reconcile.js +195 -0
  180. package/lib/issue-sync/schema.js +126 -0
  181. package/lib/kernel/backing-issue.js +305 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/planning-buckets-schema.js +109 -0
  192. package/lib/kernel/projection-jsonl-writer.js +450 -0
  193. package/lib/kernel/readiness-model.js +329 -0
  194. package/lib/kernel/schema.js +356 -0
  195. package/lib/kernel/sqlite-driver.js +2504 -0
  196. package/lib/kernel/taxonomy-validator.js +394 -0
  197. package/lib/lefthook-check.js +8 -4
  198. package/lib/lefthook-wiring.js +413 -0
  199. package/lib/mcp-config-renderer.js +288 -0
  200. package/lib/memory/graphiti-mcp.js +106 -0
  201. package/lib/memory/router.js +387 -0
  202. package/lib/memory/typed-api.js +102 -0
  203. package/lib/memory-digest.js +195 -0
  204. package/lib/merge-rules.js +395 -0
  205. package/lib/migrate-dry-run.js +466 -0
  206. package/lib/orientation.js +863 -0
  207. package/lib/package-manager-remediation.js +103 -0
  208. package/lib/package-root.js +381 -0
  209. package/lib/patch-intent.js +890 -0
  210. package/lib/plugin-catalog.js +3 -4
  211. package/lib/plugin-manager.js +0 -5
  212. package/lib/pr-bundle.js +186 -0
  213. package/lib/pr-monitor/differ.js +195 -0
  214. package/lib/pr-monitor/events.js +0 -0
  215. package/lib/pr-monitor/gather.js +124 -0
  216. package/lib/pr-monitor/journal.js +299 -0
  217. package/lib/pr-monitor/monitor.js +146 -0
  218. package/lib/pr-monitor/render-sticky.js +157 -0
  219. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  220. package/lib/pr-monitor/watch.js +247 -0
  221. package/lib/pr-pull.js +1273 -0
  222. package/lib/pr-shepherd.js +494 -0
  223. package/lib/pr-state-validator.js +59 -0
  224. package/lib/preflight/gates.js +237 -0
  225. package/lib/preflight/runner.js +116 -0
  226. package/lib/project-discovery.js +0 -53
  227. package/lib/project-memory.js +166 -0
  228. package/lib/protected-path-manifest.js +281 -0
  229. package/lib/protected-state-surfaces.js +387 -0
  230. package/lib/release-readiness.js +2089 -0
  231. package/lib/reset.js +59 -45
  232. package/lib/review-adapter.js +68 -0
  233. package/lib/rules-sync.js +260 -0
  234. package/lib/runtime-health.js +332 -23
  235. package/lib/safety-config-renderer.js +268 -0
  236. package/lib/setup-action-log.js +1 -7
  237. package/lib/setup.js +27 -65
  238. package/lib/shell-utils.js +76 -6
  239. package/lib/skills-sync.js +330 -0
  240. package/lib/smart-status/conflicts.js +205 -0
  241. package/lib/smart-status/scoring.js +191 -0
  242. package/lib/status/beads-snapshot.js +145 -0
  243. package/lib/status/presenter.js +216 -0
  244. package/lib/status/snapshot.js +186 -0
  245. package/lib/sync-backend.js +202 -0
  246. package/lib/untrusted-content.js +52 -0
  247. package/lib/upgrade-safety.js +199 -0
  248. package/lib/workflow/enforce-stage.js +298 -47
  249. package/lib/workflow/stage-transition.js +115 -0
  250. package/lib/workflow/stages.js +30 -6
  251. package/lib/workflow/state-manager.js +159 -14
  252. package/lib/workflow/state.js +23 -1
  253. package/lib/workflow-profiles.js +17 -5
  254. package/package.json +46 -36
  255. package/rules/documentation.md +19 -0
  256. package/rules/kernel-tracking.md +26 -0
  257. package/rules/security.md +22 -0
  258. package/rules/tdd.md +20 -0
  259. package/rules/workflow.md +27 -0
  260. package/scripts/auto-backing-issue.js +47 -0
  261. package/scripts/beads-context.sh +165 -22
  262. package/scripts/beads-migrate-to-dolt.sh +7 -0
  263. package/scripts/beads-upgrade-smoke.sh +284 -0
  264. package/scripts/behavioral-judge.sh +115 -11
  265. package/scripts/benchmark.js +349 -63
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard-analyze.js +52 -17
  272. package/scripts/dep-guard-keyword-ripple.js +29 -0
  273. package/scripts/dep-guard-render-review.js +86 -0
  274. package/scripts/dep-guard.sh +64 -232
  275. package/scripts/file-index.sh +3 -0
  276. package/scripts/forge-team/lib/claim.sh +34 -18
  277. package/scripts/forge-team/lib/dashboard.sh +61 -86
  278. package/scripts/forge-team/lib/epic.sh +99 -263
  279. package/scripts/forge-team/lib/hooks.sh +26 -28
  280. package/scripts/forge-team/lib/identity.sh +4 -4
  281. package/scripts/forge-team/lib/sync-github.sh +144 -47
  282. package/scripts/forge-team/lib/verify.sh +93 -83
  283. package/scripts/forge-team/lib/workload.sh +41 -65
  284. package/scripts/forge-team/tests/claim.test.sh +25 -19
  285. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  286. package/scripts/forge-team/tests/epic.test.sh +52 -71
  287. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  288. package/scripts/forge-team/tests/identity.test.sh +3 -3
  289. package/scripts/forge-team/tests/integration.test.sh +44 -66
  290. package/scripts/forge-team/tests/sync-github.test.sh +183 -79
  291. package/scripts/forge-team/tests/verify.test.sh +37 -46
  292. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  293. package/scripts/forge-team/tests/workload.test.sh +32 -66
  294. package/scripts/gen-command-manifest.js +153 -0
  295. package/scripts/gen-embedded-assets.mjs +129 -0
  296. package/scripts/install.ps1 +139 -0
  297. package/scripts/install.sh +268 -0
  298. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  299. package/scripts/lib/release-asset.mjs +84 -0
  300. package/scripts/parity-check.mjs +145 -0
  301. package/scripts/parity-check.test.mjs +58 -0
  302. package/scripts/pin-agentic-workflow-images.js +112 -0
  303. package/scripts/pr-coordinator.sh +3 -0
  304. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  305. package/scripts/preflight.sh +108 -0
  306. package/scripts/protected-state-check.js +104 -0
  307. package/scripts/smart-status-score.js +31 -0
  308. package/scripts/smart-status-sessions.js +51 -0
  309. package/scripts/smart-status.sh +117 -369
  310. package/scripts/spikes/config-race-bench.js +111 -0
  311. package/scripts/spikes/harness-capability-matrix.js +13 -0
  312. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  313. package/scripts/spikes/protected-path-manifest.js +20 -0
  314. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  315. package/scripts/sync-agent-skills.js +62 -0
  316. package/scripts/sync-agentic-workflow.js +48 -0
  317. package/scripts/sync-utils.sh +3 -0
  318. package/scripts/test-ci-shard.js +251 -0
  319. package/scripts/test-dashboard.js +188 -52
  320. package/scripts/test-full-suite.js +186 -0
  321. package/scripts/test-profile.js +278 -0
  322. package/scripts/test.js +302 -28
  323. package/scripts/validate.js +143 -0
  324. package/scripts/validate.sh +18 -1
  325. package/skills/claim-safety/SKILL.md +102 -0
  326. package/skills/claim-safety/evals/evals.json +46 -0
  327. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
  328. package/skills/dev/evals/evals.json +50 -0
  329. package/skills/hermes-forge/SKILL.md +185 -0
  330. package/skills/hermes-forge/evals/evals.json +46 -0
  331. package/skills/issue-basics/SKILL.md +111 -0
  332. package/skills/issue-basics/evals/evals.json +46 -0
  333. package/skills/kernel/SKILL.md +166 -0
  334. package/skills/kernel/evals/evals.json +50 -0
  335. package/skills/memory/SKILL.md +102 -0
  336. package/skills/parallel-deep-research/SKILL.md +14 -11
  337. package/skills/parallel-deep-research/evals/evals.json +11 -27
  338. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
  339. package/skills/plan/evals/evals.json +42 -0
  340. package/skills/research/SKILL.md +195 -0
  341. package/skills/research/evals/evals.json +42 -0
  342. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  343. package/skills/review/evals/evals.json +42 -0
  344. package/skills/rollback/SKILL.md +110 -0
  345. package/skills/rollback/evals/evals.json +46 -0
  346. package/skills/rollback/references/methods.md +204 -0
  347. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  348. package/skills/shepherd/SKILL.md +66 -0
  349. package/skills/shepherd/evals/evals.json +42 -0
  350. package/skills/ship/SKILL.md +251 -0
  351. package/skills/ship/evals/evals.json +42 -0
  352. package/skills/smith/SKILL.md +142 -0
  353. package/skills/smith/evals/evals.json +46 -0
  354. package/skills/smith/references/autonomy-and-gates.md +94 -0
  355. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  356. package/skills/sonarcloud/evals/evals.json +46 -0
  357. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  358. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  359. package/skills/status/SKILL.md +102 -0
  360. package/skills/status/evals/evals.json +50 -0
  361. package/skills/triage-ready/SKILL.md +121 -0
  362. package/skills/triage-ready/evals/evals.json +42 -0
  363. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  364. package/skills/validate/evals/evals.json +42 -0
  365. package/skills/verify/SKILL.md +299 -0
  366. package/skills/verify/evals/evals.json +50 -0
  367. package/.claude/commands/dev.md +0 -345
  368. package/.claude/commands/plan.md +0 -566
  369. package/.claude/commands/premerge.md +0 -186
  370. package/.claude/commands/research.md +0 -42
  371. package/.claude/commands/review.md +0 -451
  372. package/.claude/commands/rollback.md +0 -721
  373. package/.claude/commands/ship.md +0 -213
  374. package/.claude/commands/sonarcloud.md +0 -152
  375. package/.claude/commands/status.md +0 -90
  376. package/.claude/commands/validate.md +0 -288
  377. package/.claude/commands/verify.md +0 -269
  378. package/.claude/rules/workflow.md +0 -121
  379. package/.cline/workflows/dev.md +0 -342
  380. package/.cline/workflows/plan.md +0 -563
  381. package/.cline/workflows/premerge.md +0 -183
  382. package/.cline/workflows/research.md +0 -39
  383. package/.cline/workflows/review.md +0 -448
  384. package/.cline/workflows/rollback.md +0 -718
  385. package/.cline/workflows/ship.md +0 -210
  386. package/.cline/workflows/sonarcloud.md +0 -146
  387. package/.cline/workflows/status.md +0 -87
  388. package/.cline/workflows/validate.md +0 -285
  389. package/.cline/workflows/verify.md +0 -266
  390. package/.codex/config.toml +0 -11
  391. package/.codex/skills/dev/SKILL.md +0 -345
  392. package/.codex/skills/plan/SKILL.md +0 -566
  393. package/.codex/skills/premerge/SKILL.md +0 -186
  394. package/.codex/skills/research/SKILL.md +0 -42
  395. package/.codex/skills/review/SKILL.md +0 -451
  396. package/.codex/skills/rollback/SKILL.md +0 -721
  397. package/.codex/skills/ship/SKILL.md +0 -213
  398. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  399. package/.codex/skills/status/SKILL.md +0 -90
  400. package/.codex/skills/validate/SKILL.md +0 -288
  401. package/.codex/skills/verify/SKILL.md +0 -269
  402. package/.cursor/commands/dev.md +0 -342
  403. package/.cursor/commands/plan.md +0 -563
  404. package/.cursor/commands/premerge.md +0 -183
  405. package/.cursor/commands/research.md +0 -39
  406. package/.cursor/commands/review.md +0 -448
  407. package/.cursor/commands/ship.md +0 -210
  408. package/.cursor/commands/sonarcloud.md +0 -146
  409. package/.cursor/commands/status.md +0 -87
  410. package/.cursor/commands/validate.md +0 -285
  411. package/.cursor/commands/verify.md +0 -266
  412. package/.cursorrules +0 -149
  413. package/.github/prompts/premerge.prompt.md +0 -188
  414. package/.github/prompts/research.prompt.md +0 -44
  415. package/.github/prompts/rollback.prompt.md +0 -723
  416. package/.github/prompts/ship.prompt.md +0 -215
  417. package/.github/prompts/status.prompt.md +0 -92
  418. package/.github/prompts/verify.prompt.md +0 -271
  419. package/.github/workflows/beads-to-github.yml +0 -56
  420. package/.github/workflows/github-to-beads.yml +0 -97
  421. package/.kilocode/workflows/dev.md +0 -346
  422. package/.kilocode/workflows/plan.md +0 -567
  423. package/.kilocode/workflows/premerge.md +0 -187
  424. package/.kilocode/workflows/research.md +0 -43
  425. package/.kilocode/workflows/review.md +0 -452
  426. package/.kilocode/workflows/rollback.md +0 -722
  427. package/.kilocode/workflows/ship.md +0 -214
  428. package/.kilocode/workflows/sonarcloud.md +0 -150
  429. package/.kilocode/workflows/status.md +0 -91
  430. package/.kilocode/workflows/validate.md +0 -289
  431. package/.kilocode/workflows/verify.md +0 -270
  432. package/.opencode/commands/dev.md +0 -345
  433. package/.opencode/commands/plan.md +0 -566
  434. package/.opencode/commands/premerge.md +0 -186
  435. package/.opencode/commands/research.md +0 -42
  436. package/.opencode/commands/review.md +0 -451
  437. package/.opencode/commands/rollback.md +0 -721
  438. package/.opencode/commands/ship.md +0 -213
  439. package/.opencode/commands/sonarcloud.md +0 -149
  440. package/.opencode/commands/status.md +0 -90
  441. package/.opencode/commands/validate.md +0 -288
  442. package/.opencode/commands/verify.md +0 -269
  443. package/.roo/commands/dev.md +0 -346
  444. package/.roo/commands/plan.md +0 -567
  445. package/.roo/commands/premerge.md +0 -187
  446. package/.roo/commands/research.md +0 -43
  447. package/.roo/commands/review.md +0 -452
  448. package/.roo/commands/rollback.md +0 -722
  449. package/.roo/commands/ship.md +0 -214
  450. package/.roo/commands/sonarcloud.md +0 -150
  451. package/.roo/commands/status.md +0 -91
  452. package/.roo/commands/validate.md +0 -289
  453. package/.roo/commands/verify.md +0 -270
  454. package/docs/BEADS_GITHUB_SYNC.md +0 -255
  455. package/docs/GREPTILE_SETUP.md +0 -400
  456. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  457. package/docs/SETUP.md +0 -663
  458. package/docs/VALIDATION.md +0 -363
  459. package/lib/agents/cline.plugin.json +0 -29
  460. package/lib/agents/copilot.plugin.json +0 -24
  461. package/lib/agents/kilocode.plugin.json +0 -22
  462. package/lib/agents/opencode.plugin.json +0 -23
  463. package/lib/agents/roo.plugin.json +0 -30
  464. package/lib/beads-health-check.js +0 -143
  465. package/lib/commands/commands-reset.js +0 -147
  466. package/opencode.json +0 -67
  467. package/scripts/beads-context.test.js +0 -567
  468. package/scripts/github-beads-sync/comment.mjs +0 -64
  469. package/scripts/github-beads-sync/config.mjs +0 -148
  470. package/scripts/github-beads-sync/github-api.mjs +0 -131
  471. package/scripts/github-beads-sync/index.mjs +0 -332
  472. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  473. package/scripts/github-beads-sync/mapping.mjs +0 -78
  474. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  475. package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
  476. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  477. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  478. package/scripts/github-beads-sync.config.json +0 -26
  479. package/scripts/sync-commands.js +0 -600
@@ -1,5 +1,6 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
+ const { parse } = require('@babel/parser');
3
4
 
4
5
  /**
5
6
  * Allowlist mapping topic names to filenames in docs/.
@@ -13,6 +14,10 @@ const TOPICS = {
13
14
  roadmap: 'ROADMAP.md',
14
15
  };
15
16
 
17
+ const TOPIC_DIRS = {
18
+ setup: ['guides', 'reference', ''],
19
+ };
20
+
16
21
  /**
17
22
  * List all available topic names.
18
23
  * @returns {string[]}
@@ -38,14 +43,1164 @@ function getTopicContent(topic, packageDir) {
38
43
  return { error: `Unknown topic: "${topic}". Available topics: ${availableList}` };
39
44
  }
40
45
 
41
- const filePath = path.join(packageDir, 'docs', filename);
46
+ const searchDirs = TOPIC_DIRS[topic] || ['reference', 'guides', ''];
47
+
48
+ for (const dir of searchDirs) {
49
+ const filePath = path.join(packageDir, 'docs', dir, filename);
50
+ try {
51
+ const content = fs.readFileSync(filePath, 'utf-8');
52
+ return { content };
53
+ } catch (error) {
54
+ if (error?.code === 'ENOENT') {
55
+ // Try the next allowed documentation directory.
56
+ continue;
57
+ }
58
+ return { error: `Failed to read documentation file "${filePath}": ${error.message}` };
59
+ }
60
+ }
61
+
62
+ const searchedPaths = searchDirs
63
+ .map((dir) => path.join(packageDir, 'docs', dir, filename))
64
+ .join(', ');
65
+ return { error: `Documentation file "${filename}" not found at ${searchedPaths}` };
66
+ }
67
+
68
+ function walkFiles(rootDir, predicate, results = []) {
69
+ if (!fs.existsSync(rootDir)) {
70
+ return results;
71
+ }
72
+
73
+ const stack = [rootDir];
74
+ while (stack.length > 0) {
75
+ const dir = stack.pop();
76
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ const fullPath = path.join(dir, entry.name);
78
+ if (entry.isDirectory()) {
79
+ if (['node_modules', '.git', 'coverage', 'test-results', 'dist', 'build', '.next', 'out'].includes(entry.name)) {
80
+ continue;
81
+ }
82
+ stack.push(fullPath);
83
+ } else if (predicate(fullPath)) {
84
+ results.push(fullPath);
85
+ }
86
+ }
87
+ }
88
+
89
+ return results;
90
+ }
91
+
92
+ function getMarkdownFiles(packageDir) {
93
+ const rootDocs = ['README.md', 'CHANGELOG.md', 'AGENTS.md', 'CLAUDE.md']
94
+ .map((file) => path.join(packageDir, file))
95
+ .filter((file) => fs.existsSync(file));
96
+ const docsFiles = walkFiles(path.join(packageDir, 'docs'), (file) => file.endsWith('.md'));
97
+ const packageDocsFiles = walkFiles(path.join(packageDir, 'packages'), (file) => file.endsWith('.md'));
98
+ return [...rootDocs, ...docsFiles, ...packageDocsFiles].sort((a, b) => a.localeCompare(b));
99
+ }
100
+
101
+ function isExternalLink(target) {
102
+ const trimmed = target.trim();
103
+ return trimmed.startsWith('//') || (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^[a-z]:[\\/]/i.test(trimmed));
104
+ }
105
+
106
+ function stripLinkDecorators(target) {
107
+ return target
108
+ .trim()
109
+ .replaceAll(/^<|>$/g, '')
110
+ .split(/[?#]/)[0];
111
+ }
112
+
113
+ function slugHeading(heading) {
114
+ return stripMarkdownReferenceLabels(stripMarkdownLinkDestinations(heading))
115
+ .trim()
116
+ .toLowerCase()
117
+ .replaceAll(/\s+/g, '-')
118
+ .replaceAll(/[`*_~[\](){}:;'"!?,./\\|+=<>@#$%^&]/g, '');
119
+ }
120
+
121
+ function stripMarkdownLinkDestinations(text) {
122
+ let result = '';
123
+ let cursor = 0;
124
+ while (cursor < text.length) {
125
+ const marker = text.indexOf('](', cursor);
126
+ if (marker === -1) {
127
+ result += text.slice(cursor);
128
+ break;
129
+ }
130
+
131
+ const labelStart = text.lastIndexOf('[', marker);
132
+ if (labelStart < cursor) {
133
+ result += text.slice(cursor, marker + 2);
134
+ cursor = marker + 2;
135
+ continue;
136
+ }
137
+
138
+ const destinationEnd = findMarkdownDestinationEnd(text, marker + 2);
139
+ if (destinationEnd === -1) {
140
+ result += text.slice(cursor);
141
+ break;
142
+ }
143
+
144
+ const replacementStart = labelStart > 0 && text[labelStart - 1] === '!' ? labelStart - 1 : labelStart;
145
+ result += text.slice(cursor, replacementStart);
146
+ result += text.slice(labelStart + 1, marker);
147
+ cursor = destinationEnd + 1;
148
+ }
149
+
150
+ return result;
151
+ }
152
+
153
+ function stripMarkdownReferenceLabels(text) {
154
+ let result = '';
155
+ let cursor = 0;
156
+ while (cursor < text.length) {
157
+ const marker = text.indexOf('][', cursor);
158
+ if (marker === -1) {
159
+ result += text.slice(cursor);
160
+ break;
161
+ }
162
+
163
+ const labelStart = text.lastIndexOf('[', marker);
164
+ const referenceEnd = text.indexOf(']', marker + 2);
165
+ if (labelStart < cursor || referenceEnd === -1) {
166
+ result += text.slice(cursor, marker + 2);
167
+ cursor = marker + 2;
168
+ continue;
169
+ }
170
+
171
+ const replacementStart = labelStart > 0 && text[labelStart - 1] === '!' ? labelStart - 1 : labelStart;
172
+ result += text.slice(cursor, replacementStart);
173
+ result += text.slice(labelStart + 1, marker);
174
+ cursor = referenceEnd + 1;
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ function collectAnchors(content) {
181
+ const anchors = new Set();
182
+ const seen = new Map();
183
+ let activeFence = null;
184
+ let previousTextLine = null;
185
+ for (const line of content.split(/\r?\n/)) {
186
+ const fence = getMarkdownFence(line);
187
+ if (fence) {
188
+ activeFence = updateActiveMarkdownFence(activeFence, fence);
189
+ previousTextLine = null;
190
+ continue;
191
+ }
192
+
193
+ if (activeFence) {
194
+ previousTextLine = null;
195
+ continue;
196
+ }
197
+
198
+ const heading = getMarkdownHeadingText(line);
199
+ if (heading) {
200
+ addAnchor(anchors, seen, heading);
201
+ previousTextLine = null;
202
+ continue;
203
+ }
204
+
205
+ if (previousTextLine && isSetextHeadingUnderline(line)) {
206
+ addAnchor(anchors, seen, previousTextLine);
207
+ previousTextLine = null;
208
+ continue;
209
+ }
210
+
211
+ previousTextLine = line.trim() ? line.trim() : null;
212
+ }
213
+ return anchors;
214
+ }
215
+
216
+ function addAnchor(anchors, seen, heading) {
217
+ const slug = slugHeading(heading);
218
+ const count = seen.get(slug) || 0;
219
+ anchors.add(count === 0 ? slug : `${slug}-${count}`);
220
+ seen.set(slug, count + 1);
221
+ }
222
+
223
+ function isSetextHeadingUnderline(line) {
224
+ const trimmed = line.trim();
225
+ if (!trimmed) {
226
+ return false;
227
+ }
228
+
229
+ const marker = trimmed[0];
230
+ if (marker !== '=' && marker !== '-') {
231
+ return false;
232
+ }
233
+
234
+ for (const char of trimmed) {
235
+ if (char !== marker) {
236
+ return false;
237
+ }
238
+ }
239
+ return true;
240
+ }
241
+
242
+ function getMarkdownHeadingText(line) {
243
+ const trimmed = line.trimStart();
244
+ let level = 0;
245
+ while (trimmed[level] === '#' && level < 6) {
246
+ level++;
247
+ }
248
+
249
+ if (level === 0 || (trimmed[level] !== ' ' && trimmed[level] !== '\t')) {
250
+ return null;
251
+ }
252
+
253
+ return trimClosingHeadingHashes(trimmed.slice(level + 1).trim());
254
+ }
255
+
256
+ function trimClosingHeadingHashes(text) {
257
+ let end = text.length;
258
+ while (end > 0 && text[end - 1] === '#') {
259
+ end--;
260
+ }
261
+
262
+ return text.slice(0, end).trimEnd();
263
+ }
264
+
265
+ function findMarkdownLinks(content) {
266
+ const links = [];
267
+ const referenceDefinitions = _collectReferenceDefinitions(content);
268
+ let offset = 0;
269
+ let activeFence = null;
270
+ let allowIndentedListContinuation = false;
271
+ const lines = content.split('\n');
272
+ for (let index = 0; index < lines.length; index++) {
273
+ const line = lines[index];
274
+ const fence = getMarkdownFence(line);
275
+ if (fence) {
276
+ activeFence = updateActiveMarkdownFence(activeFence, fence);
277
+ offset += line.length + 1;
278
+ allowIndentedListContinuation = false;
279
+ continue;
280
+ }
281
+
282
+ const isListContinuation = allowIndentedListContinuation && isIndentedMarkdownCodeLine(line);
283
+ if (!activeFence && !isIndentedMarkdownCodeLine(line, { isListContinuation })) {
284
+ links.push(...findMarkdownLinksInLine(line, offset, referenceDefinitions, lines[index + 1]));
285
+ }
286
+ allowIndentedListContinuation = shouldAllowIndentedListContinuation(line, isListContinuation);
287
+ offset += line.length + 1;
288
+ }
289
+ return links;
290
+ }
291
+
292
+ function _collectReferenceDefinitions(content) {
293
+ const definitions = new Map();
294
+ let activeFence = null;
295
+ let allowIndentedListContinuation = false;
296
+ const lines = content.split('\n');
297
+ for (let index = 0; index < lines.length; index++) {
298
+ const line = lines[index];
299
+ const fence = getMarkdownFence(line);
300
+ if (fence) {
301
+ activeFence = updateActiveMarkdownFence(activeFence, fence);
302
+ allowIndentedListContinuation = false;
303
+ continue;
304
+ }
305
+ const isListContinuation = allowIndentedListContinuation && isIndentedMarkdownCodeLine(line);
306
+ if (activeFence || isIndentedMarkdownCodeLine(line, { isListContinuation })) {
307
+ allowIndentedListContinuation = shouldAllowIndentedListContinuation(line, isListContinuation);
308
+ continue;
309
+ }
310
+
311
+ const definition = _getReferenceLinkDefinition(line, lines[index + 1]);
312
+ const label = definition ? _normalizeReferenceLabel(definition.label) : null;
313
+ if (definition && !definitions.has(label)) {
314
+ definitions.set(label, definition.target);
315
+ }
316
+ allowIndentedListContinuation = shouldAllowIndentedListContinuation(line, isListContinuation);
317
+ }
318
+ return definitions;
319
+ }
320
+
321
+ function findMarkdownLinksInLine(line, offset, referenceDefinitions = new Map(), nextLine = '') {
322
+ const links = [];
323
+ const referenceDefinition = _getReferenceLinkDefinition(line, nextLine);
324
+ if (referenceDefinition) {
325
+ const label = _normalizeReferenceLabel(referenceDefinition.label);
326
+ if (referenceDefinitions.get(label) === referenceDefinition.target) {
327
+ links.push({ target: referenceDefinition.target, index: offset });
328
+ }
329
+ return links;
330
+ }
331
+
332
+ let cursor = 0;
333
+ while (cursor < line.length) {
334
+ const start = line.indexOf('](', cursor);
335
+ if (start === -1) {
336
+ break;
337
+ }
338
+
339
+ const labelStart = line.lastIndexOf('[', start);
340
+ if (
341
+ isInsideInlineCode(line, start) ||
342
+ (labelStart !== -1 && isEscapedMarkdownCharacter(line, labelStart))
343
+ ) {
344
+ cursor = start + 2;
345
+ continue;
346
+ }
347
+
348
+ const end = findMarkdownDestinationEnd(line, start + 2);
349
+ if (end === -1) {
350
+ break;
351
+ }
352
+
353
+ const target = getMarkdownLinkTarget(line.slice(start + 2, end));
354
+ if (target) {
355
+ links.push({ target, index: offset + start });
356
+ }
357
+ cursor = end + 1;
358
+ }
359
+ links.push(..._findReferenceLinkUsagesInLine(line, offset, referenceDefinitions));
360
+ return links;
361
+ }
362
+
363
+ function isIndentedMarkdownCodeLine(line, { isListContinuation = false } = {}) {
364
+ if (!line.startsWith(' ') && !line.startsWith('\t')) {
365
+ return false;
366
+ }
367
+ return !(isListContinuation && line.startsWith(' ') && !line.startsWith(' '));
368
+ }
369
+
370
+ function shouldAllowIndentedListContinuation(line, isListContinuation) {
371
+ if (isMarkdownListItemLine(line)) {
372
+ return true;
373
+ }
374
+ return isListContinuation && line.trim() !== '';
375
+ }
376
+
377
+ function isMarkdownListItemLine(line) {
378
+ return /^\s{0,3}(?:[-+*]|\d{1,9}[.)])\s+/.test(line);
379
+ }
380
+
381
+ function getMarkdownFence(line) {
382
+ let leadingSpaces = 0;
383
+ while (line[leadingSpaces] === ' ') {
384
+ leadingSpaces += 1;
385
+ }
386
+ if (leadingSpaces > 3) {
387
+ return null;
388
+ }
389
+
390
+ const trimmed = line.slice(leadingSpaces);
391
+ const marker = trimmed[0];
392
+ if (marker !== '`' && marker !== '~') {
393
+ return null;
394
+ }
395
+
396
+ let length = 0;
397
+ while (trimmed[length] === marker) {
398
+ length += 1;
399
+ }
400
+
401
+ if (length < 3) {
402
+ return null;
403
+ }
404
+
405
+ return { marker, length, hasInfo: trimmed.slice(length).trim() !== '' };
406
+ }
407
+
408
+ function updateActiveMarkdownFence(activeFence, fence) {
409
+ if (!activeFence) {
410
+ return fence;
411
+ }
412
+ return fence.marker === activeFence.marker && fence.length >= activeFence.length && !fence.hasInfo
413
+ ? null
414
+ : activeFence;
415
+ }
416
+
417
+ function _findReferenceLinkUsagesInLine(line, offset, referenceDefinitions) {
418
+ const links = [];
419
+ let cursor = 0;
420
+ while (cursor < line.length) {
421
+ const usage = findNextReferenceLinkUsage(line, cursor);
422
+ if (!usage) {
423
+ break;
424
+ }
425
+
426
+ const label = _normalizeReferenceLabel(usage.rawLabel);
427
+ const target = referenceDefinitions.get(label);
428
+ if (target) {
429
+ links.push({ target, index: offset + usage.labelStart });
430
+ } else if (usage.hasFullReference) {
431
+ const missingReference = usage.rawLabel.trim();
432
+ if (missingReference) {
433
+ links.push({ missingReference, index: offset + usage.labelStart });
434
+ }
435
+ }
436
+ cursor = usage.nextCursor;
437
+ }
438
+ return links;
439
+ }
440
+
441
+ function findNextReferenceLinkUsage(line, cursor) {
442
+ while (cursor < line.length) {
443
+ const labelStart = line.indexOf('[', cursor);
444
+ if (labelStart === -1) {
445
+ return null;
446
+ }
447
+
448
+ const skippedCursor = getSkippedReferenceLabelCursor(line, labelStart);
449
+ if (skippedCursor !== null) {
450
+ cursor = skippedCursor;
451
+ continue;
452
+ }
453
+
454
+ const labelEnd = line.indexOf(']', labelStart + 1);
455
+ if (labelEnd === -1) {
456
+ return null;
457
+ }
458
+
459
+ const reference = parseReferenceSuffix(line, labelStart, labelEnd);
460
+ if (reference) {
461
+ return reference;
462
+ }
463
+ cursor = labelEnd + 1;
464
+ }
465
+ return null;
466
+ }
467
+
468
+ function getSkippedReferenceLabelCursor(line, labelStart) {
469
+ if (
470
+ (labelStart > 0 && line[labelStart - 1] === '!') ||
471
+ isEscapedMarkdownCharacter(line, labelStart) ||
472
+ isInsideInlineCode(line, labelStart)
473
+ ) {
474
+ return labelStart + 1;
475
+ }
476
+ if (line[labelStart + 1] === '^') {
477
+ return labelStart + 1;
478
+ }
479
+ return null;
480
+ }
481
+
482
+ function isEscapedMarkdownCharacter(line, index) {
483
+ let slashCount = 0;
484
+ for (let cursor = index - 1; cursor >= 0 && line[cursor] === '\\'; cursor--) {
485
+ slashCount++;
486
+ }
487
+ return slashCount % 2 === 1;
488
+ }
489
+
490
+ function parseReferenceSuffix(line, labelStart, labelEnd) {
491
+ if (line[labelEnd + 1] === '(') {
492
+ return null;
493
+ }
494
+
495
+ const hasFullReference = line[labelEnd + 1] === '[';
496
+ const referenceEnd = hasFullReference ? line.indexOf(']', labelEnd + 2) : labelEnd;
497
+ if (referenceEnd === -1) {
498
+ return null;
499
+ }
500
+
501
+ const rawLabel = hasFullReference
502
+ ? line.slice(labelEnd + 2, referenceEnd) || line.slice(labelStart + 1, labelEnd)
503
+ : line.slice(labelStart + 1, labelEnd);
504
+ return { hasFullReference, labelStart, nextCursor: referenceEnd + 1, rawLabel };
505
+ }
506
+
507
+ function findMarkdownDestinationEnd(line, start) {
508
+ let depth = 0;
509
+ for (let index = start; index < line.length; index++) {
510
+ const char = line[index];
511
+ if (char === '\\') {
512
+ index++;
513
+ continue;
514
+ }
515
+ if (char === '(') {
516
+ depth++;
517
+ } else if (char === ')') {
518
+ if (depth === 0) {
519
+ return index;
520
+ }
521
+ depth--;
522
+ }
523
+ }
524
+ return -1;
525
+ }
526
+
527
+ function isInsideInlineCode(line, index) {
528
+ let cursor = 0;
529
+ while (cursor < index) {
530
+ if (line[cursor] !== '`') {
531
+ cursor++;
532
+ continue;
533
+ }
534
+ if (cursor > 0 && line[cursor - 1] === '\\') {
535
+ cursor++;
536
+ continue;
537
+ }
538
+
539
+ const start = cursor;
540
+ while (cursor < line.length && line[cursor] === '`') {
541
+ cursor++;
542
+ }
543
+
544
+ const marker = line.slice(start, cursor);
545
+ const end = line.indexOf(marker, cursor);
546
+ if (end === -1) {
547
+ return false;
548
+ }
549
+ if (index > cursor && index < end) {
550
+ return true;
551
+ }
552
+ cursor = end + marker.length;
553
+ }
554
+ return false;
555
+ }
556
+
557
+ function getMarkdownLinkTarget(rawDestination) {
558
+ const trimmed = rawDestination.trim();
559
+ if (trimmed.startsWith('<')) {
560
+ const end = trimmed.indexOf('>');
561
+ return end > 1 ? trimmed.slice(0, end + 1) : trimmed;
562
+ }
563
+
564
+ const titleStart = _findUnescapedWhitespaceIndex(trimmed);
565
+ const target = titleStart === -1 ? trimmed : trimmed.slice(0, titleStart);
566
+ return target.replace(/\\([ \t])/g, '$1');
567
+ }
568
+
569
+ function _findUnescapedWhitespaceIndex(text) {
570
+ for (let index = 0; index < text.length; index++) {
571
+ if (text[index] === '\\') {
572
+ index++;
573
+ continue;
574
+ }
575
+ if (/\s/.test(text[index])) {
576
+ return index;
577
+ }
578
+ }
579
+ return -1;
580
+ }
581
+
582
+ function _getReferenceLinkDefinition(line, nextLine = '') {
583
+ const trimmed = line.trimStart();
584
+ if (!trimmed.startsWith('[')) {
585
+ return null;
586
+ }
587
+
588
+ const labelEnd = trimmed.indexOf(']:');
589
+ if (labelEnd <= 1) {
590
+ return null;
591
+ }
592
+ if (trimmed.slice(1, labelEnd).startsWith('^')) {
593
+ return null;
594
+ }
595
+
596
+ const rawTarget = trimmed.slice(labelEnd + 2);
597
+ const target = getMarkdownLinkTarget(rawTarget.trim() ? rawTarget : nextLine);
598
+ return target ? { label: trimmed.slice(1, labelEnd), target } : null;
599
+ }
600
+
601
+ function _normalizeReferenceLabel(label) {
602
+ return label.trim().replace(/\s+/g, ' ').toLowerCase();
603
+ }
604
+
605
+ function lineForIndex(content, index) {
606
+ return content.slice(0, index).split(/\r?\n/).length;
607
+ }
608
+
609
+ function formatLinkTargetForReport(rawTarget) {
610
+ const normalized = rawTarget.trim().replaceAll('\\', '/');
611
+ if (
612
+ normalized.startsWith('/') ||
613
+ (!path.isAbsolute(normalized) && !/^[A-Za-z]:\//.test(normalized))
614
+ ) {
615
+ return rawTarget;
616
+ }
617
+
618
+ const worktreeIndex = normalized.indexOf('/.worktrees/');
619
+ if (worktreeIndex >= 0) {
620
+ return `<repo>${normalized.slice(worktreeIndex)}`;
621
+ }
622
+
623
+ const repoIndex = normalized.toLowerCase().lastIndexOf('/forge/');
624
+ if (repoIndex >= 0) {
625
+ return `<repo>/${normalized.slice(repoIndex + '/forge/'.length)}`;
626
+ }
627
+
628
+ return `<absolute-path>/${path.posix.basename(normalized)}`;
629
+ }
630
+
631
+ function isWithinRoot(rootDir, candidatePath) {
632
+ const relative = path.relative(
633
+ fs.realpathSync.native(path.resolve(rootDir)),
634
+ resolveRealPathForContainment(candidatePath)
635
+ );
636
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
637
+ }
638
+
639
+ function resolveRealPathForContainment(candidatePath) {
640
+ const resolved = path.resolve(candidatePath);
641
+ if (fs.existsSync(resolved)) {
642
+ return fs.realpathSync.native(resolved);
643
+ }
644
+
645
+ const missingSegments = [];
646
+ let current = resolved;
647
+ while (!fs.existsSync(current)) {
648
+ const parent = path.dirname(current);
649
+ if (parent === current) {
650
+ return resolved;
651
+ }
652
+ missingSegments.unshift(path.basename(current));
653
+ current = parent;
654
+ }
655
+
656
+ return path.join(fs.realpathSync.native(current), ...missingSegments);
657
+ }
658
+
659
+ function resolveMarkdownLink(packageDir, file, rawTarget) {
660
+ const [filePart, anchorPart] = rawTarget.replaceAll(/^<|>$/g, '').split('#');
661
+ if (!filePart) {
662
+ return { anchorPart, resolved: file };
663
+ }
664
+
665
+ const targetPath = decodeLocalMarkdownPath(stripLinkDecorators(filePart));
666
+ const resolved = targetPath.startsWith('/') && !targetPath.startsWith('//')
667
+ ? path.resolve(packageDir, targetPath.slice(1))
668
+ : path.resolve(path.dirname(file), targetPath);
669
+ return { anchorPart, resolved };
670
+ }
671
+
672
+ function decodeLocalMarkdownPath(targetPath) {
673
+ try {
674
+ return decodeURIComponent(targetPath);
675
+ } catch {
676
+ return targetPath;
677
+ }
678
+ }
679
+
680
+ function checkResolvedLink(packageDir, file, relFile, line, rawTarget) {
681
+ const { anchorPart, resolved } = resolveMarkdownLink(packageDir, file, rawTarget);
682
+ const target = formatLinkTargetForReport(rawTarget);
683
+ if (!isWithinRoot(packageDir, resolved)) {
684
+ return { file: relFile, line, target, reason: 'Link escapes project root' };
685
+ }
686
+
687
+ if (!fs.existsSync(resolved)) {
688
+ return { file: relFile, line, target, reason: 'Target file does not exist' };
689
+ }
690
+
691
+ if (anchorPart && resolved.endsWith('.md')) {
692
+ const targetContent = fs.readFileSync(resolved, 'utf8');
693
+ const anchors = collectAnchors(targetContent);
694
+ if (!anchors.has(slugHeading(decodeMarkdownAnchor(anchorPart)))) {
695
+ return { file: relFile, line, target, reason: 'Target anchor does not exist' };
696
+ }
697
+ }
698
+
699
+ return null;
700
+ }
701
+
702
+ function decodeMarkdownAnchor(anchor) {
703
+ try {
704
+ return decodeURIComponent(anchor);
705
+ } catch {
706
+ return anchor;
707
+ }
708
+ }
709
+
710
+ function checkMarkdownFileLinks(packageDir, file) {
711
+ const brokenLinks = [];
712
+ let linksChecked = 0;
713
+ const content = fs.readFileSync(file, 'utf8');
714
+ const relFile = path.relative(packageDir, file).replaceAll('\\', '/');
715
+ for (const link of findMarkdownLinks(content)) {
716
+ const line = lineForIndex(content, link.index);
717
+ if (link.missingReference) {
718
+ brokenLinks.push({
719
+ file: relFile,
720
+ line,
721
+ target: link.missingReference,
722
+ reason: 'Reference link definition does not exist',
723
+ });
724
+ linksChecked++;
725
+ continue;
726
+ }
727
+
728
+ const rawTarget = link.target.trim();
729
+ if (!rawTarget || isExternalLink(rawTarget)) {
730
+ continue;
731
+ }
732
+
733
+ linksChecked++;
734
+ const brokenLink = checkResolvedLink(packageDir, file, relFile, line, rawTarget);
735
+ if (brokenLink) {
736
+ brokenLinks.push(brokenLink);
737
+ }
738
+ }
739
+
740
+ return { linksChecked, brokenLinks };
741
+ }
742
+
743
+ function checkMarkdownLinks(packageDir) {
744
+ const files = getMarkdownFiles(packageDir);
745
+ const totals = { filesChecked: files.length, linksChecked: 0, brokenLinks: [] };
746
+ for (const file of files) {
747
+ const result = checkMarkdownFileLinks(packageDir, file);
748
+ totals.linksChecked += result.linksChecked;
749
+ totals.brokenLinks.push(...result.brokenLinks);
750
+ }
751
+ return totals;
752
+ }
753
+
754
+ function getSourceFiles(packageDir) {
755
+ const sourceRoots = ['lib', 'bin', 'scripts', 'src', 'apps', 'packages'];
756
+ const files = new Set();
757
+ for (const file of _getRootSourceEntryFiles(packageDir)) {
758
+ files.add(file);
759
+ }
760
+ for (const dir of sourceRoots) {
761
+ for (const file of walkFiles(path.join(packageDir, dir), isJavaScriptSourceFile)) {
762
+ files.add(file);
763
+ }
764
+ }
765
+
766
+ return Array.from(files)
767
+ .sort((a, b) => a.localeCompare(b));
768
+ }
769
+
770
+ function _getRootSourceEntryFiles(packageDir) {
771
+ const candidates = new Set([
772
+ 'index.js',
773
+ 'index.jsx',
774
+ 'index.mjs',
775
+ 'index.cjs',
776
+ 'index.ts',
777
+ 'index.tsx',
778
+ 'index.mts',
779
+ 'index.cts',
780
+ ]);
781
+ for (const entry of _getPackageJsonSourceEntries(packageDir)) {
782
+ candidates.add(entry);
783
+ }
784
+
785
+ return Array.from(candidates)
786
+ .map((file) => path.resolve(packageDir, file))
787
+ .filter((file) => isWithinRoot(packageDir, file) && fs.existsSync(file) && isJavaScriptSourceFile(file));
788
+ }
789
+
790
+ function _getPackageJsonSourceEntries(packageDir) {
791
+ const packageJsonPath = path.join(packageDir, 'package.json');
792
+ if (!fs.existsSync(packageJsonPath)) {
793
+ return [];
794
+ }
42
795
 
43
796
  try {
44
- const content = fs.readFileSync(filePath, 'utf-8');
45
- return { content };
46
- } catch (_error) {
47
- return { error: `Documentation file "${filename}" not found at ${filePath}` };
797
+ const manifest = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
798
+ return _collectPackageEntryValues([manifest.main, manifest.module, manifest.browser, manifest.exports, manifest.bin]);
799
+ } catch {
800
+ return [];
801
+ }
802
+ }
803
+
804
+ function _collectPackageEntryValues(values) {
805
+ const entries = [];
806
+ for (const value of Array.isArray(values) ? values : [values]) {
807
+ if (typeof value === 'string') {
808
+ entries.push(value);
809
+ } else if (value && typeof value === 'object') {
810
+ entries.push(..._collectPackageEntryValues(Object.values(value)));
811
+ }
812
+ }
813
+ return entries;
814
+ }
815
+
816
+ function isJavaScriptSourceFile(file) {
817
+ if (!['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.mts', '.cts'].includes(path.extname(file))) {
818
+ return false;
819
+ }
820
+ if (hasTestPathSegment(file)) {
821
+ return false;
822
+ }
823
+ return !/(?:^|[.-])(test|spec)\.[cm]?[jt]sx?$/.test(path.basename(file));
824
+ }
825
+
826
+ function hasTestPathSegment(file) {
827
+ return file
828
+ .replaceAll('\\', '/')
829
+ .split('/')
830
+ .some((segment) => ['test', 'tests', '__tests__', 'spec', 'specs', '__specs__'].includes(segment));
831
+ }
832
+
833
+ function hasLeadingJsDoc(source, comments, node) {
834
+ const previous = comments
835
+ .filter((comment) => comment.end <= node.start)
836
+ .sort((a, b) => b.end - a.end)[0];
837
+
838
+ if (previous?.type !== 'CommentBlock' || !previous.value.trim().startsWith('*')) {
839
+ return false;
840
+ }
841
+
842
+ return source.slice(previous.end, node.start).trim() === '';
843
+ }
844
+
845
+ function getPublicDocTargets(_source, ast) {
846
+ const targets = [];
847
+ const declarations = collectTopLevelDocDeclarations(ast);
848
+ for (const node of ast.program.body) {
849
+ if (node.type === 'ExportNamedDeclaration') {
850
+ addDocTargetsFromDeclaration(targets, node.declaration, node);
851
+ addDocTargetsFromExportSpecifiers(targets, node, declarations);
852
+ } else if (node.type === 'ExportDefaultDeclaration') {
853
+ addDocTargetsFromDeclaration(targets, node.declaration, node);
854
+ addDocTargetFromDefaultExportIdentifier(targets, node, declarations);
855
+ } else if (node.type === 'ExpressionStatement') {
856
+ addDocTargetFromCommonJsExport(targets, node, declarations);
857
+ }
858
+ }
859
+ return targets.filter((target) => !getDocTargetPublicName(target.name).startsWith('_'));
860
+ }
861
+
862
+ function getDocTargetPublicName(name) {
863
+ const parts = String(name || '').split('.');
864
+ return parts[parts.length - 1] || '';
865
+ }
866
+
867
+ function collectTopLevelDocDeclarations(ast) {
868
+ const declarations = new Map();
869
+ for (const node of ast.program.body) {
870
+ const targets = [];
871
+ addDocTargetsFromDeclaration(targets, node);
872
+ for (const target of targets) {
873
+ declarations.set(target.name, target);
874
+ }
875
+ }
876
+ return declarations;
877
+ }
878
+
879
+ function addDocTargetsFromExportSpecifiers(targets, node, declarations) {
880
+ for (const specifier of node.specifiers || []) {
881
+ if (specifier.type !== 'ExportSpecifier' || specifier.local?.type !== 'Identifier') {
882
+ continue;
883
+ }
884
+
885
+ const declaration = declarations.get(specifier.local.name);
886
+ if (declaration) {
887
+ targets.push({
888
+ name: getExportedSpecifierName(specifier) || declaration.name,
889
+ node: declaration.node,
890
+ });
891
+ }
48
892
  }
49
893
  }
50
894
 
51
- module.exports = { listTopics, getTopicContent, TOPICS };
895
+ function addDocTargetFromDefaultExportIdentifier(targets, node, declarations) {
896
+ if (node.declaration?.type !== 'Identifier') {
897
+ return;
898
+ }
899
+
900
+ const declaration = declarations.get(node.declaration.name);
901
+ if (declaration) {
902
+ targets.push({ name: 'default', node: declaration.node });
903
+ }
904
+ }
905
+
906
+ function getExportedSpecifierName(specifier) {
907
+ if (specifier.exported?.type === 'Identifier') {
908
+ return specifier.exported.name;
909
+ }
910
+ return specifier.exported?.type === 'StringLiteral' ? specifier.exported.value : null;
911
+ }
912
+
913
+ function addDocTargetsFromDeclaration(targets, node, reportNode = node) {
914
+ if (!node) {
915
+ return;
916
+ }
917
+
918
+ if (
919
+ reportNode?.type === 'ExportDefaultDeclaration' &&
920
+ [
921
+ 'ArrowFunctionExpression',
922
+ 'FunctionExpression',
923
+ 'ClassExpression',
924
+ 'FunctionDeclaration',
925
+ 'ClassDeclaration',
926
+ ].includes(node.type)
927
+ ) {
928
+ targets.push({ name: 'default', node: reportNode });
929
+ } else if (node.type === 'FunctionDeclaration' && node.id?.name) {
930
+ targets.push({ name: node.id.name, node: reportNode });
931
+ } else if (node.type === 'ClassDeclaration' && node.id?.name) {
932
+ targets.push({ name: node.id.name, node: reportNode });
933
+ } else if (node.type === 'VariableDeclaration') {
934
+ for (const declaration of node.declarations) {
935
+ const initType = declaration.init?.type;
936
+ if (
937
+ declaration.id?.type === 'Identifier' &&
938
+ ['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression'].includes(initType)
939
+ ) {
940
+ targets.push({ name: declaration.id.name, node: reportNode });
941
+ }
942
+ }
943
+ }
944
+ }
945
+
946
+ function addDocTargetFromCommonJsExport(targets, node, declarations = new Map()) {
947
+ const expression = node.expression;
948
+ if (expression?.type !== 'AssignmentExpression') {
949
+ return;
950
+ }
951
+
952
+ const initType = expression.right?.type;
953
+ const name = getCommonJsExportName(expression.left);
954
+ if (!name) {
955
+ return;
956
+ }
957
+
958
+ if (['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression'].includes(initType)) {
959
+ targets.push({ name, node });
960
+ } else if (name === 'module.exports' && initType === 'ObjectExpression') {
961
+ addDocTargetsFromObjectExport(targets, expression.right, declarations);
962
+ } else if (initType === 'Identifier') {
963
+ const declaration = declarations.get(expression.right.name);
964
+ if (declaration) {
965
+ targets.push({ name, node: declaration.node });
966
+ }
967
+ }
968
+ }
969
+
970
+ function addDocTargetsFromObjectExport(targets, node, declarations = new Map()) {
971
+ for (const property of node.properties || []) {
972
+ const hasExportedFunction = property.type === 'ObjectMethod' || (
973
+ property.type === 'ObjectProperty' &&
974
+ ['ArrowFunctionExpression', 'FunctionExpression', 'ClassExpression'].includes(property.value?.type)
975
+ );
976
+ if (hasExportedFunction) {
977
+ const name = getObjectPropertyName(property);
978
+ if (name) {
979
+ targets.push({ name: `module.exports.${name}`, node: property });
980
+ }
981
+ } else if (property.type === 'ObjectProperty' && property.value?.type === 'Identifier') {
982
+ const name = getObjectPropertyName(property);
983
+ const declaration = declarations.get(property.value.name);
984
+ if (name && declaration) {
985
+ targets.push({ name: `module.exports.${name}`, node: declaration.node });
986
+ }
987
+ }
988
+ }
989
+ }
990
+
991
+ function getCommonJsExportName(node) {
992
+ if (node?.type !== 'MemberExpression') {
993
+ return null;
994
+ }
995
+
996
+ if (node.object?.type === 'Identifier' && node.object.name === 'exports') {
997
+ return getMemberPropertyName(node);
998
+ }
999
+
1000
+ if (
1001
+ node.object?.type === 'MemberExpression' &&
1002
+ node.object.object?.type === 'Identifier' &&
1003
+ node.object.object.name === 'module' &&
1004
+ getMemberPropertyName(node.object) === 'exports'
1005
+ ) {
1006
+ return getMemberPropertyName(node);
1007
+ }
1008
+
1009
+ if (node.object?.type === 'Identifier' && node.object.name === 'module' && getMemberPropertyName(node) === 'exports') {
1010
+ return 'module.exports';
1011
+ }
1012
+
1013
+ return null;
1014
+ }
1015
+
1016
+ function getMemberPropertyName(node) {
1017
+ if (node.computed) {
1018
+ return node.property?.type === 'StringLiteral' ? node.property.value : null;
1019
+ }
1020
+ return node.property?.type === 'Identifier' ? node.property.name : null;
1021
+ }
1022
+
1023
+ function getObjectPropertyName(node) {
1024
+ if (node.computed) {
1025
+ return node.key?.type === 'StringLiteral' ? node.key.value : null;
1026
+ }
1027
+ if (node.key?.type === 'Identifier') {
1028
+ return node.key.name;
1029
+ }
1030
+ return node.key?.type === 'StringLiteral' ? node.key.value : null;
1031
+ }
1032
+
1033
+ function checkDocstringCoverage(packageDir) {
1034
+ const missing = [];
1035
+ let total = 0;
1036
+ let documented = 0;
1037
+ const files = getSourceFiles(packageDir);
1038
+
1039
+ for (const file of files) {
1040
+ const source = fs.readFileSync(file, 'utf8');
1041
+ let ast;
1042
+ try {
1043
+ ast = parse(source, {
1044
+ sourceType: 'unambiguous',
1045
+ plugins: getParserPlugins(file),
1046
+ attachComment: true,
1047
+ locations: true,
1048
+ ranges: false,
1049
+ });
1050
+ } catch (error) {
1051
+ missing.push({
1052
+ file: path.relative(packageDir, file).replaceAll('\\', '/'),
1053
+ name: '<parse-error>',
1054
+ line: 1,
1055
+ reason: error.message,
1056
+ });
1057
+ continue;
1058
+ }
1059
+
1060
+ for (const target of getPublicDocTargets(source, ast)) {
1061
+ total++;
1062
+ if (hasLeadingJsDoc(source, ast.comments || [], target.node)) {
1063
+ documented++;
1064
+ } else {
1065
+ missing.push({
1066
+ file: path.relative(packageDir, file).replaceAll('\\', '/'),
1067
+ name: target.name,
1068
+ line: target.node.loc?.start?.line || 1,
1069
+ reason: 'Missing leading JSDoc block',
1070
+ });
1071
+ }
1072
+ }
1073
+ }
1074
+
1075
+ const percent = total === 0 ? 100 : Math.round((documented / total) * 10000) / 100;
1076
+ return { filesChecked: files.length, total, documented, percent, missing };
1077
+ }
1078
+
1079
+ function getParserPlugins(file) {
1080
+ const ext = path.extname(file).toLowerCase();
1081
+ const plugins = ['topLevelAwait', ['decorators', { decoratorsBeforeExport: true }]];
1082
+ if (['.ts', '.tsx', '.mts', '.cts'].includes(ext)) {
1083
+ plugins.push('typescript');
1084
+ }
1085
+ if (['.js', '.mjs', '.jsx', '.tsx'].includes(ext)) {
1086
+ plugins.push('jsx');
1087
+ }
1088
+ return plugins;
1089
+ }
1090
+
1091
+ function brokenLinkKey(item) {
1092
+ return `${item.file}\0${item.line}\0${item.target}\0${item.reason}`;
1093
+ }
1094
+
1095
+ function loadDocsBaseline(packageDir, baselinePath) {
1096
+ if (!baselinePath) {
1097
+ return new Set();
1098
+ }
1099
+
1100
+ const resolved = path.resolve(packageDir, baselinePath);
1101
+ if (!isWithinRoot(packageDir, resolved)) {
1102
+ throw new Error(`Baseline path escapes project root: "${baselinePath}"`);
1103
+ }
1104
+ if (!fs.existsSync(resolved)) {
1105
+ return new Set();
1106
+ }
1107
+
1108
+ try {
1109
+ const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
1110
+ return new Set((parsed.brokenLinks || []).map(brokenLinkKey));
1111
+ } catch (error) {
1112
+ throw new Error(`Invalid docs baseline JSON at "${resolved}": ${error.message}`);
1113
+ }
1114
+ }
1115
+
1116
+ function writeDocsBaseline(packageDir, baselinePath, result) {
1117
+ const resolved = path.resolve(packageDir, baselinePath);
1118
+ if (!isWithinRoot(packageDir, resolved)) {
1119
+ throw new Error(`Baseline path escapes project root: "${baselinePath}"`);
1120
+ }
1121
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
1122
+ fs.writeFileSync(
1123
+ resolved,
1124
+ `${JSON.stringify({
1125
+ generatedBy: 'forge docs verify --write-baseline',
1126
+ brokenLinks: result.links.allBrokenLinks || result.links.brokenLinks,
1127
+ }, null, 2)}\n`,
1128
+ 'utf8'
1129
+ );
1130
+ }
1131
+
1132
+ function validateDocs(packageDir, options = {}) {
1133
+ const minDocstringCoverage = Number.isFinite(options.minDocstringCoverage)
1134
+ ? options.minDocstringCoverage
1135
+ : 0;
1136
+ const links = checkMarkdownLinks(packageDir);
1137
+ const baseline = loadDocsBaseline(packageDir, options.baselinePath);
1138
+ const allBrokenLinks = links.brokenLinks;
1139
+ const newBrokenLinks = allBrokenLinks.filter((item) => !baseline.has(brokenLinkKey(item)));
1140
+ links.allBrokenLinks = allBrokenLinks;
1141
+ links.knownBrokenLinks = allBrokenLinks.length - newBrokenLinks.length;
1142
+ links.brokenLinks = newBrokenLinks;
1143
+ const docstrings = checkDocstringCoverage(packageDir);
1144
+ const parseErrors = docstrings.missing
1145
+ .filter((item) => item.name === '<parse-error>')
1146
+ .map((item) => ({ type: 'docstring-parse-error', ...item }));
1147
+ const failures = [
1148
+ ...links.brokenLinks.map((item) => ({ type: 'broken-link', ...item })),
1149
+ ...parseErrors,
1150
+ ...(docstrings.percent < minDocstringCoverage
1151
+ ? [{
1152
+ type: 'docstring-coverage',
1153
+ reason: `Docstring coverage ${docstrings.percent}% is below ${minDocstringCoverage}%`,
1154
+ }]
1155
+ : []),
1156
+ ];
1157
+
1158
+ return {
1159
+ ok: failures.length === 0,
1160
+ links,
1161
+ docstrings,
1162
+ failures,
1163
+ };
1164
+ }
1165
+
1166
+ function formatDocsValidation(result) {
1167
+ const lines = [
1168
+ 'Forge docs validation',
1169
+ '',
1170
+ `Markdown files checked: ${result.links.filesChecked}`,
1171
+ `Markdown links checked: ${result.links.linksChecked}`,
1172
+ `Broken links: ${result.links.brokenLinks.length}`,
1173
+ `Known broken links in baseline: ${result.links.knownBrokenLinks || 0}`,
1174
+ `Source files checked: ${result.docstrings.filesChecked}`,
1175
+ `Docstring coverage: ${result.docstrings.documented}/${result.docstrings.total} (${result.docstrings.percent}%)`,
1176
+ ];
1177
+
1178
+ if (result.links.brokenLinks.length > 0) {
1179
+ lines.push('', 'Broken links:');
1180
+ for (const item of result.links.brokenLinks) {
1181
+ lines.push(` - ${item.file}:${item.line} ${item.target} (${item.reason})`);
1182
+ }
1183
+ }
1184
+
1185
+ if (result.docstrings.missing.length > 0) {
1186
+ lines.push('', 'Missing docstrings:');
1187
+ for (const item of result.docstrings.missing.slice(0, 50)) {
1188
+ lines.push(` - ${item.file}:${item.line} ${item.name} (${item.reason})`);
1189
+ }
1190
+ if (result.docstrings.missing.length > 50) {
1191
+ lines.push(` ... ${result.docstrings.missing.length - 50} more`);
1192
+ }
1193
+ }
1194
+
1195
+ lines.push('', result.ok ? 'Docs validation passed.' : 'Docs validation failed.');
1196
+ return lines.join('\n');
1197
+ }
1198
+
1199
+ module.exports = {
1200
+ listTopics,
1201
+ getTopicContent,
1202
+ validateDocs,
1203
+ formatDocsValidation,
1204
+ writeDocsBaseline,
1205
+ TOPICS,
1206
+ };