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.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
package/lib/commands/clean.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
74
|
-
* @
|
|
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,
|
|
275
|
+
async function cleanWorktree(dir, worktreeMap, isMergedFn, worktreesDir, dryRun, runFile, fsApi, opts) {
|
|
77
276
|
const wtPath = path.resolve(worktreesDir, dir);
|
|
78
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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', ['
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
375
|
+
return { synced: false, reason: 'ff-failed', error: err && err.message };
|
|
93
376
|
}
|
|
94
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
};
|
package/lib/commands/close.js
CHANGED