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,436 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * shepherd command — one bounded monitor pass over a pull request.
5
+ *
6
+ * `forge shepherd <pr> [--auto-rebase]` reads PR/CI state, takes at most one
7
+ * idempotent Tier-A action (rerun a flaky required check), and exits with a
8
+ * terminal state. It NEVER merges and NEVER resolves review threads. A
9
+ * `--watch` loop, if desired, lives in an external scheduler that re-invokes
10
+ * this command on an interval — there is no in-process polling loop here.
11
+ *
12
+ * `forge shepherd <pr> --bundle --json` instead prints the COMPLETE read-only
13
+ * PR-state bundle (all unresolved threads, merge state, CI, divergence,
14
+ * predicted conflicts) the monitor will hand to a fixer-agent. It still decides
15
+ * nothing and takes no action.
16
+ *
17
+ * `forge shepherd <pr> --pull --json` prints a COMPACT, bounded "why it failed +
18
+ * what to fix" payload: per-failed-check log excerpts (matrix-deduped) plus the
19
+ * unresolved review-thread fix-list (CodeRabbit included), alongside the decision
20
+ * state. All the `gh pr checks` / `gh run view --log-failed` / GraphQL work is
21
+ * done IN CODE so an agent gets one payload instead of running those by hand. It
22
+ * still NEVER merges and NEVER resolves threads.
23
+ *
24
+ * State persists via GitHub PR comments/labels and git only.
25
+ *
26
+ * @module commands/shepherd
27
+ */
28
+
29
+ const { execFileSync } = require('node:child_process');
30
+
31
+ const { runShepherdPass } = require('../pr-shepherd');
32
+ const { gatherPrBundle } = require('../pr-bundle');
33
+ const { gatherPullSignal, renderPullSummary } = require('../pr-pull');
34
+ const { PrStateAdapter } = require('../adapters/pr-state-adapter');
35
+ const { validatePrStateAdapter } = require('../pr-state-validator');
36
+ const { gatherMonitorSnapshot } = require('../pr-monitor/gather');
37
+ const { pollEvents } = require('../pr-monitor/monitor');
38
+ const { watchLoop } = require('../pr-monitor/watch');
39
+ const monitorJournal = require('../pr-monitor/journal');
40
+ const { EVENT_TYPES: T } = require('../pr-monitor/events');
41
+
42
+ const DEFAULT_RERUN_BUDGET = 3;
43
+
44
+ const defaultGhRunner = (cmd, a) => execFileSync(cmd, a, { encoding: 'utf8', timeout: 30000 });
45
+
46
+ /**
47
+ * Resolve owner/repo and base branch for the shepherd pass.
48
+ *
49
+ * The base branch is read from the PR itself (`gh pr view <pr> --json
50
+ * baseRefName`) rather than the current checkout's default branch, so PRs
51
+ * targeting `release/*`/`develop` are evaluated against the correct branch.
52
+ * `owner`/`name` come from the repository the PR is queried in — that IS the
53
+ * base repository. `cwd` (the worktree root) is threaded through so divergence
54
+ * is computed against the right checkout.
55
+ *
56
+ * @param {object} deps
57
+ * @returns {Promise<{ pr: string, owner: string, repo: string, base: string, baseRef: string, cwd?: string }>}
58
+ */
59
+ async function defaultBuildContext({ pr, gh, git, projectRoot }) {
60
+ const prJson = gh('gh', ['pr', 'view', String(pr), '--json', 'baseRefName']);
61
+ const prInfo = JSON.parse(prJson || '{}');
62
+ const base = prInfo.baseRefName || 'master';
63
+
64
+ const repoJson = gh('gh', ['repo', 'view', '--json', 'owner,name']);
65
+ const repo = JSON.parse(repoJson || '{}');
66
+ const owner = repo.owner?.login || '';
67
+ const name = repo.name || '';
68
+
69
+ let baseRemote;
70
+ try {
71
+ baseRemote = git('git', ['remote']).split(/\s+/).filter(Boolean)[0] || 'origin';
72
+ } catch (_err) {
73
+ baseRemote = 'origin';
74
+ }
75
+
76
+ return {
77
+ pr: String(pr),
78
+ owner,
79
+ repo: name,
80
+ base,
81
+ baseRef: `${baseRemote}/${base}`,
82
+ ...(projectRoot ? { cwd: projectRoot } : {}),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Detect whether the working tree is clean (precondition for --auto-rebase).
88
+ *
89
+ * @param {Function} git
90
+ * @returns {boolean}
91
+ */
92
+ function isWorkingTreeClean(git) {
93
+ try {
94
+ return git('git', ['status', '--porcelain']).trim().length === 0;
95
+ } catch (_err) {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ // Render a single pass action for the human-readable monitor line. Strings pass
101
+ // through; everything else is JSON-encoded, but JSON.stringify can throw on
102
+ // circular refs or BigInt, so surface the reason inline rather than crash the pass.
103
+ function formatAction(action) {
104
+ if (typeof action === 'string') {
105
+ return action;
106
+ }
107
+ try {
108
+ return JSON.stringify(action);
109
+ } catch (err) {
110
+ return `[unprintable action: ${err.message}]`;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Build the DEFAULT `check.failed` enrichment hook for the events pull surface.
116
+ *
117
+ * The monitor design specifies that newly-failed checks are enriched with their
118
+ * failure log excerpts before the journal append. `pollEvents` accepts an
119
+ * `enrich` hook but `handleEvents` must supply the default one, or a plain
120
+ * `forge shepherd events` call would emit bare `check.failed` events with no
121
+ * `data.excerpt` (only direct monitor callers could attach them).
122
+ *
123
+ * The hook is BEST-EFFORT: it fetches the compact pull signal ONCE (only when a
124
+ * pass actually produced a `check.failed`), maps excerpts by check name, and
125
+ * decorates matching records. Any failure to gather excerpts leaves the events
126
+ * intact rather than aborting the pass — enrichment must never block the journal.
127
+ *
128
+ * @param {object} pullCtx - ctx forwarded to `gatherPull` (owner/repo/base/adapter/runGh/self).
129
+ * @returns {(records: object[]) => Promise<void>}
130
+ */
131
+ function makeCheckFailureEnricher(pullCtx) {
132
+ const gatherPull = pullCtx.gatherPull || gatherPullSignal;
133
+ return async (records) => {
134
+ if (!Array.isArray(records) || !records.some((r) => r.type === T.CHECK_FAILED)) return;
135
+ let failures;
136
+ try {
137
+ const pull = await gatherPull(pullCtx);
138
+ failures = Array.isArray(pull?.failures) ? pull.failures : [];
139
+ } catch (err) {
140
+ // best-effort: never let enrichment abort the pass, but surface the reason
141
+ console.error(`[shepherd] check-failure enrichment skipped: ${err.message}`);
142
+ return;
143
+ }
144
+ const byName = new Map(failures.map((f) => [f.name, f]));
145
+ for (const r of records) {
146
+ if (r.type !== T.CHECK_FAILED) continue;
147
+ const f = byName.get(r.data?.name);
148
+ if (!f) continue;
149
+ if (f.excerpt) r.data.excerpt = f.excerpt;
150
+ if (f.jobUrl) r.data.jobUrl = f.jobUrl;
151
+ }
152
+ };
153
+ }
154
+
155
+ /** Parse `--since <seq>` from the raw arg list (default 0). */
156
+ function parseSince(args) {
157
+ const i = (args || []).indexOf('--since');
158
+ if (i >= 0 && args[i + 1] != null) return Number.parseInt(args[i + 1], 10) || 0;
159
+ return 0;
160
+ }
161
+
162
+ /**
163
+ * Build the shared monitor context — journal `dir`, bounded `gather`, and the
164
+ * default `check.failed` enricher — that BOTH the `events` pull surface and the
165
+ * `watch` streaming loop feed to the monitor core. Injected `dir`/`gather`/
166
+ * `enrich` (tests, programmatic callers) short-circuit the live gh build.
167
+ *
168
+ * @param {string|number} pr
169
+ * @param {string} projectRoot
170
+ * @param {object} deps
171
+ * @returns {Promise<{ dir?: string, gather?: Function, enrich?: Function, error?: string }>}
172
+ */
173
+ async function buildMonitorContext(pr, projectRoot, deps) {
174
+ let dir = deps.dir;
175
+ let gather = deps.gather;
176
+ // enrich decorates newly-failed checks with log excerpts before the journal
177
+ // append. The caller MUST supply the default (not just forward an injected
178
+ // one), or a plain `forge shepherd events`/`watch` emits bare check.failed events.
179
+ let enrich = deps.enrich;
180
+ if (!gather || !dir) {
181
+ const gh = deps.gh || defaultGhRunner;
182
+ const git = deps.git || gh;
183
+ const buildContext = deps.buildContext || defaultBuildContext;
184
+ const ctx = await buildContext({ pr, gh, git, projectRoot });
185
+ const adapter = deps.adapter || new PrStateAdapter({ gh, git });
186
+ const validation = validatePrStateAdapter(adapter);
187
+ if (!validation.valid) {
188
+ return { error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
189
+ }
190
+ dir = dir || monitorJournal.journalDir({ root: projectRoot || process.cwd(), repo: ctx.repo, pr: ctx.pr });
191
+ gather = gather || (() => gatherMonitorSnapshot({ ...ctx, adapter, self: deps.self }));
192
+ enrich = enrich || makeCheckFailureEnricher({
193
+ ...ctx,
194
+ adapter,
195
+ self: deps.self,
196
+ runGh: (ghArgs) => gh('gh', ghArgs),
197
+ gatherPull: deps.gatherPull,
198
+ });
199
+ } else if (!enrich && deps.gatherPull) {
200
+ // Injected gather (tests / programmatic callers) still gets the default
201
+ // enrichment when a pull-signal source is supplied.
202
+ enrich = makeCheckFailureEnricher({
203
+ pr, adapter: deps.adapter, self: deps.self, runGh: deps.runGh, gatherPull: deps.gatherPull,
204
+ });
205
+ }
206
+ return { dir, gather, enrich };
207
+ }
208
+
209
+ /**
210
+ * `forge shepherd events <pr> --since <seq> [--json]` — the agent-agnostic PULL
211
+ * surface. Runs one bounded gather+diff (inline, unless a watcher owns the PR),
212
+ * appends new events to the per-PR journal, and prints every journaled event
213
+ * with `seq > since` as NDJSON, one per line, to stdout. Nothing under .claude.
214
+ *
215
+ * @param {string[]} args
216
+ * @param {string} projectRoot
217
+ * @param {object} [deps]
218
+ * @returns {Promise<object>}
219
+ */
220
+ async function handleEvents(args, projectRoot, deps = {}) {
221
+ const rawArgs = args || [];
222
+ const sinceIdx = rawArgs.indexOf('--since');
223
+ const pr = rawArgs.find((a, idx) => !String(a).startsWith('--') && a !== 'events' && idx !== sinceIdx + 1);
224
+ if (!pr) {
225
+ return { success: false, error: 'Usage: forge shepherd events <pr> --since <seq> [--json]' };
226
+ }
227
+ const since = parseSince(rawArgs);
228
+
229
+ const built = await buildMonitorContext(pr, projectRoot, deps);
230
+ if (built.error) return { success: false, error: built.error };
231
+ const { dir, gather, enrich } = built;
232
+
233
+ const poll = deps.pollEvents || pollEvents;
234
+ const result = await poll({ dir, gather, since, now: deps.now, watcherRunning: deps.watcherRunning, enrich });
235
+ // `output` is the agent-agnostic pull surface: NDJSON, one event per line. The
236
+ // registry CLI dispatch prints `result.output` (same contract as --pull/--bundle),
237
+ // so this handler does NOT write to stdout itself (that would double-print).
238
+ const output = result.events.map((e) => JSON.stringify(e)).join('\n');
239
+ return { success: true, events: result.events, since: result.since, output };
240
+ }
241
+
242
+ /**
243
+ * Wire an AbortController to SIGINT/SIGTERM so a long-running watch loop stops
244
+ * cleanly on Ctrl-C. Returns the signal plus a `cleanup` that detaches the
245
+ * one-shot handlers (always called in a finally so the loop leaves no listeners).
246
+ *
247
+ * @returns {{ signal: object, cleanup: () => void }}
248
+ */
249
+ function wireSignals() {
250
+ const controller = new AbortController();
251
+ const onSignal = () => controller.abort();
252
+ process.once('SIGINT', onSignal);
253
+ process.once('SIGTERM', onSignal);
254
+ const cleanup = () => {
255
+ process.off('SIGINT', onSignal);
256
+ process.off('SIGTERM', onSignal);
257
+ };
258
+ return { signal: controller.signal, cleanup };
259
+ }
260
+
261
+ /**
262
+ * `forge shepherd watch <pr>` — the agent-agnostic PUSH surface. A long-running
263
+ * loop that every ~60s (jittered) runs ONE bounded monitor pass and STREAMS each
264
+ * new event as an NDJSON line to stdout, self-stopping on `pr.merged`/`pr.closed`.
265
+ * The loop streams live via the default stdout emit, so this handler returns NO
266
+ * `output` field (returning one would double-print). SIGINT/SIGTERM stop it clean.
267
+ *
268
+ * @param {string[]} args
269
+ * @param {string} projectRoot
270
+ * @param {object} [deps]
271
+ * @returns {Promise<object>}
272
+ */
273
+ async function handleWatch(args, projectRoot, deps = {}) {
274
+ const rawArgs = args || [];
275
+ const pr = rawArgs.find((a) => !String(a).startsWith('--') && a !== 'watch');
276
+ if (!pr) {
277
+ return { success: false, error: 'Usage: forge shepherd watch <pr>' };
278
+ }
279
+
280
+ const built = await buildMonitorContext(pr, projectRoot, deps);
281
+ if (built.error) return { success: false, error: built.error };
282
+ const { dir, gather, enrich } = built;
283
+
284
+ const loop = deps.watchLoop || watchLoop;
285
+ // Injected signal (tests) suppresses real process handlers; otherwise wire them.
286
+ const wired = deps.signal ? { signal: deps.signal, cleanup: () => {} } : wireSignals();
287
+ let result;
288
+ try {
289
+ result = await loop({
290
+ dir,
291
+ gather,
292
+ enrich,
293
+ now: deps.now,
294
+ emit: deps.emit,
295
+ sleep: deps.sleep,
296
+ rng: deps.rng,
297
+ intervalMs: deps.intervalMs,
298
+ maxPasses: deps.maxPasses,
299
+ lockOpts: deps.lockOpts,
300
+ signal: wired.signal,
301
+ watcherRunning: deps.watcherRunning,
302
+ writePid: deps.writePid,
303
+ removePid: deps.removePid,
304
+ });
305
+ } finally {
306
+ wired.cleanup();
307
+ }
308
+
309
+ return {
310
+ success: true,
311
+ started: result.started,
312
+ passes: result.passes,
313
+ stopped: result.stopped,
314
+ ...(result.reason ? { reason: result.reason } : {}),
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Command handler.
320
+ *
321
+ * @param {string[]} args - Positional + flag args (first positional is the PR).
322
+ * @param {object} _flags - Parsed flags (unused; flags are read from args).
323
+ * @param {string} projectRoot - Project root.
324
+ * @param {object} [deps] - Injected dependencies for testing.
325
+ * @returns {Promise<object>} result envelope.
326
+ */
327
+ async function handler(args, _flags, projectRoot, deps = {}) {
328
+ const positional = (args || []).filter((a) => !String(a).startsWith('--'));
329
+ const flags = new Set((args || []).filter((a) => String(a).startsWith('--')));
330
+
331
+ // Subcommand routing: `events` is the monitor pull surface (its own arg shape);
332
+ // `watch` is the constant monitor push surface (long-running stream).
333
+ if (positional[0] === 'events') {
334
+ return handleEvents(args, projectRoot, deps);
335
+ }
336
+ if (positional[0] === 'watch') {
337
+ return handleWatch(args, projectRoot, deps);
338
+ }
339
+
340
+ const pr = positional[0];
341
+
342
+ if (!pr) {
343
+ return { success: false, error: 'Usage: forge shepherd <pr> [--auto-rebase] [--bundle --json] [--pull --json]' };
344
+ }
345
+
346
+ const gh = deps.gh || ((cmd, a) => execFileSync(cmd, a, { encoding: 'utf8', timeout: 30000 }));
347
+ const git = deps.git || gh;
348
+ const buildContext = deps.buildContext || defaultBuildContext;
349
+ const runPass = deps.runPass || runShepherdPass;
350
+ const gatherBundle = deps.gatherBundle || gatherPrBundle;
351
+ const gatherPull = deps.gatherPull || gatherPullSignal;
352
+
353
+ const autoRebase = flags.has('--auto-rebase');
354
+ const wantBundle = flags.has('--bundle');
355
+ const wantPull = flags.has('--pull');
356
+ const wantJson = flags.has('--json');
357
+
358
+ const ctx = await buildContext({ pr, gh, git, projectRoot });
359
+
360
+ const adapter = deps.adapter || new PrStateAdapter({ gh, git });
361
+ const validation = validatePrStateAdapter(adapter);
362
+ if (!validation.valid) {
363
+ return { success: false, error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
364
+ }
365
+
366
+ // --pull: gather the COMPACT "why it failed + what to fix" payload (failed-check
367
+ // log excerpts, matrix-deduped, plus the review-thread fix-list). STRICTLY
368
+ // READ-ONLY — it computes the decision state via a dry-run pass but takes NO
369
+ // action (no Tier-A rerun, no rebase); acting belongs to plain `forge shepherd`.
370
+ // The `gh` calls to fetch logs run through an injected runner so this stays testable.
371
+ if (wantPull) {
372
+ const runGh = (ghArgs) => gh('gh', ghArgs);
373
+ const pull = await gatherPull({ ...ctx, adapter, runGh, runPass, self: deps.self });
374
+ // `output` is what the registry CLI dispatch (bin/forge.js) actually PRINTS —
375
+ // returning only `pull` silently dropped the whole payload on that path (it
376
+ // prints `result.output`, nothing else). `--json` → machine payload; default
377
+ // → the compact human WHY+fix summary. `pull` is kept for the legacy
378
+ // bin/forge-cmd.js path and for programmatic callers/tests.
379
+ const output = wantJson ? JSON.stringify(pull, null, 2) : renderPullSummary(pull);
380
+ return { success: true, pull, output };
381
+ }
382
+
383
+ // --bundle: gather the COMPLETE read-only PR-state bundle the monitor will
384
+ // hand to a fixer-agent, and return it for the CLI to print as JSON. This is
385
+ // the gather half only — it decides nothing and takes no action.
386
+ if (wantBundle) {
387
+ const bundle = await gatherBundle({ ...ctx, adapter });
388
+ // Same rationale as --pull: the registry dispatch prints `output`. The bundle
389
+ // is a machine payload, so it is always emitted as JSON.
390
+ return { success: true, bundle, output: JSON.stringify(bundle, null, 2) };
391
+ }
392
+
393
+ const result = await runPass({
394
+ ...ctx,
395
+ adapter,
396
+ autoRebase,
397
+ cleanTree: autoRebase ? isWorkingTreeClean(git) : false,
398
+ rerunBudget: deps.rerunBudget || DEFAULT_RERUN_BUDGET,
399
+ rerunsUsed: deps.rerunsUsed || 0,
400
+ });
401
+
402
+ // Surface the pass outcome so the monitor is legible when run interactively or
403
+ // tailed by a scheduler (the bounded state machine is otherwise silent).
404
+ const passActions = Array.isArray(result.actions) ? result.actions : [];
405
+ const reasonSuffix = result.reason ? ` — ${result.reason}` : '';
406
+ process.stdout.write(`Shepherd pass — PR #${pr}: ${result.state}${reasonSuffix}\n`);
407
+ for (const action of passActions) {
408
+ process.stdout.write(` • ${formatAction(action)}\n`);
409
+ }
410
+ if (!passActions.length) {
411
+ process.stdout.write(' • no actions this pass\n');
412
+ }
413
+
414
+ return {
415
+ success: result.state !== 'HARD_STOP',
416
+ state: result.state,
417
+ reason: result.reason,
418
+ actions: result.actions || [],
419
+ ...(result.authClass ? { authClass: result.authClass } : {}),
420
+ ...(result.retryAfter ? { retryAfter: result.retryAfter } : {}),
421
+ };
422
+ }
423
+
424
+ module.exports = {
425
+ name: 'shepherd',
426
+ description: 'Run one bounded monitor pass over a PR (rerun flaky checks, escalate, hand off — never merges)',
427
+ usage: 'Usage: forge shepherd <pr> [--auto-rebase] [--bundle --json] [--pull --json] | forge shepherd events <pr> --since <seq> [--json] | forge shepherd watch <pr>',
428
+ handler,
429
+ handleEvents,
430
+ handleWatch,
431
+ buildMonitorContext,
432
+ makeCheckFailureEnricher,
433
+ parseSince,
434
+ defaultBuildContext,
435
+ isWorkingTreeClean,
436
+ };
@@ -12,6 +12,8 @@ const { execFileSync } = require('node:child_process');
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
14
 
15
+ const { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
16
+
15
17
  function getExecOptions() {
16
18
  return { encoding: 'utf8', cwd: process.cwd(), timeout: 120000 };
17
19
  }
@@ -482,8 +484,26 @@ async function createPR(options) { // NOSONAR S3776
482
484
  }
483
485
  }
484
486
 
487
+ /**
488
+ * Best-effort, non-blocking auto-start of the constant PR monitor once a real PR
489
+ * exists. Skipped on a dry run or when no PR number is known. MUST NEVER fail
490
+ * ship: `startWatcher` (startPrWatcherDetached) already never throws, and this
491
+ * guard keeps even a surprise error from surfacing to the ship caller.
492
+ *
493
+ * @param {{ dryRun: boolean, prNumber?: string|number, startWatcher: Function }} params
494
+ * @returns {{ started: boolean, reason?: string }}
495
+ */
496
+ function maybeStartPrWatcher({ dryRun, prNumber, startWatcher }) {
497
+ if (dryRun || !prNumber) return { started: false, reason: 'skipped' };
498
+ try {
499
+ return startWatcher({ prNumber, cwd: process.cwd() });
500
+ } catch (err) {
501
+ return { started: false, reason: err.message };
502
+ }
503
+ }
504
+
485
505
  async function executeShip(options) {
486
- const { featureSlug, title, dryRun = false } = options || {};
506
+ const { featureSlug, title, dryRun = false, startWatcher = startPrWatcherDetached } = options || {};
487
507
 
488
508
  // Validate feature slug
489
509
  if (!featureSlug || typeof featureSlug !== 'string' || featureSlug.trim() === '') {
@@ -535,6 +555,7 @@ async function executeShip(options) {
535
555
  });
536
556
  const result = await createPR({ title, body: prBody, dryRun });
537
557
  if (!result.success) return result;
558
+ maybeStartPrWatcher({ dryRun, prNumber: result.prNumber, startWatcher });
538
559
  return {
539
560
  success: true,
540
561
  prUrl: result.prUrl,
@@ -567,6 +588,7 @@ module.exports = {
567
588
  output: lines.join('\n'),
568
589
  };
569
590
  },
591
+ maybeStartPrWatcher,
570
592
  extractKeyDecisions,
571
593
  extractTestScenarios,
572
594
  getTestCoverage,
@@ -1,5 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const { makeAliasCommand } = require('./_issue');
3
+ const { createIssueSubcommand } = require('./_issue');
4
4
 
5
- module.exports = makeAliasCommand('show');
5
+ module.exports = createIssueSubcommand('show');
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Forge Stage Command (f61601ab)
5
+ *
6
+ * Records the REAL workflow phase of an issue into the kernel_stage_runs table so
7
+ * the phase is queryable — instead of being guessed from status+claim (a
8
+ * claimed-open issue with a merged PR would otherwise still show "dev"). Mirrors
9
+ * the worktree-linkage registry: direct kernel writes, idempotent per
10
+ * (issue_id, stage).
11
+ *
12
+ * forge stage <issue-id> <stage> --start Open a stage (status=active)
13
+ * forge stage <issue-id> <stage> --complete Close a stage (status=done)
14
+ * forge stage <issue-id> --current Print the current stage
15
+ * forge stage <issue-id> --list Print the full stage history
16
+ *
17
+ * `<stage>` is one of the canonical workflow stages (plan|dev|validate|ship|review|verify).
18
+ * Read the current stage back on `forge show <id>` (data.current_stage).
19
+ *
20
+ * @module commands/stage
21
+ */
22
+
23
+ const { STAGE_IDS, normalizeStageId } = require('../workflow/stages');
24
+ const { resolveIssueId } = require('../kernel/issue-id-resolver');
25
+
26
+ const STAGE_FLAGS = ['--start', '--complete', '--list', '--current', '--json'];
27
+ const VALUE_FLAGS = ['--substage'];
28
+ const USAGE = 'Usage: forge stage <issue-id> <stage> --start|--complete (reads: --current | --list)';
29
+
30
+ /**
31
+ * Parse the stage command's own flags out of the raw args (the global flag parser
32
+ * only recognizes an allowlist, so — like `worktree` — this command extracts its
33
+ * own). Returns { positional, opts } or { error }.
34
+ */
35
+ function parseStageArgs(args = []) {
36
+ const positional = [];
37
+ const opts = { start: false, complete: false, list: false, current: false, json: false, substage: null };
38
+
39
+ for (let i = 0; i < args.length; i += 1) {
40
+ const arg = args[i];
41
+ if (typeof arg !== 'string') continue;
42
+
43
+ const valueFlag = VALUE_FLAGS.find(flag => arg === flag || arg.startsWith(`${flag}=`));
44
+ if (valueFlag) {
45
+ let value;
46
+ if (arg.startsWith(`${valueFlag}=`)) {
47
+ value = arg.slice(`${valueFlag}=`.length);
48
+ } else {
49
+ value = args[i + 1];
50
+ i += 1;
51
+ }
52
+ if (!value || value.startsWith('--')) {
53
+ return { error: `Missing value for ${valueFlag}. ${USAGE}` };
54
+ }
55
+ opts.substage = value;
56
+ continue;
57
+ }
58
+
59
+ if (STAGE_FLAGS.includes(arg)) {
60
+ opts[arg.slice(2)] = true;
61
+ continue;
62
+ }
63
+
64
+ if (arg.startsWith('--')) {
65
+ return { error: `Unknown flag: ${arg}. ${USAGE}` };
66
+ }
67
+
68
+ positional.push(arg);
69
+ }
70
+
71
+ return { positional, opts };
72
+ }
73
+
74
+ async function resolveDriver(projectRoot, opts) {
75
+ if (opts._kernelDriver) return opts._kernelDriver;
76
+ const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
77
+ return (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
78
+ }
79
+
80
+ function render(opts, data, humanLines) {
81
+ if (opts.json || process.env.FORGE_JSON === '1') {
82
+ return { success: true, output: JSON.stringify(data, null, 2) };
83
+ }
84
+ return { success: true, output: humanLines.join('\n'), ...data };
85
+ }
86
+
87
+ module.exports = {
88
+ name: 'stage',
89
+ description: 'Record or read an issue\'s real workflow stage (stage_runs)',
90
+ usage: USAGE,
91
+ flags: {
92
+ '--start': 'Open a stage run (status=active)',
93
+ '--complete': 'Complete a stage run (status=done)',
94
+ '--substage': 'Optional substage label to record',
95
+ '--current': 'Print the current stage (latest active, else latest completed)',
96
+ '--list': 'Print the full stage-run history for the issue',
97
+ '--json': 'Emit machine-readable JSON',
98
+ },
99
+
100
+ /**
101
+ * @param {string[]} args - Positional + flag args after `stage`
102
+ * @param {object} _flags - Global parsed flags (unused; this command parses its own)
103
+ * @param {string} projectRoot - Project root path
104
+ * @param {object} [opts] - DI options (may inject `_kernelDriver`)
105
+ * @returns {Promise<object>}
106
+ */
107
+ handler: async (args, _flags, projectRoot, opts = {}) => {
108
+ const parsed = parseStageArgs(args);
109
+ if (parsed.error) {
110
+ return { success: false, error: parsed.error };
111
+ }
112
+ const { positional, opts: stageOpts } = parsed;
113
+
114
+ const issueRef = positional[0];
115
+ if (!issueRef) {
116
+ return { success: false, error: `Missing issue id. ${USAGE}` };
117
+ }
118
+
119
+ let driver;
120
+ try {
121
+ driver = await resolveDriver(projectRoot, opts);
122
+ } catch (error) {
123
+ return { success: false, error: `Kernel unavailable: ${error.message}` };
124
+ }
125
+
126
+ // Resolve prefixes / display handles to a full issue id (same resolver the
127
+ // issue commands use), so `forge stage 1a2b3c4d dev --start` works.
128
+ const resolution = await resolveIssueId(
129
+ issueRef,
130
+ (needle, limit) => driver.findIssueIdsByPrefix(needle, limit, {}, {}),
131
+ );
132
+ if (resolution.error) {
133
+ return { success: false, error: resolution.error };
134
+ }
135
+ const issueId = resolution.id;
136
+
137
+ // Read paths -----------------------------------------------------------
138
+ if (stageOpts.current) {
139
+ const current = driver.getCurrentStage({ issue_id: issueId }, {});
140
+ const data = {
141
+ issue_id: issueId,
142
+ current_stage: current ? current.stage : null,
143
+ current_stage_status: current ? current.status : null,
144
+ };
145
+ const human = current
146
+ ? `${issueId}: ${current.stage} (${current.status})`
147
+ : `${issueId}: no stage recorded`;
148
+ return render(stageOpts, data, [human]);
149
+ }
150
+
151
+ if (stageOpts.list) {
152
+ const runs = driver.listStageRuns({ issue_id: issueId }, {});
153
+ const data = { issue_id: issueId, stage_runs: runs };
154
+ const human = runs.length === 0
155
+ ? [`${issueId}: no stage runs`]
156
+ : runs.map(run => ` ${run.stage.padEnd(9)} ${run.status.padEnd(7)} started ${run.started_at}${run.completed_at ? ` → completed ${run.completed_at}` : ''}`);
157
+ return render(stageOpts, data, [`${issueId} stage history:`, ...human]);
158
+ }
159
+
160
+ // Write paths ----------------------------------------------------------
161
+ const stage = normalizeStageId(positional[1]);
162
+ if (!stage) {
163
+ return {
164
+ success: false,
165
+ error: `Invalid or missing stage "${positional[1] ?? ''}". Expected one of: ${STAGE_IDS.join(', ')}. ${USAGE}`,
166
+ };
167
+ }
168
+
169
+ if (stageOpts.start && stageOpts.complete) {
170
+ return { success: false, error: `Pass only one of --start or --complete. ${USAGE}` };
171
+ }
172
+ const action = stageOpts.complete ? 'complete' : 'start';
173
+
174
+ let row;
175
+ try {
176
+ row = driver.recordStageRun(
177
+ { issue_id: issueId, stage, action, substage: stageOpts.substage || null },
178
+ {},
179
+ );
180
+ } catch (error) {
181
+ // A FOREIGN KEY failure means the issue id does not exist in the kernel.
182
+ if (/FOREIGN KEY/i.test(String(error.message))) {
183
+ return { success: false, error: `Issue ${issueId} not found in the kernel.` };
184
+ }
185
+ return { success: false, error: `Failed to record stage: ${error.message}` };
186
+ }
187
+
188
+ const data = { issue_id: issueId, action, stage_run: row };
189
+ const verb = action === 'complete' ? 'completed' : 'started';
190
+ return render(stageOpts, data, [`${issueId}: ${verb} stage ${stage} (${row.status})`]);
191
+ },
192
+ };
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { createIssueSubcommand } = require('./_issue');
4
+
5
+ module.exports = createIssueSubcommand('stale');