forge-workflow 0.0.10 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (468) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +82 -5
  5. package/.forge/hooks/forge-native-hook.js +431 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +151 -61
  8. package/CHANGELOG.md +709 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +175 -0
  11. package/README.md +275 -365
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +532 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +121 -0
  29. package/docs/guides/SUPPORT.md +190 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +214 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +155 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/activation/ensure-forge-home.js +135 -0
  67. package/lib/adapter-cli.js +307 -0
  68. package/lib/adapters/beads-issue-adapter.js +127 -0
  69. package/lib/adapters/beads-kernel-compat.js +1109 -0
  70. package/lib/adapters/greptile-review-adapter.js +141 -0
  71. package/lib/adapters/kernel-issue-adapter.js +101 -0
  72. package/lib/adapters/pr-state-adapter.js +484 -0
  73. package/lib/adoption-profiles.js +139 -0
  74. package/lib/agents/README.md +2 -6
  75. package/lib/agents/claude.plugin.json +3 -8
  76. package/lib/agents/codex.plugin.json +9 -1
  77. package/lib/agents/cursor.plugin.json +2 -6
  78. package/lib/agents/hermes.plugin.json +22 -0
  79. package/lib/agents-config.js +39 -1236
  80. package/lib/audit-evidence.js +282 -0
  81. package/lib/beads-detect.js +60 -0
  82. package/lib/beads-nudge.js +91 -0
  83. package/lib/beads-setup.js +121 -0
  84. package/lib/beads-sync-scaffold.js +25 -101
  85. package/lib/codex-skills.js +51 -1
  86. package/lib/commands/_aliases.js +248 -0
  87. package/lib/commands/_issue.js +780 -77
  88. package/lib/commands/_manifest.js +93 -0
  89. package/lib/commands/_registry.js +99 -34
  90. package/lib/commands/_resolve-command-opts.js +230 -0
  91. package/lib/commands/_serve-security.js +270 -0
  92. package/lib/commands/adapter.js +12 -0
  93. package/lib/commands/add.js +118 -0
  94. package/lib/commands/audit.js +70 -0
  95. package/lib/commands/blocked.js +5 -0
  96. package/lib/commands/board.js +64 -0
  97. package/lib/commands/claim.js +21 -2
  98. package/lib/commands/claims.js +7 -0
  99. package/lib/commands/clean.js +485 -75
  100. package/lib/commands/close.js +2 -2
  101. package/lib/commands/comment.js +5 -0
  102. package/lib/commands/control.js +148 -0
  103. package/lib/commands/create.js +2 -2
  104. package/lib/commands/dev.js +185 -7
  105. package/lib/commands/doc-gate.js +336 -0
  106. package/lib/commands/doctor.js +156 -0
  107. package/lib/commands/explain.js +15 -0
  108. package/lib/commands/export.js +237 -0
  109. package/lib/commands/gate.js +209 -0
  110. package/lib/commands/hooks.js +377 -0
  111. package/lib/commands/inbox.js +118 -0
  112. package/lib/commands/init.js +604 -0
  113. package/lib/commands/insights.js +79 -0
  114. package/lib/commands/issue.js +12 -1
  115. package/lib/commands/issues.js +17 -0
  116. package/lib/commands/lint.js +5 -0
  117. package/lib/commands/list.js +2 -2
  118. package/lib/commands/memory.js +81 -0
  119. package/lib/commands/merge.js +312 -0
  120. package/lib/commands/migrate.js +362 -0
  121. package/lib/commands/new.js +12 -0
  122. package/lib/commands/options.js +241 -0
  123. package/lib/commands/orient.js +13 -0
  124. package/lib/commands/orphans.js +5 -0
  125. package/lib/commands/patch.js +67 -0
  126. package/lib/commands/plan.js +481 -29
  127. package/lib/commands/pr.js +88 -0
  128. package/lib/commands/preflight.js +211 -0
  129. package/lib/commands/prime.js +13 -0
  130. package/lib/commands/push.js +135 -2
  131. package/lib/commands/ready.js +2 -2
  132. package/lib/commands/recall.js +171 -0
  133. package/lib/commands/recap.js +75 -0
  134. package/lib/commands/recommend.js +0 -1
  135. package/lib/commands/release.js +104 -0
  136. package/lib/commands/remember.js +140 -0
  137. package/lib/commands/role.js +99 -0
  138. package/lib/commands/serve.js +581 -0
  139. package/lib/commands/setup.js +900 -971
  140. package/lib/commands/shepherd.js +501 -0
  141. package/lib/commands/ship.js +59 -1
  142. package/lib/commands/show.js +2 -2
  143. package/lib/commands/stage.js +192 -0
  144. package/lib/commands/stale.js +5 -0
  145. package/lib/commands/status.js +158 -21
  146. package/lib/commands/sync.js +34 -46
  147. package/lib/commands/team.js +4 -1
  148. package/lib/commands/test.js +43 -27
  149. package/lib/commands/update.js +2 -2
  150. package/lib/commands/upgrade.js +47 -0
  151. package/lib/commands/validate.js +43 -18
  152. package/lib/commands/worktree.js +362 -99
  153. package/lib/config-writer.js +202 -0
  154. package/lib/control-plane.js +236 -0
  155. package/lib/core/runtime-graph.js +977 -0
  156. package/lib/dep-guard/keyword-ripple.js +2 -2
  157. package/lib/deprecated-sync-cleanup.js +362 -0
  158. package/lib/detect-agent.js +2 -28
  159. package/lib/detect-worktree.js +35 -9
  160. package/lib/doc-gate/declaration.js +177 -0
  161. package/lib/doc-gate/detect.js +289 -0
  162. package/lib/doc-gate/gate.js +375 -0
  163. package/lib/doc-gate/okf-config.js +128 -0
  164. package/lib/doc-gate/okf.js +429 -0
  165. package/lib/docs-command.js +1161 -6
  166. package/lib/forge-issues.js +382 -11
  167. package/lib/forge-lock.js +262 -0
  168. package/lib/gate-events.js +192 -0
  169. package/lib/global-flags.js +104 -0
  170. package/lib/greptile-match.js +7 -63
  171. package/lib/grounding/context-events.js +230 -0
  172. package/lib/grounding/read-first.js +112 -0
  173. package/lib/harness-capability-matrix.js +380 -0
  174. package/lib/hook-global-installer.js +347 -0
  175. package/lib/hook-renderer.js +541 -0
  176. package/lib/inbox.js +391 -0
  177. package/lib/insights.js +397 -0
  178. package/lib/issue-adapter.js +156 -0
  179. package/lib/issue-backend.js +145 -0
  180. package/lib/issue-render.js +220 -0
  181. package/lib/kernel/backing-issue.js +311 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/owned-kernel.js +43 -0
  192. package/lib/kernel/planning-buckets-schema.js +109 -0
  193. package/lib/kernel/projection-jsonl-writer.js +450 -0
  194. package/lib/kernel/readiness-model.js +329 -0
  195. package/lib/kernel/schema.js +356 -0
  196. package/lib/kernel/sqlite-driver.js +2540 -0
  197. package/lib/kernel/taxonomy-validator.js +394 -0
  198. package/lib/lefthook-check.js +3 -2
  199. package/lib/lefthook-wiring.js +413 -0
  200. package/lib/mcp-config-renderer.js +288 -0
  201. package/lib/memory/graphiti-mcp.js +106 -0
  202. package/lib/memory/router.js +387 -0
  203. package/lib/memory/typed-api.js +102 -0
  204. package/lib/memory-digest.js +195 -0
  205. package/lib/merge-rules.js +395 -0
  206. package/lib/migrate-dry-run.js +466 -0
  207. package/lib/orientation.js +863 -0
  208. package/lib/package-manager-remediation.js +103 -0
  209. package/lib/package-root.js +381 -0
  210. package/lib/patch-intent.js +890 -0
  211. package/lib/plugin-catalog.js +3 -4
  212. package/lib/plugin-manager.js +0 -5
  213. package/lib/pr-bundle.js +186 -0
  214. package/lib/pr-monitor/auto-actions.js +175 -0
  215. package/lib/pr-monitor/differ.js +195 -0
  216. package/lib/pr-monitor/digest.js +206 -0
  217. package/lib/pr-monitor/events.js +0 -0
  218. package/lib/pr-monitor/gather.js +124 -0
  219. package/lib/pr-monitor/journal.js +299 -0
  220. package/lib/pr-monitor/monitor.js +146 -0
  221. package/lib/pr-monitor/render-sticky.js +192 -0
  222. package/lib/pr-monitor/upsert-sticky.js +169 -0
  223. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  224. package/lib/pr-monitor/watch.js +247 -0
  225. package/lib/pr-pull.js +1314 -0
  226. package/lib/pr-shepherd.js +494 -0
  227. package/lib/pr-state-validator.js +59 -0
  228. package/lib/preflight/gates.js +237 -0
  229. package/lib/preflight/runner.js +116 -0
  230. package/lib/project-discovery.js +0 -53
  231. package/lib/project-memory.js +99 -497
  232. package/lib/protected-path-manifest.js +281 -0
  233. package/lib/protected-state-surfaces.js +387 -0
  234. package/lib/release-readiness.js +2105 -0
  235. package/lib/reset.js +59 -45
  236. package/lib/review-adapter.js +68 -0
  237. package/lib/rules-sync.js +260 -0
  238. package/lib/runtime-health.js +241 -20
  239. package/lib/safety-config-renderer.js +268 -0
  240. package/lib/setup-action-log.js +1 -7
  241. package/lib/setup.js +27 -65
  242. package/lib/shell-utils.js +76 -6
  243. package/lib/skills-sync.js +330 -0
  244. package/lib/smart-status/scoring.js +17 -3
  245. package/lib/status/beads-snapshot.js +45 -2
  246. package/lib/status/presenter.js +169 -18
  247. package/lib/status/snapshot.js +186 -0
  248. package/lib/sync-backend.js +202 -0
  249. package/lib/untrusted-content.js +52 -0
  250. package/lib/upgrade-safety.js +251 -0
  251. package/lib/workflow/enforce-stage.js +351 -45
  252. package/lib/workflow/stage-transition.js +115 -0
  253. package/lib/workflow/stages.js +30 -6
  254. package/lib/workflow/state-manager.js +11 -22
  255. package/lib/workflow/state.js +23 -1
  256. package/lib/workflow-profiles.js +17 -5
  257. package/package.json +37 -35
  258. package/rules/documentation.md +19 -0
  259. package/rules/kernel-tracking.md +26 -0
  260. package/rules/security.md +22 -0
  261. package/rules/tdd.md +20 -0
  262. package/rules/workflow.md +27 -0
  263. package/scripts/auto-backing-issue.js +47 -0
  264. package/scripts/beads-context.sh +81 -57
  265. package/scripts/beads-upgrade-smoke.sh +24 -3
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard.sh +22 -3
  272. package/scripts/file-index.sh +3 -0
  273. package/scripts/forge-team/lib/claim.sh +34 -18
  274. package/scripts/forge-team/lib/dashboard.sh +61 -86
  275. package/scripts/forge-team/lib/epic.sh +99 -263
  276. package/scripts/forge-team/lib/hooks.sh +26 -28
  277. package/scripts/forge-team/lib/identity.sh +4 -4
  278. package/scripts/forge-team/lib/sync-github.sh +49 -84
  279. package/scripts/forge-team/lib/verify.sh +93 -83
  280. package/scripts/forge-team/lib/workload.sh +41 -65
  281. package/scripts/forge-team/tests/claim.test.sh +25 -19
  282. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  283. package/scripts/forge-team/tests/epic.test.sh +52 -71
  284. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  285. package/scripts/forge-team/tests/identity.test.sh +3 -3
  286. package/scripts/forge-team/tests/integration.test.sh +44 -66
  287. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  288. package/scripts/forge-team/tests/verify.test.sh +37 -46
  289. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  290. package/scripts/forge-team/tests/workload.test.sh +32 -66
  291. package/scripts/gen-command-manifest.js +153 -0
  292. package/scripts/gen-embedded-assets.mjs +129 -0
  293. package/scripts/install.ps1 +139 -0
  294. package/scripts/install.sh +268 -0
  295. package/scripts/lib/release-asset.mjs +84 -0
  296. package/scripts/parity-check.mjs +145 -0
  297. package/scripts/parity-check.test.mjs +58 -0
  298. package/scripts/pin-agentic-workflow-images.js +112 -0
  299. package/scripts/pr-auto-actions.js +93 -0
  300. package/scripts/pr-coordinator.sh +3 -0
  301. package/scripts/pr-verdict-label.js +50 -0
  302. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  303. package/scripts/preflight.sh +21 -94
  304. package/scripts/protected-state-check.js +104 -0
  305. package/scripts/smart-status.sh +60 -57
  306. package/scripts/spikes/config-race-bench.js +111 -0
  307. package/scripts/spikes/harness-capability-matrix.js +13 -0
  308. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  309. package/scripts/spikes/protected-path-manifest.js +20 -0
  310. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  311. package/scripts/sync-agent-skills.js +62 -0
  312. package/scripts/sync-utils.sh +3 -0
  313. package/scripts/test-ci-shard.js +13 -6
  314. package/scripts/test.js +95 -12
  315. package/skills/claim-safety/SKILL.md +102 -0
  316. package/skills/claim-safety/evals/evals.json +46 -0
  317. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  318. package/skills/dev/evals/evals.json +50 -0
  319. package/skills/hermes-forge/SKILL.md +185 -0
  320. package/skills/hermes-forge/evals/evals.json +46 -0
  321. package/skills/issue-basics/SKILL.md +111 -0
  322. package/skills/issue-basics/evals/evals.json +46 -0
  323. package/skills/kernel/SKILL.md +166 -0
  324. package/skills/kernel/evals/evals.json +50 -0
  325. package/skills/memory/SKILL.md +102 -0
  326. package/skills/parallel-deep-research/SKILL.md +14 -11
  327. package/skills/parallel-deep-research/evals/evals.json +11 -27
  328. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  329. package/skills/plan/evals/evals.json +42 -0
  330. package/skills/research/SKILL.md +195 -0
  331. package/skills/research/evals/evals.json +42 -0
  332. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  333. package/skills/review/evals/evals.json +42 -0
  334. package/skills/rollback/SKILL.md +110 -0
  335. package/skills/rollback/evals/evals.json +46 -0
  336. package/skills/rollback/references/methods.md +204 -0
  337. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  338. package/skills/shepherd/SKILL.md +66 -0
  339. package/skills/shepherd/evals/evals.json +42 -0
  340. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  341. package/skills/ship/evals/evals.json +42 -0
  342. package/skills/smith/SKILL.md +142 -0
  343. package/skills/smith/evals/evals.json +46 -0
  344. package/skills/smith/references/autonomy-and-gates.md +94 -0
  345. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  346. package/skills/sonarcloud/evals/evals.json +46 -0
  347. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  348. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  349. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  350. package/skills/status/evals/evals.json +50 -0
  351. package/skills/triage-ready/SKILL.md +121 -0
  352. package/skills/triage-ready/evals/evals.json +42 -0
  353. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  354. package/skills/validate/evals/evals.json +42 -0
  355. package/skills/verify/SKILL.md +299 -0
  356. package/skills/verify/evals/evals.json +50 -0
  357. package/.claude/commands/dev.md +0 -345
  358. package/.claude/commands/plan.md +0 -566
  359. package/.claude/commands/premerge.md +0 -186
  360. package/.claude/commands/research.md +0 -42
  361. package/.claude/commands/review.md +0 -451
  362. package/.claude/commands/rollback.md +0 -721
  363. package/.claude/commands/ship.md +0 -213
  364. package/.claude/commands/sonarcloud.md +0 -152
  365. package/.claude/commands/status.md +0 -90
  366. package/.claude/commands/validate.md +0 -288
  367. package/.claude/commands/verify.md +0 -269
  368. package/.claude/rules/workflow.md +0 -121
  369. package/.cline/workflows/dev.md +0 -342
  370. package/.cline/workflows/plan.md +0 -563
  371. package/.cline/workflows/premerge.md +0 -183
  372. package/.cline/workflows/research.md +0 -39
  373. package/.cline/workflows/review.md +0 -448
  374. package/.cline/workflows/rollback.md +0 -718
  375. package/.cline/workflows/ship.md +0 -210
  376. package/.cline/workflows/sonarcloud.md +0 -146
  377. package/.cline/workflows/status.md +0 -87
  378. package/.cline/workflows/validate.md +0 -285
  379. package/.cline/workflows/verify.md +0 -266
  380. package/.codex/config.toml +0 -11
  381. package/.codex/skills/dev/SKILL.md +0 -345
  382. package/.codex/skills/plan/SKILL.md +0 -566
  383. package/.codex/skills/premerge/SKILL.md +0 -186
  384. package/.codex/skills/research/SKILL.md +0 -42
  385. package/.codex/skills/review/SKILL.md +0 -451
  386. package/.codex/skills/rollback/SKILL.md +0 -721
  387. package/.codex/skills/ship/SKILL.md +0 -213
  388. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  389. package/.codex/skills/status/SKILL.md +0 -90
  390. package/.codex/skills/validate/SKILL.md +0 -288
  391. package/.codex/skills/verify/SKILL.md +0 -269
  392. package/.cursor/commands/dev.md +0 -342
  393. package/.cursor/commands/plan.md +0 -563
  394. package/.cursor/commands/premerge.md +0 -183
  395. package/.cursor/commands/research.md +0 -39
  396. package/.cursor/commands/review.md +0 -448
  397. package/.cursor/commands/ship.md +0 -210
  398. package/.cursor/commands/sonarcloud.md +0 -146
  399. package/.cursor/commands/status.md +0 -87
  400. package/.cursor/commands/validate.md +0 -285
  401. package/.cursor/commands/verify.md +0 -266
  402. package/.cursorrules +0 -149
  403. package/.github/prompts/premerge.prompt.md +0 -188
  404. package/.github/prompts/research.prompt.md +0 -44
  405. package/.github/prompts/rollback.prompt.md +0 -723
  406. package/.github/prompts/verify.prompt.md +0 -271
  407. package/.github/workflows/beads-to-github.yml +0 -89
  408. package/.github/workflows/github-to-beads.yml +0 -100
  409. package/.kilocode/workflows/dev.md +0 -346
  410. package/.kilocode/workflows/plan.md +0 -567
  411. package/.kilocode/workflows/premerge.md +0 -187
  412. package/.kilocode/workflows/research.md +0 -43
  413. package/.kilocode/workflows/review.md +0 -452
  414. package/.kilocode/workflows/rollback.md +0 -722
  415. package/.kilocode/workflows/ship.md +0 -214
  416. package/.kilocode/workflows/sonarcloud.md +0 -150
  417. package/.kilocode/workflows/status.md +0 -91
  418. package/.kilocode/workflows/validate.md +0 -289
  419. package/.kilocode/workflows/verify.md +0 -270
  420. package/.opencode/commands/dev.md +0 -345
  421. package/.opencode/commands/plan.md +0 -566
  422. package/.opencode/commands/premerge.md +0 -186
  423. package/.opencode/commands/research.md +0 -42
  424. package/.opencode/commands/review.md +0 -451
  425. package/.opencode/commands/rollback.md +0 -721
  426. package/.opencode/commands/ship.md +0 -213
  427. package/.opencode/commands/sonarcloud.md +0 -149
  428. package/.opencode/commands/status.md +0 -90
  429. package/.opencode/commands/validate.md +0 -288
  430. package/.opencode/commands/verify.md +0 -269
  431. package/.roo/commands/dev.md +0 -346
  432. package/.roo/commands/plan.md +0 -567
  433. package/.roo/commands/premerge.md +0 -187
  434. package/.roo/commands/research.md +0 -43
  435. package/.roo/commands/review.md +0 -452
  436. package/.roo/commands/rollback.md +0 -722
  437. package/.roo/commands/ship.md +0 -214
  438. package/.roo/commands/sonarcloud.md +0 -150
  439. package/.roo/commands/status.md +0 -91
  440. package/.roo/commands/validate.md +0 -289
  441. package/.roo/commands/verify.md +0 -270
  442. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  443. package/docs/GREPTILE_SETUP.md +0 -400
  444. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  445. package/docs/SETUP.md +0 -663
  446. package/docs/VALIDATION.md +0 -363
  447. package/lib/agents/cline.plugin.json +0 -29
  448. package/lib/agents/copilot.plugin.json +0 -24
  449. package/lib/agents/kilocode.plugin.json +0 -22
  450. package/lib/agents/opencode.plugin.json +0 -23
  451. package/lib/agents/roo.plugin.json +0 -30
  452. package/lib/beads-bootstrap.js +0 -225
  453. package/lib/beads-health-check.js +0 -188
  454. package/lib/commands/commands-reset.js +0 -147
  455. package/opencode.json +0 -67
  456. package/scripts/beads-context.test.js +0 -584
  457. package/scripts/github-beads-sync/comment.mjs +0 -64
  458. package/scripts/github-beads-sync/config.mjs +0 -148
  459. package/scripts/github-beads-sync/github-api.mjs +0 -131
  460. package/scripts/github-beads-sync/index.mjs +0 -356
  461. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  462. package/scripts/github-beads-sync/mapping.mjs +0 -132
  463. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  464. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  465. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  466. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  467. package/scripts/github-beads-sync.config.json +0 -26
  468. package/scripts/sync-commands.js +0 -600
@@ -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,197 @@ 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
+ // (Re-)entering an earlier stage invalidates the work that followed it: any
72
+ // DOWNSTREAM stage previously recorded 'done' is reopened to 'active' so a gated
73
+ // stage (ship/review) re-requires a fresh completion (R2 — the dev<->validate
74
+ // rework loop must not let a stale validate=done pass ship). Reuses the idempotent
75
+ // 'start' write, which keeps the row's id + original started_at but clears its
76
+ // done status. Stages the entered stage does not precede (and non-'done' rows) are
77
+ // left untouched, so forward progression records nothing extra.
78
+ function invalidateDownstreamStages(driver, issueId, stageId, warn) {
79
+ const index = STAGE_PATH.indexOf(stageId);
80
+ if (index < 0) {
81
+ return;
82
+ }
83
+ for (let next = index + 1; next < STAGE_PATH.length; next += 1) {
84
+ const downstream = STAGE_PATH[next];
85
+ const run = findLatestStageRun(driver, issueId, downstream);
86
+ if (run?.status === 'done') {
87
+ recordStageRunSafe(driver, issueId, downstream, 'start', warn);
88
+ }
89
+ }
90
+ }
91
+
92
+ // Decide whether entering stageId is allowed given ONLY the kernel's recorded
93
+ // stage history (used when no inline/file workflow state exists):
94
+ // - Stateless stages (plan/dev/validate/verify) are always re-entrant, so the
95
+ // dev<->validate rework loop never dead-ends.
96
+ // - A gated stage (ship/review) requires its immediate predecessor to be
97
+ // COMPLETED (status 'done'); merely ENTERING validate does not unlock ship.
98
+ // - When nothing is recorded for the predecessor, returns { kernelEmpty } so
99
+ // the caller falls through to the fail-closed hard block.
100
+ function evaluateKernelStageGate(driver, issueId, stageId) {
101
+ if (STATELESS_ENTRY_STAGES.has(stageId)) {
102
+ return { allowed: true };
103
+ }
104
+
105
+ const predecessor = stagePredecessor(stageId);
106
+ const predecessorRun = predecessor ? findLatestStageRun(driver, issueId, predecessor) : null;
107
+ if (predecessorRun?.status === 'done') {
108
+ return { allowed: true };
109
+ }
110
+ if (predecessorRun) {
111
+ return {
112
+ allowed: false,
113
+ reason: `Stage ${stageId} requires ${predecessor} to be completed first (currently ${predecessorRun.status}).`,
114
+ };
115
+ }
116
+ return { allowed: false, kernelEmpty: true };
117
+ }
118
+
119
+ // Verify a kernel issue exists before binding stage state to it (F4a: a UUID
120
+ // parsed from a branch name must not bind to a phantom issue).
121
+ async function kernelIssueExists(driver, issueId) {
122
+ if (!driver || typeof driver.findIssueIdsByPrefix !== 'function') {
123
+ return false;
124
+ }
125
+
126
+ try {
127
+ // findIssueIdsByPrefix returns rows ({ id, title }); tolerate plain-id shapes too.
128
+ const matches = await driver.findIssueIdsByPrefix(issueId, 6, {}, {});
129
+ return Array.isArray(matches) && matches.some(row => (row?.id ?? row) === issueId);
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ // Resolve the kernel issue that THIS worktree/branch is working on, so stage
136
+ // state can be read and written without an explicit issue argument on `forge
137
+ // ship`. Prefers the branch->issue linkage registry (authoritative); falls back
138
+ // to a UUID encoded in the branch name, but ONLY after verifying it exists.
139
+ async function resolveActiveIssueId(driver, branch) {
140
+ if (!driver || !branch) {
141
+ return null;
142
+ }
143
+
144
+ try {
145
+ if (typeof driver.listWorktrees === 'function') {
146
+ const rows = driver.listWorktrees() || [];
147
+ // Match only ACTIVE (live) linkage rows, newest first (listWorktrees orders
148
+ // registered_at DESC). A stale/superseded registration for a reused branch
149
+ // name must not rebind stage state to the OLD issue (be18881c). Tolerate a
150
+ // null state for rows written before the state column was populated.
151
+ const match = rows.find(row => row && row.branch === branch && row.issue_id
152
+ && (row.state === 'active' || row.state == null));
153
+ if (match) {
154
+ return match.issue_id;
155
+ }
156
+ }
157
+ } catch {
158
+ // Fall through to branch-name parsing.
159
+ }
160
+
161
+ const encoded = UUID_RE.exec(String(branch));
162
+ if (encoded && await kernelIssueExists(driver, encoded[0])) {
163
+ return encoded[0];
164
+ }
165
+ return null;
166
+ }
167
+
168
+ // Lazily build a kernel driver from the project root (the real CLI path).
169
+ // Best-effort: returns null when the kernel is unavailable so the caller
170
+ // degrades to legacy file/beads state instead of crashing a stage command.
171
+ async function buildKernelDriver(projectRoot) {
172
+ if (!projectRoot) {
173
+ return null;
174
+ }
175
+
176
+ try {
177
+ const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
178
+ const deps = await buildMigratedKernelIssueDeps({ projectRoot });
179
+ return deps.kernelDriver || null;
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ function detectBranchName(projectRoot) {
186
+ try {
187
+ const { detectWorktree } = require('../detect-worktree');
188
+ const info = detectWorktree(projectRoot || process.cwd());
189
+ return info?.branch || null;
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ // Resolve { driver, issueId } for kernel stage-state: build the driver from the
196
+ // project root and resolve the active issue from the branch when not explicitly
197
+ // injected. Best-effort — returns nulls when the kernel is absent.
198
+ async function resolveKernelContext({ kernelDriver, activeIssueId, branch, projectRoot }) {
199
+ const driver = kernelDriver || await buildKernelDriver(projectRoot);
200
+ let issueId = activeIssueId || null;
201
+ if (driver && !issueId) {
202
+ issueId = await resolveActiveIssueId(driver, branch || detectBranchName(projectRoot));
203
+ }
204
+ return { driver, issueId };
205
+ }
206
+
15
207
  function getOverrideInput(flags = {}) {
16
208
  if (Object.hasOwn(flags, 'overrideStage')) {
17
209
  return flags.overrideStage;
@@ -103,7 +295,88 @@ function formatDiagnostics(diagnostics = []) {
103
295
  .join('; ');
104
296
  }
105
297
 
106
- async function enforceStageEntry({ commandName, args = [], flags = {}, projectRoot, workflowState, health, repairRuntime } = {}) {
298
+ // Enforce a stage entry against authoritative FILE/inline workflow state
299
+ // (unchanged legacy behavior: normal path-transition + override rules).
300
+ function enforceWithFileState(currentState, stageId, flags, args, finish) {
301
+ const currentStage = currentState.currentStage;
302
+ const classification = currentState.workflowDecisions?.classification;
303
+ if (!currentStage || !classification || stageId === currentStage) {
304
+ return finish({ allowed: true, stage: stageId, workflowState: currentState });
305
+ }
306
+
307
+ const allowedTransitions = getAllowedTransitionsForWorkflowState(currentState);
308
+ if (allowedTransitions.includes(stageId)) {
309
+ return finish({ allowed: true, stage: stageId, workflowState: currentState });
310
+ }
311
+
312
+ const override = parseOverride(flags, args);
313
+ if (!override) {
314
+ throw new Error(
315
+ `Stage ${stageId} is blocked from ${currentStage}. ` +
316
+ `Provide an explicit override payload via overrideStage or --override-stage.`
317
+ );
318
+ }
319
+
320
+ if (override.fromStage !== currentStage || override.toStage !== stageId) {
321
+ throw new Error(
322
+ `Stage override does not match workflow state. Expected ${currentStage} -> ${stageId}.`
323
+ );
324
+ }
325
+
326
+ return finish({ allowed: true, stage: stageId, workflowState: currentState, override });
327
+ }
328
+
329
+ function isInlineStateProvided(workflowState, flags, args) {
330
+ return Boolean(
331
+ workflowState || flags.workflowState || flags['--workflow-state'] || getCliFlagValue('--workflow-state', args)
332
+ );
333
+ }
334
+
335
+ // Enforce a stage entry against kernel-recorded stage state (completion gate).
336
+ // Returns a decided enforcement result, or null to fall through to the
337
+ // stateless / hard-block rules.
338
+ function enforceWithKernelState(driver, issueId, stageId, finish) {
339
+ const gate = evaluateKernelStageGate(driver, issueId, stageId);
340
+ if (gate.allowed) {
341
+ return finish({ allowed: true, stage: stageId, workflowState: null });
342
+ }
343
+ if (!gate.kernelEmpty) {
344
+ throw new Error(gate.reason);
345
+ }
346
+ return null;
347
+ }
348
+
349
+ async function resolveStageRuntimeHealth({ health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime }) {
350
+ const runHealthCheck = checkHealth || checkRuntimeHealth;
351
+ let runtimeHealth = health || runHealthCheck(projectRoot, { issueBackend });
352
+ if (runtimeHealth.hardStop && typeof repairRuntime === 'function') {
353
+ const repaired = await repairRuntime({ commandName, flags, projectRoot, workflowState, health: runtimeHealth });
354
+ if (repaired) {
355
+ runtimeHealth = repaired;
356
+ }
357
+ }
358
+ return runtimeHealth;
359
+ }
360
+
361
+ async function enforceStageEntry({
362
+ commandName,
363
+ args = [],
364
+ flags = {},
365
+ projectRoot,
366
+ workflowState,
367
+ health,
368
+ repairRuntime,
369
+ checkHealth,
370
+ // B1 — kernel stage-state authority. Injectable for tests; the real CLI sets
371
+ // autoResolveKernel:true so the driver + active issue are resolved from the
372
+ // worktree. When neither a driver nor autoResolveKernel is provided, the
373
+ // kernel path is inert and behavior matches the legacy file/beads state.
374
+ kernelDriver,
375
+ activeIssueId,
376
+ branch,
377
+ autoResolveKernel = false,
378
+ warn = defaultStageWarn,
379
+ } = {}) {
107
380
  const stageId = normalizeStageId(commandName);
108
381
  if (!stageId) {
109
382
  return { allowed: true };
@@ -113,67 +386,96 @@ async function enforceStageEntry({ commandName, args = [], flags = {}, projectRo
113
386
  repairWorkflowRuntimeAssets(projectRoot);
114
387
  }
115
388
 
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
- }
389
+ // Resolve the active issue backend (env > .forge/config.yaml > default 'kernel') so
390
+ // the runtime gate only treats bd as a hard prerequisite for the beads backend. The
391
+ // kernel default needs no bd, so stages must run without it.
392
+ const issueBackend = resolveIssueBackend({ deps: {}, env: process.env, projectRoot, warn: () => {} });
393
+ const runtimeHealth = await resolveStageRuntimeHealth({
394
+ health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime,
395
+ });
129
396
  if (runtimeHealth.hardStop) {
130
397
  throw new Error(`Stage ${stageId} blocked by runtime prerequisites: ${formatDiagnostics(runtimeHealth.diagnostics)}`);
131
398
  }
132
399
 
133
400
  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 };
401
+
402
+ // Kernel stage-state authority is active when a driver is injected (tests) or
403
+ // the caller opts in (real CLI via autoResolveKernel). Inline/flag state
404
+ // disables it: the caller is explicitly driving state, so no kernel side
405
+ // effects should occur.
406
+ const kernelEnabled = !isInlineStateProvided(workflowState, flags, args)
407
+ && (Boolean(kernelDriver) || autoResolveKernel === true);
408
+ const { driver, issueId } = kernelEnabled
409
+ ? await resolveKernelContext({ kernelDriver, activeIssueId, branch, projectRoot })
410
+ : { driver: null, issueId: null };
411
+ const kernelActive = Boolean(kernelEnabled && driver && issueId);
412
+
413
+ // On an allowed entry, record the stage as started (active) AND return a
414
+ // recordCompletion() the command runner calls after the handler SUCCEEDS — so
415
+ // a stage only counts as 'done' when its command actually passed (the ship
416
+ // gate below requires the predecessor to be done, not merely entered).
417
+ const finish = (result) => {
418
+ if (kernelActive) {
419
+ recordStageRunSafe(driver, issueId, stageId, 'start', warn);
420
+ invalidateDownstreamStages(driver, issueId, stageId, warn);
421
+ result.recordCompletion = () => recordStageRunSafe(driver, issueId, stageId, 'complete', warn);
138
422
  }
423
+ return result;
424
+ };
139
425
 
140
- throw new Error(
141
- `Stage ${stageId} requires authoritative workflow state. ` +
142
- `Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing.`
143
- );
426
+ const currentState = readWorkflowStateInput(stateInput);
427
+ if (currentState) {
428
+ return enforceWithFileState(currentState, stageId, flags, args, finish);
144
429
  }
145
430
 
146
- const currentStage = currentState.currentStage;
147
- const classification = currentState.workflowDecisions?.classification;
148
- if (!currentStage || !classification || stageId === currentStage) {
149
- return { allowed: true, stage: stageId, workflowState: currentState };
150
- }
431
+ // B1 strict mode is the explicit opt-in to the legacy fail-closed behavior:
432
+ // require prior stages / authoritative state. The default (unset) degrades to a
433
+ // loud warning and seeds the kernel so future gating gets real data. A recorded
434
+ // history that CONTRADICTS (e.g. validate started-but-not-done) still throws in
435
+ // both modes — that rework protection lives in enforceWithKernelState below and
436
+ // is never weakened by this flag.
437
+ const strict = process.env.FORGE_STAGE_GATE === 'strict';
151
438
 
152
- const allowedTransitions = getAllowedTransitionsForWorkflowState(currentState);
153
- if (allowedTransitions.includes(stageId)) {
154
- return { allowed: true, stage: stageId, workflowState: currentState };
439
+ // No inline/file state: the kernel is authoritative. Gate on recorded stage
440
+ // completions (tolerant read) so `ship` is reachable from a pure-CLI
441
+ // plan->dev->validate progression with no .forge-state.json.
442
+ if (kernelActive) {
443
+ const decided = enforceWithKernelState(driver, issueId, stageId, finish);
444
+ if (decided) {
445
+ return decided;
446
+ }
447
+ // enforceWithKernelState returned null → the kernel has an issue linked but
448
+ // NO recorded predecessor (a contradiction would have thrown above). Degrade
449
+ // to warn + seed unless strict: allow the stage and let finish() record it so
450
+ // future gating has real history.
451
+ if (!strict) {
452
+ warn(
453
+ `[forge] no recorded workflow history for issue ${issueId} — allowing '${stageId}' ` +
454
+ `and recording it in the kernel. Set FORGE_STAGE_GATE=strict to require prior stages.`
455
+ );
456
+ return finish({ allowed: true, stage: stageId, workflowState: null, degradedGate: 'kernel-empty' });
457
+ }
155
458
  }
156
459
 
157
- const override = parseOverride(flags, args);
158
- if (!override) {
159
- throw new Error(
160
- `Stage ${stageId} is blocked from ${currentStage}. ` +
161
- `Provide an explicit override payload via overrideStage or --override-stage.`
162
- );
460
+ if (STATELESS_ENTRY_STAGES.has(stageId)) {
461
+ return finish({ allowed: true, stage: stageId, workflowState: null });
163
462
  }
164
463
 
165
- if (override.fromStage !== currentStage || override.toStage !== stageId) {
166
- throw new Error(
167
- `Stage override does not match workflow state. Expected ${currentStage} -> ${stageId}.`
464
+ // No workflow state AND no kernel-linked issue for this branch. Degrade to warn
465
+ // unless strict so incremental adoption (fresh setup, in-flight branch, manual
466
+ // commit) is not blocked.
467
+ if (!strict) {
468
+ warn(
469
+ `[forge] no workflow state and no kernel-linked issue for this branch — stage gate skipped for '${stageId}'. ` +
470
+ `Link an issue with 'forge worktree create <slug>' to enable stage tracking.`
168
471
  );
472
+ return finish({ allowed: true, stage: stageId, workflowState: null, degradedGate: 'no-kernel-context' });
169
473
  }
170
474
 
171
- return {
172
- allowed: true,
173
- stage: stageId,
174
- workflowState: currentState,
175
- override,
176
- };
475
+ throw new Error(
476
+ `Stage ${stageId} requires authoritative workflow state. ` +
477
+ `Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing (or unset FORGE_STAGE_GATE).`
478
+ );
177
479
  }
178
480
 
179
481
  module.exports = {
@@ -182,4 +484,8 @@ module.exports = {
182
484
  parseOverride,
183
485
  resolveWorkflowStateInput,
184
486
  readWorkflowStateFile,
487
+ resolveActiveIssueId,
488
+ evaluateKernelStageGate,
489
+ recordStageRunSafe,
490
+ stagePredecessor,
185
491
  };
@@ -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
+ };
@@ -15,7 +15,6 @@ const STAGE_IDS = Object.freeze([
15
15
  'validate',
16
16
  'ship',
17
17
  'review',
18
- 'premerge',
19
18
  'verify',
20
19
  ]);
21
20
 
@@ -25,7 +24,6 @@ const STAGE_LABELS = Object.freeze({
25
24
  validate: 'Validate',
26
25
  ship: 'Ship',
27
26
  review: 'Review',
28
- premerge: 'Premerge',
29
27
  verify: 'Verify',
30
28
  });
31
29
 
@@ -35,14 +33,13 @@ const STAGE_COMMANDS = Object.freeze({
35
33
  validate: '/validate',
36
34
  ship: '/ship',
37
35
  review: '/review',
38
- premerge: '/premerge',
39
36
  verify: '/verify',
40
37
  });
41
38
 
42
39
  const WORKFLOW_STAGE_MATRIX = Object.freeze({
43
- critical: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', 'premerge', 'verify']),
44
- standard: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', 'premerge']),
45
- refactor: Object.freeze(['plan', 'dev', 'validate', 'ship', 'premerge']),
40
+ critical: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', 'verify']),
41
+ standard: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review']),
42
+ refactor: Object.freeze(['plan', 'dev', 'validate', 'ship']),
46
43
  simple: Object.freeze(['dev', 'validate', 'ship']),
47
44
  hotfix: Object.freeze(['dev', 'validate', 'ship']),
48
45
  // Docs-only work intentionally reuses /verify as a pre-ship content check to
@@ -51,6 +48,18 @@ const WORKFLOW_STAGE_MATRIX = Object.freeze({
51
48
  docs: Object.freeze(['verify', 'ship']),
52
49
  });
53
50
 
51
+ // Pre-merge is a task-type gate/checkpoint embedded inside existing stages
52
+ // (the doc-completion + PR-handoff checks that run before merge), not a
53
+ // standalone universal workflow stage. It is keyed with a hyphen ('pre-merge')
54
+ // so it never re-enters the stage model. `enabledFor` lists the classifications
55
+ // that run the gate; `embeddedIn` names the stages where it fires.
56
+ const WORKFLOW_GATES = Object.freeze({
57
+ 'pre-merge': Object.freeze({
58
+ embeddedIn: Object.freeze(['ship', 'review']),
59
+ enabledFor: Object.freeze(['critical', 'standard', 'refactor']),
60
+ }),
61
+ });
62
+
54
63
  const WORKFLOW_TERMINAL_STAGES = Object.freeze(Object.entries(WORKFLOW_STAGE_MATRIX).reduce((accumulator, [classification, path]) => {
55
64
  accumulator[classification] = path.at(-1);
56
65
  return accumulator;
@@ -77,6 +86,19 @@ function getWorkflowPath(classification) {
77
86
  return normalized ? WORKFLOW_STAGE_MATRIX[normalized] : Object.freeze([]);
78
87
  }
79
88
 
89
+ function getGatesForClassification(classification) {
90
+ const normalized = normalizeClassification(classification);
91
+ if (!normalized) {
92
+ return Object.freeze([]);
93
+ }
94
+
95
+ return Object.freeze(
96
+ Object.entries(WORKFLOW_GATES)
97
+ .filter(([, gate]) => gate.enabledFor.includes(normalized))
98
+ .map(([gateId]) => gateId),
99
+ );
100
+ }
101
+
80
102
  function getStageWorkflow(stageId, classification) {
81
103
  const normalizedStage = normalizeStageId(stageId);
82
104
  const normalizedClassification = normalizeClassification(classification);
@@ -186,6 +208,7 @@ module.exports = {
186
208
  STAGE_LABELS,
187
209
  STAGE_COMMANDS,
188
210
  WORKFLOW_STAGE_MATRIX,
211
+ WORKFLOW_GATES,
189
212
  WORKFLOW_TERMINAL_STAGES,
190
213
  STAGE_TRANSITIONS,
191
214
  STAGE_MODEL,
@@ -193,6 +216,7 @@ module.exports = {
193
216
  normalizeStageId,
194
217
  isCanonicalStageId,
195
218
  getWorkflowPath,
219
+ getGatesForClassification,
196
220
  getStageWorkflow,
197
221
  getAllowedTransitions,
198
222
  canTransition,