@paths.design/caws-cli 10.1.0 → 11.0.0
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/README.md +125 -374
- package/dist/index.js +43 -756
- package/dist/shell/binding/resolve-binding.d.ts +4 -0
- package/dist/shell/binding/resolve-binding.d.ts.map +1 -0
- package/dist/shell/binding/resolve-binding.js +228 -0
- package/dist/shell/binding/resolve-binding.js.map +1 -0
- package/dist/shell/binding/types.d.ts +42 -0
- package/dist/shell/binding/types.d.ts.map +1 -0
- package/dist/shell/binding/types.js +21 -0
- package/dist/shell/binding/types.js.map +1 -0
- package/dist/shell/commands/claim.d.ts +14 -0
- package/dist/shell/commands/claim.d.ts.map +1 -0
- package/dist/shell/commands/claim.js +197 -0
- package/dist/shell/commands/claim.js.map +1 -0
- package/dist/shell/commands/doctor.d.ts +13 -0
- package/dist/shell/commands/doctor.d.ts.map +1 -0
- package/dist/shell/commands/doctor.js +97 -0
- package/dist/shell/commands/doctor.js.map +1 -0
- package/dist/shell/commands/evidence.d.ts +28 -0
- package/dist/shell/commands/evidence.d.ts.map +1 -0
- package/dist/shell/commands/evidence.js +166 -0
- package/dist/shell/commands/evidence.js.map +1 -0
- package/dist/shell/commands/gates.d.ts +19 -0
- package/dist/shell/commands/gates.d.ts.map +1 -0
- package/dist/shell/commands/gates.js +181 -0
- package/dist/shell/commands/gates.js.map +1 -0
- package/dist/shell/commands/init.d.ts +8 -0
- package/dist/shell/commands/init.d.ts.map +1 -0
- package/dist/shell/commands/init.js +64 -0
- package/dist/shell/commands/init.js.map +1 -0
- package/dist/shell/commands/scope.d.ts +11 -0
- package/dist/shell/commands/scope.d.ts.map +1 -0
- package/dist/shell/commands/scope.js +92 -0
- package/dist/shell/commands/scope.js.map +1 -0
- package/dist/shell/commands/status.d.ts +15 -0
- package/dist/shell/commands/status.d.ts.map +1 -0
- package/dist/shell/commands/status.js +106 -0
- package/dist/shell/commands/status.js.map +1 -0
- package/dist/shell/commands/waiver.d.ts +38 -0
- package/dist/shell/commands/waiver.d.ts.map +1 -0
- package/dist/shell/commands/waiver.js +240 -0
- package/dist/shell/commands/waiver.js.map +1 -0
- package/dist/shell/gates/disposition.d.ts +23 -0
- package/dist/shell/gates/disposition.d.ts.map +1 -0
- package/dist/shell/gates/disposition.js +87 -0
- package/dist/shell/gates/disposition.js.map +1 -0
- package/dist/shell/gates/gate-result-contract.d.ts +39 -0
- package/dist/shell/gates/gate-result-contract.d.ts.map +1 -0
- package/dist/shell/gates/gate-result-contract.js +150 -0
- package/dist/shell/gates/gate-result-contract.js.map +1 -0
- package/dist/shell/gates/quality-gates-adapter.d.ts +55 -0
- package/dist/shell/gates/quality-gates-adapter.d.ts.map +1 -0
- package/dist/shell/gates/quality-gates-adapter.js +161 -0
- package/dist/shell/gates/quality-gates-adapter.js.map +1 -0
- package/dist/shell/gates/waiver-filter.d.ts +58 -0
- package/dist/shell/gates/waiver-filter.d.ts.map +1 -0
- package/dist/shell/gates/waiver-filter.js +119 -0
- package/dist/shell/gates/waiver-filter.js.map +1 -0
- package/dist/shell/index.d.ts +50 -0
- package/dist/shell/index.d.ts.map +1 -0
- package/dist/shell/index.js +73 -0
- package/dist/shell/index.js.map +1 -0
- package/dist/shell/register.d.ts +11 -0
- package/dist/shell/register.d.ts.map +1 -0
- package/dist/shell/register.js +274 -0
- package/dist/shell/register.js.map +1 -0
- package/dist/shell/render/claim.d.ts +22 -0
- package/dist/shell/render/claim.d.ts.map +1 -0
- package/dist/shell/render/claim.js +75 -0
- package/dist/shell/render/claim.js.map +1 -0
- package/dist/shell/render/decision.d.ts +15 -0
- package/dist/shell/render/decision.d.ts.map +1 -0
- package/dist/shell/render/decision.js +66 -0
- package/dist/shell/render/decision.js.map +1 -0
- package/dist/shell/render/diagnostic.d.ts +19 -0
- package/dist/shell/render/diagnostic.d.ts.map +1 -0
- package/dist/shell/render/diagnostic.js +76 -0
- package/dist/shell/render/diagnostic.js.map +1 -0
- package/dist/shell/render/finding.d.ts +15 -0
- package/dist/shell/render/finding.d.ts.map +1 -0
- package/dist/shell/render/finding.js +57 -0
- package/dist/shell/render/finding.js.map +1 -0
- package/dist/shell/render/gates.d.ts +3 -0
- package/dist/shell/render/gates.d.ts.map +1 -0
- package/dist/shell/render/gates.js +56 -0
- package/dist/shell/render/gates.js.map +1 -0
- package/dist/shell/render/init.d.ts +11 -0
- package/dist/shell/render/init.d.ts.map +1 -0
- package/dist/shell/render/init.js +32 -0
- package/dist/shell/render/init.js.map +1 -0
- package/dist/shell/render/status.d.ts +26 -0
- package/dist/shell/render/status.d.ts.map +1 -0
- package/dist/shell/render/status.js +143 -0
- package/dist/shell/render/status.js.map +1 -0
- package/dist/shell/render/waiver.d.ts +21 -0
- package/dist/shell/render/waiver.d.ts.map +1 -0
- package/dist/shell/render/waiver.js +94 -0
- package/dist/shell/render/waiver.js.map +1 -0
- package/dist/shell/rules.d.ts +37 -0
- package/dist/shell/rules.d.ts.map +1 -0
- package/dist/shell/rules.js +51 -0
- package/dist/shell/rules.js.map +1 -0
- package/dist/shell/session/actor.d.ts +14 -0
- package/dist/shell/session/actor.d.ts.map +1 -0
- package/dist/shell/session/actor.js +34 -0
- package/dist/shell/session/actor.js.map +1 -0
- package/dist/shell/session/resolve-session.d.ts +5 -0
- package/dist/shell/session/resolve-session.d.ts.map +1 -0
- package/dist/shell/session/resolve-session.js +239 -0
- package/dist/shell/session/resolve-session.js.map +1 -0
- package/dist/shell/session/types.d.ts +56 -0
- package/dist/shell/session/types.d.ts.map +1 -0
- package/dist/shell/session/types.js +15 -0
- package/dist/shell/session/types.js.map +1 -0
- package/dist/store/agents-store.d.ts +3 -0
- package/dist/store/agents-store.d.ts.map +1 -0
- package/dist/store/agents-store.js +63 -0
- package/dist/store/agents-store.js.map +1 -0
- package/dist/store/apply-patch.d.ts +16 -0
- package/dist/store/apply-patch.d.ts.map +1 -0
- package/dist/store/apply-patch.js +191 -0
- package/dist/store/apply-patch.js.map +1 -0
- package/dist/store/atomic-write.d.ts +16 -0
- package/dist/store/atomic-write.d.ts.map +1 -0
- package/dist/store/atomic-write.js +132 -0
- package/dist/store/atomic-write.js.map +1 -0
- package/dist/store/doctor-snapshot.d.ts +20 -0
- package/dist/store/doctor-snapshot.d.ts.map +1 -0
- package/dist/store/doctor-snapshot.js +176 -0
- package/dist/store/doctor-snapshot.js.map +1 -0
- package/dist/store/events-store.d.ts +33 -0
- package/dist/store/events-store.d.ts.map +1 -0
- package/dist/store/events-store.js +297 -0
- package/dist/store/events-store.js.map +1 -0
- package/dist/store/index.d.ts +21 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +47 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/init-store.d.ts +21 -0
- package/dist/store/init-store.d.ts.map +1 -0
- package/dist/store/init-store.js +295 -0
- package/dist/store/init-store.js.map +1 -0
- package/dist/store/json-store.d.ts +3 -0
- package/dist/store/json-store.d.ts.map +1 -0
- package/dist/store/json-store.js +65 -0
- package/dist/store/json-store.js.map +1 -0
- package/dist/store/policy-store.d.ts +3 -0
- package/dist/store/policy-store.d.ts.map +1 -0
- package/dist/store/policy-store.js +65 -0
- package/dist/store/policy-store.js.map +1 -0
- package/dist/store/repo-root.d.ts +46 -0
- package/dist/store/repo-root.d.ts.map +1 -0
- package/dist/store/repo-root.js +145 -0
- package/dist/store/repo-root.js.map +1 -0
- package/dist/store/rules.d.ts +53 -0
- package/dist/store/rules.d.ts.map +1 -0
- package/dist/store/rules.js +78 -0
- package/dist/store/rules.js.map +1 -0
- package/dist/store/specs-store.d.ts +3 -0
- package/dist/store/specs-store.d.ts.map +1 -0
- package/dist/store/specs-store.js +131 -0
- package/dist/store/specs-store.js.map +1 -0
- package/dist/store/types.d.ts +84 -0
- package/dist/store/types.d.ts.map +1 -0
- package/dist/store/types.js +14 -0
- package/dist/store/types.js.map +1 -0
- package/dist/store/waivers-store.d.ts +25 -0
- package/dist/store/waivers-store.d.ts.map +1 -0
- package/dist/store/waivers-store.js +232 -0
- package/dist/store/waivers-store.js.map +1 -0
- package/dist/store/worktrees-store.d.ts +3 -0
- package/dist/store/worktrees-store.d.ts.map +1 -0
- package/dist/store/worktrees-store.js +62 -0
- package/dist/store/worktrees-store.js.map +1 -0
- package/dist/store/yaml-store.d.ts +9 -0
- package/dist/store/yaml-store.d.ts.map +1 -0
- package/dist/store/yaml-store.js +121 -0
- package/dist/store/yaml-store.js.map +1 -0
- package/package.json +15 -13
- package/dist/budget-derivation.js +0 -751
- package/dist/cicd-optimizer.js +0 -504
- package/dist/commands/archive.js +0 -500
- package/dist/commands/burnup.js +0 -198
- package/dist/commands/diagnose.js +0 -525
- package/dist/commands/evaluate.js +0 -314
- package/dist/commands/gates.js +0 -149
- package/dist/commands/init.js +0 -857
- package/dist/commands/iterate.js +0 -417
- package/dist/commands/mode.js +0 -269
- package/dist/commands/parallel.js +0 -242
- package/dist/commands/plan.js +0 -438
- package/dist/commands/provenance.js +0 -1143
- package/dist/commands/quality-monitor.js +0 -284
- package/dist/commands/scope.js +0 -264
- package/dist/commands/session.js +0 -312
- package/dist/commands/sidecar.js +0 -74
- package/dist/commands/specs.js +0 -1448
- package/dist/commands/status.js +0 -1151
- package/dist/commands/templates.js +0 -237
- package/dist/commands/tool.js +0 -136
- package/dist/commands/tutorial.js +0 -480
- package/dist/commands/validate.js +0 -357
- package/dist/commands/verify-acs.js +0 -443
- package/dist/commands/waivers.js +0 -599
- package/dist/commands/workflow.js +0 -243
- package/dist/commands/worktree.js +0 -386
- package/dist/config/lite-scope.js +0 -158
- package/dist/config/modes.js +0 -347
- package/dist/constants/spec-types.js +0 -65
- package/dist/gates/budget-limit.js +0 -121
- package/dist/gates/feedback.js +0 -260
- package/dist/gates/format.js +0 -179
- package/dist/gates/god-object.js +0 -117
- package/dist/gates/pipeline.js +0 -167
- package/dist/gates/scope-boundary.js +0 -93
- package/dist/gates/spec-completeness.js +0 -109
- package/dist/gates/todo-detection.js +0 -205
- package/dist/generators/jest-config-generator.js +0 -242
- package/dist/generators/working-spec.js +0 -237
- package/dist/minimal-cli.js +0 -88
- package/dist/parallel/parallel-manager.js +0 -433
- package/dist/policy/PolicyManager.js +0 -465
- package/dist/scaffold/claude-hooks.js +0 -443
- package/dist/scaffold/cursor-hooks.js +0 -177
- package/dist/scaffold/git-hooks.js +0 -928
- package/dist/scaffold/index.js +0 -794
- package/dist/session/session-manager.js +0 -653
- package/dist/sidecars/index.js +0 -33
- package/dist/sidecars/listeners.js +0 -40
- package/dist/sidecars/provenance-summary.js +0 -238
- package/dist/sidecars/quality-gaps.js +0 -258
- package/dist/sidecars/schema.js +0 -149
- package/dist/sidecars/spec-drift.js +0 -151
- package/dist/sidecars/waiver-draft.js +0 -176
- package/dist/spec/SpecFileManager.js +0 -419
- package/dist/templates/.caws/schemas/policy.schema.json +0 -112
- package/dist/templates/.caws/schemas/scope.schema.json +0 -52
- package/dist/templates/.caws/schemas/waivers.schema.json +0 -106
- package/dist/templates/.caws/schemas/working-spec.schema.json +0 -340
- package/dist/templates/.caws/schemas/worktrees.schema.json +0 -38
- package/dist/templates/.caws/templates/working-spec.template.yml +0 -80
- package/dist/templates/.caws/tools/README.md +0 -18
- package/dist/templates/.caws/tools/scope-guard.js +0 -203
- package/dist/templates/.caws/tools-allow.json +0 -331
- package/dist/templates/.caws/waivers.yml +0 -19
- package/dist/templates/.claude/README.md +0 -190
- package/dist/templates/.claude/hooks/audit.sh +0 -121
- package/dist/templates/.claude/hooks/block-dangerous.sh +0 -203
- package/dist/templates/.claude/hooks/classify_command.py +0 -592
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
- package/dist/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
- package/dist/templates/.claude/hooks/naming-check.sh +0 -100
- package/dist/templates/.claude/hooks/protected-paths.sh +0 -39
- package/dist/templates/.claude/hooks/quality-check.sh +0 -81
- package/dist/templates/.claude/hooks/scan-secrets.sh +0 -85
- package/dist/templates/.claude/hooks/scope-guard.sh +0 -381
- package/dist/templates/.claude/hooks/session-caws-status.sh +0 -117
- package/dist/templates/.claude/hooks/session-log.sh +0 -634
- package/dist/templates/.claude/hooks/simplification-guard.sh +0 -92
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +0 -46
- package/dist/templates/.claude/hooks/test_classify_command.py +0 -370
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
- package/dist/templates/.claude/hooks/validate-spec.sh +0 -76
- package/dist/templates/.claude/hooks/worktree-guard.sh +0 -220
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +0 -190
- package/dist/templates/.claude/rules/git-safety.md +0 -26
- package/dist/templates/.claude/rules/worktree-isolation.md +0 -83
- package/dist/templates/.claude/settings.json +0 -141
- package/dist/templates/.cursor/README.md +0 -299
- package/dist/templates/.cursor/hooks/audit.sh +0 -55
- package/dist/templates/.cursor/hooks/block-dangerous.sh +0 -84
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +0 -52
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
- package/dist/templates/.cursor/hooks/format.sh +0 -38
- package/dist/templates/.cursor/hooks/naming-check.sh +0 -64
- package/dist/templates/.cursor/hooks/scan-secrets.sh +0 -51
- package/dist/templates/.cursor/hooks/scope-guard.sh +0 -52
- package/dist/templates/.cursor/hooks/session-log.sh +0 -924
- package/dist/templates/.cursor/hooks/validate-spec.sh +0 -83
- package/dist/templates/.cursor/hooks.json +0 -76
- package/dist/templates/.cursor/rules/00-claims-verification.mdc +0 -144
- package/dist/templates/.cursor/rules/01-working-style.mdc +0 -50
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +0 -368
- package/dist/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
- package/dist/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
- package/dist/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
- package/dist/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
- package/dist/templates/.cursor/rules/07-process-ops.mdc +0 -20
- package/dist/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
- package/dist/templates/.cursor/rules/09-docstrings.mdc +0 -89
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
- package/dist/templates/.cursor/rules/README.md +0 -148
- package/dist/templates/.github/copilot-instructions.md +0 -82
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
- package/dist/templates/.junie/guidelines.md +0 -73
- package/dist/templates/.vscode/launch.json +0 -17
- package/dist/templates/.vscode/settings.json +0 -95
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +0 -54
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +0 -92
- package/dist/templates/CLAUDE.md +0 -174
- package/dist/templates/COMMIT_CONVENTIONS.md +0 -86
- package/dist/templates/OIDC_SETUP.md +0 -300
- package/dist/templates/agents.md +0 -145
- package/dist/templates/codemod/README.md +0 -1
- package/dist/templates/codemod/test.js +0 -93
- package/dist/templates/docs/README.md +0 -151
- package/dist/templates/scripts/new_feature.sh +0 -80
- package/dist/templates/scripts/quality-gates/check-god-objects.js +0 -146
- package/dist/templates/scripts/quality-gates/run-quality-gates.js +0 -50
- package/dist/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
- package/dist/test-analysis.js +0 -786
- package/dist/tool-interface.js +0 -314
- package/dist/tool-loader.js +0 -303
- package/dist/tool-validator.js +0 -393
- package/dist/utils/agent-session.js +0 -202
- package/dist/utils/async-utils.js +0 -188
- package/dist/utils/command-wrapper.js +0 -200
- package/dist/utils/event-log.js +0 -584
- package/dist/utils/event-renderer.js +0 -521
- package/dist/utils/finalization.js +0 -230
- package/dist/utils/git-lock.js +0 -119
- package/dist/utils/gitignore-updater.js +0 -158
- package/dist/utils/ide-detection.js +0 -133
- package/dist/utils/lifecycle-events.js +0 -94
- package/dist/utils/project-analysis.js +0 -367
- package/dist/utils/promise-utils.js +0 -72
- package/dist/utils/quality-gates-errors.js +0 -520
- package/dist/utils/quality-gates-utils.js +0 -387
- package/dist/utils/schema-validator.js +0 -50
- package/dist/utils/spec-resolver.js +0 -711
- package/dist/utils/typescript-detector.js +0 -369
- package/dist/utils/working-state.js +0 -530
- package/dist/utils/yaml-validation.js +0 -156
- package/dist/validation/spec-validation.js +0 -921
- package/dist/waivers-manager.js +0 -732
- package/dist/worktree/worktree-manager.js +0 -1374
- package/templates/.caws/schemas/policy.schema.json +0 -112
- package/templates/.caws/schemas/scope.schema.json +0 -52
- package/templates/.caws/schemas/waivers.schema.json +0 -106
- package/templates/.caws/schemas/working-spec.schema.json +0 -340
- package/templates/.caws/schemas/worktrees.schema.json +0 -38
- package/templates/.caws/templates/working-spec.template.yml +0 -80
- package/templates/.caws/tools/README.md +0 -18
- package/templates/.caws/tools/scope-guard.js +0 -203
- package/templates/.caws/tools-allow.json +0 -331
- package/templates/.caws/waivers.yml +0 -19
- package/templates/.claude/README.md +0 -190
- package/templates/.claude/hooks/audit.sh +0 -121
- package/templates/.claude/hooks/block-dangerous.sh +0 -203
- package/templates/.claude/hooks/classify_command.py +0 -592
- package/templates/.claude/hooks/doc-frontmatter-check.sh +0 -173
- package/templates/.claude/hooks/lite-sprawl-check.sh +0 -145
- package/templates/.claude/hooks/naming-check.sh +0 -100
- package/templates/.claude/hooks/protected-paths.sh +0 -39
- package/templates/.claude/hooks/quality-check.sh +0 -81
- package/templates/.claude/hooks/scan-secrets.sh +0 -85
- package/templates/.claude/hooks/scope-guard.sh +0 -381
- package/templates/.claude/hooks/session-caws-status.sh +0 -117
- package/templates/.claude/hooks/session-log.sh +0 -634
- package/templates/.claude/hooks/simplification-guard.sh +0 -92
- package/templates/.claude/hooks/stop-worktree-check.sh +0 -46
- package/templates/.claude/hooks/test_classify_command.py +0 -370
- package/templates/.claude/hooks/test_wrapper_smoke.sh +0 -96
- package/templates/.claude/hooks/validate-spec.sh +0 -76
- package/templates/.claude/hooks/worktree-guard.sh +0 -220
- package/templates/.claude/hooks/worktree-write-guard.sh +0 -190
- package/templates/.claude/rules/git-safety.md +0 -26
- package/templates/.claude/rules/worktree-isolation.md +0 -83
- package/templates/.claude/settings.json +0 -141
- package/templates/.cursor/README.md +0 -299
- package/templates/.cursor/hooks/audit.sh +0 -55
- package/templates/.cursor/hooks/block-dangerous.sh +0 -84
- package/templates/.cursor/hooks/caws-quality-check.sh +0 -52
- package/templates/.cursor/hooks/caws-scope-guard.sh +0 -130
- package/templates/.cursor/hooks/format.sh +0 -38
- package/templates/.cursor/hooks/naming-check.sh +0 -64
- package/templates/.cursor/hooks/scan-secrets.sh +0 -51
- package/templates/.cursor/hooks/scope-guard.sh +0 -52
- package/templates/.cursor/hooks/session-log.sh +0 -924
- package/templates/.cursor/hooks/validate-spec.sh +0 -83
- package/templates/.cursor/hooks.json +0 -76
- package/templates/.cursor/rules/00-claims-verification.mdc +0 -144
- package/templates/.cursor/rules/01-working-style.mdc +0 -50
- package/templates/.cursor/rules/02-quality-gates.mdc +0 -368
- package/templates/.cursor/rules/03-naming-and-refactor.mdc +0 -33
- package/templates/.cursor/rules/04-logging-language-style.mdc +0 -23
- package/templates/.cursor/rules/05-safe-defaults-guards.mdc +0 -23
- package/templates/.cursor/rules/06-typescript-conventions.mdc +0 -36
- package/templates/.cursor/rules/07-process-ops.mdc +0 -20
- package/templates/.cursor/rules/08-solid-and-architecture.mdc +0 -16
- package/templates/.cursor/rules/09-docstrings.mdc +0 -89
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +0 -385
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +0 -381
- package/templates/.cursor/rules/12-implementation-completeness.mdc +0 -516
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +0 -578
- package/templates/.cursor/rules/README.md +0 -148
- package/templates/.github/copilot-instructions.md +0 -82
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +0 -5
- package/templates/.idea/runConfigurations/CAWS_Validate.xml +0 -5
- package/templates/.junie/guidelines.md +0 -73
- package/templates/.vscode/launch.json +0 -17
- package/templates/.vscode/settings.json +0 -95
- package/templates/.windsurf/rules/caws-quality-standards.md +0 -54
- package/templates/.windsurf/workflows/caws-guided-development.md +0 -92
- package/templates/CLAUDE.md +0 -174
- package/templates/COMMIT_CONVENTIONS.md +0 -86
- package/templates/OIDC_SETUP.md +0 -300
- package/templates/agents.md +0 -145
- package/templates/codemod/README.md +0 -1
- package/templates/codemod/test.js +0 -93
- package/templates/docs/README.md +0 -151
- package/templates/scripts/new_feature.sh +0 -80
- package/templates/scripts/quality-gates/check-god-objects.js +0 -146
- package/templates/scripts/quality-gates/run-quality-gates.js +0 -50
- package/templates/scripts/v3/analysis/todo_analyzer.py +0 -1997
|
@@ -1,1374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview CAWS Git Worktree Manager
|
|
3
|
-
* Provides CRUD operations for git worktrees with scope isolation
|
|
4
|
-
* @author @darianrosebrook
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { execFileSync } = require('child_process');
|
|
8
|
-
const fs = require('fs-extra');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const chalk = require('chalk');
|
|
11
|
-
const { createValidator, getSchemaPath } = require('../utils/schema-validator');
|
|
12
|
-
const { getAgentSessionId } = require('../utils/agent-session');
|
|
13
|
-
const { lifecycle, EVENTS } = require('../utils/lifecycle-events');
|
|
14
|
-
|
|
15
|
-
const WORKTREES_DIR = '.caws/worktrees';
|
|
16
|
-
const REGISTRY_FILE = '.caws/worktrees.json';
|
|
17
|
-
const BRANCH_PREFIX = 'caws/';
|
|
18
|
-
|
|
19
|
-
function findFeatureSpecPath(root, specId) {
|
|
20
|
-
if (!specId) return null;
|
|
21
|
-
|
|
22
|
-
const candidates = [
|
|
23
|
-
path.join(root, '.caws', 'specs', `${specId}.yaml`),
|
|
24
|
-
path.join(root, '.caws', 'specs', `${specId}.yml`),
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function writeSpecWithWorktree(filePath, worktreeName) {
|
|
31
|
-
const yaml = require('js-yaml');
|
|
32
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
33
|
-
const parsed = yaml.load(content);
|
|
34
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
35
|
-
return content;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
parsed.worktree = worktreeName;
|
|
39
|
-
return yaml.dump(parsed, { lineWidth: 120, noRefs: true });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function hasPathChanges(root, relativePath) {
|
|
43
|
-
try {
|
|
44
|
-
const output = execFileSync(
|
|
45
|
-
'git',
|
|
46
|
-
['status', '--porcelain', '--', relativePath],
|
|
47
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
48
|
-
).trim();
|
|
49
|
-
return output.length > 0;
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function ensureCanonicalSpecCommitted(root, specPath, specId, worktreeName) {
|
|
56
|
-
const relativeSpecPath = path.relative(root, specPath);
|
|
57
|
-
const nextContent = writeSpecWithWorktree(specPath, worktreeName);
|
|
58
|
-
const currentContent = fs.readFileSync(specPath, 'utf8');
|
|
59
|
-
|
|
60
|
-
if (currentContent !== nextContent) {
|
|
61
|
-
fs.writeFileSync(specPath, nextContent);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!hasPathChanges(root, relativeSpecPath)) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
execFileSync('git', ['add', '--', relativeSpecPath], {
|
|
69
|
-
cwd: root,
|
|
70
|
-
stdio: 'pipe',
|
|
71
|
-
});
|
|
72
|
-
execFileSync(
|
|
73
|
-
'git',
|
|
74
|
-
['commit', '-m', `chore(caws): bind spec ${specId} to worktree ${worktreeName}`, '--', relativeSpecPath],
|
|
75
|
-
{
|
|
76
|
-
cwd: root,
|
|
77
|
-
stdio: 'pipe',
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
|
|
84
|
-
if (!specId) return;
|
|
85
|
-
|
|
86
|
-
const canonicalSpecPath = findFeatureSpecPath(root, specId);
|
|
87
|
-
const workingSpecPath = path.join(cawsDest, 'working-spec.yaml');
|
|
88
|
-
|
|
89
|
-
if (!canonicalSpecPath) {
|
|
90
|
-
console.warn(
|
|
91
|
-
chalk.yellow(`Warning: spec '${specId}' not found in .caws/specs/ — generating default working spec for worktree`)
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (canonicalSpecPath) {
|
|
96
|
-
const destSpecsDir = path.join(cawsDest, 'specs');
|
|
97
|
-
const destSpecPath = path.join(destSpecsDir, path.basename(canonicalSpecPath));
|
|
98
|
-
fs.ensureDirSync(destSpecsDir);
|
|
99
|
-
|
|
100
|
-
// Keep a canonical feature-spec copy inside the worktree and align
|
|
101
|
-
// working-spec.yaml to that exact content for legacy-compatible commands.
|
|
102
|
-
const specContent = writeSpecWithWorktree(canonicalSpecPath, worktreeName);
|
|
103
|
-
fs.writeFileSync(destSpecPath, specContent);
|
|
104
|
-
fs.writeFileSync(workingSpecPath, specContent);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const { generateWorkingSpec } = require('../generators/working-spec');
|
|
109
|
-
let specContent = generateWorkingSpec({
|
|
110
|
-
projectId: specId,
|
|
111
|
-
projectTitle: `Worktree: ${worktreeName}`,
|
|
112
|
-
projectDescription: `Isolated worktree for ${worktreeName}`,
|
|
113
|
-
riskTier: 3,
|
|
114
|
-
projectMode: 'feature',
|
|
115
|
-
scopeIn: scope || 'src/',
|
|
116
|
-
scopeOut: 'node_modules/, dist/, build/',
|
|
117
|
-
maxFiles: 25,
|
|
118
|
-
maxLoc: 1000,
|
|
119
|
-
blastModules: scope || 'src',
|
|
120
|
-
dataMigration: false,
|
|
121
|
-
rollbackSlo: '5m',
|
|
122
|
-
projectThreats: '',
|
|
123
|
-
projectInvariants: 'System maintains data consistency',
|
|
124
|
-
acceptanceCriteria: 'Given current state, when action occurs, then expected result',
|
|
125
|
-
a11yRequirements: 'keyboard',
|
|
126
|
-
perfBudget: 250,
|
|
127
|
-
securityRequirements: 'validation',
|
|
128
|
-
contractType: '',
|
|
129
|
-
contractPath: '',
|
|
130
|
-
observabilityLogs: '',
|
|
131
|
-
observabilityMetrics: '',
|
|
132
|
-
observabilityTraces: '',
|
|
133
|
-
migrationPlan: '',
|
|
134
|
-
rollbackPlan: '',
|
|
135
|
-
needsOverride: false,
|
|
136
|
-
isExperimental: false,
|
|
137
|
-
aiConfidence: 0.8,
|
|
138
|
-
uncertaintyAreas: '',
|
|
139
|
-
complexityFactors: '',
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const yaml = require('js-yaml');
|
|
144
|
-
const parsed = yaml.load(specContent);
|
|
145
|
-
if (parsed && typeof parsed === 'object') {
|
|
146
|
-
parsed.worktree = worktreeName;
|
|
147
|
-
specContent = yaml.dump(parsed, { lineWidth: 120, noRefs: true });
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
// Keep generated spec content if augmentation fails.
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
fs.ensureDirSync(path.dirname(workingSpecPath));
|
|
154
|
-
fs.writeFileSync(workingSpecPath, specContent);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parseSpecIdFromYamlFile(filePath) {
|
|
158
|
-
try {
|
|
159
|
-
const yaml = require('js-yaml');
|
|
160
|
-
const doc = yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
161
|
-
if (doc && typeof doc.id === 'string' && doc.id.trim()) {
|
|
162
|
-
return doc.id.trim();
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
// Ignore malformed YAML during inference
|
|
166
|
-
}
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Scan .caws/specs/ for a spec that declares `worktree: <name>`.
|
|
172
|
-
* Returns the spec's id if found, null otherwise.
|
|
173
|
-
* This enables auto-binding: when a spec already names the worktree
|
|
174
|
-
* it expects, the registry entry gets the specId automatically.
|
|
175
|
-
* @param {string} root - Repository root
|
|
176
|
-
* @param {string} worktreeName - Worktree name to match
|
|
177
|
-
* @returns {string|null} Spec ID or null
|
|
178
|
-
*/
|
|
179
|
-
function findSpecByWorktreeName(root, worktreeName) {
|
|
180
|
-
const yaml = require('js-yaml');
|
|
181
|
-
const specsDir = path.join(root, '.caws', 'specs');
|
|
182
|
-
if (!fs.existsSync(specsDir)) return null;
|
|
183
|
-
|
|
184
|
-
const specFiles = fs.readdirSync(specsDir)
|
|
185
|
-
.filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'));
|
|
186
|
-
|
|
187
|
-
for (const specFile of specFiles) {
|
|
188
|
-
try {
|
|
189
|
-
const doc = yaml.load(fs.readFileSync(path.join(specsDir, specFile), 'utf8'));
|
|
190
|
-
if (doc && doc.worktree === worktreeName && typeof doc.id === 'string') {
|
|
191
|
-
return doc.id.trim();
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
// Skip malformed spec files
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function inferSpecIdForWorktree(worktreePath) {
|
|
201
|
-
if (!worktreePath) return null;
|
|
202
|
-
|
|
203
|
-
const specsDir = path.join(worktreePath, '.caws', 'specs');
|
|
204
|
-
if (fs.existsSync(specsDir)) {
|
|
205
|
-
const specFiles = fs.readdirSync(specsDir)
|
|
206
|
-
.filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'))
|
|
207
|
-
.sort();
|
|
208
|
-
|
|
209
|
-
for (const specFile of specFiles) {
|
|
210
|
-
const inferred = parseSpecIdFromYamlFile(path.join(specsDir, specFile));
|
|
211
|
-
if (inferred) {
|
|
212
|
-
return inferred;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return parseSpecIdFromYamlFile(path.join(worktreePath, '.caws', 'working-spec.yaml'));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Get the last commit info for a branch
|
|
222
|
-
* @param {string} branch - Branch name
|
|
223
|
-
* @param {string} root - Repository root
|
|
224
|
-
* @returns {{ age: string, timestamp: Date, sha: string } | null}
|
|
225
|
-
*/
|
|
226
|
-
function getLastCommitInfo(branch, root) {
|
|
227
|
-
try {
|
|
228
|
-
const output = execFileSync(
|
|
229
|
-
'git',
|
|
230
|
-
['log', branch, '-1', '--format=%H%n%aI%n%ar'],
|
|
231
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
232
|
-
).trim();
|
|
233
|
-
const [sha, iso, age] = output.split('\n');
|
|
234
|
-
return { sha, timestamp: new Date(iso), age };
|
|
235
|
-
} catch {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Check if a branch has been merged into another branch
|
|
242
|
-
* @param {string} branch - Branch to check
|
|
243
|
-
* @param {string} target - Target branch (e.g., "main")
|
|
244
|
-
* @param {string} root - Repository root
|
|
245
|
-
* @returns {boolean}
|
|
246
|
-
*/
|
|
247
|
-
function isBranchMerged(branch, target, root) {
|
|
248
|
-
try {
|
|
249
|
-
const merged = execFileSync(
|
|
250
|
-
'git',
|
|
251
|
-
['branch', '--merged', target, '--list', branch],
|
|
252
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
253
|
-
).trim();
|
|
254
|
-
return merged.length > 0;
|
|
255
|
-
} catch {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Check if a branch has divergent commits from target (commits on branch not on target).
|
|
262
|
-
* @param {string} branch - Branch to check
|
|
263
|
-
* @param {string} target - Target branch (e.g., "main")
|
|
264
|
-
* @param {string} root - Repository root
|
|
265
|
-
* @returns {boolean}
|
|
266
|
-
*/
|
|
267
|
-
function hasDivergentCommits(branch, target, root) {
|
|
268
|
-
try {
|
|
269
|
-
const count = execFileSync(
|
|
270
|
-
'git',
|
|
271
|
-
['rev-list', '--count', `${target}..${branch}`],
|
|
272
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
273
|
-
).trim();
|
|
274
|
-
return parseInt(count, 10) > 0;
|
|
275
|
-
} catch {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Check if a worktree directory has dirty (uncommitted) files.
|
|
282
|
-
* @param {string} worktreePath - Path to the worktree
|
|
283
|
-
* @returns {boolean}
|
|
284
|
-
*/
|
|
285
|
-
function hasDirtyFiles(worktreePath) {
|
|
286
|
-
try {
|
|
287
|
-
const status = execFileSync(
|
|
288
|
-
'git',
|
|
289
|
-
['status', '--porcelain'],
|
|
290
|
-
{ cwd: worktreePath, encoding: 'utf8', stdio: 'pipe' }
|
|
291
|
-
).trim();
|
|
292
|
-
return status.length > 0;
|
|
293
|
-
} catch {
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Get the canonical git repository root (main worktree, not a linked worktree).
|
|
300
|
-
*
|
|
301
|
-
* `git rev-parse --show-toplevel` returns the root of whichever worktree
|
|
302
|
-
* the CWD is inside. In a linked worktree that is NOT the main repo root,
|
|
303
|
-
* so CAWS would read the wrong (or missing) .caws/worktrees.json.
|
|
304
|
-
*
|
|
305
|
-
* `--git-common-dir` always resolves to the main repo's .git directory,
|
|
306
|
-
* even from inside a linked worktree. Its parent is the canonical repo root.
|
|
307
|
-
*
|
|
308
|
-
* @returns {string} Absolute path to the main repo root
|
|
309
|
-
*/
|
|
310
|
-
function getRepoRoot() {
|
|
311
|
-
const gitCommonDir = execFileSync(
|
|
312
|
-
'git',
|
|
313
|
-
['rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
314
|
-
{ encoding: 'utf8' }
|
|
315
|
-
).trim();
|
|
316
|
-
// gitCommonDir is /path/to/main-repo/.git — parent is the repo root
|
|
317
|
-
return path.dirname(gitCommonDir);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Get current branch name
|
|
322
|
-
* @returns {string}
|
|
323
|
-
*/
|
|
324
|
-
function getCurrentBranch() {
|
|
325
|
-
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
326
|
-
encoding: 'utf8',
|
|
327
|
-
}).trim();
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Track whether we've already warned about schema violations this process.
|
|
331
|
-
// loadRegistry() is called multiple times per command; warning every time
|
|
332
|
-
// floods stderr and contributes to Claude Code context-window exhaustion.
|
|
333
|
-
let _schemaWarned = false;
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Load the worktree registry
|
|
337
|
-
* @param {string} root - Repository root
|
|
338
|
-
* @returns {Object} Registry object
|
|
339
|
-
*/
|
|
340
|
-
function loadRegistry(root) {
|
|
341
|
-
const registryPath = path.join(root, REGISTRY_FILE);
|
|
342
|
-
try {
|
|
343
|
-
if (fs.existsSync(registryPath)) {
|
|
344
|
-
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
345
|
-
try {
|
|
346
|
-
const validate = createValidator(getSchemaPath('worktrees.schema.json', root));
|
|
347
|
-
const result = validate(data);
|
|
348
|
-
if (!result.valid && !_schemaWarned) {
|
|
349
|
-
_schemaWarned = true;
|
|
350
|
-
console.warn('Worktree registry has schema violations:', result.errors);
|
|
351
|
-
}
|
|
352
|
-
} catch (schemaErr) {
|
|
353
|
-
if (!_schemaWarned) {
|
|
354
|
-
_schemaWarned = true;
|
|
355
|
-
console.warn('Could not validate worktree registry schema:', schemaErr.message);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return data;
|
|
359
|
-
}
|
|
360
|
-
} catch {
|
|
361
|
-
// Corrupted registry, start fresh
|
|
362
|
-
}
|
|
363
|
-
return { version: 1, worktrees: {} };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Save the worktree registry
|
|
368
|
-
* @param {string} root - Repository root
|
|
369
|
-
* @param {Object} registry - Registry object
|
|
370
|
-
*/
|
|
371
|
-
function saveRegistry(root, registry) {
|
|
372
|
-
// Auto-prune destroyed entries whose branch and directory are both gone.
|
|
373
|
-
// This prevents the registry from accumulating ghost entries over time.
|
|
374
|
-
for (const [name, entry] of Object.entries(registry.worktrees || {})) {
|
|
375
|
-
if (entry.status !== 'destroyed') continue;
|
|
376
|
-
const dirGone = !fs.existsSync(entry.path);
|
|
377
|
-
let branchGone = true;
|
|
378
|
-
if (entry.branch) {
|
|
379
|
-
try {
|
|
380
|
-
execFileSync('git', ['rev-parse', '--verify', entry.branch], {
|
|
381
|
-
cwd: root, stdio: 'pipe',
|
|
382
|
-
});
|
|
383
|
-
branchGone = false;
|
|
384
|
-
} catch {
|
|
385
|
-
branchGone = true;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (dirGone && branchGone) {
|
|
389
|
-
delete registry.worktrees[name];
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const registryPath = path.join(root, REGISTRY_FILE);
|
|
394
|
-
fs.ensureDirSync(path.dirname(registryPath));
|
|
395
|
-
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Discover git worktrees under .caws/worktrees/ that are not in the registry.
|
|
400
|
-
* @param {string} root - Repository root
|
|
401
|
-
* @param {Object} registry - Current registry object
|
|
402
|
-
* @returns {Array<{ name: string, path: string, branch: string }>}
|
|
403
|
-
*/
|
|
404
|
-
function discoverUnregisteredWorktrees(root, registry) {
|
|
405
|
-
const unregistered = [];
|
|
406
|
-
try {
|
|
407
|
-
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
408
|
-
cwd: root,
|
|
409
|
-
encoding: 'utf8',
|
|
410
|
-
stdio: 'pipe',
|
|
411
|
-
});
|
|
412
|
-
let worktreesDir;
|
|
413
|
-
try {
|
|
414
|
-
worktreesDir = fs.realpathSync(path.resolve(root, WORKTREES_DIR));
|
|
415
|
-
} catch {
|
|
416
|
-
// Directory might not exist yet
|
|
417
|
-
worktreesDir = path.resolve(root, WORKTREES_DIR);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const blocks = output.split('\n\n').filter(Boolean);
|
|
421
|
-
for (const block of blocks) {
|
|
422
|
-
const lines = block.split('\n');
|
|
423
|
-
const wtLine = lines.find((l) => l.startsWith('worktree '));
|
|
424
|
-
const branchLine = lines.find((l) => l.startsWith('branch '));
|
|
425
|
-
if (!wtLine) continue;
|
|
426
|
-
|
|
427
|
-
const wtPath = wtLine.replace('worktree ', '');
|
|
428
|
-
let resolvedPath;
|
|
429
|
-
try {
|
|
430
|
-
resolvedPath = fs.realpathSync(wtPath);
|
|
431
|
-
} catch {
|
|
432
|
-
resolvedPath = path.resolve(wtPath);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Only consider worktrees under .caws/worktrees/
|
|
436
|
-
if (!resolvedPath.startsWith(worktreesDir + path.sep)) continue;
|
|
437
|
-
|
|
438
|
-
const name = path.basename(resolvedPath);
|
|
439
|
-
if (registry.worktrees[name]) continue;
|
|
440
|
-
|
|
441
|
-
const branch = branchLine
|
|
442
|
-
? branchLine.replace('branch refs/heads/', '')
|
|
443
|
-
: `${BRANCH_PREFIX}${name}`;
|
|
444
|
-
unregistered.push({ name, path: resolvedPath, branch });
|
|
445
|
-
}
|
|
446
|
-
} catch {
|
|
447
|
-
// git worktree list failed
|
|
448
|
-
}
|
|
449
|
-
return unregistered;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Auto-register an unregistered worktree. Infers baseBranch via merge-base.
|
|
454
|
-
* @param {string} root - Repository root
|
|
455
|
-
* @param {Object} registry - Registry object (mutated in place)
|
|
456
|
-
* @param {{ name: string, path: string, branch: string }} discovered
|
|
457
|
-
* @returns {Object} The registered entry
|
|
458
|
-
*/
|
|
459
|
-
function autoRegisterWorktree(root, registry, discovered) {
|
|
460
|
-
let baseBranch = 'main';
|
|
461
|
-
try {
|
|
462
|
-
execFileSync(
|
|
463
|
-
'git',
|
|
464
|
-
['merge-base', discovered.branch, 'main'],
|
|
465
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
466
|
-
);
|
|
467
|
-
} catch {
|
|
468
|
-
try {
|
|
469
|
-
execFileSync(
|
|
470
|
-
'git',
|
|
471
|
-
['merge-base', discovered.branch, 'master'],
|
|
472
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
473
|
-
);
|
|
474
|
-
baseBranch = 'master';
|
|
475
|
-
} catch {
|
|
476
|
-
// Keep 'main' as default
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const entry = {
|
|
481
|
-
name: discovered.name,
|
|
482
|
-
path: discovered.path,
|
|
483
|
-
branch: discovered.branch,
|
|
484
|
-
baseBranch,
|
|
485
|
-
scope: null,
|
|
486
|
-
specId: inferSpecIdForWorktree(discovered.path),
|
|
487
|
-
owner: null,
|
|
488
|
-
createdAt: new Date().toISOString(),
|
|
489
|
-
status: 'active',
|
|
490
|
-
autoRegistered: true,
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
registry.worktrees[discovered.name] = entry;
|
|
494
|
-
saveRegistry(root, registry);
|
|
495
|
-
return entry;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Create a new git worktree with scope isolation
|
|
500
|
-
* @param {string} name - Worktree name
|
|
501
|
-
* @param {Object} options - Creation options
|
|
502
|
-
* @param {string} [options.scope] - Sparse checkout pattern (e.g., "src/auth/**")
|
|
503
|
-
* @param {string} [options.baseBranch] - Base branch to create from
|
|
504
|
-
* @param {string} [options.specId] - Associated spec ID for standard+ modes
|
|
505
|
-
* @returns {Object} Created worktree info
|
|
506
|
-
*/
|
|
507
|
-
function createWorktree(name, options = {}) {
|
|
508
|
-
const root = getRepoRoot();
|
|
509
|
-
const { scope, baseBranch, specId } = options;
|
|
510
|
-
|
|
511
|
-
// Validate name
|
|
512
|
-
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
513
|
-
throw new Error('Worktree name must contain only letters, numbers, hyphens, and underscores');
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const registry = loadRegistry(root);
|
|
517
|
-
|
|
518
|
-
// Check for duplicate in registry
|
|
519
|
-
if (registry.worktrees[name]) {
|
|
520
|
-
const existing = registry.worktrees[name];
|
|
521
|
-
if (existing.status !== 'destroyed') {
|
|
522
|
-
const ownerInfo = existing.owner ? ` (owned by session ${existing.owner})` : '';
|
|
523
|
-
throw new Error(
|
|
524
|
-
`Worktree '${name}' already exists with status '${existing.status}'${ownerInfo}.\n` +
|
|
525
|
-
`Use 'caws worktree destroy ${name}' first, or choose a different name.`
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
// Destroyed entries: check if another session owns the branch
|
|
529
|
-
if (existing.owner && existing.owner !== getAgentSessionId(root)) {
|
|
530
|
-
// Branch may still be in use by the owning session for merge
|
|
531
|
-
try {
|
|
532
|
-
const branchExists = execFileSync('git', ['rev-parse', '--verify', BRANCH_PREFIX + name], {
|
|
533
|
-
cwd: root, stdio: 'pipe',
|
|
534
|
-
}).toString().trim();
|
|
535
|
-
if (branchExists) {
|
|
536
|
-
throw new Error(
|
|
537
|
-
`Worktree '${name}' was destroyed but branch '${BRANCH_PREFIX}${name}' still exists ` +
|
|
538
|
-
`(owned by session ${existing.owner}).\n` +
|
|
539
|
-
`The owning session may still need this branch for merging.\n` +
|
|
540
|
-
`Choose a different name, or delete the branch first: git branch -d ${BRANCH_PREFIX}${name}`
|
|
541
|
-
);
|
|
542
|
-
}
|
|
543
|
-
} catch (e) {
|
|
544
|
-
if (e.message.includes('owned by session')) throw e;
|
|
545
|
-
// Branch doesn't exist — safe to reuse the name
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const worktreePath = path.join(root, WORKTREES_DIR, name);
|
|
551
|
-
const branchName = BRANCH_PREFIX + name;
|
|
552
|
-
const base = baseBranch || getCurrentBranch();
|
|
553
|
-
const canonicalSpecPath = findFeatureSpecPath(root, specId);
|
|
554
|
-
|
|
555
|
-
// Check if the branch already exists in git (even if not in registry)
|
|
556
|
-
// This catches cases where another agent created the branch outside CAWS
|
|
557
|
-
try {
|
|
558
|
-
execFileSync('git', ['rev-parse', '--verify', branchName], {
|
|
559
|
-
cwd: root, stdio: 'pipe',
|
|
560
|
-
});
|
|
561
|
-
// Branch exists — refuse unless it's fully merged into base
|
|
562
|
-
const currentSession = getAgentSessionId(root);
|
|
563
|
-
const registryOwner = registry.worktrees[name]?.owner;
|
|
564
|
-
if (registryOwner && registryOwner !== currentSession) {
|
|
565
|
-
throw new Error(
|
|
566
|
-
`Branch '${branchName}' already exists and is owned by another session (${registryOwner}).\n` +
|
|
567
|
-
`Another agent may be using this branch. Choose a different worktree name.`
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
// Branch exists but no owner conflict — warn and reuse
|
|
571
|
-
console.warn(`Warning: Branch '${branchName}' already exists, reusing it.`);
|
|
572
|
-
} catch (e) {
|
|
573
|
-
if (e.message.includes('already exists and is owned')) throw e;
|
|
574
|
-
// Branch doesn't exist — this is the normal path
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Create the worktree directory
|
|
578
|
-
fs.ensureDirSync(path.dirname(worktreePath));
|
|
579
|
-
|
|
580
|
-
if (canonicalSpecPath) {
|
|
581
|
-
ensureCanonicalSpecCommitted(root, canonicalSpecPath, specId, name);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Create git worktree with new branch
|
|
585
|
-
try {
|
|
586
|
-
execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
|
|
587
|
-
cwd: root,
|
|
588
|
-
stdio: 'pipe',
|
|
589
|
-
});
|
|
590
|
-
} catch (error) {
|
|
591
|
-
// Branch already exists (caught above and allowed) — attach to it
|
|
592
|
-
if (error.message.includes('already exists')) {
|
|
593
|
-
execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
|
|
594
|
-
cwd: root,
|
|
595
|
-
stdio: 'pipe',
|
|
596
|
-
});
|
|
597
|
-
} else {
|
|
598
|
-
throw new Error(`Failed to create worktree: ${error.message}`);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Set up sparse checkout if scope is provided
|
|
603
|
-
if (scope) {
|
|
604
|
-
try {
|
|
605
|
-
// Parse scope patterns (comma-separated)
|
|
606
|
-
const patterns = scope.split(',').map((p) => p.trim());
|
|
607
|
-
|
|
608
|
-
// Detect glob characters — cone mode only accepts directory paths,
|
|
609
|
-
// not glob patterns like "core/reasoning/**" or "*.py".
|
|
610
|
-
const hasGlobs = patterns.some((p) => /[*?[\]]/.test(p));
|
|
611
|
-
const coneFlag = hasGlobs ? '--no-cone' : '--cone';
|
|
612
|
-
|
|
613
|
-
execFileSync('git', ['sparse-checkout', 'init', coneFlag], {
|
|
614
|
-
cwd: worktreePath,
|
|
615
|
-
stdio: 'pipe',
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
execFileSync('git', ['sparse-checkout', 'set', ...patterns], {
|
|
619
|
-
cwd: worktreePath,
|
|
620
|
-
stdio: 'pipe',
|
|
621
|
-
});
|
|
622
|
-
} catch (error) {
|
|
623
|
-
console.warn(chalk.yellow(`Sparse checkout setup failed: ${error.message}`));
|
|
624
|
-
console.warn(chalk.blue('Worktree created but without sparse checkout'));
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Copy .caws/ config into worktree
|
|
629
|
-
const cawsSource = path.join(root, '.caws');
|
|
630
|
-
const cawsDest = path.join(worktreePath, '.caws');
|
|
631
|
-
if (fs.existsSync(cawsSource)) {
|
|
632
|
-
try {
|
|
633
|
-
fs.copySync(cawsSource, cawsDest, {
|
|
634
|
-
filter: (src) => {
|
|
635
|
-
// Don't copy worktrees directory or registry into the worktree
|
|
636
|
-
const rel = path.relative(cawsSource, src);
|
|
637
|
-
return !rel.startsWith('worktrees') && rel !== 'worktrees.json';
|
|
638
|
-
},
|
|
639
|
-
});
|
|
640
|
-
} catch {
|
|
641
|
-
// Non-fatal
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Auto-bind specId: if no explicit --spec-id was passed, scan .caws/specs/
|
|
646
|
-
// for a spec that declares `worktree: <name>`. This establishes the mutual
|
|
647
|
-
// reference that the scope guard uses to treat one spec as authoritative.
|
|
648
|
-
let resolvedSpecId = specId || null;
|
|
649
|
-
if (!resolvedSpecId) {
|
|
650
|
-
resolvedSpecId = findSpecByWorktreeName(root, name);
|
|
651
|
-
if (resolvedSpecId) {
|
|
652
|
-
console.log(chalk.gray(` Auto-bound spec: ${resolvedSpecId}`));
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Materialize a worktree-local working spec. Prefer the canonical feature
|
|
657
|
-
// spec when it exists so isolated worktrees stay aligned with the main
|
|
658
|
-
// registry/resolver model.
|
|
659
|
-
if (resolvedSpecId) {
|
|
660
|
-
try {
|
|
661
|
-
materializeWorktreeSpec(root, cawsDest, resolvedSpecId, name, scope);
|
|
662
|
-
} catch (error) {
|
|
663
|
-
console.warn(
|
|
664
|
-
chalk.yellow(`Could not materialize spec '${resolvedSpecId}' for worktree '${name}': ${error.message}`)
|
|
665
|
-
);
|
|
666
|
-
// Non-fatal: spec generation is optional
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Register worktree
|
|
671
|
-
const entry = {
|
|
672
|
-
name,
|
|
673
|
-
path: worktreePath,
|
|
674
|
-
branch: branchName,
|
|
675
|
-
baseBranch: base,
|
|
676
|
-
scope: scope || null,
|
|
677
|
-
specId: resolvedSpecId,
|
|
678
|
-
owner: options.owner || getAgentSessionId(root) || null,
|
|
679
|
-
createdAt: new Date().toISOString(),
|
|
680
|
-
status: 'fresh',
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
registry.worktrees[name] = entry;
|
|
684
|
-
saveRegistry(root, registry);
|
|
685
|
-
|
|
686
|
-
return entry;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
/**
|
|
690
|
-
* Reconcile registry state against git worktree list and filesystem.
|
|
691
|
-
*
|
|
692
|
-
* Non-destructive read that classifies every known worktree entry
|
|
693
|
-
* (from registry + git discovery) into one of:
|
|
694
|
-
* active — directory exists AND in git worktree list
|
|
695
|
-
* orphaned — directory exists but NOT in git worktree list
|
|
696
|
-
* missing — directory gone, branch may or may not exist
|
|
697
|
-
* destroyed — explicitly destroyed via CAWS
|
|
698
|
-
* unregistered — in git worktree list but not in registry
|
|
699
|
-
* stale-merged — missing + branch already merged to base
|
|
700
|
-
*
|
|
701
|
-
* Does NOT mutate the registry. Callers decide what to persist.
|
|
702
|
-
*
|
|
703
|
-
* @param {string} root - Repository root
|
|
704
|
-
* @returns {{ entries: Array, gitWorktrees: string[] }}
|
|
705
|
-
*/
|
|
706
|
-
function reconcileRegistry(root) {
|
|
707
|
-
const registry = loadRegistry(root);
|
|
708
|
-
|
|
709
|
-
let gitWorktrees = [];
|
|
710
|
-
try {
|
|
711
|
-
const output = execFileSync('git', ['worktree', 'list', '--porcelain'], {
|
|
712
|
-
cwd: root,
|
|
713
|
-
encoding: 'utf8',
|
|
714
|
-
stdio: 'pipe',
|
|
715
|
-
});
|
|
716
|
-
gitWorktrees = output
|
|
717
|
-
.split('\n\n')
|
|
718
|
-
.filter(Boolean)
|
|
719
|
-
.map((block) => {
|
|
720
|
-
const lines = block.split('\n');
|
|
721
|
-
const worktreeLine = lines.find((l) => l.startsWith('worktree '));
|
|
722
|
-
return worktreeLine ? worktreeLine.replace('worktree ', '') : null;
|
|
723
|
-
})
|
|
724
|
-
.filter(Boolean);
|
|
725
|
-
} catch {
|
|
726
|
-
// Git worktree list failed
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const entries = Object.values(registry.worktrees).map((entry) => {
|
|
730
|
-
const exists = fs.existsSync(entry.path);
|
|
731
|
-
const inGit = gitWorktrees.some(
|
|
732
|
-
(wt) => path.resolve(wt) === path.resolve(entry.path)
|
|
733
|
-
);
|
|
734
|
-
|
|
735
|
-
const merged = entry.branch && entry.baseBranch
|
|
736
|
-
? isBranchMerged(entry.branch, entry.baseBranch, root)
|
|
737
|
-
: false;
|
|
738
|
-
const divergent = entry.branch && entry.baseBranch
|
|
739
|
-
? hasDivergentCommits(entry.branch, entry.baseBranch, root)
|
|
740
|
-
: false;
|
|
741
|
-
const dirty = exists ? hasDirtyFiles(entry.path) : false;
|
|
742
|
-
|
|
743
|
-
let status;
|
|
744
|
-
if (entry.status === 'destroyed') {
|
|
745
|
-
status = 'destroyed';
|
|
746
|
-
} else if (exists && inGit) {
|
|
747
|
-
// Worktree directory exists and is tracked by git
|
|
748
|
-
if (divergent || dirty) {
|
|
749
|
-
// Has commits beyond base or uncommitted work → active
|
|
750
|
-
status = 'active';
|
|
751
|
-
} else if (merged) {
|
|
752
|
-
// No divergent commits, branch aligned with base.
|
|
753
|
-
// Use stored status as history to distinguish fresh vs merged:
|
|
754
|
-
// - stored 'fresh' → never had divergent commits → still fresh
|
|
755
|
-
// - stored 'active' → had work that's now merged → merged
|
|
756
|
-
if (entry.status === 'active') {
|
|
757
|
-
status = 'merged';
|
|
758
|
-
} else {
|
|
759
|
-
status = 'fresh';
|
|
760
|
-
}
|
|
761
|
-
} else {
|
|
762
|
-
status = 'fresh';
|
|
763
|
-
}
|
|
764
|
-
} else if (exists) {
|
|
765
|
-
status = 'orphaned';
|
|
766
|
-
} else {
|
|
767
|
-
status = merged ? 'stale-merged' : 'missing';
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
|
|
771
|
-
|
|
772
|
-
return { ...entry, status, lastCommit, merged, divergent, dirty };
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
// Append unregistered worktrees discovered from git
|
|
776
|
-
const unregistered = discoverUnregisteredWorktrees(root, registry);
|
|
777
|
-
for (const discovered of unregistered) {
|
|
778
|
-
const lastCommit = getLastCommitInfo(discovered.branch, root);
|
|
779
|
-
entries.push({
|
|
780
|
-
name: discovered.name,
|
|
781
|
-
path: discovered.path,
|
|
782
|
-
branch: discovered.branch,
|
|
783
|
-
baseBranch: null,
|
|
784
|
-
scope: null,
|
|
785
|
-
specId: null,
|
|
786
|
-
owner: null,
|
|
787
|
-
createdAt: null,
|
|
788
|
-
status: 'unregistered',
|
|
789
|
-
lastCommit,
|
|
790
|
-
merged: false,
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
return { entries, gitWorktrees };
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Repair registry drift caused by manual git operations outside CAWS.
|
|
799
|
-
*
|
|
800
|
-
* Scans registry vs git vs filesystem, classifies each entry, and optionally
|
|
801
|
-
* prunes stale entries. Reports the delta before persisting.
|
|
802
|
-
*
|
|
803
|
-
* @param {Object} options
|
|
804
|
-
* @param {boolean} [options.prune=false] - Remove destroyed, stale-merged, and missing entries
|
|
805
|
-
* @param {boolean} [options.dryRun=false] - Report only, do not persist
|
|
806
|
-
* @param {boolean} [options.force=false] - Allow pruning entries owned by other sessions
|
|
807
|
-
* @returns {{ repaired: Array, pruned: Array, skipped: Array }}
|
|
808
|
-
*/
|
|
809
|
-
function repairWorktrees(options = {}) {
|
|
810
|
-
const { prune: shouldPrune = false, dryRun = false, force = false } = options;
|
|
811
|
-
const root = getRepoRoot();
|
|
812
|
-
const registry = loadRegistry(root);
|
|
813
|
-
const { entries } = reconcileRegistry(root);
|
|
814
|
-
const currentSession = getAgentSessionId(root);
|
|
815
|
-
|
|
816
|
-
const repaired = [];
|
|
817
|
-
const pruned = [];
|
|
818
|
-
const skipped = [];
|
|
819
|
-
|
|
820
|
-
for (const entry of entries) {
|
|
821
|
-
const regEntry = registry.worktrees[entry.name];
|
|
822
|
-
|
|
823
|
-
if (entry.status === 'unregistered') {
|
|
824
|
-
if (!dryRun) {
|
|
825
|
-
autoRegisterWorktree(root, registry, entry);
|
|
826
|
-
}
|
|
827
|
-
repaired.push({ name: entry.name, action: 'registered', status: entry.status });
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
if (!regEntry) continue;
|
|
832
|
-
|
|
833
|
-
// Update registry status to match filesystem reality
|
|
834
|
-
const wasAlive = regEntry.status === 'active' || regEntry.status === 'fresh';
|
|
835
|
-
const nowDead = entry.status === 'missing' || entry.status === 'stale-merged';
|
|
836
|
-
if (wasAlive && nowDead) {
|
|
837
|
-
repaired.push({
|
|
838
|
-
name: entry.name,
|
|
839
|
-
action: 'status-updated',
|
|
840
|
-
from: regEntry.status,
|
|
841
|
-
to: entry.status,
|
|
842
|
-
owner: entry.owner || null,
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Determine if entry is prunable (destroyed, stale-merged, or missing)
|
|
847
|
-
const isPrunable = entry.status === 'destroyed' ||
|
|
848
|
-
entry.status === 'stale-merged' ||
|
|
849
|
-
entry.status === 'missing';
|
|
850
|
-
|
|
851
|
-
if (!isPrunable) continue;
|
|
852
|
-
|
|
853
|
-
// Ownership check: refuse to prune another session's entries without --force
|
|
854
|
-
const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
|
|
855
|
-
|
|
856
|
-
if (shouldPrune && isPrunable) {
|
|
857
|
-
if (isOwnedByOther && !force) {
|
|
858
|
-
skipped.push({
|
|
859
|
-
name: entry.name,
|
|
860
|
-
reason: `owned by another session (${entry.owner}). Use --force to override`,
|
|
861
|
-
owner: entry.owner,
|
|
862
|
-
});
|
|
863
|
-
} else {
|
|
864
|
-
if (!dryRun) {
|
|
865
|
-
delete registry.worktrees[entry.name];
|
|
866
|
-
}
|
|
867
|
-
pruned.push({ name: entry.name, status: entry.status, owner: entry.owner || null });
|
|
868
|
-
}
|
|
869
|
-
} else if (!shouldPrune && isPrunable) {
|
|
870
|
-
skipped.push({
|
|
871
|
-
name: entry.name,
|
|
872
|
-
reason: entry.status + ' (use --prune to remove)',
|
|
873
|
-
owner: entry.owner || null,
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
if (!dryRun) {
|
|
879
|
-
saveRegistry(root, registry);
|
|
880
|
-
try {
|
|
881
|
-
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
882
|
-
} catch {
|
|
883
|
-
// Non-fatal
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return { repaired, pruned, skipped };
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* List all registered worktrees with filesystem validation.
|
|
892
|
-
* Delegates to reconcileRegistry() for state classification.
|
|
893
|
-
* Persists status transitions (fresh → active, active → merged) so
|
|
894
|
-
* future calls can distinguish "never had work" from "work was merged back".
|
|
895
|
-
* @returns {Array} Worktree entries with status
|
|
896
|
-
*/
|
|
897
|
-
function listWorktrees() {
|
|
898
|
-
const root = getRepoRoot();
|
|
899
|
-
const registry = loadRegistry(root);
|
|
900
|
-
const { entries } = reconcileRegistry(root);
|
|
901
|
-
|
|
902
|
-
// Persist status transitions so future reconcile can use stored status as history
|
|
903
|
-
let dirty = false;
|
|
904
|
-
for (const entry of entries) {
|
|
905
|
-
const regEntry = registry.worktrees[entry.name];
|
|
906
|
-
if (regEntry && regEntry.status !== entry.status &&
|
|
907
|
-
entry.status !== 'unregistered') {
|
|
908
|
-
regEntry.status = entry.status;
|
|
909
|
-
dirty = true;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
if (dirty) {
|
|
913
|
-
saveRegistry(root, registry);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
return entries;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Destroy a worktree
|
|
921
|
-
* @param {string} name - Worktree name
|
|
922
|
-
* @param {Object} options - Destruction options
|
|
923
|
-
* @param {boolean} [options.deleteBranch] - Also delete the branch
|
|
924
|
-
* @param {boolean} [options.force] - Force removal even if dirty
|
|
925
|
-
*/
|
|
926
|
-
function destroyWorktree(name, options = {}) {
|
|
927
|
-
const root = getRepoRoot();
|
|
928
|
-
// Ensure CWD is not inside the worktree we're about to destroy.
|
|
929
|
-
// If CWD is the worktree directory, removing it crashes subsequent commands.
|
|
930
|
-
try { process.chdir(root); } catch { /* non-fatal */ }
|
|
931
|
-
const registry = loadRegistry(root);
|
|
932
|
-
const { deleteBranch = false, force = false } = options;
|
|
933
|
-
|
|
934
|
-
let entry = registry.worktrees[name];
|
|
935
|
-
if (!entry) {
|
|
936
|
-
// Fallback: scan git for unregistered worktree and auto-register
|
|
937
|
-
const unregistered = discoverUnregisteredWorktrees(root, registry);
|
|
938
|
-
const discovered = unregistered.find((u) => u.name === name);
|
|
939
|
-
if (discovered) {
|
|
940
|
-
console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
|
|
941
|
-
entry = autoRegisterWorktree(root, registry, discovered);
|
|
942
|
-
} else {
|
|
943
|
-
throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Ownership check: refuse to destroy another agent's worktree without --force
|
|
948
|
-
const currentSession = getAgentSessionId(root);
|
|
949
|
-
const isLiveStatus = entry.status === 'active' || entry.status === 'fresh' || entry.status === 'merged';
|
|
950
|
-
if (
|
|
951
|
-
!force &&
|
|
952
|
-
isLiveStatus &&
|
|
953
|
-
entry.owner &&
|
|
954
|
-
currentSession &&
|
|
955
|
-
entry.owner !== currentSession
|
|
956
|
-
) {
|
|
957
|
-
const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
|
|
958
|
-
const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
|
|
959
|
-
throw new Error(
|
|
960
|
-
`Worktree '${name}' belongs to another session${recency}.\n` +
|
|
961
|
-
` Owner: ${entry.owner}\n` +
|
|
962
|
-
` You: ${currentSession}\n` +
|
|
963
|
-
`Another agent may be actively working here.\n` +
|
|
964
|
-
`Do NOT destroy worktrees you did not create. Ask the user if cleanup is needed.`
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Even with --force, warn loudly when destroying another session's worktree
|
|
969
|
-
if (
|
|
970
|
-
force &&
|
|
971
|
-
isLiveStatus &&
|
|
972
|
-
entry.owner &&
|
|
973
|
-
currentSession &&
|
|
974
|
-
entry.owner !== currentSession
|
|
975
|
-
) {
|
|
976
|
-
const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
|
|
977
|
-
const recency = lastCommit ? ` (last commit: ${lastCommit.age})` : '';
|
|
978
|
-
console.log(chalk.red(`\n ⚠ WARNING: Force-destroying worktree '${name}' owned by another session${recency}`));
|
|
979
|
-
console.log(chalk.red(` Owner: ${entry.owner}`));
|
|
980
|
-
console.log(chalk.red(` You: ${currentSession}`));
|
|
981
|
-
console.log(chalk.red(` If the other agent is still running, this WILL break their work.\n`));
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Auto-force when the branch is already merged to its base branch.
|
|
985
|
-
// Dirty files in a merged worktree are definitionally stale.
|
|
986
|
-
const merged = entry.branch && entry.baseBranch
|
|
987
|
-
? isBranchMerged(entry.branch, entry.baseBranch, root)
|
|
988
|
-
: false;
|
|
989
|
-
const effectiveForce = force || merged;
|
|
990
|
-
if (merged && !force) {
|
|
991
|
-
console.log(chalk.gray(` Branch ${entry.branch} already merged to ${entry.baseBranch}, auto-forcing cleanup`));
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Remove git worktree — handle already-deleted directories gracefully
|
|
995
|
-
const dirExists = fs.existsSync(entry.path);
|
|
996
|
-
if (dirExists) {
|
|
997
|
-
try {
|
|
998
|
-
const args = ['worktree', 'remove'];
|
|
999
|
-
if (effectiveForce) args.push('--force');
|
|
1000
|
-
args.push(entry.path);
|
|
1001
|
-
execFileSync('git', args, { cwd: root, stdio: 'pipe' });
|
|
1002
|
-
} catch (error) {
|
|
1003
|
-
if (effectiveForce) {
|
|
1004
|
-
// Force cleanup: remove directory manually
|
|
1005
|
-
fs.removeSync(entry.path);
|
|
1006
|
-
} else {
|
|
1007
|
-
throw new Error(`Failed to remove worktree: ${error.message}. Use --force to override.`);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
} else {
|
|
1011
|
-
// Directory already gone — just clean up git's tracking
|
|
1012
|
-
console.log(` Worktree directory already removed, cleaning up registry`);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// Always prune git's worktree list to stay in sync
|
|
1016
|
-
try {
|
|
1017
|
-
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
1018
|
-
} catch {
|
|
1019
|
-
// Non-fatal
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Optionally delete branch
|
|
1023
|
-
if (deleteBranch && entry.branch) {
|
|
1024
|
-
try {
|
|
1025
|
-
execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
1026
|
-
} catch {
|
|
1027
|
-
if (effectiveForce) {
|
|
1028
|
-
try {
|
|
1029
|
-
execFileSync('git', ['branch', '-D', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
1030
|
-
} catch {
|
|
1031
|
-
// Non-fatal
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Update registry
|
|
1038
|
-
const wasAlreadyDestroyed = registry.worktrees[name].status === 'destroyed';
|
|
1039
|
-
registry.worktrees[name].status = 'destroyed';
|
|
1040
|
-
registry.worktrees[name].destroyedAt = new Date().toISOString();
|
|
1041
|
-
saveRegistry(root, registry);
|
|
1042
|
-
|
|
1043
|
-
// CAWSFIX-18: auto-commit the registry so the working tree stays clean
|
|
1044
|
-
if (!wasAlreadyDestroyed) {
|
|
1045
|
-
try {
|
|
1046
|
-
const status = execFileSync('git', ['status', '--porcelain', '.caws/worktrees.json'], {
|
|
1047
|
-
cwd: root, stdio: ['pipe', 'pipe', 'pipe'],
|
|
1048
|
-
}).toString().trim();
|
|
1049
|
-
if (status) {
|
|
1050
|
-
const otherActive = Object.values(registry.worktrees || {}).some(
|
|
1051
|
-
(e) => e.status === 'active' || e.status === 'fresh'
|
|
1052
|
-
);
|
|
1053
|
-
const prefix = otherActive ? 'wip(checkpoint)' : 'chore(worktree)';
|
|
1054
|
-
execFileSync('git', ['add', '.caws/worktrees.json'], { cwd: root, stdio: 'pipe' });
|
|
1055
|
-
execFileSync('git', ['commit', '-m', `${prefix}: record destroyed ${name}`], {
|
|
1056
|
-
cwd: root, stdio: 'pipe',
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
} catch (err) {
|
|
1060
|
-
console.warn(chalk.yellow(` Warning: could not auto-commit .caws/worktrees.json: ${err.message}`));
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* Merge a worktree branch back to base in one operation.
|
|
1067
|
-
* Sequence: dry-run conflict check → destroy worktree → merge → cleanup.
|
|
1068
|
-
* @param {string} name - Worktree name
|
|
1069
|
-
* @param {Object} options - Merge options
|
|
1070
|
-
* @param {boolean} [options.dryRun] - Preview conflicts without merging
|
|
1071
|
-
* @param {boolean} [options.deleteBranch] - Delete branch after merge
|
|
1072
|
-
* @param {string} [options.message] - Custom merge commit message
|
|
1073
|
-
* @returns {Object} Merge result
|
|
1074
|
-
*/
|
|
1075
|
-
function mergeWorktree(name, options = {}) {
|
|
1076
|
-
const root = getRepoRoot();
|
|
1077
|
-
const registry = loadRegistry(root);
|
|
1078
|
-
const { dryRun = false, deleteBranch = true, message } = options;
|
|
1079
|
-
|
|
1080
|
-
let entry = registry.worktrees[name];
|
|
1081
|
-
if (!entry) {
|
|
1082
|
-
// Fallback: scan git for unregistered worktree and auto-register
|
|
1083
|
-
const unregistered = discoverUnregisteredWorktrees(root, registry);
|
|
1084
|
-
const discovered = unregistered.find((u) => u.name === name);
|
|
1085
|
-
if (discovered) {
|
|
1086
|
-
console.log(chalk.yellow(`Worktree '${name}' not in registry but found in git. Auto-registering.`));
|
|
1087
|
-
entry = autoRegisterWorktree(root, registry, discovered);
|
|
1088
|
-
} else {
|
|
1089
|
-
throw new Error(`Worktree '${name}' not found in registry or git worktree list`);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const baseBranch = entry.baseBranch || 'main';
|
|
1094
|
-
|
|
1095
|
-
// Check for uncommitted work in the worktree.
|
|
1096
|
-
// Ignore .caws/ changes (provenance chain, registry) — these are
|
|
1097
|
-
// infrastructure artifacts written by git hooks, not user work.
|
|
1098
|
-
// The post-commit hook appends to .caws/provenance/chain.json after
|
|
1099
|
-
// every commit, which immediately dirties the tree and blocks merges.
|
|
1100
|
-
if (fs.existsSync(entry.path)) {
|
|
1101
|
-
try {
|
|
1102
|
-
const rawStatus = execFileSync(
|
|
1103
|
-
'git',
|
|
1104
|
-
['status', '--porcelain'],
|
|
1105
|
-
{ cwd: entry.path, encoding: 'utf8', stdio: 'pipe' }
|
|
1106
|
-
);
|
|
1107
|
-
// Filter out .caws/ infrastructure changes (provenance, registry).
|
|
1108
|
-
// Git porcelain format: "XY PATH" — 2 status chars, space, path.
|
|
1109
|
-
// IMPORTANT: do NOT .trim() the raw output — it strips the leading
|
|
1110
|
-
// space from " M file" (unstaged), corrupting the XY prefix and
|
|
1111
|
-
// breaking substring(3) path extraction.
|
|
1112
|
-
const statusLines = rawStatus.split('\n').filter(l => l.length > 0);
|
|
1113
|
-
const userChanges = statusLines
|
|
1114
|
-
.filter(line => {
|
|
1115
|
-
const filePath = line.substring(3);
|
|
1116
|
-
return !filePath.startsWith('.caws/');
|
|
1117
|
-
}).join('\n');
|
|
1118
|
-
if (userChanges) {
|
|
1119
|
-
throw new Error(
|
|
1120
|
-
`Worktree '${name}' has uncommitted changes:\n${userChanges}\n` +
|
|
1121
|
-
`Commit or discard changes before merging.`
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
} catch (error) {
|
|
1125
|
-
if (error.message.includes('uncommitted changes')) throw error;
|
|
1126
|
-
// Non-fatal: status check failed, proceed cautiously
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// Dry-run: check for conflicts using git merge-tree (new-style, git 2.38+)
|
|
1131
|
-
let conflicts = [];
|
|
1132
|
-
try {
|
|
1133
|
-
// New-style merge-tree: takes two branches, computes merge-base automatically
|
|
1134
|
-
execFileSync(
|
|
1135
|
-
'git',
|
|
1136
|
-
['merge-tree', '--write-tree', baseBranch, entry.branch],
|
|
1137
|
-
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
1138
|
-
);
|
|
1139
|
-
// Exit 0 = clean merge, no conflicts
|
|
1140
|
-
} catch (mergeTreeError) {
|
|
1141
|
-
// Exit 1 = conflicts detected; parse them from output
|
|
1142
|
-
const output = (mergeTreeError.stdout || '') + (mergeTreeError.stderr || '');
|
|
1143
|
-
const conflictLines = output.split('\n').filter(
|
|
1144
|
-
(l) => l.includes('CONFLICT') || l.includes('conflict')
|
|
1145
|
-
);
|
|
1146
|
-
if (mergeTreeError.status === 1 && conflictLines.length > 0) {
|
|
1147
|
-
conflicts = conflictLines;
|
|
1148
|
-
} else if (mergeTreeError.status === 1) {
|
|
1149
|
-
conflicts = ['Merge conflicts detected (run merge manually to inspect)'];
|
|
1150
|
-
}
|
|
1151
|
-
// Other exit codes (e.g., merge-tree not supported) = can't detect, proceed
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
if (dryRun) {
|
|
1155
|
-
return {
|
|
1156
|
-
name,
|
|
1157
|
-
branch: entry.branch,
|
|
1158
|
-
baseBranch,
|
|
1159
|
-
conflicts,
|
|
1160
|
-
wouldMerge: conflicts.length === 0,
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Emit merge:pre event
|
|
1165
|
-
try {
|
|
1166
|
-
lifecycle.emit(EVENTS.MERGE_PRE, {
|
|
1167
|
-
worktreeName: name, branch: entry.branch, baseBranch, conflicts,
|
|
1168
|
-
timestamp: new Date().toISOString(),
|
|
1169
|
-
});
|
|
1170
|
-
} catch { /* non-fatal */ }
|
|
1171
|
-
|
|
1172
|
-
// Ensure CWD is the repo root BEFORE destroying the worktree.
|
|
1173
|
-
// If the caller's CWD is inside the worktree directory, destroying it
|
|
1174
|
-
// removes the CWD out from under the process, causing all subsequent
|
|
1175
|
-
// git commands to fail with "Unable to read current working directory".
|
|
1176
|
-
try { process.chdir(root); } catch { /* non-fatal */ }
|
|
1177
|
-
|
|
1178
|
-
// Destroy the worktree (auto-forces since we're about to merge)
|
|
1179
|
-
destroyWorktree(name, { deleteBranch: false, force: true });
|
|
1180
|
-
|
|
1181
|
-
// Switch to base branch (use cwd: root since getCurrentBranch has no cwd param)
|
|
1182
|
-
const currentBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1183
|
-
cwd: root, encoding: 'utf8', stdio: 'pipe',
|
|
1184
|
-
}).trim();
|
|
1185
|
-
if (currentBranch !== baseBranch) {
|
|
1186
|
-
execFileSync('git', ['checkout', baseBranch], { cwd: root, stdio: 'pipe' });
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// Merge
|
|
1190
|
-
// Use --no-verify to skip pre-commit/commit-msg hooks during merge.
|
|
1191
|
-
// The worktree commits were already validated by those hooks when originally
|
|
1192
|
-
// committed. Re-running them here adds seconds of blocking time (especially
|
|
1193
|
-
// in projects with heavy hooks like quality gates, YAML validation, etc.)
|
|
1194
|
-
// and can trigger OAuth token expiry races in long-running sessions.
|
|
1195
|
-
const mergeMessage = message || `merge(worktree): ${name}`;
|
|
1196
|
-
try {
|
|
1197
|
-
execFileSync(
|
|
1198
|
-
'git',
|
|
1199
|
-
['merge', '--no-ff', '--no-verify', entry.branch, '-m', mergeMessage],
|
|
1200
|
-
{ cwd: root, stdio: 'pipe' }
|
|
1201
|
-
);
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
const failResult = {
|
|
1204
|
-
name, branch: entry.branch, baseBranch, merged: false,
|
|
1205
|
-
conflicts: [`Merge failed: ${error.message}`],
|
|
1206
|
-
message: 'Merge conflicts detected. Resolve with git and commit.',
|
|
1207
|
-
};
|
|
1208
|
-
try {
|
|
1209
|
-
lifecycle.emit(EVENTS.MERGE_POST, { ...failResult, timestamp: new Date().toISOString() });
|
|
1210
|
-
} catch { /* non-fatal */ }
|
|
1211
|
-
return failResult;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Delete branch after successful merge
|
|
1215
|
-
if (deleteBranch) {
|
|
1216
|
-
try {
|
|
1217
|
-
execFileSync('git', ['branch', '-d', entry.branch], { cwd: root, stdio: 'pipe' });
|
|
1218
|
-
} catch {
|
|
1219
|
-
// Non-fatal
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
// Auto-close the bound spec if one exists. A worktree merge is the
|
|
1224
|
-
// lifecycle signal that the spec's work is done; leaving the spec
|
|
1225
|
-
// `active` after merge accumulates stale-active entries (D6). Direct
|
|
1226
|
-
// YAML status flip bypasses the ownership + worktree-reference checks
|
|
1227
|
-
// in `closeSpec` — the caller has already proven authority by merging.
|
|
1228
|
-
let autoClosedSpecId = null;
|
|
1229
|
-
if (entry.specId) {
|
|
1230
|
-
autoClosedSpecId = autoCloseBoundSpec(root, entry.specId);
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const mergeResult = {
|
|
1234
|
-
name, branch: entry.branch, baseBranch, merged: true, conflicts: [],
|
|
1235
|
-
specId: entry.specId || null, autoClosedSpecId,
|
|
1236
|
-
};
|
|
1237
|
-
try {
|
|
1238
|
-
lifecycle.emit(EVENTS.MERGE_POST, { ...mergeResult, timestamp: new Date().toISOString() });
|
|
1239
|
-
} catch { /* non-fatal */ }
|
|
1240
|
-
return mergeResult;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
/**
|
|
1244
|
-
* Flip a spec's status to `closed` by rewriting just the `status:` line.
|
|
1245
|
-
* Idempotent: no-op when the spec is already closed or the file is missing.
|
|
1246
|
-
* Returns the spec ID on success, null if skipped or failed.
|
|
1247
|
-
* @param {string} root - Repo root
|
|
1248
|
-
* @param {string} specId - Spec identifier (e.g. CAWSFIX-14)
|
|
1249
|
-
* @returns {string|null}
|
|
1250
|
-
*/
|
|
1251
|
-
function autoCloseBoundSpec(root, specId) {
|
|
1252
|
-
try {
|
|
1253
|
-
const specPath = findFeatureSpecPath(root, specId);
|
|
1254
|
-
if (!specPath || !fs.existsSync(specPath)) return null;
|
|
1255
|
-
const original = fs.readFileSync(specPath, 'utf8');
|
|
1256
|
-
// Idempotent: already closed → no-op, no write, no diff.
|
|
1257
|
-
if (/^status:\s*closed\s*$/m.test(original)) return specId;
|
|
1258
|
-
const patched = original.replace(/^status:\s*active\s*$/m, 'status: closed');
|
|
1259
|
-
if (patched === original) return null; // status was e.g. draft/archived
|
|
1260
|
-
fs.writeFileSync(specPath, patched, 'utf8');
|
|
1261
|
-
return specId;
|
|
1262
|
-
} catch {
|
|
1263
|
-
return null;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
/**
|
|
1268
|
-
* Prune stale worktree entries
|
|
1269
|
-
* @param {Object} options - Prune options
|
|
1270
|
-
* @param {number} [options.maxAgeDays] - Remove entries older than this many days
|
|
1271
|
-
* @param {number} [options.recentCommitMinutes] - Protect branches with commits newer than this (default: 60)
|
|
1272
|
-
* @param {boolean} [options.force] - Allow pruning entries owned by other sessions
|
|
1273
|
-
* @returns {{ pruned: Array, skipped: Array }} Pruned and skipped entries
|
|
1274
|
-
*/
|
|
1275
|
-
function pruneWorktrees(options = {}) {
|
|
1276
|
-
const root = getRepoRoot();
|
|
1277
|
-
const registry = loadRegistry(root);
|
|
1278
|
-
const { maxAgeDays = 30, recentCommitMinutes = 60, force = false } = options;
|
|
1279
|
-
const currentSession = getAgentSessionId(root);
|
|
1280
|
-
|
|
1281
|
-
const now = new Date();
|
|
1282
|
-
const pruned = [];
|
|
1283
|
-
const skipped = [];
|
|
1284
|
-
|
|
1285
|
-
for (const [name, entry] of Object.entries(registry.worktrees)) {
|
|
1286
|
-
const created = new Date(entry.createdAt);
|
|
1287
|
-
const ageDays = (now - created) / (1000 * 60 * 60 * 24);
|
|
1288
|
-
const dirExists = fs.existsSync(entry.path);
|
|
1289
|
-
|
|
1290
|
-
const shouldPrune =
|
|
1291
|
-
// Always prune destroyed entries
|
|
1292
|
-
entry.status === 'destroyed' ||
|
|
1293
|
-
// Prune active/fresh entries whose directory is gone (filesystem-registry desync)
|
|
1294
|
-
((entry.status === 'active' || entry.status === 'fresh') && !dirExists) ||
|
|
1295
|
-
// Prune old missing entries
|
|
1296
|
-
(!dirExists && ageDays > maxAgeDays);
|
|
1297
|
-
|
|
1298
|
-
if (shouldPrune) {
|
|
1299
|
-
// Ownership check: skip entries owned by other sessions unless --force
|
|
1300
|
-
const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
|
|
1301
|
-
if (isOwnedByOther && entry.status !== 'destroyed' && !force) {
|
|
1302
|
-
skipped.push({
|
|
1303
|
-
name,
|
|
1304
|
-
reason: `owned by another session (${entry.owner})`,
|
|
1305
|
-
entry,
|
|
1306
|
-
});
|
|
1307
|
-
continue;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// Before pruning a non-destroyed entry, check for recent commits (skip if --force)
|
|
1311
|
-
if (!force && entry.status !== 'destroyed' && entry.branch) {
|
|
1312
|
-
const lastCommit = getLastCommitInfo(entry.branch, root);
|
|
1313
|
-
if (lastCommit) {
|
|
1314
|
-
const commitAgeMinutes = (now - lastCommit.timestamp) / (1000 * 60);
|
|
1315
|
-
if (commitAgeMinutes < recentCommitMinutes) {
|
|
1316
|
-
skipped.push({ name, reason: `recent commit (${lastCommit.age})`, entry });
|
|
1317
|
-
continue;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
// Clean up filesystem if still exists
|
|
1323
|
-
if (dirExists) {
|
|
1324
|
-
try {
|
|
1325
|
-
execFileSync('git', ['worktree', 'remove', '--force', entry.path], {
|
|
1326
|
-
cwd: root,
|
|
1327
|
-
stdio: 'pipe',
|
|
1328
|
-
});
|
|
1329
|
-
} catch {
|
|
1330
|
-
fs.removeSync(entry.path);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
pruned.push(entry);
|
|
1334
|
-
delete registry.worktrees[name];
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// Prune git's worktree list
|
|
1339
|
-
try {
|
|
1340
|
-
execFileSync('git', ['worktree', 'prune'], { cwd: root, stdio: 'pipe' });
|
|
1341
|
-
} catch {
|
|
1342
|
-
// Non-fatal
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
saveRegistry(root, registry);
|
|
1346
|
-
return { pruned, skipped };
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
module.exports = {
|
|
1350
|
-
createWorktree,
|
|
1351
|
-
listWorktrees,
|
|
1352
|
-
destroyWorktree,
|
|
1353
|
-
mergeWorktree,
|
|
1354
|
-
autoCloseBoundSpec,
|
|
1355
|
-
pruneWorktrees,
|
|
1356
|
-
repairWorktrees,
|
|
1357
|
-
reconcileRegistry,
|
|
1358
|
-
loadRegistry,
|
|
1359
|
-
saveRegistry,
|
|
1360
|
-
getRepoRoot,
|
|
1361
|
-
getLastCommitInfo,
|
|
1362
|
-
isBranchMerged,
|
|
1363
|
-
hasDivergentCommits,
|
|
1364
|
-
hasDirtyFiles,
|
|
1365
|
-
discoverUnregisteredWorktrees,
|
|
1366
|
-
autoRegisterWorktree,
|
|
1367
|
-
WORKTREES_DIR,
|
|
1368
|
-
REGISTRY_FILE,
|
|
1369
|
-
BRANCH_PREFIX,
|
|
1370
|
-
findFeatureSpecPath,
|
|
1371
|
-
materializeWorktreeSpec,
|
|
1372
|
-
inferSpecIdForWorktree,
|
|
1373
|
-
findSpecByWorktreeName,
|
|
1374
|
-
};
|