forge-workflow 0.0.10 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +461 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +17 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +0 -1
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +838 -972
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +382 -11
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,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
+ };