forge-workflow 0.0.9 → 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 (479) 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 +151 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -106
  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 +466 -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/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
  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/reference/TOOLCHAIN.md +658 -0
  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 +225 -28
  81. package/lib/beads-sync-scaffold.js +36 -107
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +744 -70
  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 +66 -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 +22 -2
  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 +851 -979
  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 +329 -11
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +15 -2
  142. package/lib/commands/test.js +58 -7
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +56 -25
  146. package/lib/commands/worktree.js +308 -128
  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 +184 -0
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +42 -17
  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 +697 -0
  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/issue-sync/authority.js +100 -0
  174. package/lib/issue-sync/github-pull.js +184 -0
  175. package/lib/issue-sync/import-primitives.js +98 -0
  176. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  177. package/lib/issue-sync/link-store.js +292 -0
  178. package/lib/issue-sync/project-github.js +123 -0
  179. package/lib/issue-sync/reconcile.js +195 -0
  180. package/lib/issue-sync/schema.js +126 -0
  181. package/lib/kernel/backing-issue.js +305 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/planning-buckets-schema.js +109 -0
  192. package/lib/kernel/projection-jsonl-writer.js +450 -0
  193. package/lib/kernel/readiness-model.js +329 -0
  194. package/lib/kernel/schema.js +356 -0
  195. package/lib/kernel/sqlite-driver.js +2504 -0
  196. package/lib/kernel/taxonomy-validator.js +394 -0
  197. package/lib/lefthook-check.js +8 -4
  198. package/lib/lefthook-wiring.js +413 -0
  199. package/lib/mcp-config-renderer.js +288 -0
  200. package/lib/memory/graphiti-mcp.js +106 -0
  201. package/lib/memory/router.js +387 -0
  202. package/lib/memory/typed-api.js +102 -0
  203. package/lib/memory-digest.js +195 -0
  204. package/lib/merge-rules.js +395 -0
  205. package/lib/migrate-dry-run.js +466 -0
  206. package/lib/orientation.js +863 -0
  207. package/lib/package-manager-remediation.js +103 -0
  208. package/lib/package-root.js +381 -0
  209. package/lib/patch-intent.js +890 -0
  210. package/lib/plugin-catalog.js +3 -4
  211. package/lib/plugin-manager.js +0 -5
  212. package/lib/pr-bundle.js +186 -0
  213. package/lib/pr-monitor/differ.js +195 -0
  214. package/lib/pr-monitor/events.js +0 -0
  215. package/lib/pr-monitor/gather.js +124 -0
  216. package/lib/pr-monitor/journal.js +299 -0
  217. package/lib/pr-monitor/monitor.js +146 -0
  218. package/lib/pr-monitor/render-sticky.js +157 -0
  219. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  220. package/lib/pr-monitor/watch.js +247 -0
  221. package/lib/pr-pull.js +1273 -0
  222. package/lib/pr-shepherd.js +494 -0
  223. package/lib/pr-state-validator.js +59 -0
  224. package/lib/preflight/gates.js +237 -0
  225. package/lib/preflight/runner.js +116 -0
  226. package/lib/project-discovery.js +0 -53
  227. package/lib/project-memory.js +166 -0
  228. package/lib/protected-path-manifest.js +281 -0
  229. package/lib/protected-state-surfaces.js +387 -0
  230. package/lib/release-readiness.js +2089 -0
  231. package/lib/reset.js +59 -45
  232. package/lib/review-adapter.js +68 -0
  233. package/lib/rules-sync.js +260 -0
  234. package/lib/runtime-health.js +332 -23
  235. package/lib/safety-config-renderer.js +268 -0
  236. package/lib/setup-action-log.js +1 -7
  237. package/lib/setup.js +27 -65
  238. package/lib/shell-utils.js +76 -6
  239. package/lib/skills-sync.js +330 -0
  240. package/lib/smart-status/conflicts.js +205 -0
  241. package/lib/smart-status/scoring.js +191 -0
  242. package/lib/status/beads-snapshot.js +145 -0
  243. package/lib/status/presenter.js +216 -0
  244. package/lib/status/snapshot.js +186 -0
  245. package/lib/sync-backend.js +202 -0
  246. package/lib/untrusted-content.js +52 -0
  247. package/lib/upgrade-safety.js +199 -0
  248. package/lib/workflow/enforce-stage.js +298 -47
  249. package/lib/workflow/stage-transition.js +115 -0
  250. package/lib/workflow/stages.js +30 -6
  251. package/lib/workflow/state-manager.js +159 -14
  252. package/lib/workflow/state.js +23 -1
  253. package/lib/workflow-profiles.js +17 -5
  254. package/package.json +46 -36
  255. package/rules/documentation.md +19 -0
  256. package/rules/kernel-tracking.md +26 -0
  257. package/rules/security.md +22 -0
  258. package/rules/tdd.md +20 -0
  259. package/rules/workflow.md +27 -0
  260. package/scripts/auto-backing-issue.js +47 -0
  261. package/scripts/beads-context.sh +165 -22
  262. package/scripts/beads-migrate-to-dolt.sh +7 -0
  263. package/scripts/beads-upgrade-smoke.sh +284 -0
  264. package/scripts/behavioral-judge.sh +115 -11
  265. package/scripts/benchmark.js +349 -63
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard-analyze.js +52 -17
  272. package/scripts/dep-guard-keyword-ripple.js +29 -0
  273. package/scripts/dep-guard-render-review.js +86 -0
  274. package/scripts/dep-guard.sh +64 -232
  275. package/scripts/file-index.sh +3 -0
  276. package/scripts/forge-team/lib/claim.sh +34 -18
  277. package/scripts/forge-team/lib/dashboard.sh +61 -86
  278. package/scripts/forge-team/lib/epic.sh +99 -263
  279. package/scripts/forge-team/lib/hooks.sh +26 -28
  280. package/scripts/forge-team/lib/identity.sh +4 -4
  281. package/scripts/forge-team/lib/sync-github.sh +144 -47
  282. package/scripts/forge-team/lib/verify.sh +93 -83
  283. package/scripts/forge-team/lib/workload.sh +41 -65
  284. package/scripts/forge-team/tests/claim.test.sh +25 -19
  285. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  286. package/scripts/forge-team/tests/epic.test.sh +52 -71
  287. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  288. package/scripts/forge-team/tests/identity.test.sh +3 -3
  289. package/scripts/forge-team/tests/integration.test.sh +44 -66
  290. package/scripts/forge-team/tests/sync-github.test.sh +183 -79
  291. package/scripts/forge-team/tests/verify.test.sh +37 -46
  292. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  293. package/scripts/forge-team/tests/workload.test.sh +32 -66
  294. package/scripts/gen-command-manifest.js +153 -0
  295. package/scripts/gen-embedded-assets.mjs +129 -0
  296. package/scripts/install.ps1 +139 -0
  297. package/scripts/install.sh +268 -0
  298. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  299. package/scripts/lib/release-asset.mjs +84 -0
  300. package/scripts/parity-check.mjs +145 -0
  301. package/scripts/parity-check.test.mjs +58 -0
  302. package/scripts/pin-agentic-workflow-images.js +112 -0
  303. package/scripts/pr-coordinator.sh +3 -0
  304. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  305. package/scripts/preflight.sh +108 -0
  306. package/scripts/protected-state-check.js +104 -0
  307. package/scripts/smart-status-score.js +31 -0
  308. package/scripts/smart-status-sessions.js +51 -0
  309. package/scripts/smart-status.sh +117 -369
  310. package/scripts/spikes/config-race-bench.js +111 -0
  311. package/scripts/spikes/harness-capability-matrix.js +13 -0
  312. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  313. package/scripts/spikes/protected-path-manifest.js +20 -0
  314. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  315. package/scripts/sync-agent-skills.js +62 -0
  316. package/scripts/sync-agentic-workflow.js +48 -0
  317. package/scripts/sync-utils.sh +3 -0
  318. package/scripts/test-ci-shard.js +251 -0
  319. package/scripts/test-dashboard.js +188 -52
  320. package/scripts/test-full-suite.js +186 -0
  321. package/scripts/test-profile.js +278 -0
  322. package/scripts/test.js +302 -28
  323. package/scripts/validate.js +143 -0
  324. package/scripts/validate.sh +18 -1
  325. package/skills/claim-safety/SKILL.md +102 -0
  326. package/skills/claim-safety/evals/evals.json +46 -0
  327. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
  328. package/skills/dev/evals/evals.json +50 -0
  329. package/skills/hermes-forge/SKILL.md +185 -0
  330. package/skills/hermes-forge/evals/evals.json +46 -0
  331. package/skills/issue-basics/SKILL.md +111 -0
  332. package/skills/issue-basics/evals/evals.json +46 -0
  333. package/skills/kernel/SKILL.md +166 -0
  334. package/skills/kernel/evals/evals.json +50 -0
  335. package/skills/memory/SKILL.md +102 -0
  336. package/skills/parallel-deep-research/SKILL.md +14 -11
  337. package/skills/parallel-deep-research/evals/evals.json +11 -27
  338. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
  339. package/skills/plan/evals/evals.json +42 -0
  340. package/skills/research/SKILL.md +195 -0
  341. package/skills/research/evals/evals.json +42 -0
  342. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  343. package/skills/review/evals/evals.json +42 -0
  344. package/skills/rollback/SKILL.md +110 -0
  345. package/skills/rollback/evals/evals.json +46 -0
  346. package/skills/rollback/references/methods.md +204 -0
  347. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  348. package/skills/shepherd/SKILL.md +66 -0
  349. package/skills/shepherd/evals/evals.json +42 -0
  350. package/skills/ship/SKILL.md +251 -0
  351. package/skills/ship/evals/evals.json +42 -0
  352. package/skills/smith/SKILL.md +142 -0
  353. package/skills/smith/evals/evals.json +46 -0
  354. package/skills/smith/references/autonomy-and-gates.md +94 -0
  355. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  356. package/skills/sonarcloud/evals/evals.json +46 -0
  357. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  358. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  359. package/skills/status/SKILL.md +102 -0
  360. package/skills/status/evals/evals.json +50 -0
  361. package/skills/triage-ready/SKILL.md +121 -0
  362. package/skills/triage-ready/evals/evals.json +42 -0
  363. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  364. package/skills/validate/evals/evals.json +42 -0
  365. package/skills/verify/SKILL.md +299 -0
  366. package/skills/verify/evals/evals.json +50 -0
  367. package/.claude/commands/dev.md +0 -345
  368. package/.claude/commands/plan.md +0 -566
  369. package/.claude/commands/premerge.md +0 -186
  370. package/.claude/commands/research.md +0 -42
  371. package/.claude/commands/review.md +0 -451
  372. package/.claude/commands/rollback.md +0 -721
  373. package/.claude/commands/ship.md +0 -213
  374. package/.claude/commands/sonarcloud.md +0 -152
  375. package/.claude/commands/status.md +0 -90
  376. package/.claude/commands/validate.md +0 -288
  377. package/.claude/commands/verify.md +0 -269
  378. package/.claude/rules/workflow.md +0 -121
  379. package/.cline/workflows/dev.md +0 -342
  380. package/.cline/workflows/plan.md +0 -563
  381. package/.cline/workflows/premerge.md +0 -183
  382. package/.cline/workflows/research.md +0 -39
  383. package/.cline/workflows/review.md +0 -448
  384. package/.cline/workflows/rollback.md +0 -718
  385. package/.cline/workflows/ship.md +0 -210
  386. package/.cline/workflows/sonarcloud.md +0 -146
  387. package/.cline/workflows/status.md +0 -87
  388. package/.cline/workflows/validate.md +0 -285
  389. package/.cline/workflows/verify.md +0 -266
  390. package/.codex/config.toml +0 -11
  391. package/.codex/skills/dev/SKILL.md +0 -345
  392. package/.codex/skills/plan/SKILL.md +0 -566
  393. package/.codex/skills/premerge/SKILL.md +0 -186
  394. package/.codex/skills/research/SKILL.md +0 -42
  395. package/.codex/skills/review/SKILL.md +0 -451
  396. package/.codex/skills/rollback/SKILL.md +0 -721
  397. package/.codex/skills/ship/SKILL.md +0 -213
  398. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  399. package/.codex/skills/status/SKILL.md +0 -90
  400. package/.codex/skills/validate/SKILL.md +0 -288
  401. package/.codex/skills/verify/SKILL.md +0 -269
  402. package/.cursor/commands/dev.md +0 -342
  403. package/.cursor/commands/plan.md +0 -563
  404. package/.cursor/commands/premerge.md +0 -183
  405. package/.cursor/commands/research.md +0 -39
  406. package/.cursor/commands/review.md +0 -448
  407. package/.cursor/commands/ship.md +0 -210
  408. package/.cursor/commands/sonarcloud.md +0 -146
  409. package/.cursor/commands/status.md +0 -87
  410. package/.cursor/commands/validate.md +0 -285
  411. package/.cursor/commands/verify.md +0 -266
  412. package/.cursorrules +0 -149
  413. package/.github/prompts/premerge.prompt.md +0 -188
  414. package/.github/prompts/research.prompt.md +0 -44
  415. package/.github/prompts/rollback.prompt.md +0 -723
  416. package/.github/prompts/ship.prompt.md +0 -215
  417. package/.github/prompts/status.prompt.md +0 -92
  418. package/.github/prompts/verify.prompt.md +0 -271
  419. package/.github/workflows/beads-to-github.yml +0 -56
  420. package/.github/workflows/github-to-beads.yml +0 -97
  421. package/.kilocode/workflows/dev.md +0 -346
  422. package/.kilocode/workflows/plan.md +0 -567
  423. package/.kilocode/workflows/premerge.md +0 -187
  424. package/.kilocode/workflows/research.md +0 -43
  425. package/.kilocode/workflows/review.md +0 -452
  426. package/.kilocode/workflows/rollback.md +0 -722
  427. package/.kilocode/workflows/ship.md +0 -214
  428. package/.kilocode/workflows/sonarcloud.md +0 -150
  429. package/.kilocode/workflows/status.md +0 -91
  430. package/.kilocode/workflows/validate.md +0 -289
  431. package/.kilocode/workflows/verify.md +0 -270
  432. package/.opencode/commands/dev.md +0 -345
  433. package/.opencode/commands/plan.md +0 -566
  434. package/.opencode/commands/premerge.md +0 -186
  435. package/.opencode/commands/research.md +0 -42
  436. package/.opencode/commands/review.md +0 -451
  437. package/.opencode/commands/rollback.md +0 -721
  438. package/.opencode/commands/ship.md +0 -213
  439. package/.opencode/commands/sonarcloud.md +0 -149
  440. package/.opencode/commands/status.md +0 -90
  441. package/.opencode/commands/validate.md +0 -288
  442. package/.opencode/commands/verify.md +0 -269
  443. package/.roo/commands/dev.md +0 -346
  444. package/.roo/commands/plan.md +0 -567
  445. package/.roo/commands/premerge.md +0 -187
  446. package/.roo/commands/research.md +0 -43
  447. package/.roo/commands/review.md +0 -452
  448. package/.roo/commands/rollback.md +0 -722
  449. package/.roo/commands/ship.md +0 -214
  450. package/.roo/commands/sonarcloud.md +0 -150
  451. package/.roo/commands/status.md +0 -91
  452. package/.roo/commands/validate.md +0 -289
  453. package/.roo/commands/verify.md +0 -270
  454. package/docs/BEADS_GITHUB_SYNC.md +0 -255
  455. package/docs/GREPTILE_SETUP.md +0 -400
  456. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  457. package/docs/SETUP.md +0 -663
  458. package/docs/VALIDATION.md +0 -363
  459. package/lib/agents/cline.plugin.json +0 -29
  460. package/lib/agents/copilot.plugin.json +0 -24
  461. package/lib/agents/kilocode.plugin.json +0 -22
  462. package/lib/agents/opencode.plugin.json +0 -23
  463. package/lib/agents/roo.plugin.json +0 -30
  464. package/lib/beads-health-check.js +0 -143
  465. package/lib/commands/commands-reset.js +0 -147
  466. package/opencode.json +0 -67
  467. package/scripts/beads-context.test.js +0 -567
  468. package/scripts/github-beads-sync/comment.mjs +0 -64
  469. package/scripts/github-beads-sync/config.mjs +0 -148
  470. package/scripts/github-beads-sync/github-api.mjs +0 -131
  471. package/scripts/github-beads-sync/index.mjs +0 -332
  472. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  473. package/scripts/github-beads-sync/mapping.mjs +0 -78
  474. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  475. package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
  476. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  477. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  478. package/scripts/github-beads-sync.config.json +0 -26
  479. 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');