@paths.design/caws-cli 10.0.1 → 10.2.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 +13 -5
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/agents.js +124 -0
- package/dist/commands/evaluate.js +26 -12
- package/dist/commands/gates.js +31 -4
- package/dist/commands/init.js +7 -4
- package/dist/commands/iterate.js +7 -3
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +6 -3
- package/dist/commands/specs.js +359 -4
- package/dist/commands/status.js +29 -4
- package/dist/commands/templates.js +0 -8
- package/dist/commands/validate.js +34 -13
- package/dist/commands/verify-acs.js +25 -10
- package/dist/commands/waivers.js +147 -5
- package/dist/commands/worktree.js +200 -4
- package/dist/gates/budget-limit.js +6 -1
- package/dist/gates/scope-boundary.js +26 -7
- package/dist/gates/spec-completeness.js +8 -1
- package/dist/index.js +56 -0
- package/dist/policy/PolicyManager.js +14 -7
- package/dist/session/session-manager.js +34 -0
- package/dist/templates/.caws/schemas/policy.schema.json +101 -34
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
- package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/dist/templates/.claude/rules/worktree-isolation.md +21 -3
- package/dist/templates/.claude/settings.json +5 -0
- package/dist/templates/CLAUDE.md +56 -0
- package/dist/templates/agents.md +47 -0
- package/dist/utils/agent-display.js +210 -0
- package/dist/utils/agent-session.js +142 -0
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/schema-validator.js +10 -2
- package/dist/utils/working-state.js +25 -0
- package/dist/validation/spec-validation.js +102 -9
- package/dist/waivers-manager.js +84 -0
- package/dist/worktree/worktree-manager.js +593 -26
- package/package.json +5 -4
- package/templates/.caws/schemas/policy.schema.json +101 -34
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +91 -21
- package/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/templates/.caws/templates/working-spec.template.yml +3 -1
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/scope-guard.sh +106 -27
- package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/templates/.claude/rules/worktree-isolation.md +21 -3
- package/templates/.claude/settings.json +5 -0
- package/templates/CLAUDE.md +56 -0
- package/templates/agents.md +47 -0
|
@@ -9,7 +9,13 @@ const fs = require('fs-extra');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const chalk = require('chalk');
|
|
11
11
|
const { createValidator, getSchemaPath } = require('../utils/schema-validator');
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
getAgentSessionId,
|
|
14
|
+
loadAgentRegistry,
|
|
15
|
+
findSessionLogs,
|
|
16
|
+
refreshAgentClaim,
|
|
17
|
+
} = require('../utils/agent-session');
|
|
18
|
+
const { formatClaimNotice, formatOrphanLogHint } = require('../utils/agent-display');
|
|
13
19
|
const { lifecycle, EVENTS } = require('../utils/lifecycle-events');
|
|
14
20
|
|
|
15
21
|
const WORKTREES_DIR = '.caws/worktrees';
|
|
@@ -27,33 +33,151 @@ function findFeatureSpecPath(root, specId) {
|
|
|
27
33
|
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
28
34
|
}
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a feature spec path, preferring a worktree-local copy when cwd
|
|
38
|
+
* is inside a worktree. Falls back to the main repo.
|
|
39
|
+
*
|
|
40
|
+
* Why two-step: `caws worktree bind` may be invoked from inside a worktree
|
|
41
|
+
* that was forked off a non-main base branch (Option C fork-off-sibling
|
|
42
|
+
* pattern). In that workflow the spec is committed on the worktree's own
|
|
43
|
+
* branch and never lands on main. The pre-CAWSFIX-25 behavior of looking
|
|
44
|
+
* only in `root/.caws/specs/` made bind unusable there (D8 ledger entry).
|
|
45
|
+
*
|
|
46
|
+
* @param {string} root - Main repo root (from getRepoRoot())
|
|
47
|
+
* @param {string} specId - Spec identifier
|
|
48
|
+
* @param {string} [cwd=process.cwd()] - Directory to resolve from
|
|
49
|
+
* @returns {string|null} Absolute path to the spec file, or null
|
|
50
|
+
*/
|
|
51
|
+
function findFeatureSpecPathFromCwd(root, specId, cwd) {
|
|
52
|
+
if (!specId) return null;
|
|
53
|
+
const effectiveCwd = cwd || process.cwd();
|
|
54
|
+
|
|
55
|
+
// Normalize both sides against symlinks before comparing. On macOS
|
|
56
|
+
// `/tmp` and `/var/folders` are symlinks under `/private`, so the literal
|
|
57
|
+
// `startsWith` check fails intermittently in the test fixture. Fall back
|
|
58
|
+
// to the pre-resolution path if realpath throws (e.g., cwd removed).
|
|
59
|
+
const resolve = (p) => {
|
|
60
|
+
try { return fs.realpathSync(p); } catch { return p; }
|
|
61
|
+
};
|
|
62
|
+
const resolvedCwd = resolve(effectiveCwd);
|
|
63
|
+
const worktreesBase = resolve(path.join(root, '.caws', 'worktrees'));
|
|
64
|
+
|
|
65
|
+
if (resolvedCwd.startsWith(worktreesBase + path.sep)) {
|
|
66
|
+
const relative = path.relative(worktreesBase, resolvedCwd);
|
|
67
|
+
const worktreeName = relative.split(path.sep)[0];
|
|
68
|
+
if (worktreeName) {
|
|
69
|
+
const worktreeRoot = path.join(worktreesBase, worktreeName);
|
|
70
|
+
const local = findFeatureSpecPath(worktreeRoot, specId);
|
|
71
|
+
if (local) return local;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return findFeatureSpecPath(root, specId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeSpecWithWorktree(filePath, worktreeName) {
|
|
79
|
+
const yaml = require('js-yaml');
|
|
80
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
81
|
+
const parsed = yaml.load(content);
|
|
82
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
83
|
+
return content;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// CAWSFIX-24 / D10: if the on-disk spec already declares the target
|
|
87
|
+
// worktree and reloads to an equivalent object, return the original
|
|
88
|
+
// bytes untouched. js-yaml.dump re-wraps folded scalars at its own
|
|
89
|
+
// line-width preference, which otherwise produces spurious bytes-only
|
|
90
|
+
// diffs on every bind/create. That mechanical churn (a) leaves dirty
|
|
91
|
+
// files on main after worktree create, and (b) causes merge conflicts
|
|
92
|
+
// when two validator invocations wrap the same title at different
|
|
93
|
+
// widths.
|
|
94
|
+
if (parsed.worktree === worktreeName) {
|
|
95
|
+
return content;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
parsed.worktree = worktreeName;
|
|
99
|
+
return yaml.dump(parsed, { lineWidth: 120, noRefs: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasPathChanges(root, relativePath) {
|
|
103
|
+
try {
|
|
104
|
+
const output = execFileSync(
|
|
105
|
+
'git',
|
|
106
|
+
['status', '--porcelain', '--', relativePath],
|
|
107
|
+
{ cwd: root, encoding: 'utf8', stdio: 'pipe' }
|
|
108
|
+
).trim();
|
|
109
|
+
return output.length > 0;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ensureCanonicalSpecCommitted(root, specPath, specId, worktreeName) {
|
|
116
|
+
const relativeSpecPath = path.relative(root, specPath);
|
|
117
|
+
const nextContent = writeSpecWithWorktree(specPath, worktreeName);
|
|
118
|
+
const currentContent = fs.readFileSync(specPath, 'utf8');
|
|
119
|
+
|
|
120
|
+
if (currentContent !== nextContent) {
|
|
121
|
+
fs.writeFileSync(specPath, nextContent);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!hasPathChanges(root, relativeSpecPath)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
execFileSync('git', ['add', '--', relativeSpecPath], {
|
|
129
|
+
cwd: root,
|
|
130
|
+
stdio: 'pipe',
|
|
131
|
+
});
|
|
132
|
+
execFileSync(
|
|
133
|
+
'git',
|
|
134
|
+
['commit', '-m', `chore(caws): bind spec ${specId} to worktree ${worktreeName}`, '--', relativeSpecPath],
|
|
135
|
+
{
|
|
136
|
+
cwd: root,
|
|
137
|
+
stdio: 'pipe',
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
30
143
|
function materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
|
|
31
144
|
if (!specId) return;
|
|
32
145
|
|
|
33
146
|
const canonicalSpecPath = findFeatureSpecPath(root, specId);
|
|
34
|
-
const
|
|
147
|
+
const destSpecsDir = path.join(cawsDest, 'specs');
|
|
35
148
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
149
|
+
// CAWSFIX-24 / D5: never write to .caws/working-spec.yaml inside the
|
|
150
|
+
// worktree. That file is the shared project baseline and must remain
|
|
151
|
+
// byte-identical to what was checked out from HEAD. The feature spec
|
|
152
|
+
// is materialized only under .caws/specs/<id>.yaml, which is what
|
|
153
|
+
// spec-resolver and commands actually read via --spec-id / registry.
|
|
41
154
|
|
|
42
155
|
if (canonicalSpecPath) {
|
|
43
|
-
const destSpecsDir = path.join(cawsDest, 'specs');
|
|
44
156
|
const destSpecPath = path.join(destSpecsDir, path.basename(canonicalSpecPath));
|
|
45
157
|
fs.ensureDirSync(destSpecsDir);
|
|
46
158
|
|
|
47
|
-
// Keep a canonical feature-spec copy inside the worktree
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
159
|
+
// Keep a canonical feature-spec copy inside the worktree.
|
|
160
|
+
const specContent = writeSpecWithWorktree(canonicalSpecPath, worktreeName);
|
|
161
|
+
// writeSpecWithWorktree is idempotent (CAWSFIX-24 / D10): if the spec
|
|
162
|
+
// already has the worktree field and reloads to equivalent YAML, the
|
|
163
|
+
// returned content matches what's on disk. Skip the write in that case
|
|
164
|
+
// so `git status` stays clean.
|
|
165
|
+
const existing = fs.existsSync(destSpecPath) ? fs.readFileSync(destSpecPath, 'utf8') : null;
|
|
166
|
+
if (existing !== specContent) {
|
|
167
|
+
fs.writeFileSync(destSpecPath, specContent);
|
|
168
|
+
}
|
|
52
169
|
return;
|
|
53
170
|
}
|
|
54
171
|
|
|
172
|
+
// specId given but no canonical spec found — generate a default feature
|
|
173
|
+
// spec at .caws/specs/<specId>.yaml so the worktree has something to
|
|
174
|
+
// resolve against. Do not touch .caws/working-spec.yaml.
|
|
175
|
+
console.warn(
|
|
176
|
+
chalk.yellow(`Warning: spec '${specId}' not found in .caws/specs/ — generating default feature spec for worktree`)
|
|
177
|
+
);
|
|
178
|
+
|
|
55
179
|
const { generateWorkingSpec } = require('../generators/working-spec');
|
|
56
|
-
|
|
180
|
+
let specContent = generateWorkingSpec({
|
|
57
181
|
projectId: specId,
|
|
58
182
|
projectTitle: `Worktree: ${worktreeName}`,
|
|
59
183
|
projectDescription: `Isolated worktree for ${worktreeName}`,
|
|
@@ -86,8 +210,83 @@ function materializeWorktreeSpec(root, cawsDest, specId, worktreeName, scope) {
|
|
|
86
210
|
complexityFactors: '',
|
|
87
211
|
});
|
|
88
212
|
|
|
89
|
-
|
|
90
|
-
|
|
213
|
+
try {
|
|
214
|
+
const yaml = require('js-yaml');
|
|
215
|
+
const parsed = yaml.load(specContent);
|
|
216
|
+
if (parsed && typeof parsed === 'object') {
|
|
217
|
+
parsed.worktree = worktreeName;
|
|
218
|
+
specContent = yaml.dump(parsed, { lineWidth: 120, noRefs: true });
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// Keep generated spec content if augmentation fails.
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fs.ensureDirSync(destSpecsDir);
|
|
225
|
+
const generatedSpecPath = path.join(destSpecsDir, `${specId}.yaml`);
|
|
226
|
+
fs.writeFileSync(generatedSpecPath, specContent);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseSpecIdFromYamlFile(filePath) {
|
|
230
|
+
try {
|
|
231
|
+
const yaml = require('js-yaml');
|
|
232
|
+
const doc = yaml.load(fs.readFileSync(filePath, 'utf8'));
|
|
233
|
+
if (doc && typeof doc.id === 'string' && doc.id.trim()) {
|
|
234
|
+
return doc.id.trim();
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Ignore malformed YAML during inference
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Scan .caws/specs/ for a spec that declares `worktree: <name>`.
|
|
244
|
+
* Returns the spec's id if found, null otherwise.
|
|
245
|
+
* This enables auto-binding: when a spec already names the worktree
|
|
246
|
+
* it expects, the registry entry gets the specId automatically.
|
|
247
|
+
* @param {string} root - Repository root
|
|
248
|
+
* @param {string} worktreeName - Worktree name to match
|
|
249
|
+
* @returns {string|null} Spec ID or null
|
|
250
|
+
*/
|
|
251
|
+
function findSpecByWorktreeName(root, worktreeName) {
|
|
252
|
+
const yaml = require('js-yaml');
|
|
253
|
+
const specsDir = path.join(root, '.caws', 'specs');
|
|
254
|
+
if (!fs.existsSync(specsDir)) return null;
|
|
255
|
+
|
|
256
|
+
const specFiles = fs.readdirSync(specsDir)
|
|
257
|
+
.filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'));
|
|
258
|
+
|
|
259
|
+
for (const specFile of specFiles) {
|
|
260
|
+
try {
|
|
261
|
+
const doc = yaml.load(fs.readFileSync(path.join(specsDir, specFile), 'utf8'));
|
|
262
|
+
if (doc && doc.worktree === worktreeName && typeof doc.id === 'string') {
|
|
263
|
+
return doc.id.trim();
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// Skip malformed spec files
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function inferSpecIdForWorktree(worktreePath) {
|
|
273
|
+
if (!worktreePath) return null;
|
|
274
|
+
|
|
275
|
+
const specsDir = path.join(worktreePath, '.caws', 'specs');
|
|
276
|
+
if (fs.existsSync(specsDir)) {
|
|
277
|
+
const specFiles = fs.readdirSync(specsDir)
|
|
278
|
+
.filter((name) => name.endsWith('.yaml') || name.endsWith('.yml'))
|
|
279
|
+
.sort();
|
|
280
|
+
|
|
281
|
+
for (const specFile of specFiles) {
|
|
282
|
+
const inferred = parseSpecIdFromYamlFile(path.join(specsDir, specFile));
|
|
283
|
+
if (inferred) {
|
|
284
|
+
return inferred;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return parseSpecIdFromYamlFile(path.join(worktreePath, '.caws', 'working-spec.yaml'));
|
|
91
290
|
}
|
|
92
291
|
|
|
93
292
|
/**
|
|
@@ -205,6 +404,132 @@ function getCurrentBranch() {
|
|
|
205
404
|
// floods stderr and contributes to Claude Code context-window exhaustion.
|
|
206
405
|
let _schemaWarned = false;
|
|
207
406
|
|
|
407
|
+
/**
|
|
408
|
+
* CAWSFIX-31: Assert that the current agent session may operate on
|
|
409
|
+
* worktree `name`. The decision is purely session-id-equality based —
|
|
410
|
+
* never TTL, never log freshness — because rolled-over and resumed
|
|
411
|
+
* sessions should not be auto-blocked just because their registry
|
|
412
|
+
* entry was pruned.
|
|
413
|
+
*
|
|
414
|
+
* Returns `{ allowed, warning?, priorOwner? }`. The caller decides
|
|
415
|
+
* how to react:
|
|
416
|
+
*
|
|
417
|
+
* - allowed=true, no warning → silent proceed (same session id, no claim, etc.)
|
|
418
|
+
* - allowed=true, warning present → soft notice (orphan session log, no block)
|
|
419
|
+
* - allowed=false, warning present → soft-block; surface warning, exit non-zero
|
|
420
|
+
*
|
|
421
|
+
* On takeover (`allowTakeover: true`), the function rewrites the
|
|
422
|
+
* worktree entry's owner to the current session id and appends the
|
|
423
|
+
* prior owner to a `prior_owners` audit array (with lastSeen captured
|
|
424
|
+
* from agents.json at takeover time, or null if pruned).
|
|
425
|
+
*
|
|
426
|
+
* @param {string} root - Project root
|
|
427
|
+
* @param {string} name - Worktree name
|
|
428
|
+
* @param {object} [opts]
|
|
429
|
+
* @param {boolean} [opts.allowTakeover=false] - Apply takeover when true
|
|
430
|
+
* @param {string} [opts.takeoverCommandHint] - Suggested command for the warning
|
|
431
|
+
* @returns {{ allowed: boolean, warning?: string, priorOwner?: object }}
|
|
432
|
+
*/
|
|
433
|
+
function assertWorktreeOwnership(root, name, opts = {}) {
|
|
434
|
+
const { allowTakeover = false, takeoverCommandHint } = opts;
|
|
435
|
+
const registry = loadRegistry(root);
|
|
436
|
+
const entry = registry.worktrees[name];
|
|
437
|
+
if (!entry) {
|
|
438
|
+
return { allowed: true };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const currentSession = getAgentSessionId(root);
|
|
442
|
+
const owner = entry.owner;
|
|
443
|
+
|
|
444
|
+
// No CAWS-tracked owner — surface session-log hint if present, but
|
|
445
|
+
// allow the operation to proceed.
|
|
446
|
+
if (!owner) {
|
|
447
|
+
const branch = entry.branch || null;
|
|
448
|
+
const logs = branch ? findSessionLogs(root, { branch }) : [];
|
|
449
|
+
if (logs.length > 0) {
|
|
450
|
+
return {
|
|
451
|
+
allowed: true,
|
|
452
|
+
warning: formatOrphanLogHint({ worktree: name, sessionLogs: logs, root }),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return { allowed: true };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Same session id → silent proceed. Roll-over case included: an
|
|
459
|
+
// agent that resumed with the same session id is its own claimant.
|
|
460
|
+
if (currentSession && owner === currentSession) {
|
|
461
|
+
return { allowed: true };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Foreign claim — gather context.
|
|
465
|
+
const agentRegistry = loadAgentRegistry(root);
|
|
466
|
+
const priorOwnerEntry = agentRegistry.agents[owner] || null;
|
|
467
|
+
const priorOwnerLastSeen = priorOwnerEntry ? priorOwnerEntry.lastSeen : null;
|
|
468
|
+
const priorOwnerPlatform = priorOwnerEntry ? priorOwnerEntry.platform : 'unknown';
|
|
469
|
+
|
|
470
|
+
// Surface session-log pointers (by sid OR by branch).
|
|
471
|
+
const branch = entry.branch || null;
|
|
472
|
+
const seen = new Set();
|
|
473
|
+
const sessionLogs = [];
|
|
474
|
+
for (const log of findSessionLogs(root, { sessionId: owner })) {
|
|
475
|
+
if (seen.has(log.path)) continue;
|
|
476
|
+
seen.add(log.path);
|
|
477
|
+
sessionLogs.push(log);
|
|
478
|
+
}
|
|
479
|
+
if (branch) {
|
|
480
|
+
for (const log of findSessionLogs(root, { branch })) {
|
|
481
|
+
if (seen.has(log.path)) continue;
|
|
482
|
+
seen.add(log.path);
|
|
483
|
+
sessionLogs.push(log);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const takeoverCommand =
|
|
488
|
+
takeoverCommandHint || `caws worktree claim ${name} --takeover`;
|
|
489
|
+
const warning = formatClaimNotice({
|
|
490
|
+
worktree: name,
|
|
491
|
+
priorOwnerEntry,
|
|
492
|
+
priorOwnerSessionId: owner,
|
|
493
|
+
sessionLogs,
|
|
494
|
+
root,
|
|
495
|
+
takeoverCommand,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (!allowTakeover) {
|
|
499
|
+
return { allowed: false, warning };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Takeover: rewrite owner, append prior_owners audit entry.
|
|
503
|
+
const priorOwners = Array.isArray(entry.prior_owners) ? entry.prior_owners : [];
|
|
504
|
+
priorOwners.push({
|
|
505
|
+
sessionId: owner,
|
|
506
|
+
platform: priorOwnerPlatform,
|
|
507
|
+
lastSeen: priorOwnerLastSeen,
|
|
508
|
+
takenOver_at: new Date().toISOString(),
|
|
509
|
+
});
|
|
510
|
+
registry.worktrees[name] = {
|
|
511
|
+
...entry,
|
|
512
|
+
owner: currentSession || null,
|
|
513
|
+
prior_owners: priorOwners,
|
|
514
|
+
};
|
|
515
|
+
saveRegistry(root, registry);
|
|
516
|
+
|
|
517
|
+
// Heartbeat the new owner so agents.json reflects the takeover too.
|
|
518
|
+
// Without this, `caws status` and `caws agents list` would show the
|
|
519
|
+
// takeover'd worktree with an "unknown / pruned" current owner until
|
|
520
|
+
// some other lifecycle verb fires.
|
|
521
|
+
refreshAgentClaim(root, { worktree: name });
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
allowed: true,
|
|
525
|
+
priorOwner: {
|
|
526
|
+
sessionId: owner,
|
|
527
|
+
platform: priorOwnerPlatform,
|
|
528
|
+
lastSeen: priorOwnerLastSeen,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
208
533
|
/**
|
|
209
534
|
* Load the worktree registry
|
|
210
535
|
* @param {string} root - Repository root
|
|
@@ -242,10 +567,22 @@ function loadRegistry(root) {
|
|
|
242
567
|
* @param {Object} registry - Registry object
|
|
243
568
|
*/
|
|
244
569
|
function saveRegistry(root, registry) {
|
|
245
|
-
// Auto-prune
|
|
246
|
-
//
|
|
570
|
+
// Auto-prune ghost entries: any registry entry whose path directory AND
|
|
571
|
+
// stored branch are BOTH gone. Previously this only fired for entries
|
|
572
|
+
// explicitly marked `status: destroyed`, which missed two common cases:
|
|
573
|
+
// 1. A worktree removed via `git worktree remove` (not `caws worktree
|
|
574
|
+
// destroy`) that later had its branch manually deleted with
|
|
575
|
+
// `git branch -D`.
|
|
576
|
+
// 2. A worktree whose create failed partway, leaving a registry entry
|
|
577
|
+
// at `fresh`/`active` but no artifacts on disk.
|
|
578
|
+
// Both are pure ghost state — no recoverable work remains in either
|
|
579
|
+
// the directory or the branch. Pruning is safe. (CAWSFIX-25 / D7)
|
|
580
|
+
//
|
|
581
|
+
// Entries with ONE artifact intact (dir gone but branch still present,
|
|
582
|
+
// or vice versa) are preserved. The branch may still hold unmerged
|
|
583
|
+
// commits, or the directory may still hold uncommitted work — the user
|
|
584
|
+
// should merge or explicitly destroy.
|
|
247
585
|
for (const [name, entry] of Object.entries(registry.worktrees || {})) {
|
|
248
|
-
if (entry.status !== 'destroyed') continue;
|
|
249
586
|
const dirGone = !fs.existsSync(entry.path);
|
|
250
587
|
let branchGone = true;
|
|
251
588
|
if (entry.branch) {
|
|
@@ -356,7 +693,7 @@ function autoRegisterWorktree(root, registry, discovered) {
|
|
|
356
693
|
branch: discovered.branch,
|
|
357
694
|
baseBranch,
|
|
358
695
|
scope: null,
|
|
359
|
-
specId:
|
|
696
|
+
specId: inferSpecIdForWorktree(discovered.path),
|
|
360
697
|
owner: null,
|
|
361
698
|
createdAt: new Date().toISOString(),
|
|
362
699
|
status: 'active',
|
|
@@ -424,6 +761,21 @@ function createWorktree(name, options = {}) {
|
|
|
424
761
|
const branchName = BRANCH_PREFIX + name;
|
|
425
762
|
const base = baseBranch || getCurrentBranch();
|
|
426
763
|
|
|
764
|
+
// CAWSFIX-27: resolve the bound specId (explicit --spec-id OR auto-bind
|
|
765
|
+
// via worktree-name match) BEFORE creating the worktree, so the
|
|
766
|
+
// draft→active flip + bind commit land on the base branch before the
|
|
767
|
+
// worktree forks. Pre-CAWSFIX-27 the auto-bind path activated the spec
|
|
768
|
+
// but never committed it, leaving main with a dirty spec after
|
|
769
|
+
// `caws worktree create <name>` (no --spec-id).
|
|
770
|
+
let resolvedSpecId = specId || null;
|
|
771
|
+
if (!resolvedSpecId) {
|
|
772
|
+
resolvedSpecId = findSpecByWorktreeName(root, name);
|
|
773
|
+
if (resolvedSpecId) {
|
|
774
|
+
console.log(chalk.gray(` Auto-bound spec: ${resolvedSpecId}`));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const canonicalSpecPath = findFeatureSpecPath(root, resolvedSpecId);
|
|
778
|
+
|
|
427
779
|
// Check if the branch already exists in git (even if not in registry)
|
|
428
780
|
// This catches cases where another agent created the branch outside CAWS
|
|
429
781
|
try {
|
|
@@ -449,6 +801,16 @@ function createWorktree(name, options = {}) {
|
|
|
449
801
|
// Create the worktree directory
|
|
450
802
|
fs.ensureDirSync(path.dirname(worktreePath));
|
|
451
803
|
|
|
804
|
+
if (canonicalSpecPath && resolvedSpecId) {
|
|
805
|
+
// CAWSFIX-23: flip draft→active BEFORE the bind commit so the spec
|
|
806
|
+
// lifecycle transition lands in the same commit as the worktree field.
|
|
807
|
+
// CAWSFIX-27: this block now handles BOTH the explicit --spec-id path
|
|
808
|
+
// and the auto-bind (findSpecByWorktreeName) path — previously only
|
|
809
|
+
// the explicit path committed the flip.
|
|
810
|
+
autoActivateBoundSpec(root, resolvedSpecId);
|
|
811
|
+
ensureCanonicalSpecCommitted(root, canonicalSpecPath, resolvedSpecId, name);
|
|
812
|
+
}
|
|
813
|
+
|
|
452
814
|
// Create git worktree with new branch
|
|
453
815
|
try {
|
|
454
816
|
execFileSync('git', ['worktree', 'add', '-b', branchName, worktreePath, base], {
|
|
@@ -510,15 +872,20 @@ function createWorktree(name, options = {}) {
|
|
|
510
872
|
}
|
|
511
873
|
}
|
|
512
874
|
|
|
875
|
+
// CAWSFIX-27: resolvedSpecId is now computed before the worktree is
|
|
876
|
+
// added (see block above the `fs.ensureDirSync` call). The activation
|
|
877
|
+
// and bind-commit already ran on the base branch, so the worktree forks
|
|
878
|
+
// from a base that already includes the flip commit.
|
|
879
|
+
|
|
513
880
|
// Materialize a worktree-local working spec. Prefer the canonical feature
|
|
514
881
|
// spec when it exists so isolated worktrees stay aligned with the main
|
|
515
882
|
// registry/resolver model.
|
|
516
|
-
if (
|
|
883
|
+
if (resolvedSpecId) {
|
|
517
884
|
try {
|
|
518
|
-
materializeWorktreeSpec(root, cawsDest,
|
|
885
|
+
materializeWorktreeSpec(root, cawsDest, resolvedSpecId, name, scope);
|
|
519
886
|
} catch (error) {
|
|
520
887
|
console.warn(
|
|
521
|
-
chalk.yellow(`Could not materialize spec '${
|
|
888
|
+
chalk.yellow(`Could not materialize spec '${resolvedSpecId}' for worktree '${name}': ${error.message}`)
|
|
522
889
|
);
|
|
523
890
|
// Non-fatal: spec generation is optional
|
|
524
891
|
}
|
|
@@ -531,7 +898,7 @@ function createWorktree(name, options = {}) {
|
|
|
531
898
|
branch: branchName,
|
|
532
899
|
baseBranch: base,
|
|
533
900
|
scope: scope || null,
|
|
534
|
-
specId:
|
|
901
|
+
specId: resolvedSpecId,
|
|
535
902
|
owner: options.owner || getAgentSessionId(root) || null,
|
|
536
903
|
createdAt: new Date().toISOString(),
|
|
537
904
|
status: 'fresh',
|
|
@@ -540,6 +907,11 @@ function createWorktree(name, options = {}) {
|
|
|
540
907
|
registry.worktrees[name] = entry;
|
|
541
908
|
saveRegistry(root, registry);
|
|
542
909
|
|
|
910
|
+
// CAWSFIX-32: heartbeat the current session into agents.json so the
|
|
911
|
+
// worktree+spec context is visible to other agents and to
|
|
912
|
+
// `caws status` / `caws agents list` immediately after create.
|
|
913
|
+
refreshAgentClaim(root, { worktree: name, specId: resolvedSpecId || null });
|
|
914
|
+
|
|
543
915
|
return entry;
|
|
544
916
|
}
|
|
545
917
|
|
|
@@ -892,9 +1264,31 @@ function destroyWorktree(name, options = {}) {
|
|
|
892
1264
|
}
|
|
893
1265
|
|
|
894
1266
|
// Update registry
|
|
1267
|
+
const wasAlreadyDestroyed = registry.worktrees[name].status === 'destroyed';
|
|
895
1268
|
registry.worktrees[name].status = 'destroyed';
|
|
896
1269
|
registry.worktrees[name].destroyedAt = new Date().toISOString();
|
|
897
1270
|
saveRegistry(root, registry);
|
|
1271
|
+
|
|
1272
|
+
// CAWSFIX-18: auto-commit the registry so the working tree stays clean
|
|
1273
|
+
if (!wasAlreadyDestroyed) {
|
|
1274
|
+
try {
|
|
1275
|
+
const status = execFileSync('git', ['status', '--porcelain', '.caws/worktrees.json'], {
|
|
1276
|
+
cwd: root, stdio: ['pipe', 'pipe', 'pipe'],
|
|
1277
|
+
}).toString().trim();
|
|
1278
|
+
if (status) {
|
|
1279
|
+
const otherActive = Object.values(registry.worktrees || {}).some(
|
|
1280
|
+
(e) => e.status === 'active' || e.status === 'fresh'
|
|
1281
|
+
);
|
|
1282
|
+
const prefix = otherActive ? 'wip(checkpoint)' : 'chore(worktree)';
|
|
1283
|
+
execFileSync('git', ['add', '.caws/worktrees.json'], { cwd: root, stdio: 'pipe' });
|
|
1284
|
+
execFileSync('git', ['commit', '-m', `${prefix}: record destroyed ${name}`], {
|
|
1285
|
+
cwd: root, stdio: 'pipe',
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
} catch (err) {
|
|
1289
|
+
console.warn(chalk.yellow(` Warning: could not auto-commit .caws/worktrees.json: ${err.message}`));
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
898
1292
|
}
|
|
899
1293
|
|
|
900
1294
|
/**
|
|
@@ -910,7 +1304,7 @@ function destroyWorktree(name, options = {}) {
|
|
|
910
1304
|
function mergeWorktree(name, options = {}) {
|
|
911
1305
|
const root = getRepoRoot();
|
|
912
1306
|
const registry = loadRegistry(root);
|
|
913
|
-
const { dryRun = false, deleteBranch = true, message } = options;
|
|
1307
|
+
const { dryRun = false, deleteBranch = true, message, takeover = false } = options;
|
|
914
1308
|
|
|
915
1309
|
let entry = registry.worktrees[name];
|
|
916
1310
|
if (!entry) {
|
|
@@ -925,6 +1319,26 @@ function mergeWorktree(name, options = {}) {
|
|
|
925
1319
|
}
|
|
926
1320
|
}
|
|
927
1321
|
|
|
1322
|
+
// CAWSFIX-32: assert ownership BEFORE any merge/git work. Foreign
|
|
1323
|
+
// claim soft-blocks unless --takeover is supplied, matching the
|
|
1324
|
+
// bind/claim semantics. Throws on refusal so the CLI command handler
|
|
1325
|
+
// surfaces the structured warning.
|
|
1326
|
+
const ownership = assertWorktreeOwnership(root, name, {
|
|
1327
|
+
allowTakeover: takeover,
|
|
1328
|
+
takeoverCommandHint: `caws worktree merge ${name} --takeover`,
|
|
1329
|
+
});
|
|
1330
|
+
if (!ownership.allowed) {
|
|
1331
|
+
const err = new Error(ownership.warning);
|
|
1332
|
+
err.claimWarning = true;
|
|
1333
|
+
throw err;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// CAWSFIX-32: heartbeat the current session into agents.json now that
|
|
1337
|
+
// ownership is confirmed. Same-session merges need this since the
|
|
1338
|
+
// takeover branch only fires inside assertWorktreeOwnership when a
|
|
1339
|
+
// takeover actually occurs.
|
|
1340
|
+
refreshAgentClaim(root, { worktree: name, specId: entry.specId || null });
|
|
1341
|
+
|
|
928
1342
|
const baseBranch = entry.baseBranch || 'main';
|
|
929
1343
|
|
|
930
1344
|
// Check for uncommitted work in the worktree.
|
|
@@ -1055,13 +1469,159 @@ function mergeWorktree(name, options = {}) {
|
|
|
1055
1469
|
}
|
|
1056
1470
|
}
|
|
1057
1471
|
|
|
1058
|
-
|
|
1472
|
+
// Auto-close the bound spec if one exists. A worktree merge is the
|
|
1473
|
+
// lifecycle signal that the spec's work is done; leaving the spec
|
|
1474
|
+
// `active` (or `draft`, pre-CAWSFIX-23) after merge accumulates stale
|
|
1475
|
+
// entries (D6). Direct YAML status flip bypasses the ownership +
|
|
1476
|
+
// worktree-reference checks in `closeSpec` — the caller has already
|
|
1477
|
+
// proven authority by merging.
|
|
1478
|
+
let autoClose = {
|
|
1479
|
+
specId: null, acsPassing: null, acsFailureCount: 0, acsTotal: 0, acsFailureIds: [],
|
|
1480
|
+
didWrite: false, specPath: null,
|
|
1481
|
+
};
|
|
1482
|
+
if (entry.specId) {
|
|
1483
|
+
autoClose = autoCloseBoundSpec(root, entry.specId);
|
|
1484
|
+
if (autoClose.acsPassing === false && autoClose.acsFailureCount > 0) {
|
|
1485
|
+
console.warn(chalk.yellow(
|
|
1486
|
+
` ⚠ Spec ${entry.specId} closed with ${autoClose.acsFailureCount}/${autoClose.acsTotal} failing AC(s): ${autoClose.acsFailureIds.join(', ')}`
|
|
1487
|
+
));
|
|
1488
|
+
console.warn(chalk.yellow(
|
|
1489
|
+
` Merge succeeded — the spec reflects that — but follow up to address the failing ACs.`
|
|
1490
|
+
));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// CAWSFIX-24 / D6: if the auto-close flipped the status, commit the
|
|
1494
|
+
// change on the base branch before returning. Leaving it uncommitted
|
|
1495
|
+
// was the "dirty main" footgun: the next worktree merge would abort
|
|
1496
|
+
// on "local changes would be overwritten," after the prior worktree
|
|
1497
|
+
// was already destroyed. Use --no-verify to match the merge commit's
|
|
1498
|
+
// hook-skip discipline (the content was verified when the merge ran).
|
|
1499
|
+
if (autoClose.didWrite && autoClose.specPath) {
|
|
1500
|
+
try {
|
|
1501
|
+
const relPath = path.relative(root, autoClose.specPath);
|
|
1502
|
+
execFileSync('git', ['add', '--', relPath], { cwd: root, stdio: 'pipe' });
|
|
1503
|
+
execFileSync(
|
|
1504
|
+
'git',
|
|
1505
|
+
['commit', '--no-verify', '-m', `chore(caws): close ${autoClose.specId} spec post-merge`, '--', relPath],
|
|
1506
|
+
{ cwd: root, stdio: 'pipe' }
|
|
1507
|
+
);
|
|
1508
|
+
} catch (commitErr) {
|
|
1509
|
+
// Non-fatal: a failed auto-commit leaves the spec dirty but the
|
|
1510
|
+
// merge itself already succeeded. Warn so the caller can clean up.
|
|
1511
|
+
console.warn(chalk.yellow(
|
|
1512
|
+
` ⚠ Auto-commit of ${autoClose.specId} close flip failed: ${commitErr.message}. Commit manually.`
|
|
1513
|
+
));
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const mergeResult = {
|
|
1519
|
+
name, branch: entry.branch, baseBranch, merged: true, conflicts: [],
|
|
1520
|
+
specId: entry.specId || null,
|
|
1521
|
+
autoClosedSpecId: autoClose.specId,
|
|
1522
|
+
acsPassing: autoClose.acsPassing,
|
|
1523
|
+
acsFailureCount: autoClose.acsFailureCount,
|
|
1524
|
+
acsTotal: autoClose.acsTotal,
|
|
1525
|
+
acsFailureIds: autoClose.acsFailureIds,
|
|
1526
|
+
};
|
|
1059
1527
|
try {
|
|
1060
1528
|
lifecycle.emit(EVENTS.MERGE_POST, { ...mergeResult, timestamp: new Date().toISOString() });
|
|
1061
1529
|
} catch { /* non-fatal */ }
|
|
1062
1530
|
return mergeResult;
|
|
1063
1531
|
}
|
|
1064
1532
|
|
|
1533
|
+
/**
|
|
1534
|
+
* Flip a spec's status from `draft` to `active` by rewriting just the
|
|
1535
|
+
* `status:` line. Called on worktree-bind so specs whose work is
|
|
1536
|
+
* starting transition out of draft without manual intervention.
|
|
1537
|
+
* Idempotent: no-op when the spec is already active/closed/etc.
|
|
1538
|
+
* @param {string} root - Repo root
|
|
1539
|
+
* @param {string} specId - Spec identifier
|
|
1540
|
+
* @returns {string|null} specId on flip, null if no change
|
|
1541
|
+
*/
|
|
1542
|
+
function autoActivateBoundSpec(root, specId, specPathOverride = null) {
|
|
1543
|
+
try {
|
|
1544
|
+
// CAWSFIX-25 / D8: callers that resolved a worktree-local spec path
|
|
1545
|
+
// (via findFeatureSpecPathFromCwd) pass it in so the flip lands on
|
|
1546
|
+
// the worktree's copy, not main's. Falls through to main resolution
|
|
1547
|
+
// for backward compatibility when override is null.
|
|
1548
|
+
const specPath = specPathOverride || findFeatureSpecPath(root, specId);
|
|
1549
|
+
if (!specPath || !fs.existsSync(specPath)) return null;
|
|
1550
|
+
const original = fs.readFileSync(specPath, 'utf8');
|
|
1551
|
+
// Idempotent: already active/closed/archived → no write.
|
|
1552
|
+
if (/^status:[ \t]*active[ \t]*$/m.test(original)) return specId;
|
|
1553
|
+
const patched = original.replace(/^status:[ \t]*draft[ \t]*$/m, 'status: active');
|
|
1554
|
+
if (patched === original) return null;
|
|
1555
|
+
fs.writeFileSync(specPath, patched, 'utf8');
|
|
1556
|
+
return specId;
|
|
1557
|
+
} catch {
|
|
1558
|
+
return null;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Flip a spec's status to `closed` by rewriting just the `status:` line.
|
|
1564
|
+
* Accepts both `draft` and `active` as source states — merge is the
|
|
1565
|
+
* authoritative "work done" signal regardless of whether the spec ever
|
|
1566
|
+
* transitioned through active. Runs verify-acs in collect-only mode
|
|
1567
|
+
* before the flip and returns AC health so the caller can warn.
|
|
1568
|
+
* @param {string} root - Repo root
|
|
1569
|
+
* @param {string} specId - Spec identifier (e.g. CAWSFIX-14)
|
|
1570
|
+
* @returns {{specId: string|null, acsPassing: boolean|null, acsFailureCount: number, acsTotal: number, acsFailureIds: string[]}}
|
|
1571
|
+
*/
|
|
1572
|
+
function autoCloseBoundSpec(root, specId) {
|
|
1573
|
+
const result = {
|
|
1574
|
+
specId: null,
|
|
1575
|
+
acsPassing: null,
|
|
1576
|
+
acsFailureCount: 0,
|
|
1577
|
+
acsTotal: 0,
|
|
1578
|
+
acsFailureIds: [],
|
|
1579
|
+
didWrite: false,
|
|
1580
|
+
specPath: null,
|
|
1581
|
+
};
|
|
1582
|
+
try {
|
|
1583
|
+
const specPath = findFeatureSpecPath(root, specId);
|
|
1584
|
+
if (!specPath || !fs.existsSync(specPath)) return result;
|
|
1585
|
+
result.specPath = specPath;
|
|
1586
|
+
const original = fs.readFileSync(specPath, 'utf8');
|
|
1587
|
+
// Idempotent: already closed → no-op, no write, no diff.
|
|
1588
|
+
if (/^status:[ \t]*closed[ \t]*$/m.test(original)) {
|
|
1589
|
+
result.specId = specId;
|
|
1590
|
+
return result;
|
|
1591
|
+
}
|
|
1592
|
+
// Run verify-acs in collect-only mode before flipping. Never throws —
|
|
1593
|
+
// any error (missing tests, unavailable runner, malformed spec) leaves
|
|
1594
|
+
// acsPassing: null so the caller knows verification didn't run.
|
|
1595
|
+
try {
|
|
1596
|
+
const yaml = require('js-yaml');
|
|
1597
|
+
const { verifySpec } = require('../commands/verify-acs');
|
|
1598
|
+
const parsed = yaml.load(original);
|
|
1599
|
+
if (parsed && typeof parsed === 'object') {
|
|
1600
|
+
const verdict = verifySpec(parsed, root, { run: false });
|
|
1601
|
+
const fails = (verdict.results || []).filter((r) => r.status === 'FAIL');
|
|
1602
|
+
result.acsTotal = (verdict.results || []).length;
|
|
1603
|
+
result.acsFailureCount = fails.length;
|
|
1604
|
+
result.acsFailureIds = fails.map((r) => r.id);
|
|
1605
|
+
result.acsPassing = fails.length === 0;
|
|
1606
|
+
}
|
|
1607
|
+
} catch {
|
|
1608
|
+
// verify-acs unavailable — don't block close
|
|
1609
|
+
}
|
|
1610
|
+
// Flip status. Accept both draft and active as source so specs that
|
|
1611
|
+
// never transitioned through active (D6 pre-CAWSFIX-23 drift) still close.
|
|
1612
|
+
const patched = original.replace(/^status:[ \t]*(?:draft|active)[ \t]*$/m, 'status: closed');
|
|
1613
|
+
if (patched === original) {
|
|
1614
|
+
return result; // status was archived/unknown — leave alone
|
|
1615
|
+
}
|
|
1616
|
+
fs.writeFileSync(specPath, patched, 'utf8');
|
|
1617
|
+
result.specId = specId;
|
|
1618
|
+
result.didWrite = true;
|
|
1619
|
+
return result;
|
|
1620
|
+
} catch {
|
|
1621
|
+
return result;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1065
1625
|
/**
|
|
1066
1626
|
* Prune stale worktree entries
|
|
1067
1627
|
* @param {Object} options - Prune options
|
|
@@ -1149,10 +1709,14 @@ module.exports = {
|
|
|
1149
1709
|
listWorktrees,
|
|
1150
1710
|
destroyWorktree,
|
|
1151
1711
|
mergeWorktree,
|
|
1712
|
+
autoActivateBoundSpec,
|
|
1713
|
+
autoCloseBoundSpec,
|
|
1152
1714
|
pruneWorktrees,
|
|
1153
1715
|
repairWorktrees,
|
|
1154
1716
|
reconcileRegistry,
|
|
1155
1717
|
loadRegistry,
|
|
1718
|
+
saveRegistry,
|
|
1719
|
+
assertWorktreeOwnership,
|
|
1156
1720
|
getRepoRoot,
|
|
1157
1721
|
getLastCommitInfo,
|
|
1158
1722
|
isBranchMerged,
|
|
@@ -1164,5 +1728,8 @@ module.exports = {
|
|
|
1164
1728
|
REGISTRY_FILE,
|
|
1165
1729
|
BRANCH_PREFIX,
|
|
1166
1730
|
findFeatureSpecPath,
|
|
1731
|
+
findFeatureSpecPathFromCwd,
|
|
1167
1732
|
materializeWorktreeSpec,
|
|
1733
|
+
inferSpecIdForWorktree,
|
|
1734
|
+
findSpecByWorktreeName,
|
|
1168
1735
|
};
|