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
@@ -1,70 +1,367 @@
1
1
  'use strict';
2
2
 
3
- const { execFileSync } = require('node:child_process');
4
- const { runIssueOperation } = require('../forge-issues');
3
+ const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
4
+ const { resolveIssueBackend, hasExplicitBackendSignal, shouldUseKernelBroker } = require('../issue-backend');
5
+ const { maybeWarnUnmigratedBeads } = require('../beads-nudge');
6
+ const {
7
+ ISSUE_COMMAND_SCHEMA_VERSION,
8
+ ISSUE_COMMAND_ERROR_SCHEMA_VERSION,
9
+ ISSUE_COMMAND_EXIT_CODES,
10
+ resolveNextCommands,
11
+ normalizePriority,
12
+ } = require('../kernel/issue-command-contract');
13
+ const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
14
+ const { renderIssueEnvelope } = require('../issue-render');
15
+ const { recordStageTransition } = require('../workflow/stage-transition');
16
+ const { checkReadFirst } = require('../grounding/read-first');
17
+ const { recordContextLoaded } = require('../grounding/context-events');
5
18
 
19
+ // The Forge issue command surface. Each subcommand routes through the shared
20
+ // runIssueOperation, which selects the active backend (Kernel by --kernel /
21
+ // --issue-backend kernel / FORGE_ISSUE_BACKEND=kernel; Beads otherwise) and performs
22
+ // any backend-specific argument translation. This module therefore carries NO direct
23
+ // issue-tracker invocation or argv translation — those live in the backend
24
+ // abstraction (lib/forge-issues.js + the issue adapters).
6
25
  const SUBCOMMANDS = {
7
26
  create: {
8
- description: 'Create a Beads issue via Forge',
9
- usage: 'forge create [title] [bd-create-flags]',
10
- helpCommand: 'create',
11
- buildBdArgs: (args) => ['create', ...args],
27
+ description: 'Create an issue via Forge',
28
+ usage: 'forge create [title] [flags]',
12
29
  },
13
30
  update: {
14
- description: 'Update a Beads issue via Forge',
15
- usage: 'forge update <id...> [bd-update-flags]',
16
- helpCommand: 'update',
17
- buildBdArgs: (args) => ['update', ...args],
31
+ description: 'Update an issue via Forge',
32
+ usage: 'forge update <id...> [flags]',
18
33
  },
19
34
  claim: {
20
- description: 'Claim a Beads issue via Forge',
21
- usage: 'forge claim <id> [bd-update-flags]',
22
- helpCommand: 'update',
23
- buildBdArgs: (args) => {
24
- const [issueId, ...rest] = args;
25
- if (!issueId) {
26
- return { error: 'Missing issue id. Usage: forge claim <id> [bd-update-flags]' };
27
- }
28
- return ['update', issueId, '--claim', ...rest];
29
- },
35
+ description: 'Claim an issue via Forge',
36
+ usage: 'forge claim <id> [flags]',
37
+ },
38
+ release: {
39
+ description: 'Release a Kernel issue claim via Forge',
40
+ usage: 'forge release <id>',
41
+ },
42
+ comment: {
43
+ description: 'Add an issue comment via Forge',
44
+ usage: 'forge comment <id> <body...>',
30
45
  },
31
46
  close: {
32
- description: 'Close a Beads issue via Forge',
33
- usage: 'forge close <id...> [bd-close-flags]',
34
- helpCommand: 'close',
35
- buildBdArgs: (args) => ['close', ...args],
47
+ description: 'Close an issue via Forge',
48
+ usage: 'forge close <id...> [flags]',
36
49
  },
37
50
  show: {
38
- description: 'Show a Beads issue via Forge',
39
- usage: 'forge show <id> [bd-show-flags]',
40
- helpCommand: 'show',
41
- buildBdArgs: (args) => ['show', ...args],
51
+ description: 'Show an issue via Forge',
52
+ usage: 'forge show <id> [flags]',
42
53
  },
43
54
  list: {
44
- description: 'List Beads issues via Forge',
45
- usage: 'forge list [bd-list-flags]',
46
- helpCommand: 'list',
47
- buildBdArgs: (args) => ['list', ...args],
55
+ description: 'List issues via Forge',
56
+ usage: 'forge list [flags]',
48
57
  },
49
58
  ready: {
50
- description: 'Show ready Beads issues via Forge',
51
- usage: 'forge ready [bd-ready-flags]',
52
- helpCommand: 'ready',
53
- buildBdArgs: (args) => ['ready', ...args],
59
+ description: 'Show ready issues via Forge',
60
+ usage: 'forge ready [flags]',
61
+ },
62
+ search: {
63
+ description: 'Search issues via Forge',
64
+ usage: 'forge issue search <query> [flags]',
65
+ },
66
+ stats: {
67
+ description: 'Show issue statistics via Forge',
68
+ usage: 'forge issue stats [flags]',
69
+ },
70
+ // KAP-7: derived read queries. The backend abstraction maps each to its tracker
71
+ // equivalent (the Kernel passes the operation name through unchanged; the Beads
72
+ // backend maps each to its passthrough subcommand). They are READS, so they are
73
+ // intentionally NOT in WRITE_SUBCOMMANDS.
74
+ blocked: {
75
+ description: 'Show blocked issues via Forge',
76
+ usage: 'forge issue blocked [flags]',
77
+ },
78
+ stale: {
79
+ description: 'Show stale issues via Forge',
80
+ usage: 'forge issue stale [--days <n>]',
81
+ },
82
+ orphans: {
83
+ description: 'Show issues with dangling dependency edges via Forge',
84
+ usage: 'forge issue orphans',
85
+ },
86
+ // KAP-12: read-only content lint — issues missing required content
87
+ // (task/bug with no acceptance_criteria). A READ, so NOT in WRITE_SUBCOMMANDS.
88
+ lint: {
89
+ description: 'Show issues missing required content via Forge',
90
+ usage: 'forge issue lint',
91
+ },
92
+ // Epic support: an epic's DIRECT children + kernel-computed rollup
93
+ // (done-only percentage, per-status counts, blocked list). A READ, so
94
+ // intentionally NOT in WRITE_SUBCOMMANDS.
95
+ children: {
96
+ description: 'Show an epic\'s child issues + rollup via Forge',
97
+ usage: 'forge issue children <epic-id> [--json]',
98
+ },
99
+ // Lease-ownership verification (kernel d71a824b). A READ whose EXIT CODE encodes the
100
+ // verdict: exit 0 iff the resolving actor holds the live lease, non-zero otherwise.
101
+ // A claim returning ok:true does not prove ownership (a duplicate replay also returns
102
+ // ok:true), so a worker calls `owns` to CONFIRM before mutating a claimed issue. A
103
+ // READ, so intentionally NOT in WRITE_SUBCOMMANDS.
104
+ owns: {
105
+ description: 'Verify the current actor holds the live lease on an issue via Forge',
106
+ usage: 'forge issue owns <id> [--json]',
107
+ },
108
+ // Active-lease listing (kernel issue 7dc229d4). A bare passthrough to the Kernel
109
+ // lease table (kernel_claims); Beads has no lease table to enumerate, so
110
+ // forge-issues.js rejects the Beads path explicitly. A READ, so intentionally NOT
111
+ // in WRITE_SUBCOMMANDS.
112
+ claims: {
113
+ description: 'Show active issue leases (claims) via Forge',
114
+ usage: 'forge issue claims [--json]',
115
+ },
116
+ dep: {
117
+ description: 'Manage issue dependencies via Forge',
118
+ usage: 'forge issue dep <add|remove> <issue-id> <blocks-issue-id>',
119
+ // The dep subcommand fans out to two actions. Declaring them here keeps the
120
+ // supported action set discoverable from the command spec (and lets the
121
+ // release-readiness gate confirm dep add/remove are present).
122
+ actions: {
123
+ add: { description: 'Add a dependency edge (issue-id blocked by blocks-issue-id)' },
124
+ remove: { description: 'Remove a dependency edge' },
125
+ },
54
126
  },
55
127
  };
56
128
 
57
- function normalizeArgs(args = []) {
58
- return args.filter(arg => arg !== '--');
129
+ const WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'release', 'comment', 'close', 'dep']);
130
+
131
+ // Human-first reads (kernel issue a9bbd065, 0.1.0 critical path): these
132
+ // subcommands default to a compact text rendering (lib/issue-render.js) instead
133
+ // of the raw forge.issue.v1 envelope. --json (or FORGE_JSON=1) restores the
134
+ // byte-identical contract output for scripts. DELIBERATE breaking change to the
135
+ // DEFAULT output only, approved before the 0.1.0 API freeze — the kernel
136
+ // contract itself is untouched.
137
+ const HUMAN_RENDERED_SUBCOMMANDS = new Set(['ready', 'list', 'show']);
138
+ // Writes render a human confirmation ONLY at an interactive TTY; piped/CI callers keep
139
+ // the machine-parseable JSON envelope by default so scripts work without --json (842a8be7).
140
+ const HUMAN_RENDERED_WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'release', 'comment', 'close']);
141
+ const DEP_ACTIONS = Object.keys(SUBCOMMANDS.dep.actions);
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Check-after-write verification (gate.issue_verify, kernel issue 5f928cd0).
145
+ //
146
+ // The kernel has two PROVEN cases where a mutation's ok:true lied about the
147
+ // stored outcome: 145d9ad1 (close --reason/closed_at dropped by the
148
+ // kernel_issues projection) and d71a824b (idempotent claim replay telling a
149
+ // losing agent it won). So after a successful kernel mutation, this boundary
150
+ // re-reads the issue THROUGH THE SAME runner/broker and asserts the intended
151
+ // delta actually landed, emitting `verified: true|false` + `mismatches: []` in
152
+ // the envelope. WARN MODE ONLY: a mismatch prints a warning and the write's
153
+ // success/exit code are never touched; a verification READ error yields
154
+ // `verified: null` (unknown) rather than failing the write. Governed by the
155
+ // default-ON, unlocked `gate.issue_verify` (runtime-graph) — disable via
156
+ // `forge gate disable gate.issue_verify`, which skips the read-back entirely.
157
+ const ISSUE_VERIFY_GATE_ID = 'gate.issue_verify';
158
+ const VERIFIED_SUBCOMMANDS = new Set(['create', 'update', 'close', 'claim', 'comment']);
159
+
160
+ // Resolve whether gate.issue_verify is enabled for this project. An injected
161
+ // opts.resolveRuntimeGraph wins (tests); an unresolvable config (lint errors)
162
+ // SKIPS verification rather than breaking the write path.
163
+ function isIssueVerifyEnabled(projectRoot, opts = {}) {
164
+ const resolveGraph = opts.resolveRuntimeGraph || getResolvedRuntimeGraph;
165
+ try {
166
+ const graph = resolveGraph({ projectRoot });
167
+ const gate = (graph.gates || []).find(candidate => candidate.id === ISSUE_VERIFY_GATE_ID);
168
+ return gate ? gate.enabled !== false : true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ // Minimal --flag parser mirroring the broker's parseFlagPairs semantics
175
+ // (--key value and --key=value; last value wins) so the expected delta is
176
+ // derived from the SAME tokens the broker consumed.
177
+ function parseVerifyFlags(args = []) {
178
+ const flags = {};
179
+ for (let index = 0; index < args.length; index += 1) {
180
+ const token = args[index];
181
+ if (typeof token !== 'string' || !token.startsWith('--')) continue;
182
+ const body = token.slice(2);
183
+ const eq = body.indexOf('=');
184
+ if (eq !== -1) {
185
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
186
+ continue;
187
+ }
188
+ const next = args[index + 1];
189
+ if (typeof next === 'string' && !next.startsWith('--')) {
190
+ flags[body] = next;
191
+ index += 1;
192
+ } else {
193
+ flags[body] = true;
194
+ }
195
+ }
196
+ return flags;
197
+ }
198
+
199
+ // The exact-match fields a mutation promised, derived from its flags. Only the
200
+ // REQUESTED fields are asserted (no coupling to backend defaults). Close always
201
+ // asserts the terminal status the broker resolves (explicit --status wins, else
202
+ // 'done' — mirrors the driver's resolveMutationStatus).
203
+ function expectedIssueFields(subcommand, flags) {
204
+ const expected = {};
205
+ if (subcommand === 'create' || subcommand === 'update') {
206
+ if (typeof flags.title === 'string') expected.title = flags.title;
207
+ }
208
+ if (subcommand === 'create' && typeof flags.type === 'string') expected.type = flags.type;
209
+ if (subcommand === 'update') {
210
+ if (typeof flags.status === 'string') expected.status = flags.status;
211
+ if (typeof flags.assignee === 'string') expected.assignee = flags.assignee;
212
+ if (typeof flags.priority === 'string') expected.priority = normalizePriority(flags.priority);
213
+ }
214
+ if (subcommand === 'close') {
215
+ expected.status = typeof flags.status === 'string' ? flags.status : 'done';
216
+ }
217
+ return expected;
218
+ }
219
+
220
+ function collectFieldMismatches(expected, data) {
221
+ const mismatches = [];
222
+ for (const [field, value] of Object.entries(expected)) {
223
+ const stored = data ? data[field] : undefined;
224
+ if (stored !== value) {
225
+ mismatches.push(`${field}: expected ${JSON.stringify(value)}, read back ${JSON.stringify(stored ?? null)}`);
226
+ }
227
+ }
228
+ return mismatches;
229
+ }
230
+
231
+ // Close-specific presence checks — the regression trap for 145d9ad1, where the
232
+ // projection silently dropped closed_at and the --reason text.
233
+ function collectCloseMismatches(flags, data, mismatches) {
234
+ if (data.closed_at === null || data.closed_at === undefined) {
235
+ mismatches.push('closed_at: expected a close timestamp, read back null (145d9ad1-class projection drop)');
236
+ }
237
+ const reasonRequested = typeof flags.reason === 'string' || typeof flags['close-reason'] === 'string';
238
+ if (reasonRequested && (data.close_reason === null || data.close_reason === undefined)) {
239
+ mismatches.push('close_reason: expected the requested reason to persist, read back null (145d9ad1-class projection drop)');
240
+ }
241
+ }
242
+
243
+ // Claim verification uses the claim-safety primitive directly: `owns` encodes
244
+ // "does the RESOLVING ACTOR hold the live lease" — exactly the check that
245
+ // catches a d71a824b phantom-claim replay (ok:true for a lease someone else holds).
246
+ async function verifyClaimDelta(runner, issueId, projectRoot, opts) {
247
+ const readBack = await runner('owns', [issueId, '--json'], projectRoot, opts);
248
+ if (!readBack || readBack.ok !== true || !readBack.data || typeof readBack.data !== 'object') {
249
+ return { verified: null };
250
+ }
251
+ if (readBack.data.owned === true) {
252
+ return { verified: true, mismatches: [] };
253
+ }
254
+ const heldBy = JSON.stringify(readBack.data.claimed_by ?? null);
255
+ return {
256
+ verified: false,
257
+ mismatches: [
258
+ `claimed_by: expected the resolved actor to hold the live lease, but owns reports held by ${heldBy} `
259
+ + '(d71a824b-class phantom-claim replay)',
260
+ ],
261
+ };
59
262
  }
60
263
 
61
- function getExecOptions(projectRoot) {
264
+ // Comment verification matches the MINTED comment_id in the re-read comments
265
+ // array (show returns comments with ids), sidestepping the count-increment
266
+ // approach that would need a pre-write read.
267
+ function verifyCommentDelta(result, data) {
268
+ const commentId = result.data && typeof result.data.comment_id === 'string' ? result.data.comment_id : null;
269
+ if (!commentId) return { verified: null };
270
+ const comments = Array.isArray(data.comments) ? data.comments : [];
271
+ if (comments.some(comment => comment && comment.id === commentId)) {
272
+ return { verified: true, mismatches: [] };
273
+ }
62
274
  return {
63
- cwd: projectRoot,
64
- stdio: 'inherit',
275
+ verified: false,
276
+ mismatches: [`comment: expected comment ${commentId} on the issue, not found in the read-back`],
65
277
  };
66
278
  }
67
279
 
280
+ // Re-read the mutated issue through the SAME runner and assert the intended
281
+ // delta. Returns { verified: true|false|null, mismatches? }. NEVER throws — a
282
+ // verification failure must never make a successful write report failure.
283
+ async function verifyIssueMutation(subcommand, operationArgs, result, runner, projectRoot, opts) {
284
+ try {
285
+ const flags = parseVerifyFlags(operationArgs);
286
+ const issueId = (result.data && typeof result.data.id === 'string' && result.data.id)
287
+ ? result.data.id
288
+ : operationArgs.find(arg => typeof arg === 'string' && !arg.startsWith('-'));
289
+ if (!issueId) return { verified: null };
290
+ if (subcommand === 'claim') {
291
+ return await verifyClaimDelta(runner, issueId, projectRoot, opts);
292
+ }
293
+ const readBack = await runner('show', [issueId, '--json'], projectRoot, opts);
294
+ if (!readBack || readBack.ok !== true || !readBack.data || typeof readBack.data !== 'object') {
295
+ return { verified: null };
296
+ }
297
+ if (subcommand === 'comment') {
298
+ return verifyCommentDelta(result, readBack.data);
299
+ }
300
+ const mismatches = collectFieldMismatches(expectedIssueFields(subcommand, flags), readBack.data);
301
+ if (subcommand === 'close') {
302
+ collectCloseMismatches(flags, readBack.data, mismatches);
303
+ }
304
+ return { verified: mismatches.length === 0, mismatches };
305
+ } catch {
306
+ return { verified: null };
307
+ }
308
+ }
309
+
310
+ // Warn-mode reporting: verified:false lists the mismatches; verified:null says
311
+ // the read-back could not confirm. Either way the write stays successful.
312
+ function warnVerifyOutcome(subcommand, issueRef, verification) {
313
+ if (verification.verified === true) return;
314
+ const lines = verification.verified === false
315
+ ? [
316
+ `[forge verify] WARNING: '${subcommand}' reported ok but the read-back of ${issueRef} does not match the requested change:`,
317
+ ...verification.mismatches.map(entry => ` - ${entry}`),
318
+ ]
319
+ : [`[forge verify] WARNING: '${subcommand}' succeeded but the read-back of ${issueRef} could not confirm the change (verification read failed).`];
320
+ lines.push('The write itself succeeded (exit 0). Check-after-write is governed by gate.issue_verify (warn mode) — disable via: forge gate disable gate.issue_verify');
321
+ console.warn(lines.join('\n'));
322
+ }
323
+
324
+ // Run verification for one successful mutation result and attach the outcome
325
+ // (verified / mismatches) to it, warning on anything but verified:true.
326
+ async function applyIssueVerification(subcommand, operationArgs, result, runner, projectRoot, opts) {
327
+ const verification = await verifyIssueMutation(subcommand, operationArgs, result, runner, projectRoot, opts);
328
+ result.verified = verification.verified;
329
+ if (Array.isArray(verification.mismatches)) {
330
+ result.mismatches = verification.mismatches;
331
+ }
332
+ const issueRef = (result.data && result.data.id) || operationArgs.find(arg => typeof arg === 'string' && !arg.startsWith('-')) || '<issue>';
333
+ warnVerifyOutcome(subcommand, issueRef, verification);
334
+ }
335
+
336
+ function normalizeArgs(args = []) {
337
+ return args.filter(arg => arg !== '--');
338
+ }
339
+
340
+ // 5a5ba3a6: auto-wire stage_runs. When a kernel `comment` carries the Descriptive
341
+ // Context Convention's `stage: <from> -> <to>` line, mirror it into stage_runs
342
+ // (complete the from-stage, start the to-stage) so `current_stage` becomes real
343
+ // without a manual `forge stage` verb call. Best-effort and non-blocking:
344
+ // recordStageTransition never throws, so a stage_run write can never break the
345
+ // comment that already succeeded. Kernel path only — reuses the driver already
346
+ // assembled on opts (no second DB handle).
347
+ function recordStageTransitionFromComment(subcommand, operationArgs, result, opts) {
348
+ if (subcommand !== 'comment' || !shouldUseKernelBroker(opts)) return;
349
+ if (!result || result.ok !== true || !result.data) return;
350
+ const issueId = typeof result.data.id === 'string' ? result.data.id : null;
351
+ if (!issueId) return;
352
+ // Drop the leading issue-ref positional; the rest is the comment body.
353
+ const body = normalizeArgs(operationArgs)
354
+ .filter(arg => typeof arg === 'string' && !arg.startsWith('-'))
355
+ .slice(1)
356
+ .join(' ');
357
+ recordStageTransition({
358
+ driver: opts.kernelDriver,
359
+ issueId,
360
+ body,
361
+ config: opts.kernelDatabasePath ? { databasePath: opts.kernelDatabasePath } : {},
362
+ });
363
+ }
364
+
68
365
  function formatIssueHelp() {
69
366
  const lines = [
70
367
  'Usage: forge issue <subcommand> [...]',
@@ -82,64 +379,464 @@ function formatIssueHelp() {
82
379
  lines.push(' forge claim forge-abc');
83
380
  lines.push(' forge update forge-abc --priority 1');
84
381
  lines.push(' forge close forge-abc --reason "Done"');
382
+ lines.push(' forge comment forge-abc "Handoff note"');
85
383
  lines.push(' forge issue show forge-abc --json');
384
+ lines.push(' forge issue search "kernel contract" --json');
385
+ lines.push(' forge issue stats --json');
386
+ lines.push(' forge issue dep add forge-work forge-blocker');
86
387
 
87
388
  return lines.join('\n');
88
389
  }
89
390
 
90
- function extractErrorMessage(error) {
91
- if (error?.code === 'ENOENT') {
92
- return 'Beads (bd) command not found. Install or initialize Beads before using Forge issue commands.';
391
+ // Map a CLI subcommand to the backend operation name. `dep` fans out to
392
+ // `dep.<action>`; every other subcommand uses its own name (the backend performs
393
+ // any tracker-specific translation, e.g. Beads claim -> `update <id> --claim`).
394
+ function resolveIssueOperation(subcommand, args) {
395
+ if (subcommand === 'dep') {
396
+ return `dep.${normalizeArgs(args)[0]}`;
93
397
  }
398
+ return subcommand;
399
+ }
94
400
 
95
- // With stdio: 'inherit', error.stderr and error.stdout are always null.
96
- // Only error.message is available for diagnostics.
97
- return error?.message?.trim() || 'Beads command failed';
401
+ // The Kernel create payload (buildCreatePayload) reads only the --title flag, so a
402
+ // bare leading positional (`forge create "title"`) would be ignored and the title
403
+ // would default to the minted UUID. For parity on the KERNEL PATH ONLY, translate a
404
+ // single leading bare positional into `--title <value>` when no explicit
405
+ // --title/--title= is present. The Beads backend keeps its native positional
406
+ // handling (this never runs for the Beads path).
407
+ function withKernelCreateTitle(args) {
408
+ const hasTitle = args.some(
409
+ arg => arg === '--title' || (typeof arg === 'string' && arg.startsWith('--title=')),
410
+ );
411
+ if (hasTitle || args.length === 0) {
412
+ return args;
413
+ }
414
+ const leading = args[0];
415
+ if (typeof leading === 'string' && !leading.startsWith('-')) {
416
+ return ['--title', leading, ...args.slice(1)];
417
+ }
418
+ return args;
98
419
  }
99
420
 
100
- function buildBdArgs(subcommand, rawArgs) {
101
- const spec = SUBCOMMANDS[subcommand];
102
- if (!spec) {
103
- return { error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
421
+ // Resolve the operation args passed to the backend. `dep` drops its leading action
422
+ // token (it is carried in the operation name); `create` on the Kernel path gains the
423
+ // positional-title parity translation. Everything else passes its normalized args
424
+ // through verbatim.
425
+ function resolveOperationArgs(subcommand, args, opts = {}) {
426
+ const normalizedArgs = normalizeArgs(args);
427
+
428
+ if (subcommand === 'dep') {
429
+ return normalizedArgs.slice(1);
104
430
  }
105
431
 
106
- const args = normalizeArgs(rawArgs);
107
- if (args.includes('--help') || args.includes('-h')) {
108
- return [spec.helpCommand, '--help'];
432
+ if (subcommand === 'create' && shouldUseKernelBroker(opts)) {
433
+ return withKernelCreateTitle(normalizedArgs);
109
434
  }
110
435
 
111
- return spec.buildBdArgs(args);
436
+ return normalizedArgs;
112
437
  }
113
438
 
114
- const WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'close']);
115
-
116
- async function runIssueSubcommand(subcommand, args, projectRoot, opts = {}) {
117
- const bdArgs = buildBdArgs(subcommand, args);
439
+ // Validate the dep action up front so a bad `forge issue dep <action>` fails with a
440
+ // usage message instead of routing to a `dep.undefined` operation.
441
+ function validateDepArgs(args) {
442
+ const normalized = normalizeArgs(args);
443
+ const [action, ...rest] = normalized;
444
+ if (!DEP_ACTIONS.includes(action)) {
445
+ return {
446
+ error: `Unsupported dependency action: ${action || '(missing)'}. Usage: forge issue dep <add|remove> <issue-id> <blocks-issue-id>`,
447
+ };
448
+ }
449
+ if (rest.length < 2) {
450
+ return {
451
+ error: `Missing dependency ids. Usage: forge issue dep ${action} <issue-id> <blocks-issue-id>`,
452
+ };
453
+ }
454
+ return null;
455
+ }
118
456
 
119
- if (!Array.isArray(bdArgs)) {
120
- return { success: false, error: bdArgs.error };
457
+ // Resolve the active issue backend (kernel|beads) and thread it into opts so the
458
+ // shared runIssueOperation deps see it. OPT-IN ONLY: opts is left byte-identical
459
+ // when no explicit signal is present (env/config/explicit), preserving the Beads
460
+ // default path. A copy is returned — the caller's opts object is never mutated.
461
+ function withResolvedIssueBackend(projectRoot, opts = {}) {
462
+ const env = opts.env || process.env;
463
+ const signalContext = { deps: opts, env, projectRoot };
464
+ if (!hasExplicitBackendSignal(signalContext)) {
465
+ return opts;
121
466
  }
122
467
 
123
- if (WRITE_SUBCOMMANDS.has(subcommand)) {
124
- const runner = opts.runIssueOperation || runIssueOperation;
125
- const operation = subcommand === 'claim' ? 'update' : subcommand;
126
- return runner(operation, bdArgs.slice(1), projectRoot, opts);
468
+ // Run EVERY explicit value through the resolver — including an explicit
469
+ // opts.issueBackend so case is normalized and unknown values warn + fall back.
470
+ // opts.issueBackend still wins precedence over env/config inside the resolver.
471
+ // Identity is preserved when the resolved value already matches, keeping the
472
+ // no-op path byte-identical.
473
+ const issueBackend = resolveIssueBackend(signalContext);
474
+ if (opts.issueBackend === issueBackend) {
475
+ return opts;
127
476
  }
477
+ return { ...opts, issueBackend };
478
+ }
128
479
 
129
- const exec = opts._exec || execFileSync;
480
+ // The Kernel broker returns the issue-command contract shape
481
+ // ({ ok, schema_version, command, data, next_commands } or { ok:false, error })
482
+ // rather than the Beads-style { success, output }. The bin/forge.js result printer
483
+ // keys on `success`/`output`, so a raw kernel contract would render as
484
+ // "Command failed". Normalize ONLY the contract shape (ok defined, success
485
+ // undefined) into { success, output } here, at the command boundary — the kernel
486
+ // contract itself stays untouched. Every other result passes through byte-identical.
487
+ //
488
+ // Response-contract parity (the Beads behavior the Kernel replaced):
489
+ // * SUCCESS → the printed envelope carries `ok:true` (consumers gate on it).
490
+ // * FAILURE → the contract `exit_code` is surfaced as `result.exitCode` so the bin
491
+ // printer exits with the error class's code (not always 1); and on
492
+ // `--json` the full forge.issue.error.v1 envelope is emitted on stdout
493
+ // (without --json the human message alone goes to stderr, as before).
494
+ function normalizeIssueResult(result, operation, { json = false, humanRender = null } = {}) {
495
+ if (!result || typeof result !== 'object') {
496
+ return result;
497
+ }
498
+ if (result.ok === undefined || result.success !== undefined) {
499
+ return result;
500
+ }
130
501
 
131
- try {
132
- exec('bd', bdArgs, getExecOptions(projectRoot));
133
- return { success: true, subcommand };
134
- } catch (error) {
502
+ if (result.ok === true) {
503
+ const envelope = {
504
+ ok: true,
505
+ schema_version: result.schema_version,
506
+ command: result.command,
507
+ data: result.data ?? null,
508
+ next_commands: result.next_commands ?? [],
509
+ };
510
+ // Check-after-write outcome (gate.issue_verify): additive keys, present only
511
+ // when verification ran (verified may be null when the read-back failed).
512
+ if (result.verified !== undefined) envelope.verified = result.verified;
513
+ if (result.mismatches !== undefined) envelope.mismatches = result.mismatches;
135
514
  return {
136
- success: false,
137
- error: extractErrorMessage(error),
515
+ success: true,
516
+ operation,
517
+ // Human-first reads (a9bbd065): render the envelope as text unless the
518
+ // caller asked for the contract (--json / FORGE_JSON=1). The envelope —
519
+ // including verified/mismatches — is rendered, never dropped.
520
+ output: humanRender
521
+ ? renderIssueEnvelope(humanRender, envelope)
522
+ : JSON.stringify(envelope, null, 2),
138
523
  };
139
524
  }
525
+
526
+ const message = result.error?.message
527
+ || result.message
528
+ || `Issue ${operation} failed`;
529
+ const normalized = { success: false, error: message };
530
+ if (Number.isInteger(result.error?.exit_code)) {
531
+ normalized.exitCode = result.error.exit_code;
532
+ }
533
+ if (json) {
534
+ normalized.output = JSON.stringify({
535
+ ok: false,
536
+ schema_version: result.schema_version || ISSUE_COMMAND_ERROR_SCHEMA_VERSION,
537
+ command: result.command,
538
+ error: result.error,
539
+ next_commands: result.next_commands ?? [],
540
+ }, null, 2);
541
+ }
542
+ return normalized;
543
+ }
544
+
545
+ // `owns` is a read whose EXIT CODE encodes the ownership verdict: exit 0 when the
546
+ // resolving actor holds the live lease, non-zero otherwise. The kernel read itself
547
+ // succeeds (ok:true) whether or not the actor owns the lease — the verdict rides in
548
+ // `data.owned` — so the plain read normalizer (which maps every ok:true to success)
549
+ // would exit 0 on a NON-owned issue and let a worker mutate someone else's claim.
550
+ // This normalizer converts a `owned:false` verdict into a non-zero conflict result
551
+ // with a clear "you do not own the lease" message, while a genuine read failure (issue
552
+ // not found) still flows through normalizeIssueResult with its contract exit code. The
553
+ // `output` (the full envelope) is preserved even on the non-owned failure so `--json`
554
+ // consumers still receive `{ ok:true, data:{ owned:false, ... } }` on stdout.
555
+ function normalizeOwnsResult(result, operation, { json = false } = {}) {
556
+ // Raw read failure (e.g. not found) or a non-contract shape → defer to the standard
557
+ // normalizer so the contract exit code (notFound=3, ...) and error envelope survive.
558
+ if (!result || typeof result !== 'object' || result.ok !== true) {
559
+ return normalizeIssueResult(result, operation, { json });
560
+ }
561
+
562
+ const data = result.data && typeof result.data === 'object' ? result.data : {};
563
+ const envelope = JSON.stringify({
564
+ ok: true,
565
+ schema_version: result.schema_version,
566
+ command: result.command,
567
+ data,
568
+ next_commands: result.next_commands ?? [],
569
+ }, null, 2);
570
+
571
+ if (data.owned === true) {
572
+ return { success: true, operation, output: envelope };
573
+ }
574
+
575
+ const heldBy = typeof data.claimed_by === 'string' && data.claimed_by ? data.claimed_by : 'nobody';
576
+ const expiredNote = data.expired ? ' — the lease has expired and can be reclaimed' : '';
577
+ const issueId = typeof data.id === 'string' && data.id ? data.id : '<issue>';
578
+ const normalized = {
579
+ success: false,
580
+ operation,
581
+ error: `You do not own the lease for ${issueId} (held by ${heldBy}${expiredNote}). `
582
+ + 'You lost the race or the claim collapsed to a duplicate — reselect via '
583
+ + '`forge issue ready --json` and re-claim before working.',
584
+ exitCode: ISSUE_COMMAND_EXIT_CODES.conflict,
585
+ };
586
+ // On --json, surface the full ownership envelope on stdout (the bin printer emits
587
+ // result.output before the stderr error) so machine consumers still get the verdict.
588
+ if (json) {
589
+ normalized.output = envelope;
590
+ }
591
+ return normalized;
592
+ }
593
+
594
+ // Split close args into the leading run of positional ids and the trailing flag
595
+ // tokens (everything from the first `-`-prefixed token onward). A flag value such
596
+ // as `done` in `--reason done` sits after a dash token, so it is correctly kept
597
+ // with the flags and never mistaken for an id.
598
+ function splitLeadingIds(args = []) {
599
+ const flagIndex = args.findIndex(arg => typeof arg === 'string' && arg.startsWith('-'));
600
+ if (flagIndex === -1) {
601
+ return { ids: [...args], flags: [] };
602
+ }
603
+ return { ids: args.slice(0, flagIndex), flags: args.slice(flagIndex) };
604
+ }
605
+
606
+ // Kernel close fans out one runner call per id (the kernel close op closes a single
607
+ // id — the first positional), then aggregates the per-id outcomes into ONE
608
+ // forge.issue.v1 envelope (`mutationBatch` in the contract) — NEVER a bare array,
609
+ // which broke envelope parity for multi-id close. `ok` is true only when every id
610
+ // closed; per-id outcomes live in `data.results` and the contract `exit_code` of the
611
+ // first failure is surfaced as `exitCode` so the bin printer exits with the error
612
+ // class's code. KERNEL PATH ONLY: the Beads passthrough keeps its single
613
+ // `close id1 id2 ...` invocation.
614
+ async function runKernelBatchClose(runner, operation, ids, flags, projectRoot, opts, verifyEnabled = false) {
615
+ const results = [];
616
+ let allSucceeded = true;
617
+ let firstFailureExit;
618
+ for (const id of ids) {
619
+ const raw = await runner(operation, [id, ...flags], projectRoot, opts);
620
+ const succeeded = Boolean(raw && (raw.ok === true || raw.success === true));
621
+ if (succeeded) {
622
+ // Echo the CANONICAL id the kernel closed (raw.data.id) rather than the
623
+ // caller's token — with git-style short-id support the broker may have
624
+ // resolved a prefix to the full id, and the envelope should report the
625
+ // resolved id the read-back verified.
626
+ const closedId = (raw.data && typeof raw.data.id === 'string' && raw.data.id) ? raw.data.id : id;
627
+ const entry = { id: closedId, ok: true };
628
+ if (raw.data && Number.isInteger(raw.data.revision)) entry.revision = raw.data.revision;
629
+ if (raw.data && Array.isArray(raw.data.newly_unblocked)) entry.newly_unblocked = raw.data.newly_unblocked;
630
+ // Per-id check-after-write (gate.issue_verify): the batch path fans out one
631
+ // close per id, so each id gets its own read-back and verified/mismatches.
632
+ if (verifyEnabled && raw.ok === true) {
633
+ const verification = await verifyIssueMutation('close', [id, ...flags], raw, runner, projectRoot, opts);
634
+ entry.verified = verification.verified;
635
+ if (Array.isArray(verification.mismatches) && verification.mismatches.length > 0) {
636
+ entry.mismatches = verification.mismatches;
637
+ }
638
+ warnVerifyOutcome('close', id, verification);
639
+ }
640
+ results.push(entry);
641
+ continue;
642
+ }
643
+ allSucceeded = false;
644
+ const error = (raw && raw.error && typeof raw.error === 'object')
645
+ ? raw.error
646
+ : { message: (raw && (raw.error || raw.message)) || `Issue ${operation} failed for ${id}` };
647
+ if (firstFailureExit === undefined && Number.isInteger(error.exit_code)) {
648
+ firstFailureExit = error.exit_code;
649
+ }
650
+ results.push({ id, ok: false, error });
651
+ }
652
+
653
+ const envelope = {
654
+ ok: allSucceeded,
655
+ schema_version: ISSUE_COMMAND_SCHEMA_VERSION,
656
+ command: 'issue.close',
657
+ data: {
658
+ results,
659
+ count: results.length,
660
+ closed: results.filter(entry => entry.ok).map(entry => entry.id),
661
+ },
662
+ next_commands: resolveNextCommands('issue.close'),
663
+ };
664
+ // Aggregate the per-id verification into envelope-level verified/mismatches:
665
+ // false if any closed id mismatched, null if any read-back was inconclusive,
666
+ // true only when every closed id verified clean.
667
+ const verifiedEntries = results.filter(entry => entry.ok && entry.verified !== undefined);
668
+ if (verifiedEntries.length > 0) {
669
+ if (verifiedEntries.some(entry => entry.verified === false)) {
670
+ envelope.verified = false;
671
+ envelope.mismatches = verifiedEntries
672
+ .filter(entry => Array.isArray(entry.mismatches))
673
+ .flatMap(entry => entry.mismatches.map(mismatch => `${entry.id}: ${mismatch}`));
674
+ } else if (verifiedEntries.some(entry => entry.verified === null)) {
675
+ envelope.verified = null;
676
+ } else {
677
+ envelope.verified = true;
678
+ envelope.mismatches = [];
679
+ }
680
+ }
681
+
682
+ const normalized = {
683
+ success: allSucceeded,
684
+ operation,
685
+ output: JSON.stringify(envelope, null, 2),
686
+ };
687
+ if (!allSucceeded) {
688
+ const failed = results.filter(entry => !entry.ok).map(entry => entry.id);
689
+ normalized.error = `Failed to close ${failed.length} of ${ids.length} issue(s): ${failed.join(', ')}`;
690
+ normalized.exitCode = Number.isInteger(firstFailureExit)
691
+ ? firstFailureExit
692
+ : ISSUE_COMMAND_EXIT_CODES.internal;
693
+ }
694
+ return normalized;
695
+ }
696
+
697
+ async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
698
+ const spec = SUBCOMMANDS[subcommand];
699
+ if (!spec) {
700
+ return { success: false, error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
701
+ }
702
+
703
+ // Backend-agnostic --help short-circuit: print the subcommand's usage and return
704
+ // BEFORE resolving the backend or dispatching any operation. Without this, a help
705
+ // request was forwarded to the active backend as an operation arg — absorbed
706
+ // harmlessly by Beads (which swallowed --help), but broken under the Kernel default:
707
+ // the plural path failed with a bare "Command failed" and the singular path
708
+ // SILENTLY minted a junk issue (and could queue a GitHub projection). Help must
709
+ // never touch a backend.
710
+ if (normalizeArgs(args).some(arg => arg === '--help' || arg === '-h')) {
711
+ return { success: true, output: `${spec.usage}\n\n${spec.description}` };
712
+ }
713
+
714
+ if (subcommand === 'dep') {
715
+ const depError = validateDepArgs(args);
716
+ if (depError) {
717
+ return { success: false, error: depError.error };
718
+ }
719
+ }
720
+
721
+ // `children` needs a leading <epic-id> positional; without it the kernel read
722
+ // binds `undefined` and surfaces a raw SQLite error. Fail with usage instead.
723
+ if (subcommand === 'children' && !normalizeArgs(args).some((arg) => !arg.startsWith('--'))) {
724
+ return { success: false, error: `Usage: ${spec.usage}` };
725
+ }
726
+
727
+ // These subcommands bind their target issue from the first positional. Without it the
728
+ // kernel path fabricates a random UUID and quarantines it ("invalid_claim_scope",
729
+ // exit 0) — surface a clean usage error with a non-zero exit instead (kernel 842a8be7).
730
+ const REQUIRES_LEADING_ID = new Set(['claim', 'release', 'comment', 'show', 'update', 'close', 'owns']);
731
+ if (REQUIRES_LEADING_ID.has(subcommand) && !normalizeArgs(args).some((arg) => !arg.startsWith('--'))) {
732
+ return { success: false, error: `Missing required argument <id>.\nUsage: ${spec.usage}`, exitCode: 6 };
733
+ }
734
+
735
+ const opts = withResolvedIssueBackend(projectRoot, rawOpts);
736
+
737
+ // gate.read_first (grounding, epic 6ef96e92): HARD-BLOCK `forge claim <id>`
738
+ // until the issue has been read this session/window (a `context.loaded` kernel
739
+ // event exists — appended by `forge recap`/`forge show`). Fail-closed: the
740
+ // cheapest path through the gate is the correct behavior, since the remedy
741
+ // (`forge recap <id>`) also injects the context. Consulted here at the command
742
+ // boundary exactly like gate.issue_verify; rail.grounding/gate.read_first
743
+ // disabled -> allow (logged). Read commands never gate; only `claim` in P1.
744
+ if (subcommand === 'claim') {
745
+ const issueId = normalizeArgs(args).find((arg) => !arg.startsWith('--'));
746
+ if (issueId) {
747
+ const block = await checkReadFirst(projectRoot, issueId, opts);
748
+ if (block) return block;
749
+ }
750
+ }
751
+
752
+ // Both backends are reached through the same runIssueOperation seam. Naming the
753
+ // injected local `runIssueOperation` keeps the dispatch a literal call to a binding
754
+ // named `runIssueOperation` (the kernel-evidence gate is syntactic) while still
755
+ // honoring an injected runner; the `kernelBroker: opts.kernelBroker` passthrough is
756
+ // a runtime no-op (undefined under Beads) that documents the Kernel-capable surface.
757
+ const runIssueOperation = opts.runIssueOperation || defaultRunIssueOperation;
758
+ const operation = resolveIssueOperation(subcommand, args);
759
+ const operationArgs = resolveOperationArgs(subcommand, args, opts);
760
+
761
+ // Check-after-write (gate.issue_verify): resolved ONCE per invocation, kernel
762
+ // path only. Reads and the Beads path never trigger a read-back.
763
+ const verifyEnabled = VERIFIED_SUBCOMMANDS.has(subcommand)
764
+ && shouldUseKernelBroker(opts)
765
+ && isIssueVerifyEnabled(projectRoot, opts);
766
+
767
+ // Kernel batch close: >1 leading positional id → one runner call per id,
768
+ // aggregated. A single id falls through to the byte-identical single path.
769
+ if (subcommand === 'close' && shouldUseKernelBroker(opts)) {
770
+ const { ids, flags } = splitLeadingIds(operationArgs);
771
+ if (ids.length > 1) {
772
+ return runKernelBatchClose(runIssueOperation, operation, ids, flags, projectRoot, opts, verifyEnabled);
773
+ }
774
+ }
775
+
776
+ const result = await runIssueOperation(
777
+ operation,
778
+ operationArgs,
779
+ projectRoot,
780
+ { ...opts, kernelBroker: opts.kernelBroker },
781
+ );
782
+ // Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a Beads
783
+ // {success,output} shape). Warn-only: attaches verified/mismatches, never
784
+ // changes the result's success or exit code.
785
+ if (verifyEnabled && result && typeof result === 'object' && result.ok === true && result.success === undefined) {
786
+ await applyIssueVerification(subcommand, operationArgs, result, runIssueOperation, projectRoot, opts);
787
+ }
788
+ // Best-effort, non-blocking: mirror a stage-transition comment into stage_runs.
789
+ recordStageTransitionFromComment(subcommand, operationArgs, result, opts);
790
+ // Best-effort, non-blocking: nudge a returning 0.0.10 user whose empty Kernel
791
+ // read hides an unmigrated legacy issue store (kernel issue a5399f3d). The hint
792
+ // text lives in lib/beads-nudge.js so this hot path stays token-free.
793
+ maybeWarnUnmigratedBeads(subcommand, result, projectRoot, rawOpts);
794
+ // Grounding (gate.read_first): a successful `forge show <id>` counts as reading
795
+ // the issue, so append a `context.loaded` event. Best-effort and awaited (a
796
+ // fire-and-forget append could lose the event when the CLI process exits); a
797
+ // failure here never fails the read.
798
+ if (subcommand === 'show' && result && result.ok === true) {
799
+ const shownId = normalizeArgs(args).find((arg) => !arg.startsWith('--'));
800
+ if (shownId) {
801
+ const deps = (opts.kernelBroker && opts.kernelDriver)
802
+ ? { kernelBroker: opts.kernelBroker, kernelDriver: opts.kernelDriver }
803
+ : undefined;
804
+ try {
805
+ await recordContextLoaded(projectRoot, {
806
+ issueId: shownId, cmd: 'show', session: opts.session, env: opts.env, deps, now: opts.now,
807
+ });
808
+ } catch { /* best-effort: never fail a read on grounding bookkeeping */ }
809
+ }
810
+ }
811
+ // Contract output is opt-in for the human-first reads: an explicit --json flag
812
+ // or FORGE_JSON=1 in the environment (for scripts that cannot alter argv).
813
+ const jsonRequested = normalizeArgs(args).includes('--json')
814
+ || (opts.env || process.env).FORGE_JSON === '1';
815
+ // `owns` maps the ownership verdict (data.owned) to the process exit code; every
816
+ // other subcommand uses the plain read/mutation normalization.
817
+ if (subcommand === 'owns') {
818
+ return normalizeOwnsResult(result, operation, { json: jsonRequested });
819
+ }
820
+ // Reads render human-first always; writes only when the caller is an interactive
821
+ // terminal. The CLI entry (bin/forge.js) sets rawOpts.isInteractive from
822
+ // process.stdout.isTTY; every other caller (scripts, tests, pipes) defaults to
823
+ // non-interactive and keeps the machine-parseable JSON envelope (842a8be7).
824
+ const interactive = rawOpts.isInteractive === true;
825
+ const humanSubcommand = !jsonRequested
826
+ && (HUMAN_RENDERED_SUBCOMMANDS.has(subcommand)
827
+ || (interactive && HUMAN_RENDERED_WRITE_SUBCOMMANDS.has(subcommand)))
828
+ ? subcommand
829
+ : null;
830
+ return normalizeIssueResult(result, operation, {
831
+ json: jsonRequested,
832
+ humanRender: humanSubcommand,
833
+ });
140
834
  }
141
835
 
142
- function makeAliasCommand(subcommand) {
836
+ // Build a registry command for a single issue subcommand (e.g. `forge claim`).
837
+ // Each routes through the shared runIssueSubcommand dispatch, so the backend
838
+ // abstraction owns all tracker selection and argument translation.
839
+ function createIssueSubcommand(subcommand) {
143
840
  const spec = SUBCOMMANDS[subcommand];
144
841
  if (!spec) {
145
842
  throw new Error(`Unknown issue subcommand '${subcommand}'`);
@@ -158,8 +855,8 @@ function makeAliasCommand(subcommand) {
158
855
  function createIssueCommand() {
159
856
  return {
160
857
  name: 'issue',
161
- description: 'Manage Beads issues through the Forge command surface',
162
- usage: 'forge issue <create|update|claim|close|show|list|ready> [...]',
858
+ description: 'Manage issues through the Forge command surface',
859
+ usage: 'forge issue <create|update|claim|release|close|show|list|ready|search|stats|dep> [...]',
163
860
  flags: {},
164
861
  handler: async (args, _flags, projectRoot, opts = {}) => {
165
862
  const [subcommand, ...rest] = normalizeArgs(args);
@@ -175,8 +872,14 @@ function createIssueCommand() {
175
872
 
176
873
  module.exports = {
177
874
  SUBCOMMANDS,
178
- buildBdArgs,
875
+ WRITE_SUBCOMMANDS,
179
876
  createIssueCommand,
180
- makeAliasCommand,
877
+ createIssueSubcommand,
878
+ normalizeArgs,
879
+ normalizeIssueResult,
880
+ normalizeOwnsResult,
881
+ resolveIssueOperation,
882
+ resolveOperationArgs,
181
883
  runIssueSubcommand,
884
+ withResolvedIssueBackend,
182
885
  };