forge-workflow 0.0.10 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +82 -5
- package/.forge/hooks/forge-native-hook.js +431 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +709 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +175 -0
- package/README.md +275 -365
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +532 -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/forge/TOOLCHAIN.md +670 -0
- 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 +121 -0
- package/docs/guides/SUPPORT.md +190 -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 +214 -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/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- 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 +155 -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/activation/ensure-forge-home.js +135 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1109 -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 +139 -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-detect.js +60 -0
- package/lib/beads-nudge.js +91 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +780 -77
- package/lib/commands/_manifest.js +93 -0
- package/lib/commands/_registry.js +99 -34
- package/lib/commands/_resolve-command-opts.js +230 -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 +209 -0
- package/lib/commands/hooks.js +377 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +604 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/memory.js +81 -0
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +362 -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 +481 -29
- package/lib/commands/pr.js +88 -0
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +135 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +171 -0
- package/lib/commands/recap.js +75 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +104 -0
- package/lib/commands/remember.js +140 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +900 -971
- package/lib/commands/shepherd.js +501 -0
- package/lib/commands/ship.js +59 -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 +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +362 -99
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +977 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- 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 +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +192 -0
- package/lib/global-flags.js +104 -0
- package/lib/greptile-match.js +7 -63
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +541 -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/kernel/backing-issue.js +311 -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/owned-kernel.js +43 -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 +2540 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- 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/auto-actions.js +175 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/digest.js +206 -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 +192 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1314 -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 +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2105 -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 +241 -20
- 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/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- 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 +251 -0
- package/lib/workflow/enforce-stage.js +351 -45
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- 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 +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- 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.sh +22 -3
- 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 +49 -84
- 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 +50 -83
- 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/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-auto-actions.js +93 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- 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-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- 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} +44 -50
- 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} +132 -157
- 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/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- 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/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- 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/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- 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 -281
- 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-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- 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 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- 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/worktree.js
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
const { execFileSync, spawnSync } = require('node:child_process');
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
|
-
const {
|
|
6
|
+
const { detectDefaultBranch } = require('../beads-sync-scaffold');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Forge Worktree Command
|
|
10
|
-
* Manage isolated worktrees
|
|
10
|
+
* Manage isolated worktrees.
|
|
11
11
|
* Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
|
|
12
12
|
*
|
|
13
|
+
* The Kernel issue store lives in the git common dir, which every worktree of a
|
|
14
|
+
* repo shares, so no per-worktree issue-store bootstrap is needed — a new
|
|
15
|
+
* worktree already sees the same kernel.
|
|
16
|
+
*
|
|
13
17
|
* Subcommands:
|
|
14
|
-
* create <slug> - Create worktree at .worktrees/<slug> with branch +
|
|
18
|
+
* create <slug> - Create worktree at .worktrees/<slug> with branch + install
|
|
15
19
|
* remove <slug> - Remove worktree via git worktree remove
|
|
16
20
|
*
|
|
17
21
|
* @module commands/worktree
|
|
@@ -24,7 +28,10 @@ const { bootstrapBeads } = require('../beads-bootstrap');
|
|
|
24
28
|
* @returns {string|null} Package manager command or null
|
|
25
29
|
*/
|
|
26
30
|
function detectPackageManager(projectRoot, fsApi) {
|
|
31
|
+
// Bun ships two lockfile formats: the legacy binary `bun.lockb` and the current
|
|
32
|
+
// text `bun.lock`. Recognize both so a Bun repo isn't mis-detected as npm.
|
|
27
33
|
if (fsApi.existsSync(path.join(projectRoot, 'bun.lockb'))) return 'bun';
|
|
34
|
+
if (fsApi.existsSync(path.join(projectRoot, 'bun.lock'))) return 'bun';
|
|
28
35
|
if (fsApi.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
29
36
|
if (fsApi.existsSync(path.join(projectRoot, 'yarn.lock'))) return 'yarn';
|
|
30
37
|
if (fsApi.existsSync(path.join(projectRoot, 'package-lock.json'))) return 'npm';
|
|
@@ -32,51 +39,6 @@ function detectPackageManager(projectRoot, fsApi) {
|
|
|
32
39
|
return null;
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
/**
|
|
36
|
-
* Stop a Dolt server running in a worktree to release file locks.
|
|
37
|
-
* Tries graceful `bd dolt stop` first, then falls back to PID-based kill
|
|
38
|
-
* from lock files. Never kills ALL Dolt processes - only the specific PID.
|
|
39
|
-
*
|
|
40
|
-
* @param {string} worktreePath - Absolute path to the worktree
|
|
41
|
-
* @param {object} [opts] - Options for dependency injection
|
|
42
|
-
* @param {Function} [opts._exec] - Override for execFileSync (testing)
|
|
43
|
-
* @param {object} [opts._fs] - Override for fs module (testing)
|
|
44
|
-
* @param {string} [opts._platform] - Override for process.platform (testing)
|
|
45
|
-
* @returns {{ stopped: boolean, method: string, pid?: number }}
|
|
46
|
-
*/
|
|
47
|
-
function stopDolt(worktreePath, opts = {}) {
|
|
48
|
-
const exec = opts._exec || execFileSync;
|
|
49
|
-
const _fs = opts._fs || fs;
|
|
50
|
-
const platform = opts._platform || process.platform;
|
|
51
|
-
|
|
52
|
-
// 1. Try graceful: bd dolt stop
|
|
53
|
-
try {
|
|
54
|
-
exec('bd', ['dolt', 'stop'], { cwd: worktreePath, timeout: 5000, stdio: 'pipe' });
|
|
55
|
-
return { stopped: true, method: 'bd-dolt-stop' };
|
|
56
|
-
} catch (_e) { /* intentional: bd not available, try PID-based kill */ } // NOSONAR S2486
|
|
57
|
-
|
|
58
|
-
// 2. Try PID-based kill from lock file
|
|
59
|
-
const lockPaths = [
|
|
60
|
-
path.join(worktreePath, '.beads', 'dolt-server.lock'),
|
|
61
|
-
path.join(worktreePath, '.beads', 'daemon.lock'),
|
|
62
|
-
];
|
|
63
|
-
for (const lockPath of lockPaths) {
|
|
64
|
-
try {
|
|
65
|
-
const pid = Number.parseInt(_fs.readFileSync(lockPath, 'utf-8').trim(), 10);
|
|
66
|
-
if (!Number.isNaN(pid)) {
|
|
67
|
-
if (platform === 'win32') {
|
|
68
|
-
exec('taskkill', ['/F', '/PID', String(pid)], { stdio: 'pipe' });
|
|
69
|
-
} else {
|
|
70
|
-
process.kill(pid, 'SIGTERM');
|
|
71
|
-
}
|
|
72
|
-
return { stopped: true, method: 'pid-kill', pid };
|
|
73
|
-
}
|
|
74
|
-
} catch (_e) { /* intentional: try next lock file */ } // NOSONAR S2486
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return { stopped: false, method: 'none' };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
42
|
/**
|
|
81
43
|
* Check if a git branch already exists.
|
|
82
44
|
* @param {string} branchName - Branch name to check
|
|
@@ -92,42 +54,224 @@ function branchExists(branchName, runFile) {
|
|
|
92
54
|
}
|
|
93
55
|
}
|
|
94
56
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
_platform: platform,
|
|
104
|
-
mainProjectRoot: projectRoot,
|
|
105
|
-
_safeBeadsInit: options._safeBeadsInit,
|
|
106
|
-
});
|
|
107
|
-
|
|
57
|
+
/**
|
|
58
|
+
* True when a git ref resolves to a commit (branch, tag, remote-tracking ref, or SHA).
|
|
59
|
+
* @param {string} ref - The ref to check
|
|
60
|
+
* @param {string} projectRoot - Repo root to run git in
|
|
61
|
+
* @param {Function} runFile - execFileSync function (for DI)
|
|
62
|
+
* @returns {boolean}
|
|
63
|
+
*/
|
|
64
|
+
function refExists(ref, projectRoot, runFile) {
|
|
108
65
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
return 'Beads verification failed - .beads linked but bd may not work';
|
|
66
|
+
runFile('git', ['-C', projectRoot, 'rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
67
|
+
return true;
|
|
68
|
+
} catch (_err) { /* intentional: ref does not resolve */ // NOSONAR S2486
|
|
69
|
+
return false;
|
|
115
70
|
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the base ref a NEW worktree branch should fork from. Defaults to the
|
|
75
|
+
* repo's DEFAULT branch (NOT the checkout's current HEAD) so a worktree created
|
|
76
|
+
* off a WIP branch never silently inherits unrelated commits. Prefers the remote
|
|
77
|
+
* default (`origin/<default>`) when present, else the local default branch, and
|
|
78
|
+
* falls back to the detected name so git surfaces a clear error if neither exists.
|
|
79
|
+
* @param {string} projectRoot - Repo root
|
|
80
|
+
* @param {Function} runFile - execFileSync function (for DI)
|
|
81
|
+
* @returns {string} The base ref to pass to `git worktree add ... <base>`
|
|
82
|
+
*/
|
|
83
|
+
function resolveDefaultBase(projectRoot, runFile) {
|
|
84
|
+
const def = detectDefaultBranch(projectRoot, { _exec: runFile });
|
|
85
|
+
const originRef = `origin/${def}`;
|
|
86
|
+
if (refExists(originRef, projectRoot, runFile)) return originRef;
|
|
87
|
+
if (refExists(def, projectRoot, runFile)) return def;
|
|
88
|
+
return def;
|
|
89
|
+
}
|
|
116
90
|
|
|
117
|
-
|
|
91
|
+
/**
|
|
92
|
+
* True when a link failure is a privilege/support problem (Windows without the
|
|
93
|
+
* symlink privilege, restricted FS) rather than a real error. In that case we
|
|
94
|
+
* degrade to a package install instead of hard-failing.
|
|
95
|
+
* @param {Error} error
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
function isLinkPermissionError(error) {
|
|
99
|
+
return ['EPERM', 'EACCES', 'ENOSYS', 'UV_EPERM'].includes(error && error.code);
|
|
118
100
|
}
|
|
119
101
|
|
|
120
102
|
/**
|
|
121
|
-
* Run package install in the new worktree if a package manager is detected
|
|
103
|
+
* Run package install in the new worktree if a package manager is detected, and
|
|
104
|
+
* SURFACE failures (spawn error or non-zero exit) instead of swallowing them.
|
|
122
105
|
* @param {string} worktreePath - Absolute path to the new worktree
|
|
123
106
|
* @param {string} projectRoot - Absolute path to the project root
|
|
124
107
|
* @param {Function} spawnFn - spawnSync-compatible function
|
|
125
108
|
* @param {object} fsApi - fs module (for DI)
|
|
109
|
+
* @returns {{ linked: boolean, installed: boolean }}
|
|
110
|
+
* @throws {Error} when the install cannot be spawned or exits non-zero.
|
|
126
111
|
*/
|
|
127
112
|
function runInstall(worktreePath, projectRoot, spawnFn, fsApi) {
|
|
128
113
|
const pkgManager = detectPackageManager(projectRoot, fsApi);
|
|
129
|
-
if (pkgManager) {
|
|
130
|
-
|
|
114
|
+
if (!pkgManager) return { linked: false, installed: false };
|
|
115
|
+
|
|
116
|
+
// shell:true on Windows: npm/pnpm/yarn are .cmd shims that cannot be spawned
|
|
117
|
+
// directly (ENOENT / EINVAL). pkgManager comes from lockfile detection (fixed
|
|
118
|
+
// set) and args are hardcoded, so no user input reaches the shell.
|
|
119
|
+
const result = spawnFn(pkgManager, ['install'], {
|
|
120
|
+
cwd: worktreePath,
|
|
121
|
+
stdio: 'pipe',
|
|
122
|
+
shell: process.platform === 'win32',
|
|
123
|
+
});
|
|
124
|
+
if (result && result.error) {
|
|
125
|
+
throw new Error(`Dependency install failed: could not run '${pkgManager} install' in ${worktreePath}: ${result.error.message}`);
|
|
126
|
+
}
|
|
127
|
+
if (result && typeof result.status === 'number' && result.status !== 0) {
|
|
128
|
+
throw new Error(`Dependency install failed: '${pkgManager} install' exited with code ${result.status} in ${worktreePath}`);
|
|
129
|
+
}
|
|
130
|
+
return { linked: false, installed: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Populate the new worktree's node_modules. Fast path: link to the main repo's
|
|
135
|
+
* shared install (a junction on Windows, a directory symlink on POSIX) so a fresh
|
|
136
|
+
* worktree is immediately usable without a full reinstall — this repo's own
|
|
137
|
+
* worktrees follow that pattern. Fallback: run the detected package manager's
|
|
138
|
+
* install when there is no shared install to link. Both paths surface failures.
|
|
139
|
+
* @param {string} worktreePath - Absolute path to the new worktree
|
|
140
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
141
|
+
* @param {object} deps
|
|
142
|
+
* @param {Function} deps.spawnFn - spawnSync-compatible function
|
|
143
|
+
* @param {object} deps.fsApi - fs module (for DI)
|
|
144
|
+
* @param {string} deps.platform - process.platform value
|
|
145
|
+
* @returns {{ linked: boolean, installed: boolean }}
|
|
146
|
+
* @throws {Error} when linking fails for a non-privilege reason or install fails.
|
|
147
|
+
*/
|
|
148
|
+
function setupWorktreeDeps(worktreePath, projectRoot, { spawnFn, fsApi, platform }) {
|
|
149
|
+
const srcModules = path.join(projectRoot, 'node_modules');
|
|
150
|
+
const destModules = path.join(worktreePath, 'node_modules');
|
|
151
|
+
|
|
152
|
+
// Already populated (e.g. git checkout carried it) — nothing to do.
|
|
153
|
+
if (fsApi.existsSync(destModules)) return { linked: false, installed: false };
|
|
154
|
+
|
|
155
|
+
// Fast path: link to the shared install when the main repo has one.
|
|
156
|
+
if (fsApi.existsSync(srcModules)) {
|
|
157
|
+
const linkType = platform === 'win32' ? 'junction' : 'dir';
|
|
158
|
+
try {
|
|
159
|
+
fsApi.symlinkSync(srcModules, destModules, linkType);
|
|
160
|
+
return { linked: true, installed: false };
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Degrade to a real install only when the link was refused for lack of
|
|
163
|
+
// privilege/support; any other failure is surfaced to the caller.
|
|
164
|
+
if (!isLinkPermissionError(error)) throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return runInstall(worktreePath, projectRoot, spawnFn, fsApi);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Best-effort: record the issue → worktree → work-folder linkage in the Kernel so
|
|
173
|
+
* `forge worktree list` and orientation READ it instead of guessing the work-folder by
|
|
174
|
+
* a filesystem heuristic. Writes a `kernel_worktrees` row keyed by the absolute worktree
|
|
175
|
+
* path (idempotent upsert) and, when an issue + work-folder are given, drops a
|
|
176
|
+
* machine-readable `.forge-issue` marker so a folder resolves to its issue deterministically.
|
|
177
|
+
*
|
|
178
|
+
* Never throws: a repo without a reachable Kernel (or without the 007 linkage columns)
|
|
179
|
+
* must still get a usable worktree — orientation falls back to the folder heuristic.
|
|
180
|
+
*
|
|
181
|
+
* @param {object} params
|
|
182
|
+
* @param {string} params.projectRoot
|
|
183
|
+
* @param {string} params.worktreePath - Absolute worktree path (the row key).
|
|
184
|
+
* @param {string} params.branch
|
|
185
|
+
* @param {string|null} params.issueId
|
|
186
|
+
* @param {string|null} params.workFolder - Repo-relative work-folder path.
|
|
187
|
+
* @param {object} params.opts - DI options (may inject `_kernelDriver`, `_exec`, `_fs`).
|
|
188
|
+
* @returns {Promise<{registered: boolean, issueId: string|null, workFolder: string|null, reason?: string}>}
|
|
189
|
+
*/
|
|
190
|
+
async function registerWorktreeLinkage({ projectRoot, worktreePath, branch, issueId, workFolder, opts }) {
|
|
191
|
+
const runFile = opts._exec || execFileSync;
|
|
192
|
+
const fsApi = opts._fs || fs;
|
|
193
|
+
try {
|
|
194
|
+
// Drop the folder → issue marker first — deterministic even if the DB write fails.
|
|
195
|
+
if (workFolder && issueId) {
|
|
196
|
+
const folderAbs = path.resolve(projectRoot, workFolder);
|
|
197
|
+
fsApi.mkdirSync(folderAbs, { recursive: true });
|
|
198
|
+
fsApi.writeFileSync(path.join(folderAbs, '.forge-issue'), `${issueId}\n`, 'utf8');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let driver = opts._kernelDriver;
|
|
202
|
+
if (!driver) {
|
|
203
|
+
// No reachable git repo → no shared Kernel; skip quietly (orientation still works
|
|
204
|
+
// off the folder heuristic). Avoids spawning `git` in a non-repo directory.
|
|
205
|
+
if (!fsApi.existsSync(path.join(projectRoot, '.git'))) {
|
|
206
|
+
return { registered: false, issueId: issueId || null, workFolder: workFolder || null, reason: 'no git repository' };
|
|
207
|
+
}
|
|
208
|
+
// Lazy require: keep the worktree command light and avoid paying kernel setup
|
|
209
|
+
// when linkage is not needed. Migrated deps guarantee the 007 columns exist.
|
|
210
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
211
|
+
driver = (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { resolveGitCommonDir } = require('../kernel/broker');
|
|
215
|
+
const gitCommonDir = resolveGitCommonDir(projectRoot, { execFileSync: runFile });
|
|
216
|
+
|
|
217
|
+
driver.registerWorktree({
|
|
218
|
+
git_common_dir: gitCommonDir,
|
|
219
|
+
path: worktreePath,
|
|
220
|
+
branch,
|
|
221
|
+
actor: process.env.FORGE_ACTOR || null,
|
|
222
|
+
issue_id: issueId || null,
|
|
223
|
+
work_folder: workFolder || null,
|
|
224
|
+
registered_at: new Date().toISOString(),
|
|
225
|
+
state: 'active',
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return { registered: true, issueId: issueId || null, workFolder: workFolder || null };
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// Non-fatal: keep the worktree usable. Only warn when the caller explicitly asked
|
|
231
|
+
// for linkage (--issue/--work-folder); a plain `worktree create` stays quiet.
|
|
232
|
+
if (issueId || workFolder) {
|
|
233
|
+
process.stderr.write(`forge worktree: kernel linkage not recorded (${error.message})\n`);
|
|
234
|
+
}
|
|
235
|
+
return { registered: false, issueId: issueId || null, workFolder: workFolder || null, reason: error.message };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Auto-file rail: guarantee the new worktree's branch has a backing Kernel issue,
|
|
241
|
+
* WITHOUT anyone remembering `forge issue create` (kernel issue a4b8f56f). Best-effort
|
|
242
|
+
* and NON-BLOCKING — the worktree must stay usable even if tracking fails, so any error
|
|
243
|
+
* degrades to a stderr warning and null. Skipped when `--issue` already links a real
|
|
244
|
+
* issue. Idempotent via ensureBackingIssue (deduped by branch), so re-creating a
|
|
245
|
+
* worktree or later `forge push`/pre-push firing never mints a duplicate.
|
|
246
|
+
*
|
|
247
|
+
* @param {object} params
|
|
248
|
+
* @param {string} params.projectRoot
|
|
249
|
+
* @param {string} params.worktreePath
|
|
250
|
+
* @param {string} params.branch
|
|
251
|
+
* @param {string|null} params.issueId - explicit --issue (skip auto-file when set).
|
|
252
|
+
* @param {object} params.opts - DI (may inject `_ensureBackingIssue`, `_kernelDriver`, `_kernelBroker`, `_fs`).
|
|
253
|
+
* @returns {Promise<object|null>} the backing-issue descriptor, or null when skipped/unavailable.
|
|
254
|
+
*/
|
|
255
|
+
async function autoFileBackingIssue({ projectRoot, worktreePath, branch, issueId, opts }) {
|
|
256
|
+
if (issueId) return null; // explicit --issue already links a real issue.
|
|
257
|
+
const fsApi = opts._fs || fs;
|
|
258
|
+
try {
|
|
259
|
+
// Lazy require INSIDE the try: a module-load failure (missing file / broken
|
|
260
|
+
// sub-dependency) must degrade to null, never crash `forge worktree create`.
|
|
261
|
+
const ensureFn = opts._ensureBackingIssue || require('../kernel/backing-issue').ensureBackingIssue;
|
|
262
|
+
let driver = opts._kernelDriver;
|
|
263
|
+
let broker = opts._kernelBroker;
|
|
264
|
+
if (!driver || !broker) {
|
|
265
|
+
if (!fsApi.existsSync(path.join(projectRoot, '.git'))) return null; // no repo → no kernel
|
|
266
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
267
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot });
|
|
268
|
+
driver = driver || deps.kernelDriver;
|
|
269
|
+
broker = broker || deps.kernelBroker;
|
|
270
|
+
}
|
|
271
|
+
return await ensureFn({ branch, projectRoot, worktreePath, driver, broker });
|
|
272
|
+
} catch (error) {
|
|
273
|
+
process.stderr.write(`forge worktree: backing issue not auto-filed (${error.message})\n`);
|
|
274
|
+
return null;
|
|
131
275
|
}
|
|
132
276
|
}
|
|
133
277
|
|
|
@@ -143,9 +287,10 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
143
287
|
const runFile = opts._exec || execFileSync;
|
|
144
288
|
const runSpawn = opts._spawn || spawnSync;
|
|
145
289
|
const fsApi = opts._fs || fs;
|
|
146
|
-
const platform = opts._platform || process.platform;
|
|
147
290
|
|
|
148
291
|
const branchName = flags['--branch'] || flags.branch || `feat/${slug}`;
|
|
292
|
+
const issueId = flags['--issue'] || null;
|
|
293
|
+
const workFolder = flags['--work-folder'] || null;
|
|
149
294
|
const worktreesDir = path.resolve(projectRoot, '.worktrees');
|
|
150
295
|
|
|
151
296
|
// Guard: Detect bare repo state - worktrees created from bare repos produce broken branches
|
|
@@ -168,42 +313,103 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
168
313
|
|
|
169
314
|
const worktreePath = path.resolve(worktreesDir, slug);
|
|
170
315
|
|
|
316
|
+
// Validate an explicit --base up front so a bad ref errors BEFORE anything is
|
|
317
|
+
// created (no branch, no worktree dir). Base only applies when creating a NEW
|
|
318
|
+
// branch; an existing branch is checked out as-is.
|
|
319
|
+
const explicitBase = flags['--base'] || null;
|
|
320
|
+
if (explicitBase && !refExists(explicitBase, projectRoot, runFile)) {
|
|
321
|
+
return { success: false, error: `Invalid --base: ref '${explicitBase}' not found. Verify it exists (git rev-parse --verify ${explicitBase}).` };
|
|
322
|
+
}
|
|
323
|
+
|
|
171
324
|
// Step 0: Check if worktree already exists
|
|
172
325
|
if (fsApi.existsSync(worktreePath)) {
|
|
326
|
+
// A pre-existing worktree may be checked out on a different branch than the
|
|
327
|
+
// requested branchName — read the actual HEAD so the kernel row is accurate.
|
|
328
|
+
let existingBranch = branchName;
|
|
329
|
+
try {
|
|
330
|
+
const head = runFile('git', ['-C', worktreePath, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
331
|
+
// A detached worktree reports the literal "HEAD" — don't persist that; keep branchName.
|
|
332
|
+
if (head && head !== 'HEAD') existingBranch = head;
|
|
333
|
+
} catch (_error) { /* not a resolvable worktree HEAD — fall back to branchName */ }
|
|
334
|
+
const linkage = await registerWorktreeLinkage({ projectRoot, worktreePath, branch: existingBranch, issueId, workFolder, opts });
|
|
335
|
+
const backing = await autoFileBackingIssue({ projectRoot, worktreePath, branch: existingBranch, issueId, opts });
|
|
173
336
|
return {
|
|
174
337
|
success: true,
|
|
175
338
|
reused: true,
|
|
176
339
|
message: `Worktree already exists at ${worktreePath}`,
|
|
177
340
|
worktreePath,
|
|
341
|
+
linkage,
|
|
342
|
+
backing,
|
|
178
343
|
};
|
|
179
344
|
}
|
|
180
345
|
|
|
181
346
|
// Step 1: Ensure .worktrees/ dir exists
|
|
182
347
|
fsApi.mkdirSync(worktreesDir, { recursive: true });
|
|
183
348
|
|
|
184
|
-
// Step 2: Create git worktree
|
|
349
|
+
// Step 2: Create git worktree. For a NEW branch, fork from an explicit --base or
|
|
350
|
+
// the repo's DEFAULT branch — NOT the checkout's current HEAD — so the worktree
|
|
351
|
+
// never silently inherits unrelated WIP commits (B2). An existing branch is
|
|
352
|
+
// checked out as-is (no base applies).
|
|
185
353
|
const hasBranch = branchExists(branchName, runFile);
|
|
354
|
+
let base = null;
|
|
186
355
|
if (hasBranch) {
|
|
187
356
|
runFile('git', ['worktree', 'add', worktreePath, branchName], { stdio: 'pipe' });
|
|
188
357
|
} else {
|
|
189
|
-
|
|
358
|
+
base = explicitBase || resolveDefaultBase(projectRoot, runFile);
|
|
359
|
+
runFile('git', ['worktree', 'add', worktreePath, '-b', branchName, base], { stdio: 'pipe' });
|
|
190
360
|
}
|
|
191
361
|
|
|
192
|
-
// Step 3:
|
|
193
|
-
|
|
362
|
+
// Step 3: Populate node_modules (link to the shared install, else install).
|
|
363
|
+
// No per-worktree issue-store bootstrap is needed — the Kernel DB lives in the
|
|
364
|
+
// shared git common dir, so the new worktree already sees the same kernel.
|
|
365
|
+
let deps;
|
|
366
|
+
try {
|
|
367
|
+
deps = setupWorktreeDeps(worktreePath, projectRoot, {
|
|
368
|
+
spawnFn: runSpawn,
|
|
369
|
+
fsApi,
|
|
370
|
+
platform: opts._platform || process.platform,
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
// Surface the failure instead of leaving a worktree with no usable deps.
|
|
374
|
+
return { success: false, error: error.message, worktreePath, branch: branchName };
|
|
375
|
+
}
|
|
194
376
|
|
|
195
|
-
|
|
196
|
-
|
|
377
|
+
const linkage = await registerWorktreeLinkage({ projectRoot, worktreePath, branch: branchName, issueId, workFolder, opts });
|
|
378
|
+
const backing = await autoFileBackingIssue({ projectRoot, worktreePath, branch: branchName, issueId, opts });
|
|
197
379
|
|
|
198
|
-
|
|
380
|
+
// Report the base so the fork point is never silent. `base` is null when an
|
|
381
|
+
// existing branch was checked out (no fork happened).
|
|
382
|
+
const baseNote = base ? `based on ${base}` : `existing branch ${branchName}`;
|
|
383
|
+
return {
|
|
199
384
|
success: true,
|
|
200
385
|
worktreePath,
|
|
201
386
|
branch: branchName,
|
|
387
|
+
base,
|
|
388
|
+
depsLinked: deps.linked,
|
|
389
|
+
depsInstalled: deps.installed,
|
|
390
|
+
linkage,
|
|
391
|
+
backing,
|
|
392
|
+
output: `Created worktree ${worktreePath} on ${branchName} (${baseNote}).`,
|
|
202
393
|
};
|
|
203
|
-
|
|
204
|
-
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Handle the "list" subcommand — read the worktree registry from the Kernel.
|
|
398
|
+
* @param {string} projectRoot - Project root path
|
|
399
|
+
* @param {object} opts - DI options (may inject `_kernelDriver`)
|
|
400
|
+
* @returns {Promise<object>} Result object with a `worktrees` array
|
|
401
|
+
*/
|
|
402
|
+
async function handleList(projectRoot, opts) {
|
|
403
|
+
try {
|
|
404
|
+
let driver = opts._kernelDriver;
|
|
405
|
+
if (!driver) {
|
|
406
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
407
|
+
driver = (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
|
|
408
|
+
}
|
|
409
|
+
return { success: true, worktrees: driver.listWorktrees({}) };
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return { success: false, error: error.message, worktrees: [] };
|
|
205
412
|
}
|
|
206
|
-
return result;
|
|
207
413
|
}
|
|
208
414
|
|
|
209
415
|
/**
|
|
@@ -220,29 +426,66 @@ async function handleRemove(slug, projectRoot, opts) {
|
|
|
220
426
|
}
|
|
221
427
|
|
|
222
428
|
const runFile = opts._exec || execFileSync;
|
|
223
|
-
const fsModule = opts._fs || fs;
|
|
224
429
|
const worktreePath = path.resolve(projectRoot, '.worktrees', slug);
|
|
225
430
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
if (stopResult.stopped) {
|
|
229
|
-
// Brief wait for file locks to release (Windows needs this)
|
|
230
|
-
await new Promise(r => setTimeout(r, 500));
|
|
231
|
-
}
|
|
232
|
-
|
|
431
|
+
// No issue-store server to stop: the Kernel SQLite DB lives in the shared git
|
|
432
|
+
// common dir, so removing a worktree directory never touches it.
|
|
233
433
|
runFile('git', ['worktree', 'remove', worktreePath], { stdio: 'pipe' });
|
|
234
434
|
|
|
235
435
|
return { success: true, removed: worktreePath };
|
|
236
436
|
}
|
|
237
437
|
|
|
438
|
+
// Long flags that take a value, in both `--flag value` and `--flag=value` forms.
|
|
439
|
+
const WORKTREE_VALUE_FLAGS = ['--branch', '--issue', '--work-folder', '--base'];
|
|
440
|
+
const WORKTREE_USAGE_HINT = 'Usage: forge worktree create <slug> [--branch <name>] [--base <ref>] [--issue <id>] [--work-folder <path>]';
|
|
441
|
+
|
|
442
|
+
function parseWorktreeArgs(args, flags) {
|
|
443
|
+
const positional = [];
|
|
444
|
+
const parsedFlags = { ...flags };
|
|
445
|
+
|
|
446
|
+
for (let i = 0; i < args.length; i++) {
|
|
447
|
+
const arg = args[i];
|
|
448
|
+
|
|
449
|
+
const spaceFlag = WORKTREE_VALUE_FLAGS.find(flag => arg === flag);
|
|
450
|
+
if (spaceFlag) {
|
|
451
|
+
const value = args[i + 1];
|
|
452
|
+
if (!value || value.startsWith('--')) {
|
|
453
|
+
return { error: `Missing value for ${spaceFlag}. ${WORKTREE_USAGE_HINT}` };
|
|
454
|
+
}
|
|
455
|
+
parsedFlags[spaceFlag] = value;
|
|
456
|
+
i++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const eqFlag = WORKTREE_VALUE_FLAGS.find(flag => arg.startsWith(`${flag}=`));
|
|
461
|
+
if (eqFlag) {
|
|
462
|
+
const value = arg.slice(`${eqFlag}=`.length);
|
|
463
|
+
if (!value) {
|
|
464
|
+
return { error: `Missing value for ${eqFlag}. ${WORKTREE_USAGE_HINT}` };
|
|
465
|
+
}
|
|
466
|
+
parsedFlags[eqFlag] = value;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
positional.push(arg);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Back-compat alias consumed by handleCreate (flags['--branch'] || flags.branch).
|
|
474
|
+
if (parsedFlags['--branch']) parsedFlags.branch = parsedFlags['--branch'];
|
|
475
|
+
|
|
476
|
+
return { args: positional, flags: parsedFlags };
|
|
477
|
+
}
|
|
478
|
+
|
|
238
479
|
module.exports = {
|
|
239
480
|
name: 'worktree',
|
|
240
|
-
description: 'Manage isolated worktrees
|
|
241
|
-
usage: 'forge worktree <create|remove> <slug>',
|
|
481
|
+
description: 'Manage isolated worktrees',
|
|
482
|
+
usage: 'forge worktree <create|remove|list> <slug>',
|
|
242
483
|
flags: {
|
|
243
484
|
'--branch': 'Custom branch name (default: feat/<slug>)',
|
|
485
|
+
'--base': 'Base ref a new branch forks from (default: the repo default branch, e.g. origin/main)',
|
|
486
|
+
'--issue': 'Kernel issue id to link this worktree to (records issue → worktree)',
|
|
487
|
+
'--work-folder': 'Repo-relative work-folder this issue owns (records worktree → work-folder + drops a .forge-issue marker)',
|
|
244
488
|
},
|
|
245
|
-
stopDolt,
|
|
246
489
|
|
|
247
490
|
/**
|
|
248
491
|
* Main handler for the worktree command.
|
|
@@ -257,27 +500,47 @@ module.exports = {
|
|
|
257
500
|
* @returns {Promise<object>}
|
|
258
501
|
*/
|
|
259
502
|
handler: async (args, flags, projectRoot, opts = {}) => {
|
|
260
|
-
const
|
|
261
|
-
|
|
503
|
+
const parsed = parseWorktreeArgs(args, flags);
|
|
504
|
+
if (parsed.error) {
|
|
505
|
+
return { success: false, error: parsed.error };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const subcommand = parsed.args[0];
|
|
262
509
|
|
|
263
|
-
if (!subcommand || !['create', 'remove'].includes(subcommand)) {
|
|
510
|
+
if (!subcommand || !['create', 'remove', 'list'].includes(subcommand)) {
|
|
264
511
|
return {
|
|
265
512
|
success: false,
|
|
266
|
-
error: 'Missing or invalid subcommand. Usage: forge worktree <create|remove> <slug>',
|
|
513
|
+
error: 'Missing or invalid subcommand. Usage: forge worktree <create|remove|list> <slug>',
|
|
267
514
|
};
|
|
268
515
|
}
|
|
269
516
|
|
|
517
|
+
// `list` reads the Kernel registry and takes no slug.
|
|
518
|
+
if (subcommand === 'list') {
|
|
519
|
+
return handleList(projectRoot, opts);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const slug = parsed.args[1];
|
|
270
523
|
if (!slug) {
|
|
271
524
|
return {
|
|
272
525
|
success: false,
|
|
273
|
-
error: 'Missing slug. Usage: forge worktree <create|remove> <slug>',
|
|
526
|
+
error: 'Missing slug. Usage: forge worktree <create|remove|list> <slug>',
|
|
274
527
|
};
|
|
275
528
|
}
|
|
276
529
|
|
|
277
530
|
if (subcommand === 'create') {
|
|
278
|
-
return handleCreate(slug, flags, projectRoot, opts);
|
|
531
|
+
return handleCreate(slug, parsed.flags, projectRoot, opts);
|
|
279
532
|
}
|
|
280
533
|
|
|
281
534
|
return handleRemove(slug, projectRoot, opts);
|
|
282
535
|
},
|
|
536
|
+
|
|
537
|
+
// Exposed for unit tests; not part of the CLI surface.
|
|
538
|
+
_internal: {
|
|
539
|
+
detectPackageManager,
|
|
540
|
+
setupWorktreeDeps,
|
|
541
|
+
runInstall,
|
|
542
|
+
autoFileBackingIssue,
|
|
543
|
+
refExists,
|
|
544
|
+
resolveDefaultBase,
|
|
545
|
+
},
|
|
283
546
|
};
|