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
@@ -12,6 +12,8 @@ const path = require('node:path');
12
12
  const { execFileSync: defaultExecFileSync } = require('node:child_process');
13
13
 
14
14
  const { checkLefthookStatus } = require('./lefthook-check');
15
+ const { resolveIssueBackend } = require('./issue-backend');
16
+ const { getInstallCommand, getAddDevCommand } = require('./package-manager-remediation');
15
17
 
16
18
  const WINDOWS_GIT_BASH_CANDIDATES = [
17
19
  String.raw`C:\Program Files\Git\bin\bash.exe`,
@@ -43,11 +45,149 @@ function normalizeHooksPath(value, platform = process.platform) {
43
45
  return normalized;
44
46
  }
45
47
 
46
- function createDiagnostic(code, subject, message, repair) {
48
+ function isAbsoluteForPlatform(value, platform = process.platform) {
49
+ const raw = toText(value).trim();
50
+ return platform === 'win32' ? path.win32.isAbsolute(raw) : path.posix.isAbsolute(raw);
51
+ }
52
+
53
+ function resolvesToExecutableHookCommand(line, matcher) {
54
+ const trimmed = line.trim();
55
+ if (!trimmed || trimmed.startsWith('#')) return false;
56
+ const firstCommand = commandName(commandTokens(trimmed)[0]);
57
+ if (firstCommand === 'echo' || firstCommand === 'printf') {
58
+ const chainIndex = trimmed.indexOf('&&');
59
+ return chainIndex === -1 ? false : matcher(trimmed.slice(chainIndex + 2).trim());
60
+ }
61
+ return matcher(trimmed);
62
+ }
63
+
64
+ function hasExecutableHookCommand(content, matcher) {
65
+ let heredocEnd = null;
66
+
67
+ for (const line of toText(content).split(/\r?\n/)) {
68
+ const trimmed = line.trim();
69
+ if (heredocEnd) {
70
+ if (trimmed === heredocEnd) heredocEnd = null;
71
+ continue;
72
+ }
73
+
74
+ if (resolvesToExecutableHookCommand(line, matcher)) return true;
75
+
76
+ const delimiter = heredocDelimiter(line);
77
+ if (delimiter) heredocEnd = delimiter;
78
+ }
79
+
80
+ return false;
81
+ }
82
+
83
+ function commandTokens(line) {
84
+ return line
85
+ .replace(/[;"'()]/g, ' ')
86
+ .split(/\s+/)
87
+ .filter(Boolean);
88
+ }
89
+
90
+ function commandName(token) {
91
+ return toText(token)
92
+ .replaceAll('\\', '/')
93
+ .split('/')
94
+ .pop()
95
+ .toLowerCase()
96
+ .replace(/\.(?:exe|cmd)$/, '');
97
+ }
98
+
99
+ function isWhitespace(char) {
100
+ return char === ' ' || char === '\t' || char === '\r' || char === '\n';
101
+ }
102
+
103
+ function isVariableNameStart(char) {
104
+ return (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char === '_';
105
+ }
106
+
107
+ function isVariableNameChar(char) {
108
+ return isVariableNameStart(char) || (char >= '0' && char <= '9');
109
+ }
110
+
111
+ function isAssignmentToken(token) {
112
+ const text = toText(token);
113
+ const equalsIndex = text.indexOf('=');
114
+ if (equalsIndex <= 0 || !isVariableNameStart(text[0])) return false;
115
+
116
+ for (let index = 1; index < equalsIndex; index += 1) {
117
+ if (!isVariableNameChar(text[index])) return false;
118
+ }
119
+
120
+ return true;
121
+ }
122
+
123
+ function heredocDelimiter(line) {
124
+ const markerIndex = line.indexOf('<<');
125
+ if (markerIndex === -1 || line[markerIndex + 2] === '<') return null;
126
+
127
+ let cursor = markerIndex + 2;
128
+ if (line[cursor] === '-') cursor += 1;
129
+ while (cursor < line.length && isWhitespace(line[cursor])) cursor += 1;
130
+
131
+ const quote = line[cursor];
132
+ if (quote === '"' || quote === "'") {
133
+ cursor += 1;
134
+ const endQuote = line.indexOf(quote, cursor);
135
+ return endQuote === -1 ? line.slice(cursor).trim() || null : line.slice(cursor, endQuote) || null;
136
+ }
137
+
138
+ const start = cursor;
139
+ while (cursor < line.length && !isWhitespace(line[cursor])) cursor += 1;
140
+ return line.slice(start, cursor).trim() || null;
141
+ }
142
+
143
+ function isExecutingTokenPosition(tokens, index) {
144
+ const nonExecutingPreviousTokens = new Set(['-e', '-f', '-r', '-x', '[', 'test']);
145
+ const executingPreviousCommands = new Set(['bash', 'bun', 'env', 'exec', 'if', 'node', 'sh', 'then', '&&']);
146
+
147
+ let previousIndex = index - 1;
148
+ while (previousIndex >= 0 && isAssignmentToken(tokens[previousIndex])) {
149
+ previousIndex -= 1;
150
+ }
151
+
152
+ if (previousIndex < 0) return true;
153
+
154
+ const previous = toText(tokens[previousIndex]).toLowerCase();
155
+ if (nonExecutingPreviousTokens.has(previous)) return false;
156
+ return executingPreviousCommands.has(commandName(previous));
157
+ }
158
+
159
+ function invokesLefthook(line) {
160
+ return commandTokens(line).some((token) => commandName(token) === 'lefthook');
161
+ }
162
+
163
+ function invokesForgeHook(line) {
164
+ return commandTokens(line).some((token, index, tokens) => {
165
+ if (token.includes('=')) return false;
166
+ const normalized = token.replaceAll('\\', '/').toLowerCase();
167
+ if (!normalized.endsWith('.forge/hooks/check-tdd.js') && commandName(token) !== 'check-tdd.js') {
168
+ return false;
169
+ }
170
+
171
+ return isExecutingTokenPosition(tokens, index);
172
+ });
173
+ }
174
+
175
+ function invokesBeadsHook(line, hookName) {
176
+ const tokens = commandTokens(line);
177
+ return tokens.some((token, index) => (
178
+ commandName(token) === 'bd'
179
+ && toText(tokens[index + 1]).toLowerCase() === 'hooks'
180
+ && toText(tokens[index + 2]).toLowerCase() === 'run'
181
+ && toText(tokens[index + 3]).toLowerCase() === hookName
182
+ && isExecutingTokenPosition(tokens, index)
183
+ ));
184
+ }
185
+
186
+ function createDiagnostic(code, subject, message, repair, severity = 'hard-stop') {
47
187
  return {
48
188
  code,
49
189
  subject,
50
- severity: 'hard-stop',
190
+ severity,
51
191
  message,
52
192
  ...(repair ? { repair } : {})
53
193
  };
@@ -76,7 +216,7 @@ function isUsableWindowsShellCandidate(candidate, options = {}) {
76
216
  function checkHookInstallation(projectRoot, options = {}) {
77
217
  const exec = options._exec || defaultExecFileSync;
78
218
  const platform = options.platform || process.platform;
79
- const expectedRelativeHooksPath = normalizeHooksPath('.lefthook/hooks', platform);
219
+ const expectedRelativeHooksPaths = ['.lefthook/hooks', '.beads/hooks'];
80
220
  const requiredHooks = ['pre-commit', 'pre-push'];
81
221
 
82
222
  function resolveGitRoot() {
@@ -92,7 +232,10 @@ function checkHookInstallation(projectRoot, options = {}) {
92
232
  }
93
233
 
94
234
  const gitRoot = resolveGitRoot();
95
- const expectedAbsoluteHooksPath = normalizeHooksPath(`${gitRoot}/${expectedRelativeHooksPath}`, platform);
235
+ const expectedHooksPaths = new Set(expectedRelativeHooksPaths.flatMap((hooksPath) => [
236
+ normalizeHooksPath(hooksPath, platform),
237
+ normalizeHooksPath(`${gitRoot}/${normalizeHooksPath(hooksPath, platform)}`, platform)
238
+ ]));
96
239
 
97
240
  function resolveGitHooksDir() {
98
241
  try {
@@ -108,17 +251,44 @@ function checkHookInstallation(projectRoot, options = {}) {
108
251
  }
109
252
  }
110
253
 
111
- function fileLooksLikeActiveHook(filePath) {
254
+ function classifyHookFile(filePath, hookName, visited = new Set()) {
112
255
  try {
256
+ const normalizedFilePath = path.resolve(filePath);
257
+ if (visited.has(normalizedFilePath)) {
258
+ return { active: false, provider: 'cycle' };
259
+ }
260
+ visited.add(normalizedFilePath);
261
+
113
262
  const stat = fs.statSync(filePath);
114
263
  const executable = platform === 'win32'
115
264
  || (typeof options._canExecuteHook === 'function'
116
265
  ? Boolean(options._canExecuteHook(filePath, stat))
117
266
  : (stat.mode & 0o111) !== 0);
118
- if (!stat.isFile() || stat.size === 0 || !executable) return false;
119
- return /\blefthook\b/i.test(fs.readFileSync(filePath, 'utf8'));
267
+ if (!stat.isFile() || stat.size === 0 || !executable) return { active: false, provider: 'missing' };
268
+
269
+ const content = fs.readFileSync(filePath, 'utf8');
270
+ const runsLefthook = hasExecutableHookCommand(content, invokesLefthook);
271
+ const runsForgeHook = hasExecutableHookCommand(content, invokesForgeHook);
272
+ const runsBeadsHook = hasExecutableHookCommand(content, (line) => invokesBeadsHook(line, hookName));
273
+
274
+ if (runsLefthook) return { active: true, provider: 'lefthook' };
275
+ if (runsForgeHook) return { active: true, provider: 'forge' };
276
+
277
+ if (runsBeadsHook) {
278
+ const chainedHookPath = path.join(gitRoot, '.beads', 'hooks', hookName);
279
+ if (path.resolve(chainedHookPath) !== normalizedFilePath && fs.existsSync(chainedHookPath)) {
280
+ const chained = classifyHookFile(chainedHookPath, hookName, visited);
281
+ if (chained.active) {
282
+ return { active: true, provider: `beads->${chained.provider}` };
283
+ }
284
+ }
285
+
286
+ return { active: false, provider: 'beads-unverified' };
287
+ }
288
+
289
+ return { active: false, provider: 'unknown' };
120
290
  } catch {
121
- return false;
291
+ return { active: false, provider: 'missing' };
122
292
  }
123
293
  }
124
294
 
@@ -130,11 +300,18 @@ function checkHookInstallation(projectRoot, options = {}) {
130
300
  });
131
301
 
132
302
  hooksPath = normalizeHooksPath(output, platform);
133
- const active = hooksPath === expectedRelativeHooksPath || hooksPath === expectedAbsoluteHooksPath;
303
+ const active = expectedHooksPaths.has(hooksPath);
134
304
 
135
305
  if (active) {
136
- const hooksDir = path.join(gitRoot, '.lefthook', 'hooks');
137
- const missingHooks = requiredHooks.filter(hook => !fileLooksLikeActiveHook(path.join(hooksDir, hook)));
306
+ const rawHooksPath = toText(output).trim();
307
+ const hooksDir = isAbsoluteForPlatform(rawHooksPath, platform)
308
+ ? rawHooksPath
309
+ : path.resolve(gitRoot, normalizeHooksPath(rawHooksPath, platform));
310
+ const hookChecks = requiredHooks.map((hook) => ({
311
+ hook,
312
+ ...classifyHookFile(path.join(hooksDir, hook), hook)
313
+ }));
314
+ const missingHooks = hookChecks.filter(check => !check.active).map(check => check.hook);
138
315
  const verifiedActive = missingHooks.length === 0;
139
316
  return {
140
317
  active: verifiedActive,
@@ -142,6 +319,7 @@ function checkHookInstallation(projectRoot, options = {}) {
142
319
  verification: 'core.hooksPath',
143
320
  hooksPath: hooksPath || null,
144
321
  hooksDir,
322
+ providers: Object.fromEntries(hookChecks.map(check => [check.hook, check.provider])),
145
323
  missingHooks,
146
324
  message: verifiedActive
147
325
  ? ''
@@ -155,7 +333,7 @@ function checkHookInstallation(projectRoot, options = {}) {
155
333
  state: 'inactive',
156
334
  verification: 'core.hooksPath',
157
335
  hooksPath,
158
- message: `Git core.hooksPath is set to "${hooksPath}", not ".lefthook/hooks".`
336
+ message: `Git core.hooksPath is set to "${hooksPath}", not ".lefthook/hooks" or ".beads/hooks".`
159
337
  };
160
338
  }
161
339
  } catch {
@@ -164,7 +342,11 @@ function checkHookInstallation(projectRoot, options = {}) {
164
342
 
165
343
  const hooksDir = resolveGitHooksDir();
166
344
  if (hooksDir) {
167
- const missingHooks = requiredHooks.filter(hook => !fileLooksLikeActiveHook(path.join(hooksDir, hook)));
345
+ const hookChecks = requiredHooks.map((hook) => ({
346
+ hook,
347
+ ...classifyHookFile(path.join(hooksDir, hook), hook)
348
+ }));
349
+ const missingHooks = hookChecks.filter(check => !check.active).map(check => check.hook);
168
350
  const active = missingHooks.length === 0;
169
351
  return {
170
352
  active,
@@ -172,6 +354,7 @@ function checkHookInstallation(projectRoot, options = {}) {
172
354
  verification: 'git-path-hooks',
173
355
  hooksPath: hooksPath || null,
174
356
  hooksDir,
357
+ providers: Object.fromEntries(hookChecks.map(check => [check.hook, check.provider])),
175
358
  missingHooks,
176
359
  message: active
177
360
  ? ''
@@ -300,6 +483,22 @@ function normalizeProjectRoot(projectRoot) {
300
483
  : process.cwd();
301
484
  }
302
485
 
486
+ // Resolve the active issue backend for the health gate. An already-normalized
487
+ // `options.issueBackend` (passed by enforce-stage) wins; otherwise fall back to the
488
+ // shared resolver (env > .forge/config.yaml > default 'kernel'). Never throws and
489
+ // never warns from the health path.
490
+ function resolveHealthIssueBackend(options, projectRoot) {
491
+ if (options.issueBackend === 'kernel' || options.issueBackend === 'beads') {
492
+ return options.issueBackend;
493
+ }
494
+ return resolveIssueBackend({
495
+ deps: options.backendDeps || {},
496
+ env: options.env || process.env,
497
+ projectRoot,
498
+ warn: () => {}
499
+ });
500
+ }
501
+
303
502
  function checkRuntimeHealth(projectRoot, options = {}) {
304
503
  const platform = options.platform || process.platform;
305
504
  const root = normalizeProjectRoot(projectRoot);
@@ -311,17 +510,23 @@ function checkRuntimeHealth(projectRoot, options = {}) {
311
510
  const jq = checkCommandAvailability('jq', root, options);
312
511
  const shell = normalizeShellRuntime(options.shellRuntime, platform, options);
313
512
 
513
+ const issueBackend = resolveHealthIssueBackend(options, root);
514
+
314
515
  const diagnostics = [];
516
+ const advisories = [];
315
517
 
316
518
  const lefthookMissing = lefthook.state === 'missing-dependency'
317
519
  || lefthook.state === 'missing-binary';
318
520
 
319
521
  if (lefthookMissing) {
522
+ const lefthookRepair = lefthook.state === 'missing-binary'
523
+ ? getInstallCommand(root)
524
+ : getAddDevCommand(root, 'lefthook');
320
525
  diagnostics.push(createDiagnostic(
321
526
  'LEFTHOOK_MISSING',
322
527
  'lefthook',
323
528
  lefthook.message || 'lefthook is required for hook installation.',
324
- 'bun add -D lefthook && bun install'
529
+ lefthookRepair
325
530
  ));
326
531
  }
327
532
 
@@ -335,11 +540,25 @@ function checkRuntimeHealth(projectRoot, options = {}) {
335
540
  }
336
541
 
337
542
  if (!bd.available) {
338
- diagnostics.push(createDiagnostic(
339
- 'BD_MISSING',
340
- 'bd',
341
- 'bd is required for stage-entry workflow checks.'
342
- ));
543
+ if (issueBackend === 'beads') {
544
+ // Beads is the active issue backend, so bd is a genuine hard prerequisite.
545
+ diagnostics.push(createDiagnostic(
546
+ 'BD_MISSING',
547
+ 'bd',
548
+ 'bd is required for stage-entry workflow checks when the beads issue backend is selected.'
549
+ ));
550
+ } else {
551
+ // Kernel is the default issue backend and needs no bd binary, so a missing bd
552
+ // must NOT hard-stop stage entry (the "no Beads install required" contract).
553
+ // Surface it as a non-blocking advisory instead.
554
+ advisories.push(createDiagnostic(
555
+ 'BD_MISSING',
556
+ 'bd',
557
+ 'bd is not installed. The kernel issue backend does not require it; install bd only if you switch to the beads backend.',
558
+ undefined,
559
+ 'advisory'
560
+ ));
561
+ }
343
562
  }
344
563
 
345
564
  if (!gh.available) {
@@ -373,6 +592,7 @@ function checkRuntimeHealth(projectRoot, options = {}) {
373
592
  ready: healthy,
374
593
  hardStop: !healthy,
375
594
  diagnostics,
595
+ advisories,
376
596
  checks: {
377
597
  projectRoot: root,
378
598
  lefthook,
@@ -380,7 +600,8 @@ function checkRuntimeHealth(projectRoot, options = {}) {
380
600
  bd,
381
601
  gh,
382
602
  jq,
383
- shell
603
+ shell,
604
+ issueBackend
384
605
  }
385
606
  };
386
607
  }
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Native SAFETY-surface renderer.
5
+ *
6
+ * Renders sane, non-surprising security defaults into the two safety surfaces
7
+ * Forge can write PROJECT-LOCALLY today:
8
+ *
9
+ * - Claude Code : `.claude/settings.json` -> `permissions` (allow/deny/ask)
10
+ * - Cursor : `.cursorignore` (gitignore-style AI read/index boundary)
11
+ *
12
+ * Both use read -> merge -> write (idempotent, preserves user entries). An existing
13
+ * `.claude/settings.json` that cannot be parsed is BACKED UP and left untouched
14
+ * (mirrors the MCP renderer's data-loss guard) rather than clobbering user settings.
15
+ *
16
+ * NOT rendered here (honest deferral): the Codex execution sandbox / approvals policy
17
+ * (`sandbox_mode` / `approval_policy`). Codex honors a project-local `.codex/config.toml`
18
+ * ONLY when the project is marked trusted in the GLOBAL `$CODEX_HOME/config.toml`
19
+ * (`projects.<path>.trust_level`). Forge cannot grant that global trust during
20
+ * project-local setup, so a rendered project-local sandbox policy would not take
21
+ * effect. It is deferred to global-scope wiring and marked `not-delivered` in the
22
+ * capability matrix (kernel epic 90f2f631 / #311).
23
+ *
24
+ * Defaults follow the principle: ALLOW the routine forge/dev workflow, DENY the
25
+ * obviously-dangerous, and let everything else fall through to the harness's normal
26
+ * "ask" behavior (Claude evaluates deny -> ask -> allow; unmatched tools still prompt).
27
+ * We intentionally do NOT set `defaultMode`, so nothing is silently auto-approved.
28
+ *
29
+ * Dependency-free beyond node builtins so it runs under `bun test` and release gates.
30
+ *
31
+ * @module safety-config-renderer
32
+ */
33
+
34
+ const fs = require('node:fs');
35
+ const path = require('node:path');
36
+
37
+ const { backupFile } = require('./mcp-config-renderer');
38
+
39
+ /** Thrown when an existing settings file cannot be parsed — signals "do not overwrite". */
40
+ class SafetyConfigParseError extends Error {
41
+ constructor(message, cause) {
42
+ super(message);
43
+ this.name = 'SafetyConfigParseError';
44
+ this.cause = cause;
45
+ }
46
+ }
47
+
48
+ // Safe Claude Code permission defaults. Rule syntax per Claude Code settings docs
49
+ // (https://docs.claude.com/en/docs/claude-code/settings ,
50
+ // https://docs.claude.com/en/docs/claude-code/permissions):
51
+ // Bash(<prefix>:*) — command-prefix match; Read(<glob>) — gitignore-style path.
52
+ // Evaluated deny -> ask -> allow (deny always wins), so the deny Read(...) rules
53
+ // protect secrets even though bare Read is not blanket-allowed.
54
+ const CLAUDE_PERMISSION_DEFAULTS = Object.freeze({
55
+ // Routine, reversible forge/dev workflow — safe to run without a prompt.
56
+ allow: Object.freeze([
57
+ 'Bash(git status:*)',
58
+ 'Bash(git log:*)',
59
+ 'Bash(git diff:*)',
60
+ 'Bash(git branch:*)',
61
+ 'Bash(git show:*)',
62
+ 'Bash(git add:*)',
63
+ 'Bash(git commit:*)',
64
+ 'Bash(git stash:*)',
65
+ 'Bash(git checkout:*)',
66
+ 'Bash(git switch:*)',
67
+ 'Bash(git pull:*)',
68
+ 'Bash(git fetch:*)',
69
+ 'Bash(git push:*)',
70
+ 'Bash(ls:*)',
71
+ 'Bash(cat:*)',
72
+ 'Bash(pwd)',
73
+ 'Bash(which:*)',
74
+ 'Bash(mkdir:*)',
75
+ 'Bash(cp:*)',
76
+ 'Bash(mv:*)',
77
+ 'Bash(bun run:*)',
78
+ 'Bash(bun test:*)',
79
+ 'Bash(bun install)',
80
+ 'Bash(npm run:*)',
81
+ 'Bash(npm test:*)',
82
+ 'Bash(forge:*)',
83
+ 'Bash(gh pr view:*)',
84
+ 'Bash(gh pr list:*)',
85
+ 'Bash(gh issue list:*)',
86
+ ]),
87
+ // Careful, rewrites-history / rewinds-state — worth a confirmation, not a hard block.
88
+ ask: Object.freeze([
89
+ 'Bash(git rebase:*)',
90
+ 'Bash(git reset:*)',
91
+ 'Bash(git clean:*)',
92
+ ]),
93
+ // Obviously-dangerous commands + secret reads. deny wins over allow/ask.
94
+ deny: Object.freeze([
95
+ 'Bash(rm -rf:*)',
96
+ 'Bash(rm -fr:*)',
97
+ 'Bash(git push --force:*)',
98
+ 'Bash(git push -f:*)',
99
+ 'Bash(git reset --hard:*)',
100
+ // Read/secret boundary — private keys and secret env files are never readable.
101
+ 'Read(./.env)',
102
+ 'Read(**/.env)',
103
+ 'Read(**/.env.local)',
104
+ 'Read(**/.env.*.local)',
105
+ 'Read(**/*.pem)',
106
+ 'Read(**/*.key)',
107
+ 'Read(**/id_rsa)',
108
+ 'Read(**/id_ed25519)',
109
+ 'Read(./secrets/**)',
110
+ 'Read(**/credentials.json)',
111
+ ]),
112
+ });
113
+
114
+ // Safe `.cursorignore` defaults (gitignore-style, per
115
+ // https://cursor.com/docs/reference/ignore-file). Cursor already ignores .gitignore
116
+ // and common lock/.env files by default; listing them here makes the secret/large-dir
117
+ // boundary explicit and self-documenting. `.cursorignore` blocks Cursor AI features +
118
+ // indexing (it is not a hard security boundary — terminal/MCP tools can still reach files).
119
+ const CURSORIGNORE_DEFAULTS = Object.freeze([
120
+ '.env',
121
+ '.env.local',
122
+ '.env.*.local',
123
+ '*.pem',
124
+ '*.key',
125
+ 'id_rsa',
126
+ 'id_ed25519',
127
+ 'secrets/',
128
+ 'node_modules/',
129
+ 'dist/',
130
+ 'build/',
131
+ 'out/',
132
+ 'coverage/',
133
+ '.next/',
134
+ '*.log',
135
+ ]);
136
+
137
+ const CURSORIGNORE_HEADER = [
138
+ '# ---- Forge safe defaults (managed) ----',
139
+ '# Keeps secrets and large/generated dirs out of Cursor AI context + index.',
140
+ '# Edit freely — Forge only appends missing lines and never removes yours.',
141
+ ];
142
+
143
+ function unionPreserve(existing, defaults) {
144
+ const base = Array.isArray(existing) ? existing.filter(v => typeof v === 'string') : [];
145
+ const seen = new Set(base);
146
+ for (const rule of defaults) {
147
+ if (!seen.has(rule)) {
148
+ base.push(rule);
149
+ seen.add(rule);
150
+ }
151
+ }
152
+ return base;
153
+ }
154
+
155
+ /**
156
+ * Merge safe permission defaults into an existing `.claude/settings.json` string
157
+ * (read -> merge -> write). Preserves every other settings key and every existing
158
+ * allow/deny/ask entry (union, no duplicates). Idempotent.
159
+ * @param {string} existingText
160
+ * @returns {string} pretty JSON with a trailing newline
161
+ */
162
+ function mergeClaudePermissions(existingText) {
163
+ let obj = {};
164
+ if (existingText && existingText.trim()) {
165
+ try {
166
+ obj = JSON.parse(existingText);
167
+ } catch (err) {
168
+ // DATA-LOSS GUARD: never silently discard a populated-but-unparseable settings
169
+ // file (e.g. JSONC with comments). Signal the caller to back up and skip.
170
+ throw new SafetyConfigParseError('existing .claude/settings.json is not valid JSON', err);
171
+ }
172
+ }
173
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) obj = {};
174
+ const existingPerms = (obj.permissions && typeof obj.permissions === 'object' && !Array.isArray(obj.permissions))
175
+ ? obj.permissions
176
+ : {};
177
+ existingPerms.allow = unionPreserve(existingPerms.allow, CLAUDE_PERMISSION_DEFAULTS.allow);
178
+ existingPerms.ask = unionPreserve(existingPerms.ask, CLAUDE_PERMISSION_DEFAULTS.ask);
179
+ existingPerms.deny = unionPreserve(existingPerms.deny, CLAUDE_PERMISSION_DEFAULTS.deny);
180
+ obj.permissions = existingPerms;
181
+ return JSON.stringify(obj, null, 2) + '\n';
182
+ }
183
+
184
+ /**
185
+ * Merge safe ignore defaults into an existing `.cursorignore` string (read -> merge
186
+ * -> write). Preserves all user lines/comments verbatim and only appends default
187
+ * patterns that are not already present. Idempotent.
188
+ * @param {string} existingText
189
+ * @returns {string}
190
+ */
191
+ function mergeCursorIgnore(existingText) {
192
+ const text = typeof existingText === 'string' ? existingText : '';
193
+ const existingPatterns = new Set(
194
+ text.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#')),
195
+ );
196
+ const missing = CURSORIGNORE_DEFAULTS.filter(pattern => !existingPatterns.has(pattern));
197
+ const base = text.replace(/\s+$/, '');
198
+ if (missing.length === 0) {
199
+ return base ? `${base}\n` : '';
200
+ }
201
+ const lines = [];
202
+ if (base) {
203
+ lines.push(base, '');
204
+ }
205
+ // Only add the Forge header block if it isn't already present — otherwise a later
206
+ // re-run (once CURSORIGNORE_DEFAULTS gains a new entry) would duplicate the header.
207
+ if (!text.includes(CURSORIGNORE_HEADER[0])) {
208
+ lines.push(...CURSORIGNORE_HEADER);
209
+ }
210
+ lines.push(...missing);
211
+ return lines.join('\n') + '\n';
212
+ }
213
+
214
+ /**
215
+ * Render safe Claude permission defaults into `.claude/settings.json`.
216
+ * Read -> merge -> write. An unparseable existing file is BACKED UP and left
217
+ * untouched (never overwritten) to avoid destroying user settings.
218
+ * @param {object} params
219
+ * @param {string} params.targetRoot - Project root.
220
+ * @returns {{ file: string, existed: boolean, skipped: boolean, backup?: string }}
221
+ */
222
+ function renderClaudePermissions({ targetRoot }) {
223
+ const filePath = path.join(targetRoot, '.claude', 'settings.json');
224
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
225
+ const existed = fs.existsSync(filePath);
226
+ const existing = existed ? fs.readFileSync(filePath, 'utf-8') : '';
227
+
228
+ let merged;
229
+ try {
230
+ merged = mergeClaudePermissions(existing);
231
+ } catch (err) {
232
+ if (err instanceof SafetyConfigParseError && existed) {
233
+ const backup = backupFile(filePath);
234
+ return { file: filePath, existed, skipped: true, backup };
235
+ }
236
+ throw err;
237
+ }
238
+
239
+ fs.writeFileSync(filePath, merged, 'utf-8');
240
+ return { file: filePath, existed, skipped: false };
241
+ }
242
+
243
+ /**
244
+ * Render safe `.cursorignore` defaults at the project root.
245
+ * Read -> merge -> write, preserving user lines. Line-based, so there is no
246
+ * parse-failure path (never destructive).
247
+ * @param {object} params
248
+ * @param {string} params.targetRoot - Project root.
249
+ * @returns {{ file: string, existed: boolean, skipped: boolean }}
250
+ */
251
+ function renderCursorIgnore({ targetRoot }) {
252
+ const filePath = path.join(targetRoot, '.cursorignore');
253
+ const existed = fs.existsSync(filePath);
254
+ const existing = existed ? fs.readFileSync(filePath, 'utf-8') : '';
255
+ const merged = mergeCursorIgnore(existing);
256
+ fs.writeFileSync(filePath, merged, 'utf-8');
257
+ return { file: filePath, existed, skipped: false };
258
+ }
259
+
260
+ module.exports = {
261
+ SafetyConfigParseError,
262
+ CLAUDE_PERMISSION_DEFAULTS,
263
+ CURSORIGNORE_DEFAULTS,
264
+ mergeClaudePermissions,
265
+ mergeCursorIgnore,
266
+ renderClaudePermissions,
267
+ renderCursorIgnore,
268
+ };
@@ -11,15 +11,9 @@
11
11
  const AGENT_PREFIXES = {
12
12
  '.claude/': 'Claude Code',
13
13
  '.cursor/': 'Cursor',
14
- '.windsurf/': 'Windsurf',
15
- '.cline/': 'Cline',
16
14
  '.codex/': 'Codex',
17
15
  '~/.codex/': 'Codex',
18
- '$CODEX_HOME/': 'Codex',
19
- '.opencode/': 'OpenCode',
20
- '.kilocode/': 'Kilocode',
21
- '.roo/': 'Roo Code',
22
- '.github/prompts/': 'GitHub Copilot'
16
+ '$CODEX_HOME/': 'Codex'
23
17
  };
24
18
 
25
19
  /**