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
@@ -3,16 +3,36 @@
3
3
  const { execFileSync } = require('node:child_process');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
- const { stopDolt } = require('./worktree');
7
6
 
8
7
  /**
9
8
  * Forge Clean Command
10
- * Remove worktrees for merged branches, stopping Dolt servers first.
9
+ * Remove worktrees for merged branches (squash-merge aware) and optionally
10
+ * fast-forward the main checkout's default branch after merges.
11
+ *
11
12
  * Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
12
13
  *
14
+ * The Kernel issue store lives in the shared git common dir, so there is no
15
+ * per-worktree server to stop before removal — cleanup is pure git.
16
+ *
13
17
  * @module commands/clean
14
18
  */
15
19
 
20
+ /**
21
+ * Run a git/gh command through the injected runner, returning trimmed stdout.
22
+ * Returns '' on any failure (callers treat empty as "no signal").
23
+ * @param {Function} runFile - execFileSync-compatible function
24
+ * @param {string} cmd - Executable (e.g. 'git', 'gh')
25
+ * @param {string[]} args - Arguments
26
+ * @returns {string} Trimmed stdout, or '' on error
27
+ */
28
+ function tryRun(runFile, cmd, args) {
29
+ try {
30
+ return runFile(cmd, args, { stdio: 'pipe' }).toString().trim();
31
+ } catch (_e) { /* intentional: caller treats '' as no-signal */ // NOSONAR S2486
32
+ return '';
33
+ }
34
+ }
35
+
16
36
  /**
17
37
  * Detect the default branch (main, master, develop, trunk).
18
38
  * Tries origin/HEAD first, then probes common names.
@@ -34,10 +54,28 @@ function getDefaultBranch(runFile) {
34
54
  }
35
55
  }
36
56
 
57
+ /**
58
+ * Canonicalize a worktree path for MAP KEYS ONLY (86b04c20): on Windows, git
59
+ * worktree list --porcelain emits forward-slash paths (C:/...) while
60
+ * path.resolve emits backslashes (C:\...), so a raw-string lookup never matches
61
+ * and every merged worktree reads "active". Normalizes separators + drive-letter
62
+ * case. Real fs/git calls keep the native path form.
63
+ * @param {string} p - Path in either separator style
64
+ * @returns {string} Canonical forward-slash key
65
+ */
66
+ function normalizeWorktreeKey(p, platform = process.platform) {
67
+ const key = String(p).replace(/\\/g, '/').replace(/^([a-z]):/, (_m, d) => `${d.toUpperCase()}:`);
68
+ // Windows filesystems are case-insensitive, so fold the WHOLE key there —
69
+ // segment-level case differences between porcelain output and path.resolve
70
+ // must not miss the lookup. POSIX paths stay case-sensitive.
71
+ return platform === 'win32' ? key.toLowerCase() : key;
72
+ }
73
+
37
74
  /**
38
75
  * Parse `git worktree list --porcelain` output into a map of path -> branch.
76
+ * Keys are canonicalized via normalizeWorktreeKey so lookups match on Windows.
39
77
  * @param {string} output - Raw porcelain output
40
- * @returns {Map<string, string>} Map of worktree path -> branch name
78
+ * @returns {Map<string, string>} Map of canonical worktree path -> branch name
41
79
  */
42
80
  function parseWorktreeList(output) {
43
81
  const map = new Map();
@@ -56,42 +94,386 @@ function parseWorktreeList(output) {
56
94
  }
57
95
  }
58
96
  if (wtPath && branch) {
59
- map.set(wtPath, branch);
97
+ map.set(normalizeWorktreeKey(wtPath), branch);
60
98
  }
61
99
  }
62
100
  return map;
63
101
  }
64
102
 
65
103
  /**
66
- * Clean a single worktree directory if its branch is merged.
104
+ * Return the first worktree block from `git worktree list --porcelain` — the
105
+ * main working tree — as { path, branch } (branch is null when detached).
106
+ * @param {string} output - Raw porcelain output
107
+ * @returns {{ path: string, branch: string|null }|null}
108
+ */
109
+ function parseMainWorktree(output) {
110
+ const firstBlock = output.split('\n\n')[0] || '';
111
+ let wtPath = null;
112
+ let branch = null;
113
+ for (const line of firstBlock.trim().split('\n')) {
114
+ if (line.startsWith('worktree ')) wtPath = line.slice('worktree '.length);
115
+ if (line.startsWith('branch ')) branch = line.slice('branch refs/heads/'.length);
116
+ }
117
+ return wtPath ? { path: wtPath, branch } : null;
118
+ }
119
+
120
+ /**
121
+ * GitHub tier: collect head refs of merged PRs (one memoized call).
122
+ * Returns an empty Set when gh is unavailable or errors — a safe no-signal.
123
+ * @param {Function} runFile - execFileSync-compatible function
124
+ * @returns {Map<string, string>} Map of merged PR head branch name -> head commit OID
125
+ */
126
+ function getGhMergedRefs(runFile) {
127
+ const out = tryRun(runFile, 'gh', ['pr', 'list', '--state', 'merged', '--json', 'headRefName,headRefOid', '--limit', '200']);
128
+ if (!out) return new Map();
129
+ try {
130
+ const arr = JSON.parse(out);
131
+ if (!Array.isArray(arr)) return new Map();
132
+ const map = new Map();
133
+ // Record the head OID too: a reused/advanced branch name must NOT be treated as
134
+ // merged unless its current tip still matches the OID GitHub merged.
135
+ for (const p of arr) {
136
+ if (p && p.headRefName && p.headRefOid) map.set(p.headRefName, p.headRefOid);
137
+ }
138
+ return map;
139
+ } catch (_e) { /* intentional: malformed gh output → no signal */ // NOSONAR S2486
140
+ return new Map();
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Squash-merge tier (git-only, deterministic). A squash-merged branch tip is
146
+ * NOT an ancestor of the default branch, so `git branch --merged` misses it.
147
+ * Instead, synthesize a single commit from the branch's tree onto the
148
+ * merge-base and ask `git cherry` whether the default branch already contains a
149
+ * patch-equivalent commit. `git cherry` prints `- <sha>` when an equivalent
150
+ * exists (i.e. the branch was squash-merged) and `+ <sha>` otherwise.
151
+ * Empty output / any error → treated as NOT merged (never remove on doubt).
152
+ * @param {string} branch - Branch under test
153
+ * @param {string} defaultBranch - Default branch name
154
+ * @param {Function} runFile - execFileSync-compatible function
155
+ * @returns {boolean} True iff the branch is patch-equivalent-merged into default
156
+ */
157
+ function isSquashMerged(branch, defaultBranch, runFile) {
158
+ const mergeBase = tryRun(runFile, 'git', ['merge-base', defaultBranch, branch]);
159
+ if (!mergeBase) return false;
160
+ const tree = tryRun(runFile, 'git', ['rev-parse', `${branch}^{tree}`]);
161
+ if (!tree) return false;
162
+ const synthetic = tryRun(runFile, 'git', ['commit-tree', tree, '-p', mergeBase, '-m', '_']);
163
+ if (!synthetic) return false;
164
+ const cherry = tryRun(runFile, 'git', ['cherry', defaultBranch, synthetic]);
165
+ // A single `- <sha>` line means the combined diff is already in `default`.
166
+ return cherry.startsWith('-');
167
+ }
168
+
169
+ /**
170
+ * Squash-aware merged detection over three short-circuiting tiers:
171
+ * (a) ancestry list (`git branch --merged`), (b) merged-PR head refs (gh),
172
+ * (c) git-only squash patch-equivalence. A branch confirmed by none stays.
173
+ * @param {string} branch - Branch name
174
+ * @param {object} ctx - Detection context
175
+ * @param {string} ctx.defaultBranch - Default branch name
176
+ * @param {string[]} ctx.mergedBranches - Ancestry-merged branch names
177
+ * @param {Map<string, string>} ctx.ghMergedRefs - Merged PR head branch -> head OID
178
+ * @param {Function} ctx.runFile - execFileSync-compatible function
179
+ * @returns {boolean} True iff the branch is merged (any tier)
180
+ */
181
+ function detectMerged(branch, ctx) {
182
+ if (ctx.mergedBranches.includes(branch)) return true;
183
+ if (ctx.ghMergedRefs) {
184
+ const mergedOid = ctx.ghMergedRefs.get(branch);
185
+ // Only trust the gh "merged" signal when the merged PR's head OID still matches
186
+ // the branch tip; a reused/advanced branch name with new commits falls through
187
+ // to the squash patch-equivalence check (never remove on doubt).
188
+ if (mergedOid) {
189
+ const tip = tryRun(ctx.runFile, 'git', ['rev-parse', branch]);
190
+ if (tip && tip === mergedOid) return true;
191
+ }
192
+ }
193
+ return isSquashMerged(branch, ctx.defaultBranch, ctx.runFile);
194
+ }
195
+
196
+ /**
197
+ * Whether a worktree has uncommitted (staged or unstaged) changes.
198
+ * Merged branches have their commits in the default branch already, so the only
199
+ * loss risk is uncommitted working-tree edits — those block removal.
200
+ * @param {string} wtPath - Absolute worktree path
201
+ * @param {Function} runFile - execFileSync-compatible function
202
+ * @returns {boolean} True iff dirty
203
+ */
204
+ function isWorktreeDirty(wtPath, runFile) {
205
+ try {
206
+ const out = runFile('git', ['-C', wtPath, 'status', '--porcelain'], { stdio: 'pipe' }).toString().trim();
207
+ return out.length > 0;
208
+ } catch (_e) { /* status unknowable → treat as UNSAFE (dirty) so we never remove on doubt */ // NOSONAR S2486
209
+ return true;
210
+ }
211
+ }
212
+
213
+ const DEFAULT_MAX_TRIES = 3;
214
+
215
+ /**
216
+ * Windows-robust worktree removal. FS locks on Windows frequently fail
217
+ * `git worktree remove` with "Directory not empty" / "Permission denied", so:
218
+ * 1. retry plain remove with backoff,
219
+ * 2. fall back to `--force`,
220
+ * 3. `git worktree prune` + manual recursive dir removal.
221
+ * Never throws — returns a structured outcome so survivors can be reported.
222
+ * @param {string} wtPath - Absolute worktree path
223
+ * @param {Function} runFile - execFileSync-compatible function
224
+ * @param {object} fsApi - fs-compatible module
225
+ * @param {object} opts - Injection: _sleep, _maxTries
226
+ * @returns {Promise<{ removed: boolean, method?: string, error?: string }>}
227
+ */
228
+ async function removeWorktreeRobust(wtPath, runFile, fsApi, opts = {}) {
229
+ const sleep = opts._sleep || (ms => new Promise(resolve => setTimeout(resolve, ms)));
230
+ const maxTries = Number.isInteger(opts._maxTries) ? opts._maxTries : DEFAULT_MAX_TRIES;
231
+ const errors = [];
232
+
233
+ for (let attempt = 0; attempt < maxTries; attempt++) {
234
+ try {
235
+ runFile('git', ['worktree', 'remove', wtPath], { stdio: 'pipe' });
236
+ return { removed: true, method: 'remove' };
237
+ } catch (err) {
238
+ errors.push(err.message);
239
+ if (attempt < maxTries - 1) await sleep(200 * (attempt + 1));
240
+ }
241
+ }
242
+
243
+ try {
244
+ runFile('git', ['worktree', 'remove', '--force', wtPath], { stdio: 'pipe' });
245
+ return { removed: true, method: 'force' };
246
+ } catch (err) { errors.push(err.message); }
247
+
248
+ // Last resort: prune the ref, then manually remove the directory.
249
+ try {
250
+ runFile('git', ['worktree', 'prune'], { stdio: 'pipe' });
251
+ if (fsApi.existsSync(wtPath)) {
252
+ fsApi.rmSync(wtPath, { recursive: true, force: true });
253
+ }
254
+ if (!fsApi.existsSync(wtPath)) {
255
+ runFile('git', ['worktree', 'prune'], { stdio: 'pipe' });
256
+ return { removed: true, method: 'prune+rm' };
257
+ }
258
+ } catch (err) { errors.push(err.message); }
259
+
260
+ return { removed: false, error: errors[errors.length - 1] || 'unknown removal failure' };
261
+ }
262
+
263
+ /**
264
+ * Categorize + (optionally) remove a single worktree directory.
67
265
  * @param {string} dir - Directory name within .worktrees/
68
266
  * @param {Map<string, string>} worktreeMap - Path-to-branch mapping
69
- * @param {string[]} mergedBranches - List of merged branch names
267
+ * @param {Function} isMergedFn - (branch) => boolean squash-aware detector
70
268
  * @param {string} worktreesDir - Absolute path to .worktrees/
71
269
  * @param {boolean} dryRun - If true, skip actual removal
72
270
  * @param {Function} runFile - execFileSync-compatible function
73
- * @param {object} fsApi - fs module (for DI)
74
- * @returns {Promise<boolean>} True if the worktree was cleaned (or would be in dry-run)
271
+ * @param {object} fsApi - fs-compatible module
272
+ * @param {object} opts - Removal injection options
273
+ * @returns {Promise<{ status: string, path: string, branch: string|null, error?: string }>}
75
274
  */
76
- async function cleanWorktree(dir, worktreeMap, mergedBranches, worktreesDir, dryRun, runFile, fsApi) {
275
+ async function cleanWorktree(dir, worktreeMap, isMergedFn, worktreesDir, dryRun, runFile, fsApi, opts) {
77
276
  const wtPath = path.resolve(worktreesDir, dir);
78
- const branch = worktreeMap.get(wtPath);
277
+ // Lookup by canonical key (86b04c20): porcelain emits C:/ paths, resolve emits C:\.
278
+ const branch = worktreeMap.get(normalizeWorktreeKey(wtPath)) || null;
79
279
 
80
- if (branch && mergedBranches.includes(branch)) {
81
- if (!dryRun) {
82
- const stopResult = stopDolt(wtPath, { _exec: runFile, _fs: fsApi });
83
- if (stopResult.stopped) {
84
- await new Promise(r => setTimeout(r, 500));
85
- }
280
+ if (!branch || !isMergedFn(branch)) {
281
+ return { status: 'active', path: wtPath, branch };
282
+ }
283
+
284
+ // Merged, but never blow away uncommitted local edits. Checked BEFORE the dry-run
285
+ // branch so a dry run reports the same "dirty" skip the real run would take.
286
+ if (isWorktreeDirty(wtPath, runFile)) {
287
+ return { status: 'dirty', path: wtPath, branch };
288
+ }
289
+
290
+ if (dryRun) {
291
+ return { status: 'cleaned', path: wtPath, branch };
292
+ }
293
+
294
+ const outcome = await removeWorktreeRobust(wtPath, runFile, fsApi, opts);
295
+ if (outcome.removed) {
296
+ return { status: 'cleaned', path: wtPath, branch, method: outcome.method };
297
+ }
298
+ return { status: 'survivor', path: wtPath, branch, error: outcome.error };
299
+ }
300
+
301
+ /**
302
+ * Parse the file list from git's "untracked working tree files would be
303
+ * overwritten by merge" error text.
304
+ * @param {string} stderr - Combined stderr text from the failed merge
305
+ * @returns {string[]} Repo-relative file paths
306
+ */
307
+ function parseUntrackedOverwrites(stderr) {
308
+ const files = [];
309
+ let collecting = false;
310
+ for (const raw of stderr.split('\n')) {
311
+ const line = raw.replace(/\r$/, '');
312
+ if (/would be overwritten by merge:/i.test(line)) { collecting = true; continue; }
313
+ if (/^please move or remove them|^aborting/i.test(line.trim())) { collecting = false; continue; }
314
+ if (collecting) {
315
+ const f = line.trim();
316
+ if (f) files.push(f);
317
+ }
318
+ }
319
+ return files;
320
+ }
321
+
322
+ /**
323
+ * Move would-be-overwritten untracked files into a timestamped backup dir so a
324
+ * fast-forward can proceed without losing local work.
325
+ * @returns {string[]} Relative paths that were backed up
326
+ */
327
+ function backupUntracked(fsApi, mainPath, stderr, opts) {
328
+ const files = parseUntrackedOverwrites(stderr);
329
+ if (files.length === 0) return [];
330
+ const stamp = opts && opts._now ? opts._now() : Date.now();
331
+ const backupDir = path.join(mainPath, '.forge', `clean-backup-${stamp}`);
332
+ const moved = [];
333
+ for (const rel of files) {
334
+ try {
335
+ const dest = path.join(backupDir, rel);
336
+ fsApi.mkdirSync(path.dirname(dest), { recursive: true });
337
+ fsApi.renameSync(path.join(mainPath, rel), dest);
338
+ moved.push(rel);
339
+ } catch (_e) { /* intentional: best-effort per-file backup */ } // NOSONAR S2486
340
+ }
341
+ return moved;
342
+ }
343
+
344
+ /**
345
+ * Count revisions in a range (e.g. `main..origin/main`). Returns null on error/no
346
+ * signal so callers can distinguish "verified 0 commits" from "could not verify"
347
+ * (a successful rev-list --count always prints at least "0").
348
+ */
349
+ function countRevs(runFile, cwd, range) {
350
+ const out = tryRun(runFile, 'git', ['-C', cwd, 'rev-list', '--count', range]);
351
+ if (out === '') return null;
352
+ const n = parseInt(out, 10);
353
+ return Number.isFinite(n) ? n : null;
354
+ }
355
+
356
+ /**
357
+ * Fast-forward the checked-out default branch to origin, backing up any
358
+ * untracked files that would block the merge.
359
+ */
360
+ function fastForwardCheckedOut(runFile, fsApi, mainPath, defaultBranch, opts) {
361
+ try {
362
+ runFile('git', ['-C', mainPath, 'merge', '--ff-only', `origin/${defaultBranch}`], { stdio: 'pipe' });
363
+ return { synced: true, method: 'ff' };
364
+ } catch (err) {
365
+ const stderr = err && err.stderr ? err.stderr.toString() : (err && err.message) || '';
366
+ if (/untracked working tree files would be overwritten/i.test(stderr)) {
367
+ const backedUp = backupUntracked(fsApi, mainPath, stderr, opts);
86
368
  try {
87
- runFile('git', ['worktree', 'remove', wtPath], { stdio: 'pipe' });
88
- } catch (_removeErr) { /* intentional: worktree has uncommitted changes, skip */ // NOSONAR S2486
89
- return false;
369
+ runFile('git', ['-C', mainPath, 'merge', '--ff-only', `origin/${defaultBranch}`], { stdio: 'pipe' });
370
+ return { synced: true, method: 'ff-after-backup', backedUp };
371
+ } catch (err2) {
372
+ return { synced: false, reason: 'ff-failed-after-backup', error: err2.message, backedUp };
90
373
  }
91
374
  }
92
- return true;
375
+ return { synced: false, reason: 'ff-failed', error: err && err.message };
93
376
  }
94
- return false;
377
+ }
378
+
379
+ /**
380
+ * Post-merge master auto-update: fetch origin and fast-forward the MAIN
381
+ * checkout's default branch when it is strictly behind (never on divergence).
382
+ * Untracked local files are preserved (backed up if they would block the FF).
383
+ * Default-on; opt out with `--no-master-sync`. Never throws.
384
+ * @returns {Promise<object>} Structured outcome for reporting
385
+ */
386
+ async function syncMasterBranch(runFile, fsApi, opts = {}) {
387
+ const result = { attempted: true, synced: false };
388
+ try {
389
+ const listOut = tryRun(runFile, 'git', ['worktree', 'list', '--porcelain']);
390
+ const main = parseMainWorktree(listOut);
391
+ if (!main) { result.reason = 'no-main-worktree'; return result; }
392
+
393
+ const defaultBranch = getDefaultBranch(runFile);
394
+ result.defaultBranch = defaultBranch;
395
+
396
+ // Refresh origin/<default> without touching the working tree. A fetch failure is
397
+ // surfaced (not swallowed into a misleading "up-to-date").
398
+ try {
399
+ runFile('git', ['-C', main.path, 'fetch', 'origin', defaultBranch], { stdio: 'pipe' });
400
+ } catch (err) {
401
+ result.reason = 'fetch-failed';
402
+ result.error = err && err.message;
403
+ return result;
404
+ }
405
+
406
+ const behind = countRevs(runFile, main.path, `${defaultBranch}..origin/${defaultBranch}`);
407
+ const ahead = countRevs(runFile, main.path, `origin/${defaultBranch}..${defaultBranch}`);
408
+ // null = rev-list could not verify the range; do NOT claim up-to-date on doubt.
409
+ if (behind === null || ahead === null) { result.reason = 'rev-list-failed'; return result; }
410
+ result.behind = behind;
411
+ result.ahead = ahead;
412
+
413
+ if (behind === 0) { result.reason = 'up-to-date'; return result; }
414
+ if (ahead > 0) { result.reason = 'diverged'; return result; } // not a fast-forward — leave it
415
+
416
+ if (main.branch === defaultBranch) {
417
+ Object.assign(result, fastForwardCheckedOut(runFile, fsApi, main.path, defaultBranch, opts));
418
+ return result;
419
+ }
420
+
421
+ // Main checkout is on a feature branch: fast-forward the local ref without checkout.
422
+ try {
423
+ runFile('git', ['-C', main.path, 'fetch', 'origin', `${defaultBranch}:${defaultBranch}`], { stdio: 'pipe' });
424
+ result.synced = true;
425
+ result.method = 'ref-update';
426
+ } catch (err) {
427
+ result.reason = 'ref-update-failed';
428
+ result.error = err && err.message;
429
+ }
430
+ return result;
431
+ } catch (err) {
432
+ result.reason = 'error';
433
+ result.error = err && err.message;
434
+ return result;
435
+ }
436
+ }
437
+
438
+ /** Append the dirty-worktree lines (extracted to keep formatOutput simple). */
439
+ function renderDirty(dirty, lines) {
440
+ if (!dirty || dirty.length === 0) return;
441
+ lines.push(`Skipped ${dirty.length} dirty worktree(s) with uncommitted changes:`);
442
+ for (const p of dirty) lines.push(` - ${p}`);
443
+ }
444
+
445
+ /** Append the survivor warning lines. */
446
+ function renderSurvivors(survivors, lines) {
447
+ if (!survivors || survivors.length === 0) return;
448
+ lines.push(`WARNING: ${survivors.length} merged worktree(s) could not be removed (manual cleanup needed):`);
449
+ for (const s of survivors) lines.push(` - ${s.path}${s.error ? ` (${s.error})` : ''}`);
450
+ }
451
+
452
+ /** Append the master-sync outcome line(s). */
453
+ function renderMasterSync(masterSync, lines) {
454
+ if (!masterSync || !masterSync.attempted) return;
455
+ const ms = masterSync;
456
+ if (ms.synced) {
457
+ lines.push(`Fast-forwarded ${ms.defaultBranch} (${ms.behind} commit(s) behind, method: ${ms.method}).`);
458
+ if (ms.backedUp && ms.backedUp.length > 0) {
459
+ lines.push(` Backed up ${ms.backedUp.length} untracked file(s) before fast-forward.`);
460
+ }
461
+ } else if (ms.reason && ms.reason !== 'up-to-date' && ms.reason !== 'no-main-worktree') {
462
+ lines.push(`Master sync skipped (${ms.reason}${ms.error ? `: ${ms.error}` : ''}).`);
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Build the user-facing report line(s) for the clean run.
468
+ */
469
+ function formatOutput(summary) {
470
+ const lines = [];
471
+ const verb = summary.dryRun ? 'Would remove' : 'Removed';
472
+ lines.push(`${verb} ${summary.cleaned} merged worktree(s); ${summary.active} active kept.`);
473
+ renderDirty(summary.dirty, lines);
474
+ renderSurvivors(summary.survivors, lines);
475
+ renderMasterSync(summary.masterSync, lines);
476
+ return lines.join('\n');
95
477
  }
96
478
 
97
479
  /**
@@ -102,80 +484,108 @@ async function cleanWorktree(dir, worktreeMap, mergedBranches, worktreesDir, dry
102
484
  * @param {object} [opts] - Options for dependency injection
103
485
  * @param {Function} [opts._exec] - Override for execFileSync (testing)
104
486
  * @param {object} [opts._fs] - Override for fs module (testing)
105
- * @returns {Promise<{ success: boolean, cleaned: number, active: number, dryRun: boolean }>}
487
+ * @param {Function} [opts._isMerged] - Override merged detection (testing)
488
+ * @param {Function} [opts._syncMaster] - Override master sync (testing)
489
+ * @returns {Promise<object>} Structured result
106
490
  */
107
- async function handler(_args, flags, projectRoot, opts = {}) {
108
- const runFile = opts._exec || execFileSync;
109
- const fsApi = opts._fs || fs;
110
- const dryRun = !!(flags['--dry-run'] || flags.dryRun);
111
-
112
- const worktreesDir = path.resolve(projectRoot, '.worktrees');
113
-
114
- // If .worktrees/ doesn't exist, nothing to clean
115
- if (!fsApi.existsSync(worktreesDir)) {
116
- return { success: true, cleaned: 0, active: 0, dryRun };
117
- }
491
+ /**
492
+ * Scan .worktrees/ and remove the merged ones (squash-aware). Returns the tallies;
493
+ * a no-op ({0,0,[],[]}) when .worktrees/ is absent or empty. Extracted from handler
494
+ * so the top-level orchestration stays under the complexity gate.
495
+ * @returns {Promise<{ cleaned: number, active: number, survivors: object[], dirty: string[] }>}
496
+ */
497
+ async function cleanWorktrees(worktreesDir, runFile, fsApi, dryRun, opts) {
498
+ const acc = { cleaned: 0, active: 0, survivors: [], dirty: [] };
499
+ if (!fsApi.existsSync(worktreesDir)) return acc;
118
500
 
119
- // 1. List dirs in .worktrees/
120
501
  const entries = fsApi.readdirSync(worktreesDir, { withFileTypes: true });
121
502
  const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
503
+ if (dirs.length === 0) return acc;
122
504
 
123
- if (dirs.length === 0) {
124
- return { success: true, cleaned: 0, active: 0, dryRun };
125
- }
126
-
127
- // 2. Detect default branch, then get merged branches
505
+ // Detection context: ancestry list + gh merged refs + squash fallback.
128
506
  const defaultBranch = getDefaultBranch(runFile);
129
- let mergedBranches;
130
- try {
131
- const mergedOutput = runFile('git', ['branch', '--merged', defaultBranch], { stdio: 'pipe' });
132
- mergedBranches = mergedOutput
133
- .toString()
134
- .split('\n')
135
- .map(b => b.trim().replace(/^\*\s*/, ''))
136
- .filter(b => b.length > 0);
137
- } catch (_e) { /* intentional: fallback to empty list */ // NOSONAR S2486
138
- mergedBranches = [];
139
- }
140
-
141
- // 3. Get worktree -> branch mapping from git
142
- let worktreeMap;
143
- try {
144
- const listOutput = runFile('git', ['worktree', 'list', '--porcelain'], { stdio: 'pipe' });
145
- worktreeMap = parseWorktreeList(listOutput.toString());
146
- } catch (_e) { /* intentional: fallback to empty map */ // NOSONAR S2486
147
- worktreeMap = new Map();
507
+ let mergedBranches = [];
508
+ const mergedOut = tryRun(runFile, 'git', ['branch', '--merged', defaultBranch]);
509
+ if (mergedOut) {
510
+ // Strip the current-branch `*` and the linked-worktree `+` prefixes that
511
+ // `git branch --merged` emits, so worktree-checked-out branches still hit the
512
+ // ancestry fast path instead of falling through to the slower squash tier.
513
+ mergedBranches = mergedOut.split('\n').map(b => b.trim().replace(/^[*+]\s*/, '')).filter(Boolean);
148
514
  }
515
+ const ghMergedRefs = getGhMergedRefs(runFile);
516
+ const ctx = { defaultBranch, mergedBranches, ghMergedRefs, runFile };
517
+ const isMergedFn = opts._isMerged || (branch => detectMerged(branch, ctx));
149
518
 
150
- // 4. For each worktree dir, check if its branch is merged
151
- let cleaned = 0;
152
- let active = 0;
519
+ const listOutput = tryRun(runFile, 'git', ['worktree', 'list', '--porcelain']);
520
+ const worktreeMap = listOutput ? parseWorktreeList(listOutput) : new Map();
153
521
 
154
522
  for (const dir of dirs) {
155
- const wasCleaned = await cleanWorktree(dir, worktreeMap, mergedBranches, worktreesDir, dryRun, runFile, fsApi);
156
- if (wasCleaned) {
157
- cleaned++;
158
- } else {
159
- active++;
160
- }
523
+ const res = await cleanWorktree(dir, worktreeMap, isMergedFn, worktreesDir, dryRun, runFile, fsApi, opts);
524
+ if (res.status === 'cleaned') acc.cleaned++;
525
+ else if (res.status === 'survivor') acc.survivors.push(res);
526
+ else if (res.status === 'dirty') acc.dirty.push(res.path);
527
+ else acc.active++;
161
528
  }
162
529
 
163
- // Prune stale worktree refs to prevent bare repo state
164
- if (!dryRun && cleaned > 0) {
165
- try {
166
- runFile('git', ['worktree', 'prune'], { stdio: 'pipe' });
167
- } catch (_e) { /* intentional: prune is best-effort cleanup */ } // NOSONAR S2486
530
+ if (!dryRun && acc.cleaned > 0) {
531
+ tryRun(runFile, 'git', ['worktree', 'prune']);
168
532
  }
533
+ return acc;
534
+ }
169
535
 
170
- return { success: true, cleaned, active, dryRun };
536
+ async function handler(_args, flags, projectRoot, opts = {}) {
537
+ const runFile = opts._exec || execFileSync;
538
+ const fsApi = opts._fs || fs;
539
+ const dryRun = !!(flags['--dry-run'] || flags.dryRun);
540
+ const masterSyncEnabled = !(flags['--no-master-sync'] || flags.noMasterSync);
541
+ const worktreesDir = path.resolve(projectRoot, '.worktrees');
542
+
543
+ // Worktree cleanup (no-op when .worktrees/ absent); master auto-update is independent.
544
+ const { cleaned, active, survivors, dirty } = await cleanWorktrees(worktreesDir, runFile, fsApi, dryRun, opts);
545
+
546
+ // Post-merge master auto-update (default-on; skipped in dry-run).
547
+ let masterSync = null;
548
+ if (masterSyncEnabled && !dryRun) {
549
+ const doSync = opts._syncMaster || (() => syncMasterBranch(runFile, fsApi, opts));
550
+ masterSync = await doSync();
551
+ }
552
+
553
+ return finalize({ success: true, cleaned, active, dryRun, survivors, dirty }, masterSync);
554
+ }
555
+
556
+ /**
557
+ * Attach the master-sync outcome + a rendered report to the result.
558
+ */
559
+ function finalize(summary, masterSync) {
560
+ const withSync = { ...summary, masterSync };
561
+ return { ...withSync, output: formatOutput(withSync) };
171
562
  }
172
563
 
173
564
  module.exports = {
174
565
  name: 'clean',
175
- description: 'Remove worktrees for merged branches (stops Dolt servers)',
176
- usage: 'forge clean [--dry-run]',
566
+ description: 'Remove worktrees for merged branches (squash-aware) and fast-forward the default branch',
567
+ usage: 'forge clean [--dry-run] [--no-master-sync]',
177
568
  flags: {
178
569
  '--dry-run': 'Show what would be cleaned without removing',
570
+ '--no-master-sync': 'Do not fast-forward the main checkout default branch after cleaning',
179
571
  },
180
572
  handler,
573
+ // Exported for unit tests / reuse.
574
+ _internals: {
575
+ getDefaultBranch,
576
+ normalizeWorktreeKey,
577
+ parseWorktreeList,
578
+ parseMainWorktree,
579
+ getGhMergedRefs,
580
+ isSquashMerged,
581
+ detectMerged,
582
+ isWorktreeDirty,
583
+ removeWorktreeRobust,
584
+ cleanWorktree,
585
+ cleanWorktrees,
586
+ parseUntrackedOverwrites,
587
+ backupUntracked,
588
+ syncMasterBranch,
589
+ formatOutput,
590
+ },
181
591
  };
@@ -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('close');
5
+ module.exports = createIssueSubcommand('close');
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ const { createIssueSubcommand } = require('./_issue');
4
+
5
+ module.exports = createIssueSubcommand('comment');