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