forge-workflow 0.0.10 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- 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 +461 -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 +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/{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 +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 +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- 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 +17 -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 +0 -1
- 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 +838 -972
- 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 +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 +307 -100
- 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 +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 +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/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 +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/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 +99 -497
- 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 +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 +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- 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-coordinator.sh +3 -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/validate.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const { execFileSync } = require('node:child_process');
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
+
const { secureExecFileSync } = require('../shell-utils');
|
|
14
15
|
|
|
15
16
|
// Constants
|
|
16
17
|
const CHECK_TYPES = {
|
|
@@ -31,15 +32,11 @@ const CONFLICT_MARKER_IGNORE_DIRS = new Set([
|
|
|
31
32
|
]);
|
|
32
33
|
const CONFLICT_MARKER_ALLOWED_DOT_DIRS = new Set([
|
|
33
34
|
'.claude',
|
|
34
|
-
'.cline',
|
|
35
35
|
'.codex',
|
|
36
36
|
'.cursor',
|
|
37
37
|
'.forge',
|
|
38
38
|
'.github',
|
|
39
39
|
'.husky',
|
|
40
|
-
'.kilocode',
|
|
41
|
-
'.opencode',
|
|
42
|
-
'.roo',
|
|
43
40
|
'.sonarlint',
|
|
44
41
|
]);
|
|
45
42
|
const CONFLICT_START_PATTERN = /^<<<<<<<(?: .*)?$/;
|
|
@@ -466,7 +463,7 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
466
463
|
try {
|
|
467
464
|
let npmRawOutput = null;
|
|
468
465
|
try {
|
|
469
|
-
npmRawOutput =
|
|
466
|
+
npmRawOutput = secureExecFileSync('npm', ['audit', '--json', '--production'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
470
467
|
} catch (npmExitError) {
|
|
471
468
|
if (npmExitError.killed && npmExitError.signal === 'SIGTERM') {
|
|
472
469
|
return {
|
|
@@ -566,23 +563,46 @@ async function runSecurityScan() { // NOSONAR S3776
|
|
|
566
563
|
* const result = await runAllTests();
|
|
567
564
|
* console.log(`${result.passed}/${result.total} tests passed`);
|
|
568
565
|
*/
|
|
569
|
-
|
|
566
|
+
function parseTestCounts(output) {
|
|
567
|
+
const text = String(output || '');
|
|
568
|
+
const passed = parseNumber(/(\d+) pass/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
569
|
+
const failed = parseNumber(/(\d+) fail/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
570
|
+
const skipped = parseNumber(/(\d+) skip/.exec(text)); // NOSONAR S5852 - bounded \d+ pattern
|
|
571
|
+
const ranMatch = /Ran (\d+) tests/.exec(text); // NOSONAR S5852 - bounded \d+ pattern
|
|
572
|
+
const total = ranMatch ? parseNumber(ranMatch, 1) : passed + failed + skipped;
|
|
573
|
+
return { passed, failed, skipped, total, testsFound: total > 0 };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function runAllTests(exec = execFileSync) {
|
|
570
577
|
const startTime = Date.now();
|
|
571
578
|
|
|
572
579
|
try {
|
|
573
|
-
const result =
|
|
580
|
+
const result = exec('bun', ['test', '--timeout', '30000'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
581
|
+
|
|
582
|
+
const { passed, failed, skipped, total, testsFound } = parseTestCounts(result);
|
|
574
583
|
|
|
575
|
-
//
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
584
|
+
// B2 (N1): a bun run that executes 0 tests must NEVER report PASS — that
|
|
585
|
+
// manufactures false confidence. Surface it as an explicit SKIP instead.
|
|
586
|
+
if (!testsFound) {
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
skipped: true,
|
|
590
|
+
testsFound: false,
|
|
591
|
+
duration: Date.now() - startTime,
|
|
592
|
+
passed,
|
|
593
|
+
failed,
|
|
594
|
+
total,
|
|
595
|
+
message: 'No tests found — 0 tests executed (SKIP, not a pass). Add tests or check the runner config.',
|
|
596
|
+
};
|
|
597
|
+
}
|
|
580
598
|
|
|
581
599
|
return {
|
|
582
600
|
success: failed === 0,
|
|
601
|
+
testsFound: true,
|
|
583
602
|
duration: Date.now() - startTime,
|
|
584
603
|
passed,
|
|
585
604
|
failed,
|
|
605
|
+
skipped,
|
|
586
606
|
total,
|
|
587
607
|
message: failed === 0
|
|
588
608
|
? `All ${total} tests passed`
|
|
@@ -601,28 +621,32 @@ async function runAllTests() {
|
|
|
601
621
|
};
|
|
602
622
|
}
|
|
603
623
|
|
|
604
|
-
// Test
|
|
624
|
+
// Test runner absent — say so explicitly (do NOT silently pass).
|
|
605
625
|
if (isCommandNotFound(error)) {
|
|
606
626
|
return {
|
|
607
627
|
success: true,
|
|
608
628
|
skipped: true,
|
|
629
|
+
testsFound: false,
|
|
609
630
|
duration: Date.now() - startTime,
|
|
610
631
|
passed: 0,
|
|
611
632
|
failed: 0,
|
|
612
633
|
total: 0,
|
|
613
|
-
message: 'Tests skipped: bun not found.',
|
|
634
|
+
message: 'Tests skipped: bun test runner not found. Install bun to run tests.',
|
|
614
635
|
};
|
|
615
636
|
}
|
|
616
637
|
|
|
617
638
|
// Parse test failures from output
|
|
618
639
|
const output = error.stdout || error.message;
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
640
|
+
const counts = parseTestCounts(output);
|
|
641
|
+
const passed = counts.passed;
|
|
642
|
+
// bun exited non-zero: if the output shows no explicit failure count, treat
|
|
643
|
+
// it as at least one failure rather than swallowing the non-zero exit.
|
|
644
|
+
const failed = counts.failed > 0 ? counts.failed : Math.max(1, counts.failed);
|
|
645
|
+
const total = counts.total > 0 ? counts.total : passed + failed + counts.skipped;
|
|
623
646
|
|
|
624
647
|
return {
|
|
625
648
|
success: false,
|
|
649
|
+
testsFound: counts.testsFound,
|
|
626
650
|
duration: Date.now() - startTime,
|
|
627
651
|
passed,
|
|
628
652
|
failed,
|
|
@@ -832,6 +856,7 @@ module.exports = {
|
|
|
832
856
|
runLint,
|
|
833
857
|
runSecurityScan,
|
|
834
858
|
runAllTests,
|
|
859
|
+
parseTestCounts,
|
|
835
860
|
scanForConflictMarkers,
|
|
836
861
|
executeValidate,
|
|
837
862
|
executeDebugMode,
|
package/lib/commands/worktree.js
CHANGED
|
@@ -3,15 +3,18 @@
|
|
|
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 { bootstrapBeads } = require('../beads-bootstrap');
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Forge Worktree Command
|
|
10
|
-
* Manage isolated worktrees
|
|
9
|
+
* Manage isolated worktrees.
|
|
11
10
|
* Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
|
|
12
11
|
*
|
|
12
|
+
* The Kernel issue store lives in the git common dir, which every worktree of a
|
|
13
|
+
* repo shares, so no per-worktree issue-store bootstrap is needed — a new
|
|
14
|
+
* worktree already sees the same kernel.
|
|
15
|
+
*
|
|
13
16
|
* Subcommands:
|
|
14
|
-
* create <slug> - Create worktree at .worktrees/<slug> with branch +
|
|
17
|
+
* create <slug> - Create worktree at .worktrees/<slug> with branch + install
|
|
15
18
|
* remove <slug> - Remove worktree via git worktree remove
|
|
16
19
|
*
|
|
17
20
|
* @module commands/worktree
|
|
@@ -24,7 +27,10 @@ const { bootstrapBeads } = require('../beads-bootstrap');
|
|
|
24
27
|
* @returns {string|null} Package manager command or null
|
|
25
28
|
*/
|
|
26
29
|
function detectPackageManager(projectRoot, fsApi) {
|
|
30
|
+
// Bun ships two lockfile formats: the legacy binary `bun.lockb` and the current
|
|
31
|
+
// text `bun.lock`. Recognize both so a Bun repo isn't mis-detected as npm.
|
|
27
32
|
if (fsApi.existsSync(path.join(projectRoot, 'bun.lockb'))) return 'bun';
|
|
33
|
+
if (fsApi.existsSync(path.join(projectRoot, 'bun.lock'))) return 'bun';
|
|
28
34
|
if (fsApi.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
29
35
|
if (fsApi.existsSync(path.join(projectRoot, 'yarn.lock'))) return 'yarn';
|
|
30
36
|
if (fsApi.existsSync(path.join(projectRoot, 'package-lock.json'))) return 'npm';
|
|
@@ -32,51 +38,6 @@ function detectPackageManager(projectRoot, fsApi) {
|
|
|
32
38
|
return null;
|
|
33
39
|
}
|
|
34
40
|
|
|
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
41
|
/**
|
|
81
42
|
* Check if a git branch already exists.
|
|
82
43
|
* @param {string} branchName - Branch name to check
|
|
@@ -92,42 +53,190 @@ function branchExists(branchName, runFile) {
|
|
|
92
53
|
}
|
|
93
54
|
}
|
|
94
55
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
mainProjectRoot: projectRoot,
|
|
105
|
-
_safeBeadsInit: options._safeBeadsInit,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
exec('bd', ['--version'], { cwd: worktreePath, timeout: 5000, stdio: 'pipe' });
|
|
110
|
-
} catch (_bdErr) { /* intentional: bd may not be on PATH */ // NOSONAR S2486
|
|
111
|
-
if (bootstrapResult.success === false && bootstrapResult.warning) {
|
|
112
|
-
return bootstrapResult.warning;
|
|
113
|
-
}
|
|
114
|
-
return 'Beads verification failed - .beads linked but bd may not work';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return bootstrapResult.warning;
|
|
56
|
+
/**
|
|
57
|
+
* True when a link failure is a privilege/support problem (Windows without the
|
|
58
|
+
* symlink privilege, restricted FS) rather than a real error. In that case we
|
|
59
|
+
* degrade to a package install instead of hard-failing.
|
|
60
|
+
* @param {Error} error
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
function isLinkPermissionError(error) {
|
|
64
|
+
return ['EPERM', 'EACCES', 'ENOSYS', 'UV_EPERM'].includes(error && error.code);
|
|
118
65
|
}
|
|
119
66
|
|
|
120
67
|
/**
|
|
121
|
-
* Run package install in the new worktree if a package manager is detected
|
|
68
|
+
* Run package install in the new worktree if a package manager is detected, and
|
|
69
|
+
* SURFACE failures (spawn error or non-zero exit) instead of swallowing them.
|
|
122
70
|
* @param {string} worktreePath - Absolute path to the new worktree
|
|
123
71
|
* @param {string} projectRoot - Absolute path to the project root
|
|
124
72
|
* @param {Function} spawnFn - spawnSync-compatible function
|
|
125
73
|
* @param {object} fsApi - fs module (for DI)
|
|
74
|
+
* @returns {{ linked: boolean, installed: boolean }}
|
|
75
|
+
* @throws {Error} when the install cannot be spawned or exits non-zero.
|
|
126
76
|
*/
|
|
127
77
|
function runInstall(worktreePath, projectRoot, spawnFn, fsApi) {
|
|
128
78
|
const pkgManager = detectPackageManager(projectRoot, fsApi);
|
|
129
|
-
if (pkgManager) {
|
|
130
|
-
|
|
79
|
+
if (!pkgManager) return { linked: false, installed: false };
|
|
80
|
+
|
|
81
|
+
// shell:true on Windows: npm/pnpm/yarn are .cmd shims that cannot be spawned
|
|
82
|
+
// directly (ENOENT / EINVAL). pkgManager comes from lockfile detection (fixed
|
|
83
|
+
// set) and args are hardcoded, so no user input reaches the shell.
|
|
84
|
+
const result = spawnFn(pkgManager, ['install'], {
|
|
85
|
+
cwd: worktreePath,
|
|
86
|
+
stdio: 'pipe',
|
|
87
|
+
shell: process.platform === 'win32',
|
|
88
|
+
});
|
|
89
|
+
if (result && result.error) {
|
|
90
|
+
throw new Error(`Dependency install failed: could not run '${pkgManager} install' in ${worktreePath}: ${result.error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (result && typeof result.status === 'number' && result.status !== 0) {
|
|
93
|
+
throw new Error(`Dependency install failed: '${pkgManager} install' exited with code ${result.status} in ${worktreePath}`);
|
|
94
|
+
}
|
|
95
|
+
return { linked: false, installed: true };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Populate the new worktree's node_modules. Fast path: link to the main repo's
|
|
100
|
+
* shared install (a junction on Windows, a directory symlink on POSIX) so a fresh
|
|
101
|
+
* worktree is immediately usable without a full reinstall — this repo's own
|
|
102
|
+
* worktrees follow that pattern. Fallback: run the detected package manager's
|
|
103
|
+
* install when there is no shared install to link. Both paths surface failures.
|
|
104
|
+
* @param {string} worktreePath - Absolute path to the new worktree
|
|
105
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
106
|
+
* @param {object} deps
|
|
107
|
+
* @param {Function} deps.spawnFn - spawnSync-compatible function
|
|
108
|
+
* @param {object} deps.fsApi - fs module (for DI)
|
|
109
|
+
* @param {string} deps.platform - process.platform value
|
|
110
|
+
* @returns {{ linked: boolean, installed: boolean }}
|
|
111
|
+
* @throws {Error} when linking fails for a non-privilege reason or install fails.
|
|
112
|
+
*/
|
|
113
|
+
function setupWorktreeDeps(worktreePath, projectRoot, { spawnFn, fsApi, platform }) {
|
|
114
|
+
const srcModules = path.join(projectRoot, 'node_modules');
|
|
115
|
+
const destModules = path.join(worktreePath, 'node_modules');
|
|
116
|
+
|
|
117
|
+
// Already populated (e.g. git checkout carried it) — nothing to do.
|
|
118
|
+
if (fsApi.existsSync(destModules)) return { linked: false, installed: false };
|
|
119
|
+
|
|
120
|
+
// Fast path: link to the shared install when the main repo has one.
|
|
121
|
+
if (fsApi.existsSync(srcModules)) {
|
|
122
|
+
const linkType = platform === 'win32' ? 'junction' : 'dir';
|
|
123
|
+
try {
|
|
124
|
+
fsApi.symlinkSync(srcModules, destModules, linkType);
|
|
125
|
+
return { linked: true, installed: false };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Degrade to a real install only when the link was refused for lack of
|
|
128
|
+
// privilege/support; any other failure is surfaced to the caller.
|
|
129
|
+
if (!isLinkPermissionError(error)) throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return runInstall(worktreePath, projectRoot, spawnFn, fsApi);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Best-effort: record the issue → worktree → work-folder linkage in the Kernel so
|
|
138
|
+
* `forge worktree list` and orientation READ it instead of guessing the work-folder by
|
|
139
|
+
* a filesystem heuristic. Writes a `kernel_worktrees` row keyed by the absolute worktree
|
|
140
|
+
* path (idempotent upsert) and, when an issue + work-folder are given, drops a
|
|
141
|
+
* machine-readable `.forge-issue` marker so a folder resolves to its issue deterministically.
|
|
142
|
+
*
|
|
143
|
+
* Never throws: a repo without a reachable Kernel (or without the 007 linkage columns)
|
|
144
|
+
* must still get a usable worktree — orientation falls back to the folder heuristic.
|
|
145
|
+
*
|
|
146
|
+
* @param {object} params
|
|
147
|
+
* @param {string} params.projectRoot
|
|
148
|
+
* @param {string} params.worktreePath - Absolute worktree path (the row key).
|
|
149
|
+
* @param {string} params.branch
|
|
150
|
+
* @param {string|null} params.issueId
|
|
151
|
+
* @param {string|null} params.workFolder - Repo-relative work-folder path.
|
|
152
|
+
* @param {object} params.opts - DI options (may inject `_kernelDriver`, `_exec`, `_fs`).
|
|
153
|
+
* @returns {Promise<{registered: boolean, issueId: string|null, workFolder: string|null, reason?: string}>}
|
|
154
|
+
*/
|
|
155
|
+
async function registerWorktreeLinkage({ projectRoot, worktreePath, branch, issueId, workFolder, opts }) {
|
|
156
|
+
const runFile = opts._exec || execFileSync;
|
|
157
|
+
const fsApi = opts._fs || fs;
|
|
158
|
+
try {
|
|
159
|
+
// Drop the folder → issue marker first — deterministic even if the DB write fails.
|
|
160
|
+
if (workFolder && issueId) {
|
|
161
|
+
const folderAbs = path.resolve(projectRoot, workFolder);
|
|
162
|
+
fsApi.mkdirSync(folderAbs, { recursive: true });
|
|
163
|
+
fsApi.writeFileSync(path.join(folderAbs, '.forge-issue'), `${issueId}\n`, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let driver = opts._kernelDriver;
|
|
167
|
+
if (!driver) {
|
|
168
|
+
// No reachable git repo → no shared Kernel; skip quietly (orientation still works
|
|
169
|
+
// off the folder heuristic). Avoids spawning `git` in a non-repo directory.
|
|
170
|
+
if (!fsApi.existsSync(path.join(projectRoot, '.git'))) {
|
|
171
|
+
return { registered: false, issueId: issueId || null, workFolder: workFolder || null, reason: 'no git repository' };
|
|
172
|
+
}
|
|
173
|
+
// Lazy require: keep the worktree command light and avoid paying kernel setup
|
|
174
|
+
// when linkage is not needed. Migrated deps guarantee the 007 columns exist.
|
|
175
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
176
|
+
driver = (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { resolveGitCommonDir } = require('../kernel/broker');
|
|
180
|
+
const gitCommonDir = resolveGitCommonDir(projectRoot, { execFileSync: runFile });
|
|
181
|
+
|
|
182
|
+
driver.registerWorktree({
|
|
183
|
+
git_common_dir: gitCommonDir,
|
|
184
|
+
path: worktreePath,
|
|
185
|
+
branch,
|
|
186
|
+
actor: process.env.FORGE_ACTOR || null,
|
|
187
|
+
issue_id: issueId || null,
|
|
188
|
+
work_folder: workFolder || null,
|
|
189
|
+
registered_at: new Date().toISOString(),
|
|
190
|
+
state: 'active',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return { registered: true, issueId: issueId || null, workFolder: workFolder || null };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Non-fatal: keep the worktree usable. Only warn when the caller explicitly asked
|
|
196
|
+
// for linkage (--issue/--work-folder); a plain `worktree create` stays quiet.
|
|
197
|
+
if (issueId || workFolder) {
|
|
198
|
+
process.stderr.write(`forge worktree: kernel linkage not recorded (${error.message})\n`);
|
|
199
|
+
}
|
|
200
|
+
return { registered: false, issueId: issueId || null, workFolder: workFolder || null, reason: error.message };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Auto-file rail: guarantee the new worktree's branch has a backing Kernel issue,
|
|
206
|
+
* WITHOUT anyone remembering `forge issue create` (kernel issue a4b8f56f). Best-effort
|
|
207
|
+
* and NON-BLOCKING — the worktree must stay usable even if tracking fails, so any error
|
|
208
|
+
* degrades to a stderr warning and null. Skipped when `--issue` already links a real
|
|
209
|
+
* issue. Idempotent via ensureBackingIssue (deduped by branch), so re-creating a
|
|
210
|
+
* worktree or later `forge push`/pre-push firing never mints a duplicate.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} params
|
|
213
|
+
* @param {string} params.projectRoot
|
|
214
|
+
* @param {string} params.worktreePath
|
|
215
|
+
* @param {string} params.branch
|
|
216
|
+
* @param {string|null} params.issueId - explicit --issue (skip auto-file when set).
|
|
217
|
+
* @param {object} params.opts - DI (may inject `_ensureBackingIssue`, `_kernelDriver`, `_kernelBroker`, `_fs`).
|
|
218
|
+
* @returns {Promise<object|null>} the backing-issue descriptor, or null when skipped/unavailable.
|
|
219
|
+
*/
|
|
220
|
+
async function autoFileBackingIssue({ projectRoot, worktreePath, branch, issueId, opts }) {
|
|
221
|
+
if (issueId) return null; // explicit --issue already links a real issue.
|
|
222
|
+
const fsApi = opts._fs || fs;
|
|
223
|
+
try {
|
|
224
|
+
// Lazy require INSIDE the try: a module-load failure (missing file / broken
|
|
225
|
+
// sub-dependency) must degrade to null, never crash `forge worktree create`.
|
|
226
|
+
const ensureFn = opts._ensureBackingIssue || require('../kernel/backing-issue').ensureBackingIssue;
|
|
227
|
+
let driver = opts._kernelDriver;
|
|
228
|
+
let broker = opts._kernelBroker;
|
|
229
|
+
if (!driver || !broker) {
|
|
230
|
+
if (!fsApi.existsSync(path.join(projectRoot, '.git'))) return null; // no repo → no kernel
|
|
231
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
232
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot });
|
|
233
|
+
driver = driver || deps.kernelDriver;
|
|
234
|
+
broker = broker || deps.kernelBroker;
|
|
235
|
+
}
|
|
236
|
+
return await ensureFn({ branch, projectRoot, worktreePath, driver, broker });
|
|
237
|
+
} catch (error) {
|
|
238
|
+
process.stderr.write(`forge worktree: backing issue not auto-filed (${error.message})\n`);
|
|
239
|
+
return null;
|
|
131
240
|
}
|
|
132
241
|
}
|
|
133
242
|
|
|
@@ -143,9 +252,10 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
143
252
|
const runFile = opts._exec || execFileSync;
|
|
144
253
|
const runSpawn = opts._spawn || spawnSync;
|
|
145
254
|
const fsApi = opts._fs || fs;
|
|
146
|
-
const platform = opts._platform || process.platform;
|
|
147
255
|
|
|
148
256
|
const branchName = flags['--branch'] || flags.branch || `feat/${slug}`;
|
|
257
|
+
const issueId = flags['--issue'] || null;
|
|
258
|
+
const workFolder = flags['--work-folder'] || null;
|
|
149
259
|
const worktreesDir = path.resolve(projectRoot, '.worktrees');
|
|
150
260
|
|
|
151
261
|
// Guard: Detect bare repo state - worktrees created from bare repos produce broken branches
|
|
@@ -170,11 +280,23 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
170
280
|
|
|
171
281
|
// Step 0: Check if worktree already exists
|
|
172
282
|
if (fsApi.existsSync(worktreePath)) {
|
|
283
|
+
// A pre-existing worktree may be checked out on a different branch than the
|
|
284
|
+
// requested branchName — read the actual HEAD so the kernel row is accurate.
|
|
285
|
+
let existingBranch = branchName;
|
|
286
|
+
try {
|
|
287
|
+
const head = runFile('git', ['-C', worktreePath, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
288
|
+
// A detached worktree reports the literal "HEAD" — don't persist that; keep branchName.
|
|
289
|
+
if (head && head !== 'HEAD') existingBranch = head;
|
|
290
|
+
} catch (_error) { /* not a resolvable worktree HEAD — fall back to branchName */ }
|
|
291
|
+
const linkage = await registerWorktreeLinkage({ projectRoot, worktreePath, branch: existingBranch, issueId, workFolder, opts });
|
|
292
|
+
const backing = await autoFileBackingIssue({ projectRoot, worktreePath, branch: existingBranch, issueId, opts });
|
|
173
293
|
return {
|
|
174
294
|
success: true,
|
|
175
295
|
reused: true,
|
|
176
296
|
message: `Worktree already exists at ${worktreePath}`,
|
|
177
297
|
worktreePath,
|
|
298
|
+
linkage,
|
|
299
|
+
backing,
|
|
178
300
|
};
|
|
179
301
|
}
|
|
180
302
|
|
|
@@ -189,21 +311,52 @@ async function handleCreate(slug, flags, projectRoot, opts) {
|
|
|
189
311
|
runFile('git', ['worktree', 'add', worktreePath, '-b', branchName], { stdio: 'pipe' });
|
|
190
312
|
}
|
|
191
313
|
|
|
192
|
-
// Step 3:
|
|
193
|
-
|
|
314
|
+
// Step 3: Populate node_modules (link to the shared install, else install).
|
|
315
|
+
// No per-worktree issue-store bootstrap is needed — the Kernel DB lives in the
|
|
316
|
+
// shared git common dir, so the new worktree already sees the same kernel.
|
|
317
|
+
let deps;
|
|
318
|
+
try {
|
|
319
|
+
deps = setupWorktreeDeps(worktreePath, projectRoot, {
|
|
320
|
+
spawnFn: runSpawn,
|
|
321
|
+
fsApi,
|
|
322
|
+
platform: opts._platform || process.platform,
|
|
323
|
+
});
|
|
324
|
+
} catch (error) {
|
|
325
|
+
// Surface the failure instead of leaving a worktree with no usable deps.
|
|
326
|
+
return { success: false, error: error.message, worktreePath, branch: branchName };
|
|
327
|
+
}
|
|
194
328
|
|
|
195
|
-
|
|
196
|
-
|
|
329
|
+
const linkage = await registerWorktreeLinkage({ projectRoot, worktreePath, branch: branchName, issueId, workFolder, opts });
|
|
330
|
+
const backing = await autoFileBackingIssue({ projectRoot, worktreePath, branch: branchName, issueId, opts });
|
|
197
331
|
|
|
198
|
-
|
|
332
|
+
return {
|
|
199
333
|
success: true,
|
|
200
334
|
worktreePath,
|
|
201
335
|
branch: branchName,
|
|
336
|
+
depsLinked: deps.linked,
|
|
337
|
+
depsInstalled: deps.installed,
|
|
338
|
+
linkage,
|
|
339
|
+
backing,
|
|
202
340
|
};
|
|
203
|
-
|
|
204
|
-
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Handle the "list" subcommand — read the worktree registry from the Kernel.
|
|
345
|
+
* @param {string} projectRoot - Project root path
|
|
346
|
+
* @param {object} opts - DI options (may inject `_kernelDriver`)
|
|
347
|
+
* @returns {Promise<object>} Result object with a `worktrees` array
|
|
348
|
+
*/
|
|
349
|
+
async function handleList(projectRoot, opts) {
|
|
350
|
+
try {
|
|
351
|
+
let driver = opts._kernelDriver;
|
|
352
|
+
if (!driver) {
|
|
353
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
354
|
+
driver = (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
|
|
355
|
+
}
|
|
356
|
+
return { success: true, worktrees: driver.listWorktrees({}) };
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return { success: false, error: error.message, worktrees: [] };
|
|
205
359
|
}
|
|
206
|
-
return result;
|
|
207
360
|
}
|
|
208
361
|
|
|
209
362
|
/**
|
|
@@ -220,29 +373,65 @@ async function handleRemove(slug, projectRoot, opts) {
|
|
|
220
373
|
}
|
|
221
374
|
|
|
222
375
|
const runFile = opts._exec || execFileSync;
|
|
223
|
-
const fsModule = opts._fs || fs;
|
|
224
376
|
const worktreePath = path.resolve(projectRoot, '.worktrees', slug);
|
|
225
377
|
|
|
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
|
-
|
|
378
|
+
// No issue-store server to stop: the Kernel SQLite DB lives in the shared git
|
|
379
|
+
// common dir, so removing a worktree directory never touches it.
|
|
233
380
|
runFile('git', ['worktree', 'remove', worktreePath], { stdio: 'pipe' });
|
|
234
381
|
|
|
235
382
|
return { success: true, removed: worktreePath };
|
|
236
383
|
}
|
|
237
384
|
|
|
385
|
+
// Long flags that take a value, in both `--flag value` and `--flag=value` forms.
|
|
386
|
+
const WORKTREE_VALUE_FLAGS = ['--branch', '--issue', '--work-folder'];
|
|
387
|
+
const WORKTREE_USAGE_HINT = 'Usage: forge worktree create <slug> [--branch <name>] [--issue <id>] [--work-folder <path>]';
|
|
388
|
+
|
|
389
|
+
function parseWorktreeArgs(args, flags) {
|
|
390
|
+
const positional = [];
|
|
391
|
+
const parsedFlags = { ...flags };
|
|
392
|
+
|
|
393
|
+
for (let i = 0; i < args.length; i++) {
|
|
394
|
+
const arg = args[i];
|
|
395
|
+
|
|
396
|
+
const spaceFlag = WORKTREE_VALUE_FLAGS.find(flag => arg === flag);
|
|
397
|
+
if (spaceFlag) {
|
|
398
|
+
const value = args[i + 1];
|
|
399
|
+
if (!value || value.startsWith('--')) {
|
|
400
|
+
return { error: `Missing value for ${spaceFlag}. ${WORKTREE_USAGE_HINT}` };
|
|
401
|
+
}
|
|
402
|
+
parsedFlags[spaceFlag] = value;
|
|
403
|
+
i++;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const eqFlag = WORKTREE_VALUE_FLAGS.find(flag => arg.startsWith(`${flag}=`));
|
|
408
|
+
if (eqFlag) {
|
|
409
|
+
const value = arg.slice(`${eqFlag}=`.length);
|
|
410
|
+
if (!value) {
|
|
411
|
+
return { error: `Missing value for ${eqFlag}. ${WORKTREE_USAGE_HINT}` };
|
|
412
|
+
}
|
|
413
|
+
parsedFlags[eqFlag] = value;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
positional.push(arg);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Back-compat alias consumed by handleCreate (flags['--branch'] || flags.branch).
|
|
421
|
+
if (parsedFlags['--branch']) parsedFlags.branch = parsedFlags['--branch'];
|
|
422
|
+
|
|
423
|
+
return { args: positional, flags: parsedFlags };
|
|
424
|
+
}
|
|
425
|
+
|
|
238
426
|
module.exports = {
|
|
239
427
|
name: 'worktree',
|
|
240
|
-
description: 'Manage isolated worktrees
|
|
241
|
-
usage: 'forge worktree <create|remove> <slug>',
|
|
428
|
+
description: 'Manage isolated worktrees',
|
|
429
|
+
usage: 'forge worktree <create|remove|list> <slug>',
|
|
242
430
|
flags: {
|
|
243
431
|
'--branch': 'Custom branch name (default: feat/<slug>)',
|
|
432
|
+
'--issue': 'Kernel issue id to link this worktree to (records issue → worktree)',
|
|
433
|
+
'--work-folder': 'Repo-relative work-folder this issue owns (records worktree → work-folder + drops a .forge-issue marker)',
|
|
244
434
|
},
|
|
245
|
-
stopDolt,
|
|
246
435
|
|
|
247
436
|
/**
|
|
248
437
|
* Main handler for the worktree command.
|
|
@@ -257,27 +446,45 @@ module.exports = {
|
|
|
257
446
|
* @returns {Promise<object>}
|
|
258
447
|
*/
|
|
259
448
|
handler: async (args, flags, projectRoot, opts = {}) => {
|
|
260
|
-
const
|
|
261
|
-
|
|
449
|
+
const parsed = parseWorktreeArgs(args, flags);
|
|
450
|
+
if (parsed.error) {
|
|
451
|
+
return { success: false, error: parsed.error };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const subcommand = parsed.args[0];
|
|
262
455
|
|
|
263
|
-
if (!subcommand || !['create', 'remove'].includes(subcommand)) {
|
|
456
|
+
if (!subcommand || !['create', 'remove', 'list'].includes(subcommand)) {
|
|
264
457
|
return {
|
|
265
458
|
success: false,
|
|
266
|
-
error: 'Missing or invalid subcommand. Usage: forge worktree <create|remove> <slug>',
|
|
459
|
+
error: 'Missing or invalid subcommand. Usage: forge worktree <create|remove|list> <slug>',
|
|
267
460
|
};
|
|
268
461
|
}
|
|
269
462
|
|
|
463
|
+
// `list` reads the Kernel registry and takes no slug.
|
|
464
|
+
if (subcommand === 'list') {
|
|
465
|
+
return handleList(projectRoot, opts);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const slug = parsed.args[1];
|
|
270
469
|
if (!slug) {
|
|
271
470
|
return {
|
|
272
471
|
success: false,
|
|
273
|
-
error: 'Missing slug. Usage: forge worktree <create|remove> <slug>',
|
|
472
|
+
error: 'Missing slug. Usage: forge worktree <create|remove|list> <slug>',
|
|
274
473
|
};
|
|
275
474
|
}
|
|
276
475
|
|
|
277
476
|
if (subcommand === 'create') {
|
|
278
|
-
return handleCreate(slug, flags, projectRoot, opts);
|
|
477
|
+
return handleCreate(slug, parsed.flags, projectRoot, opts);
|
|
279
478
|
}
|
|
280
479
|
|
|
281
480
|
return handleRemove(slug, projectRoot, opts);
|
|
282
481
|
},
|
|
482
|
+
|
|
483
|
+
// Exposed for unit tests; not part of the CLI surface.
|
|
484
|
+
_internal: {
|
|
485
|
+
detectPackageManager,
|
|
486
|
+
setupWorktreeDeps,
|
|
487
|
+
runInstall,
|
|
488
|
+
autoFileBackingIssue,
|
|
489
|
+
},
|
|
283
490
|
};
|