forge-workflow 0.0.10 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +461 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +17 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +0 -1
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +838 -972
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +382 -11
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { lintRuntimeGraphConfig } = require('./core/runtime-graph');
7
+ const { resolvePatchIntentRecords } = require('./patch-intent');
8
+ const { verifyForgeLock, readForgeLock } = require('./forge-lock');
9
+
10
+ function checkStatus(ok) {
11
+ return ok ? 'pass' : 'fail';
12
+ }
13
+
14
+ function countUntrustedOptIns(lock) {
15
+ return lock.extensions.filter(entry => entry.trust?.allowUntrusted === true).length;
16
+ }
17
+
18
+ function buildPatchIntentSummary(projectRoot) {
19
+ try {
20
+ const status = resolvePatchIntentRecords(projectRoot);
21
+ return {
22
+ ok: status.orphans.length === 0,
23
+ path: status.path,
24
+ records: status.records.length,
25
+ orphans: status.orphans.length,
26
+ };
27
+ } catch (error) {
28
+ return {
29
+ ok: false,
30
+ path: '.forge/patch.md',
31
+ records: 0,
32
+ orphans: 0,
33
+ error: error.message,
34
+ };
35
+ }
36
+ }
37
+
38
+ function buildSelfHealCandidates(projectRoot) {
39
+ const forgeDir = path.join(projectRoot, '.forge');
40
+ const logPath = path.join(forgeDir, 'log.jsonl');
41
+ const candidates = [];
42
+ if (!fs.existsSync(forgeDir)) {
43
+ candidates.push({
44
+ id: 'forge-dir',
45
+ path: '.forge/',
46
+ description: 'Create missing Forge metadata directory',
47
+ });
48
+ }
49
+ if (!fs.existsSync(logPath)) {
50
+ candidates.push({
51
+ id: 'audit-log',
52
+ path: '.forge/log.jsonl',
53
+ description: 'Create missing Forge audit log file',
54
+ });
55
+ }
56
+ return candidates;
57
+ }
58
+
59
+ function buildUpgradeDryRunReport(projectRoot = process.cwd()) {
60
+ const root = path.resolve(projectRoot);
61
+ const runtime = lintRuntimeGraphConfig({ projectRoot: root });
62
+ const patchIntent = buildPatchIntentSummary(root);
63
+ const lockReport = verifyForgeLock(root);
64
+ const lock = readForgeLock(root);
65
+ const selfHealCandidates = buildSelfHealCandidates(root);
66
+ const failedLockEntries = lockReport.results.filter(result => result.status === 'fail');
67
+ const untrustedOptIns = countUntrustedOptIns(lock);
68
+ const lockTrustOk = lockReport.ok && untrustedOptIns === 0;
69
+
70
+ return {
71
+ ok: runtime.ok && patchIntent.ok && lockTrustOk,
72
+ projectRoot: root,
73
+ runtime,
74
+ patchIntent,
75
+ lock,
76
+ lockReport,
77
+ lockTrustOk,
78
+ selfHealCandidates,
79
+ failedLockEntries,
80
+ };
81
+ }
82
+
83
+ function formatCheck(status, label, detail) {
84
+ return `[${status.toUpperCase()}] ${label}: ${detail}`;
85
+ }
86
+
87
+ function runtimeDetail(runtime) {
88
+ return runtime.ok
89
+ ? 'resolved runtime graph config'
90
+ : runtime.errors.map(error => error.message).join('; ');
91
+ }
92
+
93
+ function patchIntentDetail(patchIntent) {
94
+ return patchIntent.error
95
+ ? patchIntent.error
96
+ : `${patchIntent.records} record(s), ${patchIntent.orphans} orphan(s)`;
97
+ }
98
+
99
+ function readinessLines(report) {
100
+ const untrustedOptIns = countUntrustedOptIns(report.lock);
101
+ const lockStatus = report.lockTrustOk ? 'pass' : 'fail';
102
+ return [
103
+ formatCheck(checkStatus(report.runtime.ok), 'Runtime config', runtimeDetail(report.runtime)),
104
+ formatCheck(checkStatus(report.patchIntent.ok), 'Patch intent', patchIntentDetail(report.patchIntent)),
105
+ formatCheck(
106
+ lockStatus,
107
+ 'Lock trust',
108
+ `${report.lock.extensions.length} extension(s), ${untrustedOptIns} untrusted opt-in${untrustedOptIns === 1 ? '' : 's'}`
109
+ ),
110
+ ...report.lockReport.results.map(result => formatCheck(result.status, result.name, result.reason)),
111
+ ];
112
+ }
113
+
114
+ function appendPlannedSelfHeal(lines, candidates) {
115
+ lines.push('', 'Planned self-heal');
116
+ if (candidates.length === 0) {
117
+ lines.push('No self-heal actions needed.');
118
+ return;
119
+ }
120
+ for (const candidate of candidates) {
121
+ lines.push(`- ${candidate.path}: ${candidate.description}`);
122
+ }
123
+ }
124
+
125
+ function appendSelfHealResult(lines, selfHealResult) {
126
+ if (!selfHealResult) return;
127
+ lines.push('', 'Self-heal result');
128
+ if (selfHealResult.refused) {
129
+ lines.push('Self-heal refused unrecoverable lock integrity failure.');
130
+ return;
131
+ }
132
+ if (selfHealResult.applied.length === 0) {
133
+ lines.push('No self-heal actions needed.');
134
+ return;
135
+ }
136
+ lines.push(`Self-heal applied ${selfHealResult.applied.length} action(s).`);
137
+ for (const action of selfHealResult.applied) {
138
+ lines.push(`- ${action.path}`);
139
+ }
140
+ }
141
+
142
+ function renderUpgradeDryRunReport(report, selfHealResult = null) {
143
+ const lines = [
144
+ 'Forge upgrade dry-run',
145
+ `Target: ${report.projectRoot}`,
146
+ `Result: ${report.ok ? 'PASS' : 'FAIL'}`,
147
+ '',
148
+ 'Readiness',
149
+ ...readinessLines(report),
150
+ ];
151
+
152
+ appendPlannedSelfHeal(lines, report.selfHealCandidates);
153
+ appendSelfHealResult(lines, selfHealResult);
154
+
155
+ lines.push(
156
+ '',
157
+ 'Limitations',
158
+ 'Non-scope: rollback snapshots and full restore are not implemented in this PR.',
159
+ 'Remote/package source integrity is recorded as explicit trust policy only until a resolver can materialize bytes for SRI verification.'
160
+ );
161
+
162
+ return `${lines.join('\n')}\n`;
163
+ }
164
+
165
+ function applySelfHeal(projectRoot, report) {
166
+ if (report.failedLockEntries.length > 0) {
167
+ return {
168
+ refused: true,
169
+ applied: [],
170
+ reason: 'unrecoverable lock integrity failure',
171
+ };
172
+ }
173
+
174
+ const applied = [];
175
+ const forgeDir = path.join(projectRoot, '.forge');
176
+ const logPath = path.join(forgeDir, 'log.jsonl');
177
+
178
+ if (!fs.existsSync(forgeDir)) {
179
+ fs.mkdirSync(forgeDir, { recursive: true });
180
+ applied.push({ path: '.forge/' });
181
+ }
182
+
183
+ if (!fs.existsSync(logPath)) {
184
+ fs.writeFileSync(logPath, '', 'utf8');
185
+ applied.push({ path: '.forge/log.jsonl' });
186
+ }
187
+
188
+ return {
189
+ refused: false,
190
+ applied,
191
+ };
192
+ }
193
+
194
+ module.exports = {
195
+ applySelfHeal,
196
+ buildUpgradeDryRunReport,
197
+ buildSelfHealCandidates,
198
+ renderUpgradeDryRunReport,
199
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { repairWorkflowRuntimeAssets } = require('../commands/setup');
4
4
  const { checkRuntimeHealth } = require('../runtime-health');
5
+ const { resolveIssueBackend } = require('../issue-backend');
5
6
  const { normalizeStageId } = require('./stages');
6
7
  const {
7
8
  getAllowedTransitionsForWorkflowState,
@@ -12,6 +13,171 @@ const { loadState, WORKFLOW_STATE_FILENAME } = require('./state-manager');
12
13
 
13
14
  const STATELESS_ENTRY_STAGES = new Set(['plan', 'dev', 'validate', 'verify']);
14
15
 
16
+ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
17
+
18
+ // Canonical full-ladder path (critical superset). Used to derive a stage's
19
+ // immediate predecessor for the kernel completion gate.
20
+ const STAGE_PATH = Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', 'verify']);
21
+
22
+ function stagePredecessor(stageId) {
23
+ const index = STAGE_PATH.indexOf(stageId);
24
+ return index > 0 ? STAGE_PATH[index - 1] : null;
25
+ }
26
+
27
+ // Emit a warning to stderr. Writes even under FORGE_JSON=1 on purpose: stderr
28
+ // never pollutes machine-readable stdout, and a dropped kernel write must always
29
+ // leave a trace ("never silent") so a JSON-mode agent can see it.
30
+ function defaultStageWarn(message) {
31
+ process.stderr.write(`${message}\n`);
32
+ }
33
+
34
+ // Best-effort kernel write (action 'start' | 'complete') at the stage
35
+ // chokepoint. Idempotent per (issue_id, stage). A failure warns to stderr but
36
+ // never throws — ship's gate stays tolerant of whatever IS durably recorded.
37
+ function recordStageRunSafe(driver, issueId, stageId, action, warn = defaultStageWarn) {
38
+ if (!driver || !issueId || !stageId || typeof driver.recordStageRun !== 'function') {
39
+ return false;
40
+ }
41
+
42
+ try {
43
+ driver.recordStageRun({ issue_id: issueId, stage: stageId, action }, {});
44
+ return true;
45
+ } catch (error) {
46
+ warn(`[forge] could not record stage '${stageId}' (${action}) for ${issueId} in the kernel: ${error.message}`);
47
+ return false;
48
+ }
49
+ }
50
+
51
+ // Latest stage-run row for a specific (issue, stage), or null.
52
+ function findLatestStageRun(driver, issueId, stage) {
53
+ if (!driver || typeof driver.listStageRuns !== 'function') {
54
+ return null;
55
+ }
56
+
57
+ try {
58
+ const runs = driver.listStageRuns({ issue_id: issueId }, {}) || [];
59
+ let latest = null;
60
+ for (const run of runs) {
61
+ if (run?.stage === stage) {
62
+ latest = run;
63
+ }
64
+ }
65
+ return latest;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ // Decide whether entering stageId is allowed given ONLY the kernel's recorded
72
+ // stage history (used when no inline/file workflow state exists):
73
+ // - Stateless stages (plan/dev/validate/verify) are always re-entrant, so the
74
+ // dev<->validate rework loop never dead-ends.
75
+ // - A gated stage (ship/review) requires its immediate predecessor to be
76
+ // COMPLETED (status 'done'); merely ENTERING validate does not unlock ship.
77
+ // - When nothing is recorded for the predecessor, returns { kernelEmpty } so
78
+ // the caller falls through to the fail-closed hard block.
79
+ function evaluateKernelStageGate(driver, issueId, stageId) {
80
+ if (STATELESS_ENTRY_STAGES.has(stageId)) {
81
+ return { allowed: true };
82
+ }
83
+
84
+ const predecessor = stagePredecessor(stageId);
85
+ const predecessorRun = predecessor ? findLatestStageRun(driver, issueId, predecessor) : null;
86
+ if (predecessorRun?.status === 'done') {
87
+ return { allowed: true };
88
+ }
89
+ if (predecessorRun) {
90
+ return {
91
+ allowed: false,
92
+ reason: `Stage ${stageId} requires ${predecessor} to be completed first (currently ${predecessorRun.status}).`,
93
+ };
94
+ }
95
+ return { allowed: false, kernelEmpty: true };
96
+ }
97
+
98
+ // Verify a kernel issue exists before binding stage state to it (F4a: a UUID
99
+ // parsed from a branch name must not bind to a phantom issue).
100
+ async function kernelIssueExists(driver, issueId) {
101
+ if (!driver || typeof driver.findIssueIdsByPrefix !== 'function') {
102
+ return false;
103
+ }
104
+
105
+ try {
106
+ // findIssueIdsByPrefix returns rows ({ id, title }); tolerate plain-id shapes too.
107
+ const matches = await driver.findIssueIdsByPrefix(issueId, 6, {}, {});
108
+ return Array.isArray(matches) && matches.some(row => (row?.id ?? row) === issueId);
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ // Resolve the kernel issue that THIS worktree/branch is working on, so stage
115
+ // state can be read and written without an explicit issue argument on `forge
116
+ // ship`. Prefers the branch->issue linkage registry (authoritative); falls back
117
+ // to a UUID encoded in the branch name, but ONLY after verifying it exists.
118
+ async function resolveActiveIssueId(driver, branch) {
119
+ if (!driver || !branch) {
120
+ return null;
121
+ }
122
+
123
+ try {
124
+ if (typeof driver.listWorktrees === 'function') {
125
+ const rows = driver.listWorktrees() || [];
126
+ const match = rows.find(row => row && row.branch === branch && row.issue_id);
127
+ if (match) {
128
+ return match.issue_id;
129
+ }
130
+ }
131
+ } catch {
132
+ // Fall through to branch-name parsing.
133
+ }
134
+
135
+ const encoded = UUID_RE.exec(String(branch));
136
+ if (encoded && await kernelIssueExists(driver, encoded[0])) {
137
+ return encoded[0];
138
+ }
139
+ return null;
140
+ }
141
+
142
+ // Lazily build a kernel driver from the project root (the real CLI path).
143
+ // Best-effort: returns null when the kernel is unavailable so the caller
144
+ // degrades to legacy file/beads state instead of crashing a stage command.
145
+ async function buildKernelDriver(projectRoot) {
146
+ if (!projectRoot) {
147
+ return null;
148
+ }
149
+
150
+ try {
151
+ const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
152
+ const deps = await buildMigratedKernelIssueDeps({ projectRoot });
153
+ return deps.kernelDriver || null;
154
+ } catch {
155
+ return null;
156
+ }
157
+ }
158
+
159
+ function detectBranchName(projectRoot) {
160
+ try {
161
+ const { detectWorktree } = require('../detect-worktree');
162
+ const info = detectWorktree(projectRoot || process.cwd());
163
+ return info?.branch || null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ // Resolve { driver, issueId } for kernel stage-state: build the driver from the
170
+ // project root and resolve the active issue from the branch when not explicitly
171
+ // injected. Best-effort — returns nulls when the kernel is absent.
172
+ async function resolveKernelContext({ kernelDriver, activeIssueId, branch, projectRoot }) {
173
+ const driver = kernelDriver || await buildKernelDriver(projectRoot);
174
+ let issueId = activeIssueId || null;
175
+ if (driver && !issueId) {
176
+ issueId = await resolveActiveIssueId(driver, branch || detectBranchName(projectRoot));
177
+ }
178
+ return { driver, issueId };
179
+ }
180
+
15
181
  function getOverrideInput(flags = {}) {
16
182
  if (Object.hasOwn(flags, 'overrideStage')) {
17
183
  return flags.overrideStage;
@@ -103,55 +269,18 @@ function formatDiagnostics(diagnostics = []) {
103
269
  .join('; ');
104
270
  }
105
271
 
106
- async function enforceStageEntry({ commandName, args = [], flags = {}, projectRoot, workflowState, health, repairRuntime } = {}) {
107
- const stageId = normalizeStageId(commandName);
108
- if (!stageId) {
109
- return { allowed: true };
110
- }
111
-
112
- if (projectRoot) {
113
- repairWorkflowRuntimeAssets(projectRoot);
114
- }
115
-
116
- let runtimeHealth = health || checkRuntimeHealth(projectRoot);
117
- if (runtimeHealth.hardStop && typeof repairRuntime === 'function') {
118
- const repairedHealth = await repairRuntime({
119
- commandName,
120
- flags,
121
- projectRoot,
122
- workflowState,
123
- health: runtimeHealth,
124
- });
125
- if (repairedHealth) {
126
- runtimeHealth = repairedHealth;
127
- }
128
- }
129
- if (runtimeHealth.hardStop) {
130
- throw new Error(`Stage ${stageId} blocked by runtime prerequisites: ${formatDiagnostics(runtimeHealth.diagnostics)}`);
131
- }
132
-
133
- const stateInput = resolveWorkflowStateInput(workflowState, flags, args, projectRoot);
134
- const currentState = readWorkflowStateInput(stateInput);
135
- if (!currentState) {
136
- if (STATELESS_ENTRY_STAGES.has(stageId)) {
137
- return { allowed: true, stage: stageId, workflowState: null };
138
- }
139
-
140
- throw new Error(
141
- `Stage ${stageId} requires authoritative workflow state. ` +
142
- `Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing.`
143
- );
144
- }
145
-
272
+ // Enforce a stage entry against authoritative FILE/inline workflow state
273
+ // (unchanged legacy behavior: normal path-transition + override rules).
274
+ function enforceWithFileState(currentState, stageId, flags, args, finish) {
146
275
  const currentStage = currentState.currentStage;
147
276
  const classification = currentState.workflowDecisions?.classification;
148
277
  if (!currentStage || !classification || stageId === currentStage) {
149
- return { allowed: true, stage: stageId, workflowState: currentState };
278
+ return finish({ allowed: true, stage: stageId, workflowState: currentState });
150
279
  }
151
280
 
152
281
  const allowedTransitions = getAllowedTransitionsForWorkflowState(currentState);
153
282
  if (allowedTransitions.includes(stageId)) {
154
- return { allowed: true, stage: stageId, workflowState: currentState };
283
+ return finish({ allowed: true, stage: stageId, workflowState: currentState });
155
284
  }
156
285
 
157
286
  const override = parseOverride(flags, args);
@@ -168,12 +297,128 @@ async function enforceStageEntry({ commandName, args = [], flags = {}, projectRo
168
297
  );
169
298
  }
170
299
 
171
- return {
172
- allowed: true,
173
- stage: stageId,
174
- workflowState: currentState,
175
- override,
300
+ return finish({ allowed: true, stage: stageId, workflowState: currentState, override });
301
+ }
302
+
303
+ function isInlineStateProvided(workflowState, flags, args) {
304
+ return Boolean(
305
+ workflowState || flags.workflowState || flags['--workflow-state'] || getCliFlagValue('--workflow-state', args)
306
+ );
307
+ }
308
+
309
+ // Enforce a stage entry against kernel-recorded stage state (completion gate).
310
+ // Returns a decided enforcement result, or null to fall through to the
311
+ // stateless / hard-block rules.
312
+ function enforceWithKernelState(driver, issueId, stageId, finish) {
313
+ const gate = evaluateKernelStageGate(driver, issueId, stageId);
314
+ if (gate.allowed) {
315
+ return finish({ allowed: true, stage: stageId, workflowState: null });
316
+ }
317
+ if (!gate.kernelEmpty) {
318
+ throw new Error(gate.reason);
319
+ }
320
+ return null;
321
+ }
322
+
323
+ async function resolveStageRuntimeHealth({ health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime }) {
324
+ const runHealthCheck = checkHealth || checkRuntimeHealth;
325
+ let runtimeHealth = health || runHealthCheck(projectRoot, { issueBackend });
326
+ if (runtimeHealth.hardStop && typeof repairRuntime === 'function') {
327
+ const repaired = await repairRuntime({ commandName, flags, projectRoot, workflowState, health: runtimeHealth });
328
+ if (repaired) {
329
+ runtimeHealth = repaired;
330
+ }
331
+ }
332
+ return runtimeHealth;
333
+ }
334
+
335
+ async function enforceStageEntry({
336
+ commandName,
337
+ args = [],
338
+ flags = {},
339
+ projectRoot,
340
+ workflowState,
341
+ health,
342
+ repairRuntime,
343
+ checkHealth,
344
+ // B1 — kernel stage-state authority. Injectable for tests; the real CLI sets
345
+ // autoResolveKernel:true so the driver + active issue are resolved from the
346
+ // worktree. When neither a driver nor autoResolveKernel is provided, the
347
+ // kernel path is inert and behavior matches the legacy file/beads state.
348
+ kernelDriver,
349
+ activeIssueId,
350
+ branch,
351
+ autoResolveKernel = false,
352
+ warn = defaultStageWarn,
353
+ } = {}) {
354
+ const stageId = normalizeStageId(commandName);
355
+ if (!stageId) {
356
+ return { allowed: true };
357
+ }
358
+
359
+ if (projectRoot) {
360
+ repairWorkflowRuntimeAssets(projectRoot);
361
+ }
362
+
363
+ // Resolve the active issue backend (env > .forge/config.yaml > default 'kernel') so
364
+ // the runtime gate only treats bd as a hard prerequisite for the beads backend. The
365
+ // kernel default needs no bd, so stages must run without it.
366
+ const issueBackend = resolveIssueBackend({ deps: {}, env: process.env, projectRoot, warn: () => {} });
367
+ const runtimeHealth = await resolveStageRuntimeHealth({
368
+ health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime,
369
+ });
370
+ if (runtimeHealth.hardStop) {
371
+ throw new Error(`Stage ${stageId} blocked by runtime prerequisites: ${formatDiagnostics(runtimeHealth.diagnostics)}`);
372
+ }
373
+
374
+ const stateInput = resolveWorkflowStateInput(workflowState, flags, args, projectRoot);
375
+
376
+ // Kernel stage-state authority is active when a driver is injected (tests) or
377
+ // the caller opts in (real CLI via autoResolveKernel). Inline/flag state
378
+ // disables it: the caller is explicitly driving state, so no kernel side
379
+ // effects should occur.
380
+ const kernelEnabled = !isInlineStateProvided(workflowState, flags, args)
381
+ && (Boolean(kernelDriver) || autoResolveKernel === true);
382
+ const { driver, issueId } = kernelEnabled
383
+ ? await resolveKernelContext({ kernelDriver, activeIssueId, branch, projectRoot })
384
+ : { driver: null, issueId: null };
385
+ const kernelActive = Boolean(kernelEnabled && driver && issueId);
386
+
387
+ // On an allowed entry, record the stage as started (active) AND return a
388
+ // recordCompletion() the command runner calls after the handler SUCCEEDS — so
389
+ // a stage only counts as 'done' when its command actually passed (the ship
390
+ // gate below requires the predecessor to be done, not merely entered).
391
+ const finish = (result) => {
392
+ if (kernelActive) {
393
+ recordStageRunSafe(driver, issueId, stageId, 'start', warn);
394
+ result.recordCompletion = () => recordStageRunSafe(driver, issueId, stageId, 'complete', warn);
395
+ }
396
+ return result;
176
397
  };
398
+
399
+ const currentState = readWorkflowStateInput(stateInput);
400
+ if (currentState) {
401
+ return enforceWithFileState(currentState, stageId, flags, args, finish);
402
+ }
403
+
404
+ // No inline/file state: the kernel is authoritative. Gate on recorded stage
405
+ // completions (tolerant read) so `ship` is reachable from a pure-CLI
406
+ // plan->dev->validate progression with no .forge-state.json.
407
+ if (kernelActive) {
408
+ const decided = enforceWithKernelState(driver, issueId, stageId, finish);
409
+ if (decided) {
410
+ return decided;
411
+ }
412
+ }
413
+
414
+ if (STATELESS_ENTRY_STAGES.has(stageId)) {
415
+ return finish({ allowed: true, stage: stageId, workflowState: null });
416
+ }
417
+
418
+ throw new Error(
419
+ `Stage ${stageId} requires authoritative workflow state. ` +
420
+ `Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing.`
421
+ );
177
422
  }
178
423
 
179
424
  module.exports = {
@@ -182,4 +427,8 @@ module.exports = {
182
427
  parseOverride,
183
428
  resolveWorkflowStateInput,
184
429
  readWorkflowStateFile,
430
+ resolveActiveIssueId,
431
+ evaluateKernelStageGate,
432
+ recordStageRunSafe,
433
+ stagePredecessor,
185
434
  };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stage-transition auto-recording (5a5ba3a6).
5
+ *
6
+ * PR #348 (f61601ab) added the stage_runs record/read capability but nothing
7
+ * populated it automatically — `current_stage` stayed unknown. The Descriptive
8
+ * Context Convention (AGENTS.md) already has agents record each stage boundary as
9
+ * a kernel issue comment shaped:
10
+ *
11
+ * stage: <from> -> <to>
12
+ * summary: ...
13
+ *
14
+ * This module turns that existing, already-followed convention into a structured
15
+ * stage_run WITHOUT any new manual verb call: parse the `stage:` line and, best
16
+ * effort, complete the from-stage and start the to-stage. Recording is strictly
17
+ * non-blocking — a stage_run write failure must NEVER break the comment that
18
+ * triggered it.
19
+ *
20
+ * @module workflow/stage-transition
21
+ */
22
+
23
+ const { normalizeStageId } = require('./stages');
24
+
25
+ // `stage: <from> -> <to>` on its own line, case-insensitive, arrow spacing optional.
26
+ const STAGE_LINE = /^\s*stage:\s*([a-z]+)\s*->\s*([a-z]+)\s*$/im;
27
+
28
+ /**
29
+ * Parse a stage-transition line out of a comment body.
30
+ *
31
+ * @param {string} body - Comment body (may be multi-line).
32
+ * @returns {{from: string, to: string}|null} Normalized stages, or null when the
33
+ * body has no valid `stage: X -> Y` line (both tokens must be canonical stages).
34
+ */
35
+ function parseStageTransition(body) {
36
+ if (typeof body !== 'string' || body.length === 0) {
37
+ return null;
38
+ }
39
+ const match = body.match(STAGE_LINE);
40
+ if (!match) {
41
+ return null;
42
+ }
43
+ // STAGE_LINE is case-insensitive, so an uppercase token (e.g. `stage: DEV ->
44
+ // VALIDATE`) matches. normalizeStageId only knows lowercase canonical ids, so
45
+ // lowercase both captures first or an uppercase-but-valid stage is wrongly rejected.
46
+ const from = normalizeStageId(match[1].toLowerCase());
47
+ const to = normalizeStageId(match[2].toLowerCase());
48
+ if (!from || !to) {
49
+ return null;
50
+ }
51
+ return { from, to };
52
+ }
53
+
54
+ /**
55
+ * Best-effort record of a stage transition into stage_runs. Parses the comment
56
+ * body; on a valid transition it completes the from-stage and starts the to-stage.
57
+ *
58
+ * The write must be ATOMIC: completing `from` and starting `to` are one logical
59
+ * transition, so a failure partway through must leave neither persisted (otherwise
60
+ * `current_stage` reflects a half-transition — from marked done, to never started).
61
+ * The preferred path is a single transactional `driver.recordStageTransition` that
62
+ * wraps both writes in one transaction; a driver that only exposes `recordStageRun`
63
+ * falls back to two sequential writes (non-atomic, tolerated only because the whole
64
+ * operation is non-blocking).
65
+ *
66
+ * ANY failure (parse miss, missing driver, DB error) is swallowed and reported as
67
+ * `{ recorded: false }` — this function never throws.
68
+ *
69
+ * @param {object} params
70
+ * @param {object} [params.driver] - Kernel driver exposing recordStageTransition
71
+ * (preferred) and/or recordStageRun(input, config).
72
+ * @param {string} params.issueId - Full kernel issue id the comment targets.
73
+ * @param {string} params.body - The comment body just written.
74
+ * @param {object} [params.config] - Driver config (e.g. { databasePath }).
75
+ * @returns {{recorded: boolean, from?: string, to?: string}}
76
+ */
77
+ function recordStageTransition({ driver, issueId, body, config = {} } = {}) {
78
+ try {
79
+ const transition = parseStageTransition(body);
80
+ if (!transition || !issueId || !driver) {
81
+ return { recorded: false };
82
+ }
83
+ // Preferred: one atomic driver op — complete(from) + start(to) inside a single
84
+ // transaction, so a mid-transition failure rolls back and never persists a
85
+ // wrong `current_stage`.
86
+ if (typeof driver.recordStageTransition === 'function') {
87
+ driver.recordStageTransition(
88
+ { issue_id: issueId, from: transition.from, to: transition.to },
89
+ config,
90
+ );
91
+ return { from: transition.from, to: transition.to, recorded: true };
92
+ }
93
+ // Fallback for a minimal driver without the atomic op: two sequential writes.
94
+ if (typeof driver.recordStageRun === 'function') {
95
+ driver.recordStageRun(
96
+ { issue_id: issueId, stage: transition.from, action: 'complete' },
97
+ config,
98
+ );
99
+ driver.recordStageRun(
100
+ { issue_id: issueId, stage: transition.to, action: 'start' },
101
+ config,
102
+ );
103
+ return { from: transition.from, to: transition.to, recorded: true };
104
+ }
105
+ return { recorded: false };
106
+ } catch {
107
+ // Non-blocking by contract: never let a stage_run write break the comment.
108
+ return { recorded: false };
109
+ }
110
+ }
111
+
112
+ module.exports = {
113
+ parseStageTransition,
114
+ recordStageTransition,
115
+ };