@paths.design/caws-cli 9.3.2 → 10.0.1
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 +58 -27
- package/dist/commands/archive.js +67 -28
- package/dist/commands/burnup.js +20 -11
- package/dist/commands/diagnose.js +34 -22
- package/dist/commands/evaluate.js +27 -15
- package/dist/commands/gates.js +122 -0
- package/dist/commands/init.js +143 -15
- package/dist/commands/iterate.js +77 -4
- package/dist/commands/parallel.js +4 -0
- package/dist/commands/plan.js +9 -19
- package/dist/commands/provenance.js +53 -17
- package/dist/commands/quality-monitor.js +64 -45
- package/dist/commands/sidecar.js +71 -0
- package/dist/commands/specs.js +233 -44
- package/dist/commands/status.js +113 -9
- package/dist/commands/tutorial.js +10 -9
- package/dist/commands/validate.js +49 -6
- package/dist/commands/verify-acs.js +35 -78
- package/dist/commands/waivers.js +69 -12
- package/dist/commands/worktree.js +50 -25
- package/dist/error-handler.js +2 -13
- package/dist/gates/budget-limit.js +116 -0
- package/dist/gates/feedback.js +260 -0
- package/dist/gates/format.js +179 -0
- package/dist/gates/god-object.js +117 -0
- package/dist/gates/pipeline.js +167 -0
- package/dist/gates/scope-boundary.js +93 -0
- package/dist/gates/spec-completeness.js +102 -0
- package/dist/gates/todo-detection.js +205 -0
- package/dist/index.js +130 -151
- package/dist/parallel/parallel-manager.js +3 -3
- package/dist/policy/PolicyManager.js +42 -10
- package/dist/scaffold/claude-hooks.js +24 -1
- package/dist/scaffold/git-hooks.js +45 -102
- package/dist/scaffold/index.js +4 -3
- package/dist/session/session-manager.js +71 -14
- package/dist/sidecars/index.js +33 -0
- package/dist/sidecars/listeners.js +40 -0
- package/dist/sidecars/provenance-summary.js +238 -0
- package/dist/sidecars/quality-gaps.js +258 -0
- package/dist/sidecars/schema.js +149 -0
- package/dist/sidecars/spec-drift.js +151 -0
- package/dist/sidecars/waiver-draft.js +176 -0
- package/dist/templates/.caws/schemas/policy.schema.json +50 -0
- package/dist/templates/.caws/schemas/waivers.schema.json +30 -24
- package/dist/templates/.caws/schemas/working-spec.schema.json +51 -8
- package/dist/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/dist/templates/.caws/templates/working-spec.template.yml +7 -3
- package/dist/templates/.claude/hooks/audit.sh +0 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/dist/templates/.claude/hooks/classify_command.py +592 -0
- package/dist/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/dist/templates/.claude/hooks/quality-check.sh +23 -10
- package/dist/templates/.claude/hooks/scope-guard.sh +34 -32
- package/dist/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/dist/templates/.claude/hooks/session-log.sh +76 -3
- package/dist/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/dist/templates/.claude/hooks/test_classify_command.py +370 -0
- package/dist/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/dist/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +1 -1
- package/dist/templates/.claude/settings.json +26 -0
- package/dist/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/dist/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/dist/templates/.cursor/hooks/session-log.sh +924 -0
- package/dist/templates/.cursor/hooks.json +25 -0
- package/dist/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/dist/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/dist/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/dist/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/dist/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/dist/templates/.github/copilot-instructions.md +5 -5
- package/dist/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/dist/templates/.junie/guidelines.md +2 -2
- package/dist/templates/.vscode/settings.json +3 -1
- package/dist/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/dist/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/dist/templates/CLAUDE.md +43 -8
- package/dist/templates/agents.md +29 -9
- package/dist/templates/docs/README.md +8 -7
- package/dist/templates/scripts/new_feature.sh +80 -0
- package/dist/test-analysis.js +43 -30
- package/dist/tool-loader.js +1 -1
- package/dist/utils/agent-session.js +202 -0
- package/dist/utils/detection.js +8 -2
- package/dist/utils/finalization.js +7 -6
- package/dist/utils/gitignore-updater.js +3 -0
- package/dist/utils/lifecycle-events.js +94 -0
- package/dist/utils/quality-gates-utils.js +29 -44
- package/dist/utils/schema-validator.js +42 -0
- package/dist/utils/spec-resolver.js +93 -21
- package/dist/utils/working-state.js +505 -0
- package/dist/validation/spec-validation.js +92 -22
- package/dist/waivers-manager.js +60 -6
- package/dist/worktree/worktree-manager.js +390 -93
- package/package.json +6 -6
- package/templates/.caws/schemas/policy.schema.json +50 -0
- package/templates/.caws/schemas/waivers.schema.json +30 -24
- package/templates/.caws/schemas/working-spec.schema.json +51 -8
- package/templates/.caws/schemas/worktrees.schema.json +3 -1
- package/templates/.caws/templates/working-spec.template.yml +7 -3
- package/templates/.claude/hooks/block-dangerous.sh +52 -11
- package/templates/.claude/hooks/classify_command.py +592 -0
- package/templates/.claude/hooks/doc-frontmatter-check.sh +173 -0
- package/templates/.claude/hooks/quality-check.sh +23 -10
- package/templates/.claude/hooks/scope-guard.sh +34 -32
- package/templates/.claude/hooks/session-caws-status.sh +2 -2
- package/templates/.claude/hooks/session-log.sh +76 -3
- package/templates/.claude/hooks/stop-worktree-check.sh +1 -1
- package/templates/.claude/hooks/test_classify_command.py +370 -0
- package/templates/.claude/hooks/test_wrapper_smoke.sh +96 -0
- package/templates/.claude/hooks/worktree-guard.sh +2 -2
- package/templates/.claude/hooks/worktree-write-guard.sh +1 -1
- package/templates/.claude/settings.json +26 -0
- package/templates/.cursor/hooks/caws-quality-check.sh +4 -4
- package/templates/.cursor/hooks/caws-scope-guard.sh +1 -1
- package/templates/.cursor/hooks/session-log.sh +924 -0
- package/templates/.cursor/hooks.json +25 -0
- package/templates/.cursor/rules/02-quality-gates.mdc +3 -5
- package/templates/.cursor/rules/10-documentation-quality-standards.mdc +6 -11
- package/templates/.cursor/rules/11-scope-management-waivers.mdc +14 -18
- package/templates/.cursor/rules/12-implementation-completeness.mdc +4 -4
- package/templates/.cursor/rules/13-language-agnostic-standards.mdc +3 -13
- package/templates/.github/copilot-instructions.md +5 -5
- package/templates/.idea/runConfigurations/CAWS_Evaluate.xml +1 -1
- package/templates/.junie/guidelines.md +2 -2
- package/templates/.vscode/settings.json +3 -1
- package/templates/.windsurf/rules/caws-quality-standards.md +2 -2
- package/templates/.windsurf/workflows/caws-guided-development.md +3 -3
- package/templates/CLAUDE.md +43 -8
- package/templates/{AGENTS.md → agents.md} +29 -9
- package/templates/docs/README.md +8 -7
- package/templates/scripts/new_feature.sh +80 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/archive.d.ts +0 -51
- package/dist/commands/archive.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/mode.d.ts +0 -25
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/parallel.d.ts +0 -7
- package/dist/commands/parallel.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-gates.d.ts +0 -6
- package/dist/commands/quality-gates.d.ts.map +0 -1
- package/dist/commands/quality-gates.js +0 -444
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/session.d.ts +0 -7
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/specs.d.ts +0 -77
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/troubleshoot.js +0 -104
- package/dist/commands/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/worktree.d.ts +0 -7
- package/dist/commands/worktree.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/lite-scope.d.ts +0 -33
- package/dist/config/lite-scope.d.ts.map +0 -1
- package/dist/config/modes.d.ts +0 -264
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -93
- package/dist/constants/spec-types.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -151
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config-generator.d.ts +0 -32
- package/dist/generators/jest-config-generator.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/jest-config.js +0 -242
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/parallel/parallel-manager.d.ts +0 -67
- package/dist/parallel/parallel-manager.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/claude-hooks.d.ts +0 -28
- package/dist/scaffold/claude-hooks.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -38
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -17
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/session/session-manager.d.ts +0 -94
- package/dist/session/session-manager.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/templates/.cursor/hooks/caws-tool-validation.sh +0 -121
- package/dist/templates/.github/copilot/instructions.md +0 -311
- package/dist/test-analysis.d.ts +0 -231
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/async-utils.d.ts +0 -73
- package/dist/utils/async-utils.d.ts.map +0 -1
- package/dist/utils/command-wrapper.d.ts +0 -66
- package/dist/utils/command-wrapper.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -14
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/error-categories.d.ts +0 -52
- package/dist/utils/error-categories.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/git-lock.d.ts +0 -13
- package/dist/utils/git-lock.d.ts.map +0 -1
- package/dist/utils/gitignore-updater.d.ts +0 -39
- package/dist/utils/gitignore-updater.d.ts.map +0 -1
- package/dist/utils/ide-detection.d.ts +0 -89
- package/dist/utils/ide-detection.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -34
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/promise-utils.d.ts +0 -30
- package/dist/utils/promise-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates-utils.d.ts +0 -49
- package/dist/utils/quality-gates-utils.d.ts.map +0 -1
- package/dist/utils/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/quality-gates.js +0 -402
- package/dist/utils/spec-resolver.d.ts +0 -80
- package/dist/utils/spec-resolver.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -66
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/utils/yaml-validation.d.ts +0 -32
- package/dist/utils/yaml-validation.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/dist/worktree/worktree-manager.d.ts +0 -54
- package/dist/worktree/worktree-manager.d.ts.map +0 -1
|
@@ -8,11 +8,88 @@ const { execFileSync } = require('child_process');
|
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const path = require('path');
|
|
10
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');
|
|
11
14
|
|
|
12
15
|
const WORKTREES_DIR = '.caws/worktrees';
|
|
13
16
|
const REGISTRY_FILE = '.caws/worktrees.json';
|
|
14
17
|
const BRANCH_PREFIX = 'caws/';
|
|
15
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 materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
|
|
31
|
+
if (!specId) return;
|
|
32
|
+
|
|
33
|
+
const canonicalSpecPath = findFeatureSpecPath(root, specId);
|
|
34
|
+
const workingSpecPath = path.join(cawsDest, 'working-spec.yaml');
|
|
35
|
+
|
|
36
|
+
if (!canonicalSpecPath) {
|
|
37
|
+
console.warn(
|
|
38
|
+
chalk.yellow(`Warning: spec '${specId}' not found in .caws/specs/ — generating default working spec for worktree`)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (canonicalSpecPath) {
|
|
43
|
+
const destSpecsDir = path.join(cawsDest, 'specs');
|
|
44
|
+
const destSpecPath = path.join(destSpecsDir, path.basename(canonicalSpecPath));
|
|
45
|
+
fs.ensureDirSync(destSpecsDir);
|
|
46
|
+
|
|
47
|
+
// Keep a canonical feature-spec copy inside the worktree and align
|
|
48
|
+
// working-spec.yaml to that exact content for legacy-compatible commands.
|
|
49
|
+
const specContent = fs.readFileSync(canonicalSpecPath, 'utf8');
|
|
50
|
+
fs.writeFileSync(destSpecPath, specContent);
|
|
51
|
+
fs.writeFileSync(workingSpecPath, specContent);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { generateWorkingSpec } = require('../generators/working-spec');
|
|
56
|
+
const specContent = generateWorkingSpec({
|
|
57
|
+
projectId: specId,
|
|
58
|
+
projectTitle: `Worktree: ${worktreeName}`,
|
|
59
|
+
projectDescription: `Isolated worktree for ${worktreeName}`,
|
|
60
|
+
riskTier: 3,
|
|
61
|
+
projectMode: 'feature',
|
|
62
|
+
scopeIn: scope || 'src/',
|
|
63
|
+
scopeOut: 'node_modules/, dist/, build/',
|
|
64
|
+
maxFiles: 25,
|
|
65
|
+
maxLoc: 1000,
|
|
66
|
+
blastModules: scope || 'src',
|
|
67
|
+
dataMigration: false,
|
|
68
|
+
rollbackSlo: '5m',
|
|
69
|
+
projectThreats: '',
|
|
70
|
+
projectInvariants: 'System maintains data consistency',
|
|
71
|
+
acceptanceCriteria: 'Given current state, when action occurs, then expected result',
|
|
72
|
+
a11yRequirements: 'keyboard',
|
|
73
|
+
perfBudget: 250,
|
|
74
|
+
securityRequirements: 'validation',
|
|
75
|
+
contractType: '',
|
|
76
|
+
contractPath: '',
|
|
77
|
+
observabilityLogs: '',
|
|
78
|
+
observabilityMetrics: '',
|
|
79
|
+
observabilityTraces: '',
|
|
80
|
+
migrationPlan: '',
|
|
81
|
+
rollbackPlan: '',
|
|
82
|
+
needsOverride: false,
|
|
83
|
+
isExperimental: false,
|
|
84
|
+
aiConfidence: 0.8,
|
|
85
|
+
uncertaintyAreas: '',
|
|
86
|
+
complexityFactors: '',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
fs.ensureDirSync(path.dirname(workingSpecPath));
|
|
90
|
+
fs.writeFileSync(workingSpecPath, specContent);
|
|
91
|
+
}
|
|
92
|
+
|
|
16
93
|
/**
|
|
17
94
|
* Get the last commit info for a branch
|
|
18
95
|
* @param {string} branch - Branch name
|
|
@@ -53,6 +130,44 @@ function isBranchMerged(branch, target, root) {
|
|
|
53
130
|
}
|
|
54
131
|
}
|
|
55
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Check if a branch has divergent commits from target (commits on branch not on target).
|
|
135
|
+
* @param {string} branch - Branch to check
|
|
136
|
+
* @param {string} target - Target branch (e.g., "main")
|
|
137
|
+
* @param {string} root - Repository root
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function hasDivergentCommits(branch, target, root) {
|
|
141
|
+
try {
|
|
142
|
+
const count = execFileSync(
|
|
143
|
+
'git',
|
|
144
|
+
['rev-list', '--count', `${target}..${branch}`],
|
|
145
|
+
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
146
|
+
).trim();
|
|
147
|
+
return parseInt(count, 10) > 0;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a worktree directory has dirty (uncommitted) files.
|
|
155
|
+
* @param {string} worktreePath - Path to the worktree
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
function hasDirtyFiles(worktreePath) {
|
|
159
|
+
try {
|
|
160
|
+
const status = execFileSync(
|
|
161
|
+
'git',
|
|
162
|
+
['status', '--porcelain'],
|
|
163
|
+
{ cwd: worktreePath, encoding: 'utf8', stdio: 'pipe' }
|
|
164
|
+
).trim();
|
|
165
|
+
return status.length > 0;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
56
171
|
/**
|
|
57
172
|
* Get the canonical git repository root (main worktree, not a linked worktree).
|
|
58
173
|
*
|
|
@@ -85,6 +200,11 @@ function getCurrentBranch() {
|
|
|
85
200
|
}).trim();
|
|
86
201
|
}
|
|
87
202
|
|
|
203
|
+
// Track whether we've already warned about schema violations this process.
|
|
204
|
+
// loadRegistry() is called multiple times per command; warning every time
|
|
205
|
+
// floods stderr and contributes to Claude Code context-window exhaustion.
|
|
206
|
+
let _schemaWarned = false;
|
|
207
|
+
|
|
88
208
|
/**
|
|
89
209
|
* Load the worktree registry
|
|
90
210
|
* @param {string} root - Repository root
|
|
@@ -94,7 +214,21 @@ function loadRegistry(root) {
|
|
|
94
214
|
const registryPath = path.join(root, REGISTRY_FILE);
|
|
95
215
|
try {
|
|
96
216
|
if (fs.existsSync(registryPath)) {
|
|
97
|
-
|
|
217
|
+
const data = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
218
|
+
try {
|
|
219
|
+
const validate = createValidator(getSchemaPath('worktrees.schema.json', root));
|
|
220
|
+
const result = validate(data);
|
|
221
|
+
if (!result.valid && !_schemaWarned) {
|
|
222
|
+
_schemaWarned = true;
|
|
223
|
+
console.warn('Worktree registry has schema violations:', result.errors);
|
|
224
|
+
}
|
|
225
|
+
} catch (schemaErr) {
|
|
226
|
+
if (!_schemaWarned) {
|
|
227
|
+
_schemaWarned = true;
|
|
228
|
+
console.warn('Could not validate worktree registry schema:', schemaErr.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return data;
|
|
98
232
|
}
|
|
99
233
|
} catch {
|
|
100
234
|
// Corrupted registry, start fresh
|
|
@@ -108,6 +242,27 @@ function loadRegistry(root) {
|
|
|
108
242
|
* @param {Object} registry - Registry object
|
|
109
243
|
*/
|
|
110
244
|
function saveRegistry(root, registry) {
|
|
245
|
+
// Auto-prune destroyed entries whose branch and directory are both gone.
|
|
246
|
+
// This prevents the registry from accumulating ghost entries over time.
|
|
247
|
+
for (const [name, entry] of Object.entries(registry.worktrees || {})) {
|
|
248
|
+
if (entry.status !== 'destroyed') continue;
|
|
249
|
+
const dirGone = !fs.existsSync(entry.path);
|
|
250
|
+
let branchGone = true;
|
|
251
|
+
if (entry.branch) {
|
|
252
|
+
try {
|
|
253
|
+
execFileSync('git', ['rev-parse', '--verify', entry.branch], {
|
|
254
|
+
cwd: root, stdio: 'pipe',
|
|
255
|
+
});
|
|
256
|
+
branchGone = false;
|
|
257
|
+
} catch {
|
|
258
|
+
branchGone = true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (dirGone && branchGone) {
|
|
262
|
+
delete registry.worktrees[name];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
111
266
|
const registryPath = path.join(root, REGISTRY_FILE);
|
|
112
267
|
fs.ensureDirSync(path.dirname(registryPath));
|
|
113
268
|
fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
|
|
@@ -233,15 +388,64 @@ function createWorktree(name, options = {}) {
|
|
|
233
388
|
|
|
234
389
|
const registry = loadRegistry(root);
|
|
235
390
|
|
|
236
|
-
// Check for duplicate
|
|
391
|
+
// Check for duplicate in registry
|
|
237
392
|
if (registry.worktrees[name]) {
|
|
238
|
-
|
|
393
|
+
const existing = registry.worktrees[name];
|
|
394
|
+
if (existing.status !== 'destroyed') {
|
|
395
|
+
const ownerInfo = existing.owner ? ` (owned by session ${existing.owner})` : '';
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Worktree '${name}' already exists with status '${existing.status}'${ownerInfo}.\n` +
|
|
398
|
+
`Use 'caws worktree destroy ${name}' first, or choose a different name.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
// Destroyed entries: check if another session owns the branch
|
|
402
|
+
if (existing.owner && existing.owner !== getAgentSessionId(root)) {
|
|
403
|
+
// Branch may still be in use by the owning session for merge
|
|
404
|
+
try {
|
|
405
|
+
const branchExists = execFileSync('git', ['rev-parse', '--verify', BRANCH_PREFIX + name], {
|
|
406
|
+
cwd: root, stdio: 'pipe',
|
|
407
|
+
}).toString().trim();
|
|
408
|
+
if (branchExists) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
`Worktree '${name}' was destroyed but branch '${BRANCH_PREFIX}${name}' still exists ` +
|
|
411
|
+
`(owned by session ${existing.owner}).\n` +
|
|
412
|
+
`The owning session may still need this branch for merging.\n` +
|
|
413
|
+
`Choose a different name, or delete the branch first: git branch -d ${BRANCH_PREFIX}${name}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
if (e.message.includes('owned by session')) throw e;
|
|
418
|
+
// Branch doesn't exist — safe to reuse the name
|
|
419
|
+
}
|
|
420
|
+
}
|
|
239
421
|
}
|
|
240
422
|
|
|
241
423
|
const worktreePath = path.join(root, WORKTREES_DIR, name);
|
|
242
424
|
const branchName = BRANCH_PREFIX + name;
|
|
243
425
|
const base = baseBranch || getCurrentBranch();
|
|
244
426
|
|
|
427
|
+
// Check if the branch already exists in git (even if not in registry)
|
|
428
|
+
// This catches cases where another agent created the branch outside CAWS
|
|
429
|
+
try {
|
|
430
|
+
execFileSync('git', ['rev-parse', '--verify', branchName], {
|
|
431
|
+
cwd: root, stdio: 'pipe',
|
|
432
|
+
});
|
|
433
|
+
// Branch exists — refuse unless it's fully merged into base
|
|
434
|
+
const currentSession = getAgentSessionId(root);
|
|
435
|
+
const registryOwner = registry.worktrees[name]?.owner;
|
|
436
|
+
if (registryOwner && registryOwner !== currentSession) {
|
|
437
|
+
throw new Error(
|
|
438
|
+
`Branch '${branchName}' already exists and is owned by another session (${registryOwner}).\n` +
|
|
439
|
+
`Another agent may be using this branch. Choose a different worktree name.`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
// Branch exists but no owner conflict — warn and reuse
|
|
443
|
+
console.warn(`Warning: Branch '${branchName}' already exists, reusing it.`);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
if (e.message.includes('already exists and is owned')) throw e;
|
|
446
|
+
// Branch doesn't exist — this is the normal path
|
|
447
|
+
}
|
|
448
|
+
|
|
245
449
|
// Create the worktree directory
|
|
246
450
|
fs.ensureDirSync(path.dirname(worktreePath));
|
|
247
451
|
|
|
@@ -252,7 +456,7 @@ function createWorktree(name, options = {}) {
|
|
|
252
456
|
stdio: 'pipe',
|
|
253
457
|
});
|
|
254
458
|
} catch (error) {
|
|
255
|
-
// Branch
|
|
459
|
+
// Branch already exists (caught above and allowed) — attach to it
|
|
256
460
|
if (error.message.includes('already exists')) {
|
|
257
461
|
execFileSync('git', ['worktree', 'add', worktreePath, branchName], {
|
|
258
462
|
cwd: root,
|
|
@@ -306,46 +510,16 @@ function createWorktree(name, options = {}) {
|
|
|
306
510
|
}
|
|
307
511
|
}
|
|
308
512
|
|
|
309
|
-
//
|
|
513
|
+
// Materialize a worktree-local working spec. Prefer the canonical feature
|
|
514
|
+
// spec when it exists so isolated worktrees stay aligned with the main
|
|
515
|
+
// registry/resolver model.
|
|
310
516
|
if (specId) {
|
|
311
517
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
riskTier: 3,
|
|
318
|
-
projectMode: 'feature',
|
|
319
|
-
scopeIn: scope || 'src/',
|
|
320
|
-
scopeOut: 'node_modules/, dist/, build/',
|
|
321
|
-
maxFiles: 25,
|
|
322
|
-
maxLoc: 1000,
|
|
323
|
-
blastModules: scope || 'src',
|
|
324
|
-
dataMigration: false,
|
|
325
|
-
rollbackSlo: '5m',
|
|
326
|
-
projectThreats: '',
|
|
327
|
-
projectInvariants: 'System maintains data consistency',
|
|
328
|
-
acceptanceCriteria: 'Given current state, when action occurs, then expected result',
|
|
329
|
-
a11yRequirements: 'keyboard',
|
|
330
|
-
perfBudget: 250,
|
|
331
|
-
securityRequirements: 'validation',
|
|
332
|
-
contractType: '',
|
|
333
|
-
contractPath: '',
|
|
334
|
-
observabilityLogs: '',
|
|
335
|
-
observabilityMetrics: '',
|
|
336
|
-
observabilityTraces: '',
|
|
337
|
-
migrationPlan: '',
|
|
338
|
-
rollbackPlan: '',
|
|
339
|
-
needsOverride: false,
|
|
340
|
-
isExperimental: false,
|
|
341
|
-
aiConfidence: 0.8,
|
|
342
|
-
uncertaintyAreas: '',
|
|
343
|
-
complexityFactors: '',
|
|
344
|
-
});
|
|
345
|
-
const specPath = path.join(cawsDest, 'working-spec.yaml');
|
|
346
|
-
fs.ensureDirSync(path.dirname(specPath));
|
|
347
|
-
fs.writeFileSync(specPath, specContent);
|
|
348
|
-
} catch {
|
|
518
|
+
materializeWorktreeSpec(root, cawsDest, specId, name, scope);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.warn(
|
|
521
|
+
chalk.yellow(`Could not materialize spec '${specId}' for worktree '${name}': ${error.message}`)
|
|
522
|
+
);
|
|
349
523
|
// Non-fatal: spec generation is optional
|
|
350
524
|
}
|
|
351
525
|
}
|
|
@@ -358,9 +532,9 @@ function createWorktree(name, options = {}) {
|
|
|
358
532
|
baseBranch: base,
|
|
359
533
|
scope: scope || null,
|
|
360
534
|
specId: specId || null,
|
|
361
|
-
owner: options.owner ||
|
|
535
|
+
owner: options.owner || getAgentSessionId(root) || null,
|
|
362
536
|
createdAt: new Date().toISOString(),
|
|
363
|
-
status: '
|
|
537
|
+
status: 'fresh',
|
|
364
538
|
};
|
|
365
539
|
|
|
366
540
|
registry.worktrees[name] = entry;
|
|
@@ -415,26 +589,44 @@ function reconcileRegistry(root) {
|
|
|
415
589
|
(wt) => path.resolve(wt) === path.resolve(entry.path)
|
|
416
590
|
);
|
|
417
591
|
|
|
592
|
+
const merged = entry.branch && entry.baseBranch
|
|
593
|
+
? isBranchMerged(entry.branch, entry.baseBranch, root)
|
|
594
|
+
: false;
|
|
595
|
+
const divergent = entry.branch && entry.baseBranch
|
|
596
|
+
? hasDivergentCommits(entry.branch, entry.baseBranch, root)
|
|
597
|
+
: false;
|
|
598
|
+
const dirty = exists ? hasDirtyFiles(entry.path) : false;
|
|
599
|
+
|
|
418
600
|
let status;
|
|
419
601
|
if (entry.status === 'destroyed') {
|
|
420
602
|
status = 'destroyed';
|
|
421
603
|
} else if (exists && inGit) {
|
|
422
|
-
|
|
604
|
+
// Worktree directory exists and is tracked by git
|
|
605
|
+
if (divergent || dirty) {
|
|
606
|
+
// Has commits beyond base or uncommitted work → active
|
|
607
|
+
status = 'active';
|
|
608
|
+
} else if (merged) {
|
|
609
|
+
// No divergent commits, branch aligned with base.
|
|
610
|
+
// Use stored status as history to distinguish fresh vs merged:
|
|
611
|
+
// - stored 'fresh' → never had divergent commits → still fresh
|
|
612
|
+
// - stored 'active' → had work that's now merged → merged
|
|
613
|
+
if (entry.status === 'active') {
|
|
614
|
+
status = 'merged';
|
|
615
|
+
} else {
|
|
616
|
+
status = 'fresh';
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
status = 'fresh';
|
|
620
|
+
}
|
|
423
621
|
} else if (exists) {
|
|
424
622
|
status = 'orphaned';
|
|
425
623
|
} else {
|
|
426
|
-
const merged = entry.branch && entry.baseBranch
|
|
427
|
-
? isBranchMerged(entry.branch, entry.baseBranch, root)
|
|
428
|
-
: false;
|
|
429
624
|
status = merged ? 'stale-merged' : 'missing';
|
|
430
625
|
}
|
|
431
626
|
|
|
432
627
|
const lastCommit = entry.branch ? getLastCommitInfo(entry.branch, root) : null;
|
|
433
|
-
const merged = entry.branch && entry.baseBranch
|
|
434
|
-
? isBranchMerged(entry.branch, entry.baseBranch, root)
|
|
435
|
-
: false;
|
|
436
628
|
|
|
437
|
-
return { ...entry, status, lastCommit, merged };
|
|
629
|
+
return { ...entry, status, lastCommit, merged, divergent, dirty };
|
|
438
630
|
});
|
|
439
631
|
|
|
440
632
|
// Append unregistered worktrees discovered from git
|
|
@@ -466,15 +658,17 @@ function reconcileRegistry(root) {
|
|
|
466
658
|
* prunes stale entries. Reports the delta before persisting.
|
|
467
659
|
*
|
|
468
660
|
* @param {Object} options
|
|
469
|
-
* @param {boolean} [options.prune=false] - Remove destroyed
|
|
661
|
+
* @param {boolean} [options.prune=false] - Remove destroyed, stale-merged, and missing entries
|
|
470
662
|
* @param {boolean} [options.dryRun=false] - Report only, do not persist
|
|
663
|
+
* @param {boolean} [options.force=false] - Allow pruning entries owned by other sessions
|
|
471
664
|
* @returns {{ repaired: Array, pruned: Array, skipped: Array }}
|
|
472
665
|
*/
|
|
473
666
|
function repairWorktrees(options = {}) {
|
|
474
|
-
const { prune: shouldPrune = false, dryRun = false } = options;
|
|
667
|
+
const { prune: shouldPrune = false, dryRun = false, force = false } = options;
|
|
475
668
|
const root = getRepoRoot();
|
|
476
669
|
const registry = loadRegistry(root);
|
|
477
670
|
const { entries } = reconcileRegistry(root);
|
|
671
|
+
const currentSession = getAgentSessionId(root);
|
|
478
672
|
|
|
479
673
|
const repaired = [];
|
|
480
674
|
const pruned = [];
|
|
@@ -494,18 +688,47 @@ function repairWorktrees(options = {}) {
|
|
|
494
688
|
if (!regEntry) continue;
|
|
495
689
|
|
|
496
690
|
// Update registry status to match filesystem reality
|
|
497
|
-
|
|
498
|
-
|
|
691
|
+
const wasAlive = regEntry.status === 'active' || regEntry.status === 'fresh';
|
|
692
|
+
const nowDead = entry.status === 'missing' || entry.status === 'stale-merged';
|
|
693
|
+
if (wasAlive && nowDead) {
|
|
694
|
+
repaired.push({
|
|
695
|
+
name: entry.name,
|
|
696
|
+
action: 'status-updated',
|
|
697
|
+
from: regEntry.status,
|
|
698
|
+
to: entry.status,
|
|
699
|
+
owner: entry.owner || null,
|
|
700
|
+
});
|
|
499
701
|
}
|
|
500
702
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
703
|
+
// Determine if entry is prunable (destroyed, stale-merged, or missing)
|
|
704
|
+
const isPrunable = entry.status === 'destroyed' ||
|
|
705
|
+
entry.status === 'stale-merged' ||
|
|
706
|
+
entry.status === 'missing';
|
|
707
|
+
|
|
708
|
+
if (!isPrunable) continue;
|
|
709
|
+
|
|
710
|
+
// Ownership check: refuse to prune another session's entries without --force
|
|
711
|
+
const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
|
|
712
|
+
|
|
713
|
+
if (shouldPrune && isPrunable) {
|
|
714
|
+
if (isOwnedByOther && !force) {
|
|
715
|
+
skipped.push({
|
|
716
|
+
name: entry.name,
|
|
717
|
+
reason: `owned by another session (${entry.owner}). Use --force to override`,
|
|
718
|
+
owner: entry.owner,
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
if (!dryRun) {
|
|
722
|
+
delete registry.worktrees[entry.name];
|
|
723
|
+
}
|
|
724
|
+
pruned.push({ name: entry.name, status: entry.status, owner: entry.owner || null });
|
|
505
725
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
726
|
+
} else if (!shouldPrune && isPrunable) {
|
|
727
|
+
skipped.push({
|
|
728
|
+
name: entry.name,
|
|
729
|
+
reason: entry.status + ' (use --prune to remove)',
|
|
730
|
+
owner: entry.owner || null,
|
|
731
|
+
});
|
|
509
732
|
}
|
|
510
733
|
}
|
|
511
734
|
|
|
@@ -524,11 +747,29 @@ function repairWorktrees(options = {}) {
|
|
|
524
747
|
/**
|
|
525
748
|
* List all registered worktrees with filesystem validation.
|
|
526
749
|
* Delegates to reconcileRegistry() for state classification.
|
|
750
|
+
* Persists status transitions (fresh → active, active → merged) so
|
|
751
|
+
* future calls can distinguish "never had work" from "work was merged back".
|
|
527
752
|
* @returns {Array} Worktree entries with status
|
|
528
753
|
*/
|
|
529
754
|
function listWorktrees() {
|
|
530
755
|
const root = getRepoRoot();
|
|
756
|
+
const registry = loadRegistry(root);
|
|
531
757
|
const { entries } = reconcileRegistry(root);
|
|
758
|
+
|
|
759
|
+
// Persist status transitions so future reconcile can use stored status as history
|
|
760
|
+
let dirty = false;
|
|
761
|
+
for (const entry of entries) {
|
|
762
|
+
const regEntry = registry.worktrees[entry.name];
|
|
763
|
+
if (regEntry && regEntry.status !== entry.status &&
|
|
764
|
+
entry.status !== 'unregistered') {
|
|
765
|
+
regEntry.status = entry.status;
|
|
766
|
+
dirty = true;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (dirty) {
|
|
770
|
+
saveRegistry(root, registry);
|
|
771
|
+
}
|
|
772
|
+
|
|
532
773
|
return entries;
|
|
533
774
|
}
|
|
534
775
|
|
|
@@ -541,6 +782,9 @@ function listWorktrees() {
|
|
|
541
782
|
*/
|
|
542
783
|
function destroyWorktree(name, options = {}) {
|
|
543
784
|
const root = getRepoRoot();
|
|
785
|
+
// Ensure CWD is not inside the worktree we're about to destroy.
|
|
786
|
+
// If CWD is the worktree directory, removing it crashes subsequent commands.
|
|
787
|
+
try { process.chdir(root); } catch { /* non-fatal */ }
|
|
544
788
|
const registry = loadRegistry(root);
|
|
545
789
|
const { deleteBranch = false, force = false } = options;
|
|
546
790
|
|
|
@@ -557,11 +801,12 @@ function destroyWorktree(name, options = {}) {
|
|
|
557
801
|
}
|
|
558
802
|
}
|
|
559
803
|
|
|
560
|
-
// Ownership check: refuse to destroy another agent's
|
|
561
|
-
const currentSession =
|
|
804
|
+
// Ownership check: refuse to destroy another agent's worktree without --force
|
|
805
|
+
const currentSession = getAgentSessionId(root);
|
|
806
|
+
const isLiveStatus = entry.status === 'active' || entry.status === 'fresh' || entry.status === 'merged';
|
|
562
807
|
if (
|
|
563
808
|
!force &&
|
|
564
|
-
|
|
809
|
+
isLiveStatus &&
|
|
565
810
|
entry.owner &&
|
|
566
811
|
currentSession &&
|
|
567
812
|
entry.owner !== currentSession
|
|
@@ -580,7 +825,7 @@ function destroyWorktree(name, options = {}) {
|
|
|
580
825
|
// Even with --force, warn loudly when destroying another session's worktree
|
|
581
826
|
if (
|
|
582
827
|
force &&
|
|
583
|
-
|
|
828
|
+
isLiveStatus &&
|
|
584
829
|
entry.owner &&
|
|
585
830
|
currentSession &&
|
|
586
831
|
entry.owner !== currentSession
|
|
@@ -682,17 +927,32 @@ function mergeWorktree(name, options = {}) {
|
|
|
682
927
|
|
|
683
928
|
const baseBranch = entry.baseBranch || 'main';
|
|
684
929
|
|
|
685
|
-
// Check for uncommitted work in the worktree
|
|
930
|
+
// Check for uncommitted work in the worktree.
|
|
931
|
+
// Ignore .caws/ changes (provenance chain, registry) — these are
|
|
932
|
+
// infrastructure artifacts written by git hooks, not user work.
|
|
933
|
+
// The post-commit hook appends to .caws/provenance/chain.json after
|
|
934
|
+
// every commit, which immediately dirties the tree and blocks merges.
|
|
686
935
|
if (fs.existsSync(entry.path)) {
|
|
687
936
|
try {
|
|
688
|
-
const
|
|
937
|
+
const rawStatus = execFileSync(
|
|
689
938
|
'git',
|
|
690
939
|
['status', '--porcelain'],
|
|
691
940
|
{ cwd: entry.path, encoding: 'utf8', stdio: 'pipe' }
|
|
692
|
-
)
|
|
693
|
-
|
|
941
|
+
);
|
|
942
|
+
// Filter out .caws/ infrastructure changes (provenance, registry).
|
|
943
|
+
// Git porcelain format: "XY PATH" — 2 status chars, space, path.
|
|
944
|
+
// IMPORTANT: do NOT .trim() the raw output — it strips the leading
|
|
945
|
+
// space from " M file" (unstaged), corrupting the XY prefix and
|
|
946
|
+
// breaking substring(3) path extraction.
|
|
947
|
+
const statusLines = rawStatus.split('\n').filter(l => l.length > 0);
|
|
948
|
+
const userChanges = statusLines
|
|
949
|
+
.filter(line => {
|
|
950
|
+
const filePath = line.substring(3);
|
|
951
|
+
return !filePath.startsWith('.caws/');
|
|
952
|
+
}).join('\n');
|
|
953
|
+
if (userChanges) {
|
|
694
954
|
throw new Error(
|
|
695
|
-
`Worktree '${name}' has uncommitted changes:\n${
|
|
955
|
+
`Worktree '${name}' has uncommitted changes:\n${userChanges}\n` +
|
|
696
956
|
`Commit or discard changes before merging.`
|
|
697
957
|
);
|
|
698
958
|
}
|
|
@@ -736,32 +996,54 @@ function mergeWorktree(name, options = {}) {
|
|
|
736
996
|
};
|
|
737
997
|
}
|
|
738
998
|
|
|
999
|
+
// Emit merge:pre event
|
|
1000
|
+
try {
|
|
1001
|
+
lifecycle.emit(EVENTS.MERGE_PRE, {
|
|
1002
|
+
worktreeName: name, branch: entry.branch, baseBranch, conflicts,
|
|
1003
|
+
timestamp: new Date().toISOString(),
|
|
1004
|
+
});
|
|
1005
|
+
} catch { /* non-fatal */ }
|
|
1006
|
+
|
|
1007
|
+
// Ensure CWD is the repo root BEFORE destroying the worktree.
|
|
1008
|
+
// If the caller's CWD is inside the worktree directory, destroying it
|
|
1009
|
+
// removes the CWD out from under the process, causing all subsequent
|
|
1010
|
+
// git commands to fail with "Unable to read current working directory".
|
|
1011
|
+
try { process.chdir(root); } catch { /* non-fatal */ }
|
|
1012
|
+
|
|
739
1013
|
// Destroy the worktree (auto-forces since we're about to merge)
|
|
740
1014
|
destroyWorktree(name, { deleteBranch: false, force: true });
|
|
741
1015
|
|
|
742
|
-
// Switch to base branch
|
|
743
|
-
const currentBranch =
|
|
1016
|
+
// Switch to base branch (use cwd: root since getCurrentBranch has no cwd param)
|
|
1017
|
+
const currentBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
1018
|
+
cwd: root, encoding: 'utf8', stdio: 'pipe',
|
|
1019
|
+
}).trim();
|
|
744
1020
|
if (currentBranch !== baseBranch) {
|
|
745
1021
|
execFileSync('git', ['checkout', baseBranch], { cwd: root, stdio: 'pipe' });
|
|
746
1022
|
}
|
|
747
1023
|
|
|
748
1024
|
// Merge
|
|
1025
|
+
// Use --no-verify to skip pre-commit/commit-msg hooks during merge.
|
|
1026
|
+
// The worktree commits were already validated by those hooks when originally
|
|
1027
|
+
// committed. Re-running them here adds seconds of blocking time (especially
|
|
1028
|
+
// in projects with heavy hooks like quality gates, YAML validation, etc.)
|
|
1029
|
+
// and can trigger OAuth token expiry races in long-running sessions.
|
|
749
1030
|
const mergeMessage = message || `merge(worktree): ${name}`;
|
|
750
1031
|
try {
|
|
751
1032
|
execFileSync(
|
|
752
1033
|
'git',
|
|
753
|
-
['merge', '--no-ff', entry.branch, '-m', mergeMessage],
|
|
1034
|
+
['merge', '--no-ff', '--no-verify', entry.branch, '-m', mergeMessage],
|
|
754
1035
|
{ cwd: root, stdio: 'pipe' }
|
|
755
1036
|
);
|
|
756
1037
|
} catch (error) {
|
|
757
|
-
|
|
758
|
-
name,
|
|
759
|
-
branch: entry.branch,
|
|
760
|
-
baseBranch,
|
|
761
|
-
merged: false,
|
|
1038
|
+
const failResult = {
|
|
1039
|
+
name, branch: entry.branch, baseBranch, merged: false,
|
|
762
1040
|
conflicts: [`Merge failed: ${error.message}`],
|
|
763
1041
|
message: 'Merge conflicts detected. Resolve with git and commit.',
|
|
764
1042
|
};
|
|
1043
|
+
try {
|
|
1044
|
+
lifecycle.emit(EVENTS.MERGE_POST, { ...failResult, timestamp: new Date().toISOString() });
|
|
1045
|
+
} catch { /* non-fatal */ }
|
|
1046
|
+
return failResult;
|
|
765
1047
|
}
|
|
766
1048
|
|
|
767
1049
|
// Delete branch after successful merge
|
|
@@ -773,13 +1055,11 @@ function mergeWorktree(name, options = {}) {
|
|
|
773
1055
|
}
|
|
774
1056
|
}
|
|
775
1057
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
conflicts: [],
|
|
782
|
-
};
|
|
1058
|
+
const mergeResult = { name, branch: entry.branch, baseBranch, merged: true, conflicts: [] };
|
|
1059
|
+
try {
|
|
1060
|
+
lifecycle.emit(EVENTS.MERGE_POST, { ...mergeResult, timestamp: new Date().toISOString() });
|
|
1061
|
+
} catch { /* non-fatal */ }
|
|
1062
|
+
return mergeResult;
|
|
783
1063
|
}
|
|
784
1064
|
|
|
785
1065
|
/**
|
|
@@ -787,12 +1067,14 @@ function mergeWorktree(name, options = {}) {
|
|
|
787
1067
|
* @param {Object} options - Prune options
|
|
788
1068
|
* @param {number} [options.maxAgeDays] - Remove entries older than this many days
|
|
789
1069
|
* @param {number} [options.recentCommitMinutes] - Protect branches with commits newer than this (default: 60)
|
|
790
|
-
* @
|
|
1070
|
+
* @param {boolean} [options.force] - Allow pruning entries owned by other sessions
|
|
1071
|
+
* @returns {{ pruned: Array, skipped: Array }} Pruned and skipped entries
|
|
791
1072
|
*/
|
|
792
1073
|
function pruneWorktrees(options = {}) {
|
|
793
1074
|
const root = getRepoRoot();
|
|
794
1075
|
const registry = loadRegistry(root);
|
|
795
|
-
const { maxAgeDays = 30, recentCommitMinutes = 60 } = options;
|
|
1076
|
+
const { maxAgeDays = 30, recentCommitMinutes = 60, force = false } = options;
|
|
1077
|
+
const currentSession = getAgentSessionId(root);
|
|
796
1078
|
|
|
797
1079
|
const now = new Date();
|
|
798
1080
|
const pruned = [];
|
|
@@ -806,14 +1088,25 @@ function pruneWorktrees(options = {}) {
|
|
|
806
1088
|
const shouldPrune =
|
|
807
1089
|
// Always prune destroyed entries
|
|
808
1090
|
entry.status === 'destroyed' ||
|
|
809
|
-
// Prune active entries whose directory is gone (filesystem-registry desync)
|
|
810
|
-
(entry.status === 'active' && !dirExists) ||
|
|
1091
|
+
// Prune active/fresh entries whose directory is gone (filesystem-registry desync)
|
|
1092
|
+
((entry.status === 'active' || entry.status === 'fresh') && !dirExists) ||
|
|
811
1093
|
// Prune old missing entries
|
|
812
1094
|
(!dirExists && ageDays > maxAgeDays);
|
|
813
1095
|
|
|
814
1096
|
if (shouldPrune) {
|
|
815
|
-
//
|
|
816
|
-
|
|
1097
|
+
// Ownership check: skip entries owned by other sessions unless --force
|
|
1098
|
+
const isOwnedByOther = entry.owner && currentSession && entry.owner !== currentSession;
|
|
1099
|
+
if (isOwnedByOther && entry.status !== 'destroyed' && !force) {
|
|
1100
|
+
skipped.push({
|
|
1101
|
+
name,
|
|
1102
|
+
reason: `owned by another session (${entry.owner})`,
|
|
1103
|
+
entry,
|
|
1104
|
+
});
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Before pruning a non-destroyed entry, check for recent commits (skip if --force)
|
|
1109
|
+
if (!force && entry.status !== 'destroyed' && entry.branch) {
|
|
817
1110
|
const lastCommit = getLastCommitInfo(entry.branch, root);
|
|
818
1111
|
if (lastCommit) {
|
|
819
1112
|
const commitAgeMinutes = (now - lastCommit.timestamp) / (1000 * 60);
|
|
@@ -863,9 +1156,13 @@ module.exports = {
|
|
|
863
1156
|
getRepoRoot,
|
|
864
1157
|
getLastCommitInfo,
|
|
865
1158
|
isBranchMerged,
|
|
1159
|
+
hasDivergentCommits,
|
|
1160
|
+
hasDirtyFiles,
|
|
866
1161
|
discoverUnregisteredWorktrees,
|
|
867
1162
|
autoRegisterWorktree,
|
|
868
1163
|
WORKTREES_DIR,
|
|
869
1164
|
REGISTRY_FILE,
|
|
870
1165
|
BRANCH_PREFIX,
|
|
1166
|
+
findFeatureSpecPath,
|
|
1167
|
+
materializeWorktreeSpec,
|
|
871
1168
|
};
|