forge-workflow 0.0.10 → 0.1.0-beta.3

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