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
@@ -0,0 +1,375 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * doc-gate enforcement gate.
5
+ *
6
+ * Built ON TOP of the validated repo-structure detector
7
+ * (lib/doc-gate/detect.js). The detector resolves a repo's source surface and
8
+ * emits a `verdict` (CODE-RESOLVED | MANUAL-CONFIG | ESCALATE-TO-AGENT). This
9
+ * module turns that surface into a PR gate that enforces the rule:
10
+ *
11
+ * "a code change must be accompanied by a doc update."
12
+ *
13
+ * Design (do not regress):
14
+ * - ABSTAIN-first: if the detector could not confidently resolve the source
15
+ * surface (verdict MANUAL-CONFIG or ESCALATE-TO-AGENT) we NEVER hard-fail.
16
+ * This naturally exempts monorepos (e.g. forge itself, whose npm-workspaces
17
+ * layout escalates), so the gate is safe to make a required check.
18
+ * - Doc-path exclusion is mandatory: a flat-root `source:["."]` repo spans the
19
+ * whole tree (including docs), so doc-only edits must never be counted as a
20
+ * "code change".
21
+ * - Only Added/Modified files matter (the changelog-enforcer A/M pattern);
22
+ * deletions never require a doc update.
23
+ * - Dependency-free: Node built-ins + the tracked-git helper style from
24
+ * detect.js.
25
+ *
26
+ * @module doc-gate/gate
27
+ */
28
+
29
+ const cp = require('node:child_process');
30
+ const { detect } = require('./detect');
31
+
32
+ // Strict git: THROWS on any failure. The gate must NEVER treat a git error (a bad
33
+ // ref, a diff failure) as "no changes" — that would fail the gate OPEN and defeat
34
+ // the "safe to require" design. Callers turn a throw into an explicit fail-closed.
35
+ function gitStrict(root, args) {
36
+ const res = cp.spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' }); // NOSONAR S4036 - hardcoded CLI command, no user input.
37
+ if (res.error) throw new Error(`git ${args.join(' ')}: ${res.error.message}`);
38
+ if (res.status !== 0) throw new Error(`git ${args.join(' ')} exited ${res.status}: ${String(res.stderr || '').trim()}`);
39
+ return res.stdout;
40
+ }
41
+
42
+ const toPosix = p => String(p).replaceAll('\\', '/').replace(/^\.\//, '');
43
+ const baseName = p => toPosix(p).split('/').pop();
44
+ const extOf = p => {
45
+ const b = baseName(p);
46
+ const i = b.lastIndexOf('.');
47
+ return i > 0 ? b.slice(i).toLowerCase() : '';
48
+ };
49
+
50
+ // --- declaration glob matching (excludeFromGate / rules) ---------------------
51
+ // Deterministic, ReDoS-safe glob → RegExp. Only linear tokens are emitted
52
+ // (`[^/]*`, `[^/]`, `.*`, `(?:.*/)?`), anchored ^…$ — no nested/overlapping
53
+ // quantifiers, so committed (trusted) globs cannot cause catastrophic backtracking.
54
+ const GLOB_SPECIALS = new Set(['.', '+', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']);
55
+ function globToRegExp(glob) {
56
+ const g = toPosix(glob);
57
+ let re = '^';
58
+ let i = 0;
59
+ while (i < g.length) {
60
+ const c = g[i];
61
+ if (c === '*' && g[i + 1] === '*') {
62
+ if (g[i + 2] === '/') { re += '(?:.*/)?'; i += 3; } // '**/' — zero or more dirs
63
+ else { re += '.*'; i += 2; } // trailing '**' — anything, including '/'
64
+ } else if (c === '*') {
65
+ re += '[^/]*'; i += 1; // single-segment wildcard
66
+ } else if (c === '?') {
67
+ re += '[^/]'; i += 1;
68
+ } else if (GLOB_SPECIALS.has(c)) {
69
+ re += `\\${c}`; i += 1;
70
+ } else {
71
+ re += c; i += 1;
72
+ }
73
+ }
74
+ return new RegExp(`${re}$`);
75
+ }
76
+
77
+ /**
78
+ * True when repo-relative path `rel` matches ANY of `globs`. A bare directory
79
+ * glob (no wildcard) also matches everything beneath it (prefix semantics).
80
+ *
81
+ * @param {string} rel - Repo-relative POSIX path.
82
+ * @param {string[]} globs - Declaration globs.
83
+ * @returns {boolean}
84
+ */
85
+ function matchesAnyGlob(rel, globs) {
86
+ for (const raw of globs) {
87
+ const glob = toPosix(raw).replace(/\/+$/, '');
88
+ if (!glob) continue;
89
+ if (rel === glob || rel.startsWith(`${glob}/`)) return true; // exact or dir prefix
90
+ if (globToRegExp(glob).test(rel)) return true;
91
+ }
92
+ return false;
93
+ }
94
+
95
+ // --- doc-path classification (mandatory exclusion) ---------------------------
96
+ const DOC_EXTS = new Set(['.md', '.mdx', '.rst']);
97
+ // Anchored so NEWSLETTER.js / LICENSEMANAGER.go (real code) are NOT treated as
98
+ // docs: the base name must be exactly the word, or word + a [._-] separator.
99
+ const DOC_BASENAME_RE = /^(README|CHANGELOG|HISTORY|NEWS|LICENSE)([._-].*)?$/i;
100
+ const DOC_DIR_PREFIXES = ['docs/', '.changeset/'];
101
+ const DOC_EXACT = new Set(['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']);
102
+
103
+ /**
104
+ * True when a repo-relative path is documentation (never counted as "code").
105
+ * Covers markdown/rst family, README/CHANGELOG/HISTORY/NEWS/LICENSE files,
106
+ * `docs/**`, `.changeset/**`, and the agent-instruction docs.
107
+ *
108
+ * @param {string} p - Repo-relative path (any OS separator).
109
+ * @returns {boolean}
110
+ */
111
+ function isDocPath(p) {
112
+ const rel = toPosix(p);
113
+ if (DOC_EXTS.has(extOf(rel))) return true;
114
+ if (DOC_DIR_PREFIXES.some(d => rel.startsWith(d))) return true;
115
+ const base = baseName(rel);
116
+ if (DOC_EXACT.has(base)) return true;
117
+ return DOC_BASENAME_RE.test(base);
118
+ }
119
+
120
+ // --- config classification (used only for a flat-root `.` source) ------------
121
+ const CONFIG_BASENAMES = new Set([
122
+ 'package.json', 'package-lock.json', 'npm-shrinkwrap.json', 'bun.lockb', 'bun.lock',
123
+ 'yarn.lock', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'turbo.json', 'lerna.json',
124
+ 'tsconfig.json', 'jsconfig.json', 'go.mod', 'go.sum', 'go.work', 'go.work.sum',
125
+ 'cargo.toml', 'cargo.lock', 'pyproject.toml', 'setup.py', 'setup.cfg', 'tox.ini',
126
+ 'requirements.txt', 'pipfile', 'pipfile.lock', 'poetry.lock', 'uv.lock',
127
+ 'gemfile', 'gemfile.lock', 'composer.json', 'composer.lock', 'makefile', 'dockerfile',
128
+ '.gitignore', '.gitattributes', '.editorconfig', '.npmrc', '.nvmrc', '.dockerignore',
129
+ 'lefthook.yml',
130
+ ]);
131
+ const CONFIG_EXTS = new Set(['.yml', '.yaml', '.toml', '.ini', '.cfg', '.lock']);
132
+
133
+ /**
134
+ * True when a path is project configuration rather than source. Applied only
135
+ * when the source surface is the whole tree (`["."]`), so config edits in a
136
+ * flat-root repo do not require a doc update.
137
+ *
138
+ * @param {string} p - Repo-relative path.
139
+ * @returns {boolean}
140
+ */
141
+ function isConfigPath(p) {
142
+ const rel = toPosix(p);
143
+ // Dotfiles / dot-directories (.github, .circleci, .vscode, .config, ...) are config.
144
+ if (rel.split('/')[0].startsWith('.')) return true;
145
+ if (CONFIG_BASENAMES.has(baseName(rel).toLowerCase())) return true;
146
+ return CONFIG_EXTS.has(extOf(rel));
147
+ }
148
+
149
+ // --- change parsing ----------------------------------------------------------
150
+ /**
151
+ * Normalise a git status letter (or word) to ADDED / MODIFIED / DELETED.
152
+ * @param {string} s
153
+ * @returns {'ADDED'|'MODIFIED'|'DELETED'}
154
+ */
155
+ function normalizeStatus(s) {
156
+ const v = String(s || 'M').toUpperCase();
157
+ if (v.startsWith('A')) return 'ADDED';
158
+ if (v.startsWith('D')) return 'DELETED';
159
+ return 'MODIFIED';
160
+ }
161
+
162
+ /**
163
+ * Parse `git diff --name-status` output into `{ status, path }` records.
164
+ * Renames/copies (R###/C###) resolve to the NEW path, classified as ADDED.
165
+ *
166
+ * @param {string} out - Raw name-status output.
167
+ * @returns {Array<{status:string, path:string}>}
168
+ */
169
+ function parseNameStatus(out) {
170
+ const changes = [];
171
+ for (const line of out.split('\n')) {
172
+ const trimmed = line.trim();
173
+ if (!trimmed) continue;
174
+ const parts = trimmed.split('\t');
175
+ const letter = parts[0][0];
176
+ if (letter === 'R' || letter === 'C') {
177
+ changes.push({ status: 'ADDED', path: parts[parts.length - 1] });
178
+ } else if (parts[1]) {
179
+ changes.push({ status: normalizeStatus(letter), path: parts[1] });
180
+ }
181
+ }
182
+ return changes;
183
+ }
184
+
185
+ /**
186
+ * Normalise caller-supplied `changedFiles` (strings or `{status,path}` objects)
187
+ * into `{ status, path }` records. Bare strings default to MODIFIED.
188
+ *
189
+ * @param {Array<string|{status?:string, path:string}>} changedFiles
190
+ * @returns {Array<{status:string, path:string}>}
191
+ */
192
+ function normalizeChangedFiles(changedFiles) {
193
+ const out = [];
194
+ for (const entry of changedFiles) {
195
+ if (typeof entry === 'string') {
196
+ out.push({ status: 'MODIFIED', path: entry });
197
+ } else if (entry?.path) {
198
+ out.push({ status: normalizeStatus(entry.status), path: entry.path });
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+
204
+ /**
205
+ * From the Added/Modified changes, return the ones that count as CODE under the
206
+ * resolved source surface. Docs are always excluded; paths matching a declared
207
+ * `excludeFromGate` glob are excluded; for a flat-root `["."]` surface only
208
+ * top-level, non-config files count.
209
+ *
210
+ * @param {Array<{status:string, path:string}>} changes
211
+ * @param {string[]|null} sourceDirs
212
+ * @param {string[]} [excludeGlobs] - Declared `excludeFromGate` globs.
213
+ * @returns {string[]} repo-relative code paths
214
+ */
215
+ function codeChangesUnderSource(changes, sourceDirs, excludeGlobs = []) {
216
+ // Strip trailing slashes so a declared `packages/a/` matches `packages/a/index.js`.
217
+ const dirs = Array.isArray(sourceDirs) ? sourceDirs.map(p => toPosix(p).replace(/\/+$/, '')) : [];
218
+ const flatRoot = dirs.includes('.');
219
+ const code = [];
220
+ for (const change of changes) {
221
+ if (change.status === 'DELETED') continue;
222
+ const rel = toPosix(change.path);
223
+ if (isDocPath(rel)) continue;
224
+ if (excludeGlobs.length > 0 && matchesAnyGlob(rel, excludeGlobs)) continue; // declared exclusion
225
+ if (flatRoot) {
226
+ if (!rel.includes('/') && !isConfigPath(rel)) code.push(rel);
227
+ continue;
228
+ }
229
+ if (dirs.some(d => rel === d || rel.startsWith(`${d}/`))) code.push(rel);
230
+ }
231
+ return code;
232
+ }
233
+
234
+ /**
235
+ * Apply declared `rules`: a changed non-doc file matching `rule.when` REQUIRES
236
+ * the `rule.requires` path to be Added/Modified in the same change set. Returns
237
+ * a precise message for each violated rule (empty when all satisfied).
238
+ *
239
+ * @param {Array<{status:string, path:string}>} changes
240
+ * @param {Array<{when:string, requires:string}>} rules
241
+ * @returns {string[]} violation messages
242
+ */
243
+ function checkDeclaredRules(changes, rules) {
244
+ if (!Array.isArray(rules) || rules.length === 0) return [];
245
+ const touched = changes.filter(c => c.status !== 'DELETED').map(c => toPosix(c.path));
246
+ const violations = [];
247
+ for (const rule of rules) {
248
+ const triggered = touched.filter(p => !isDocPath(p) && matchesAnyGlob(p, [rule.when]));
249
+ if (triggered.length === 0) continue;
250
+ const requires = toPosix(rule.requires);
251
+ const satisfied = touched.some(p => p === requires || matchesAnyGlob(p, [rule.requires]));
252
+ if (!satisfied) {
253
+ violations.push(`change to ${triggered[0]} requires an update to "${rule.requires}" (rule when="${rule.when}")`);
254
+ }
255
+ }
256
+ return violations;
257
+ }
258
+
259
+ /**
260
+ * Resolve the changed-file set: caller-supplied `changedFiles` when present,
261
+ * otherwise the tracked `git diff --name-status <base>...<head>` (three-dot, PR
262
+ * semantics).
263
+ *
264
+ * @param {{root:string, base?:string, head?:string, changedFiles?:Array}} opts
265
+ * @returns {Array<{status:string, path:string}>}
266
+ */
267
+ function resolveChanges({ root, base, head, changedFiles }) {
268
+ if (Array.isArray(changedFiles)) return normalizeChangedFiles(changedFiles);
269
+ if (!base || !head) return [];
270
+ // Validate BOTH refs resolve to a commit before diffing — a bad ref throws
271
+ // (fail-closed) rather than yielding an empty, gate-passing diff.
272
+ gitStrict(root, ['rev-parse', '--verify', '--quiet', `${base}^{commit}`]);
273
+ gitStrict(root, ['rev-parse', '--verify', '--quiet', `${head}^{commit}`]);
274
+ return parseNameStatus(gitStrict(root, ['diff', '--name-status', `${base}...${head}`]));
275
+ }
276
+
277
+ /**
278
+ * Evaluate the doc-gate for a pull request.
279
+ *
280
+ * @param {Object} opts
281
+ * @param {string} opts.root - Absolute repo root (a git working tree).
282
+ * @param {string} [opts.base] - Base ref/SHA (required unless `changedFiles`).
283
+ * @param {string} [opts.head] - Head ref/SHA (required unless `changedFiles`).
284
+ * @param {Array<string|{status?:string, path:string}>} [opts.changedFiles] -
285
+ * Pre-computed change set; bypasses `git diff`.
286
+ * @param {boolean} [opts.skip] - Force a pass (e.g. a `no-docs-needed` label).
287
+ * @returns {{ decision:'pass'|'fail'|'abstain', reason:string,
288
+ * offendingCodeFiles:string[], docChangesSeen:string[],
289
+ * sourceSurface:(string[]|null), verdict:string }}
290
+ */
291
+ function evaluateGate({ root, base, head, changedFiles, skip } = {}) {
292
+ const result = detect(root);
293
+ const sourceSurface = result.source ? result.source.value : null;
294
+ const verdict = result.verdict;
295
+ const summary = { sourceSurface, verdict };
296
+
297
+ // FAIL-CLOSED on an INVALID committed `.docgate.json`: a malformed declaration
298
+ // is a config error that must block a required check, never silently pass —
299
+ // checked BEFORE `skip` so a broken declaration can't be waved through.
300
+ if (Array.isArray(result.declarationErrors) && result.declarationErrors.length > 0) {
301
+ return {
302
+ decision: 'fail',
303
+ reason: `invalid .docgate.json declaration: ${result.declarationErrors.join('; ')}`,
304
+ offendingCodeFiles: [],
305
+ docChangesSeen: [],
306
+ ...summary,
307
+ };
308
+ }
309
+
310
+ if (skip) {
311
+ return { decision: 'pass', reason: 'skipped', offendingCodeFiles: [], docChangesSeen: [], ...summary };
312
+ }
313
+
314
+ if (verdict === 'ESCALATE-TO-AGENT' || verdict === 'MANUAL-CONFIG') {
315
+ return {
316
+ decision: 'abstain',
317
+ reason: `detector verdict ${verdict}: source surface not confidently resolved; not enforcing`,
318
+ offendingCodeFiles: [],
319
+ docChangesSeen: [],
320
+ ...summary,
321
+ };
322
+ }
323
+
324
+ // CODE-RESOLVED or DECLARED (a committed declaration is enforced exactly like
325
+ // CODE-RESOLVED): the source surface is a concrete set of dirs (or ".").
326
+ let changes;
327
+ try {
328
+ changes = resolveChanges({ root, base, head, changedFiles });
329
+ } catch (err) {
330
+ // FAIL-CLOSED: a git error (bad refs / diff failure) must not silently pass a
331
+ // required check — surface it as a failure so the PR is investigated.
332
+ return {
333
+ decision: 'fail',
334
+ reason: `could not compute the PR diff (${err.message}); failing closed`,
335
+ offendingCodeFiles: [],
336
+ docChangesSeen: [],
337
+ ...summary,
338
+ };
339
+ }
340
+ const excludeGlobs = result.declaration?.excludeFromGate ?? [];
341
+ const docChangesSeen = changes
342
+ .filter(c => c.status !== 'DELETED' && isDocPath(c.path))
343
+ .map(c => toPosix(c.path));
344
+ const offending = codeChangesUnderSource(changes, sourceSurface, excludeGlobs);
345
+
346
+ if (offending.length > 0 && docChangesSeen.length === 0) {
347
+ return {
348
+ decision: 'fail',
349
+ reason: `${offending.length} code change(s) under source surface [${(sourceSurface || []).join(', ')}] with no accompanying doc update`,
350
+ offendingCodeFiles: offending,
351
+ docChangesSeen,
352
+ ...summary,
353
+ };
354
+ }
355
+
356
+ // Declared `rules` are enforced independently of the doc-companion check: a
357
+ // triggered rule fails even when a doc update accompanied the code change.
358
+ const ruleViolations = checkDeclaredRules(changes, result.declaration?.rules ?? []);
359
+ if (ruleViolations.length > 0) {
360
+ return {
361
+ decision: 'fail',
362
+ reason: ruleViolations.join('; '),
363
+ offendingCodeFiles: offending,
364
+ docChangesSeen,
365
+ ...summary,
366
+ };
367
+ }
368
+
369
+ const reason = offending.length === 0
370
+ ? 'no code change under the source surface'
371
+ : 'code change accompanied by a doc update';
372
+ return { decision: 'pass', reason, offendingCodeFiles: [], docChangesSeen, ...summary };
373
+ }
374
+
375
+ module.exports = { evaluateGate, isDocPath, isConfigPath, parseNameStatus };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * doc-gate OKF feature toggle — `.forge/doc-gate.json`.
5
+ *
6
+ * OKF (Google's Open Knowledge Format) support is an OPT-IN, user-toggleable
7
+ * knowledge-base feature. It is DISABLED by default: OKF v0.1 is a DRAFT and
8
+ * explicitly "not an official Google product", so nothing generates a bundle or
9
+ * touches AGENTS.md until a user turns it on.
10
+ *
11
+ * This module is the toggle store. It mirrors the `.forge/*.json` config pattern
12
+ * used by `lib/adapter-cli.js` (see `setAdapterEnabled`) but writes a dedicated
13
+ * `.forge/doc-gate.json` shaped `{ okf: { enabled: boolean } }`.
14
+ *
15
+ * IMPORTANT: this file is NOT the repo-root `.docgate.json` declaration
16
+ * (lib/doc-gate/declaration.js). Those two are deliberately separate — the
17
+ * declaration authoritatively describes repo structure and flips the detector
18
+ * verdict to DECLARED; this toggle ONLY gates OKF bundle generation. Neither
19
+ * reads or writes the other's file.
20
+ *
21
+ * Fail-safe: a missing OR malformed config resolves to DISABLED, never an error.
22
+ *
23
+ * @module doc-gate/okf-config
24
+ */
25
+
26
+ const fs = require('node:fs');
27
+ const path = require('node:path');
28
+
29
+ const CONFIG_DIR = '.forge';
30
+ const CONFIG_FILE = 'doc-gate.json';
31
+
32
+ /** Absolute path to a repo's `.forge/doc-gate.json`. */
33
+ function configPath(root) {
34
+ return path.join(root, CONFIG_DIR, CONFIG_FILE);
35
+ }
36
+
37
+ /** True only for a plain (non-array) object. */
38
+ function isPlainObject(value) {
39
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
40
+ }
41
+
42
+ /**
43
+ * Normalize any parsed value into a well-formed `{ okf: { enabled: boolean } }`,
44
+ * preserving unrelated top-level keys. `enabled` is `true` ONLY when it is
45
+ * strictly the boolean `true`, so any junk (missing, string, number, null) is a
46
+ * safe `false`.
47
+ */
48
+ function normalizeConfig(parsed) {
49
+ const base = isPlainObject(parsed) ? parsed : {};
50
+ const okf = isPlainObject(base.okf) ? base.okf : {};
51
+ return { ...base, okf: { ...okf, enabled: okf.enabled === true } };
52
+ }
53
+
54
+ /**
55
+ * Load + normalize a repo's `.forge/doc-gate.json`.
56
+ *
57
+ * A missing file, an unreadable file, or invalid JSON all resolve to the DISABLED
58
+ * default — this must never throw, so the toggle can be queried anywhere.
59
+ *
60
+ * @param {string} root - Repository root.
61
+ * @returns {{ okf: { enabled: boolean } }}
62
+ */
63
+ function loadOkfConfig(root) {
64
+ let raw;
65
+ try {
66
+ raw = fs.readFileSync(configPath(root), 'utf8');
67
+ } catch (_err) {
68
+ // Missing / unreadable config => disabled (fail-safe). NOSONAR S2486
69
+ return normalizeConfig(null);
70
+ }
71
+ try {
72
+ return normalizeConfig(JSON.parse(raw));
73
+ } catch (_err) {
74
+ // Malformed JSON => disabled (fail-safe), never surfaced as an error. NOSONAR S2486
75
+ return normalizeConfig(null);
76
+ }
77
+ }
78
+
79
+ /** True when OKF generation is enabled for `root`. */
80
+ function isOkfEnabled(root) {
81
+ return loadOkfConfig(root).okf.enabled === true;
82
+ }
83
+
84
+ /**
85
+ * Symlink-safe config write: refuse to write THROUGH a symlink (a checked-in
86
+ * symlink could clobber a file outside the repo), matching `doc-gate init`.
87
+ */
88
+ function writeConfig(root, config) {
89
+ const dir = path.join(root, CONFIG_DIR);
90
+ // Refuse a symlinked CONFIG_DIR (.forge) BEFORE mkdir/write — a checked-in
91
+ // symlink there could redirect the write to a file OUTSIDE the repo.
92
+ let dirStat = null;
93
+ try { dirStat = fs.lstatSync(dir); } catch { /* absent: will be created */ }
94
+ if (dirStat?.isSymbolicLink()) {
95
+ throw new Error(`${CONFIG_DIR} is a symlink; refusing to write through it.`);
96
+ }
97
+ fs.mkdirSync(dir, { recursive: true });
98
+ const file = configPath(root);
99
+ let stat = null;
100
+ try { stat = fs.lstatSync(file); } catch { /* absent: stat stays null */ }
101
+ if (stat?.isSymbolicLink()) {
102
+ throw new Error(`${CONFIG_DIR}/${CONFIG_FILE} is a symlink; refusing to write through it.`);
103
+ }
104
+ fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`);
105
+ }
106
+
107
+ /**
108
+ * Set the OKF `enabled` flag, preserving any unrelated config keys.
109
+ *
110
+ * @param {string} root - Repository root.
111
+ * @param {boolean} enabled - Desired state.
112
+ * @returns {{ okf: { enabled: boolean } }} The written config.
113
+ */
114
+ function setOkfEnabled(root, enabled) {
115
+ const current = loadOkfConfig(root);
116
+ const next = { ...current, okf: { ...current.okf, enabled: enabled === true } };
117
+ writeConfig(root, next);
118
+ return next;
119
+ }
120
+
121
+ module.exports = {
122
+ loadOkfConfig,
123
+ isOkfEnabled,
124
+ setOkfEnabled,
125
+ configPath,
126
+ CONFIG_DIR,
127
+ CONFIG_FILE,
128
+ };