@paths.design/caws-cli 11.0.0 → 11.1.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 +2 -2
- package/dist/index.js +2 -2
- package/dist/init/harness-detect.d.ts +18 -0
- package/dist/init/harness-detect.d.ts.map +1 -0
- package/dist/init/harness-detect.js +90 -0
- package/dist/init/harness-detect.js.map +1 -0
- package/dist/init/hook-install.d.ts +53 -0
- package/dist/init/hook-install.d.ts.map +1 -0
- package/dist/init/hook-install.js +421 -0
- package/dist/init/hook-install.js.map +1 -0
- package/dist/init/hook-packs/manifest-claude-code.d.ts +4 -0
- package/dist/init/hook-packs/manifest-claude-code.d.ts.map +1 -0
- package/dist/init/hook-packs/manifest-claude-code.js +190 -0
- package/dist/init/hook-packs/manifest-claude-code.js.map +1 -0
- package/dist/init/hook-packs/register.d.ts +19 -0
- package/dist/init/hook-packs/register.d.ts.map +1 -0
- package/dist/init/hook-packs/register.js +37 -0
- package/dist/init/hook-packs/register.js.map +1 -0
- package/dist/init/hook-packs/types.d.ts +123 -0
- package/dist/init/hook-packs/types.d.ts.map +1 -0
- package/dist/init/hook-packs/types.js +29 -0
- package/dist/init/hook-packs/types.js.map +1 -0
- package/dist/shell/commands/gates.d.ts.map +1 -1
- package/dist/shell/commands/gates.js +28 -1
- package/dist/shell/commands/gates.js.map +1 -1
- package/dist/shell/commands/init.d.ts +9 -0
- package/dist/shell/commands/init.d.ts.map +1 -1
- package/dist/shell/commands/init.js +131 -27
- package/dist/shell/commands/init.js.map +1 -1
- package/dist/shell/commands/specs.d.ts +41 -0
- package/dist/shell/commands/specs.d.ts.map +1 -0
- package/dist/shell/commands/specs.js +264 -0
- package/dist/shell/commands/specs.js.map +1 -0
- package/dist/shell/commands/worktree.d.ts +38 -0
- package/dist/shell/commands/worktree.d.ts.map +1 -0
- package/dist/shell/commands/worktree.js +286 -0
- package/dist/shell/commands/worktree.js.map +1 -0
- package/dist/shell/gates/disposition.d.ts.map +1 -1
- package/dist/shell/gates/disposition.js +33 -3
- package/dist/shell/gates/disposition.js.map +1 -1
- package/dist/shell/gates/local-evaluators/budget-limit.d.ts +24 -0
- package/dist/shell/gates/local-evaluators/budget-limit.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/budget-limit.js +67 -0
- package/dist/shell/gates/local-evaluators/budget-limit.js.map +1 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.d.ts +25 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.js +74 -0
- package/dist/shell/gates/local-evaluators/diff-helpers.js.map +1 -0
- package/dist/shell/gates/local-evaluators/index.d.ts +28 -0
- package/dist/shell/gates/local-evaluators/index.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/index.js +67 -0
- package/dist/shell/gates/local-evaluators/index.js.map +1 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.d.ts +23 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.js +67 -0
- package/dist/shell/gates/local-evaluators/scope-boundary.js.map +1 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.d.ts +12 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.d.ts.map +1 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.js +73 -0
- package/dist/shell/gates/local-evaluators/spec-completeness.js.map +1 -0
- package/dist/shell/index.d.ts +4 -0
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +13 -1
- package/dist/shell/index.js.map +1 -1
- package/dist/shell/register.d.ts.map +1 -1
- package/dist/shell/register.js +192 -2
- package/dist/shell/register.js.map +1 -1
- package/dist/shell/render/init-hook-pack.d.ts +16 -0
- package/dist/shell/render/init-hook-pack.d.ts.map +1 -0
- package/dist/shell/render/init-hook-pack.js +206 -0
- package/dist/shell/render/init-hook-pack.js.map +1 -0
- package/dist/store/atomic-write.d.ts +20 -2
- package/dist/store/atomic-write.d.ts.map +1 -1
- package/dist/store/atomic-write.js +44 -2
- package/dist/store/atomic-write.js.map +1 -1
- package/dist/store/lifecycle-lock.d.ts +34 -0
- package/dist/store/lifecycle-lock.d.ts.map +1 -0
- package/dist/store/lifecycle-lock.js +168 -0
- package/dist/store/lifecycle-lock.js.map +1 -0
- package/dist/store/lifecycle-transaction.d.ts +79 -0
- package/dist/store/lifecycle-transaction.d.ts.map +1 -0
- package/dist/store/lifecycle-transaction.js +319 -0
- package/dist/store/lifecycle-transaction.js.map +1 -0
- package/dist/store/rules.d.ts +16 -0
- package/dist/store/rules.d.ts.map +1 -1
- package/dist/store/rules.js +17 -0
- package/dist/store/rules.js.map +1 -1
- package/dist/store/specs-writer.d.ts +61 -0
- package/dist/store/specs-writer.d.ts.map +1 -0
- package/dist/store/specs-writer.js +506 -0
- package/dist/store/specs-writer.js.map +1 -0
- package/dist/store/worktrees-writer.d.ts +77 -0
- package/dist/store/worktrees-writer.d.ts.map +1 -0
- package/dist/store/worktrees-writer.js +674 -0
- package/dist/store/worktrees-writer.js.map +1 -0
- package/dist/store/yaml-patch.d.ts +7 -0
- package/dist/store/yaml-patch.d.ts.map +1 -0
- package/dist/store/yaml-patch.js +250 -0
- package/dist/store/yaml-patch.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Worktree lifecycle writer (CLI-WORKTREE-001).
|
|
3
|
+
//
|
|
4
|
+
// Composes:
|
|
5
|
+
// - kernel worktree functions (bindWorktree, deriveBindingState,
|
|
6
|
+
// assertOwnership) for legality/derivation
|
|
7
|
+
// - applyRegistryPatch for worktrees.json + agents.json writes
|
|
8
|
+
// - yaml-patch for spec.worktree field mutations
|
|
9
|
+
// - lifecycle-transaction for atomic multi-file writes + event append
|
|
10
|
+
// - specs-writer.closeSpec for auto-close on merge
|
|
11
|
+
//
|
|
12
|
+
// What this module owns:
|
|
13
|
+
// - createWorktree: git worktree add + registry entry + spec binding
|
|
14
|
+
// + worktree_created + worktree_bound events (two distinct facts)
|
|
15
|
+
// - bindWorktree: bidirectional binding repair (one-sided → bound)
|
|
16
|
+
// - destroyWorktree: safe destroy (refuses dirty, foreign, unmerged
|
|
17
|
+
// unless explicit non-default flag). NO --force.
|
|
18
|
+
// - mergeWorktree: dry-run + git merge --no-ff + auto-close via
|
|
19
|
+
// specs-writer + worktree_merged event + destroy
|
|
20
|
+
//
|
|
21
|
+
// What this module does NOT do:
|
|
22
|
+
// - Re-implement v10 worktree-manager.js behavior (repair, prune,
|
|
23
|
+
// reconcile, auto-register, materializeWorktreeSpec — all out).
|
|
24
|
+
// - Append events directly to events.jsonl.
|
|
25
|
+
// - Mutate worktrees.json without going through applyRegistryPatch.
|
|
26
|
+
// - Run rm -rf on any path.
|
|
27
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
30
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
31
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
32
|
+
}
|
|
33
|
+
Object.defineProperty(o, k2, desc);
|
|
34
|
+
}) : (function(o, m, k, k2) {
|
|
35
|
+
if (k2 === undefined) k2 = k;
|
|
36
|
+
o[k2] = m[k];
|
|
37
|
+
}));
|
|
38
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
39
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
40
|
+
}) : function(o, v) {
|
|
41
|
+
o["default"] = v;
|
|
42
|
+
});
|
|
43
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
44
|
+
var ownKeys = function(o) {
|
|
45
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
46
|
+
var ar = [];
|
|
47
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
48
|
+
return ar;
|
|
49
|
+
};
|
|
50
|
+
return ownKeys(o);
|
|
51
|
+
};
|
|
52
|
+
return function (mod) {
|
|
53
|
+
if (mod && mod.__esModule) return mod;
|
|
54
|
+
var result = {};
|
|
55
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
56
|
+
__setModuleDefault(result, mod);
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
})();
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports.loadSpecs = void 0;
|
|
62
|
+
exports.createWorktree = createWorktree;
|
|
63
|
+
exports.bindWorktreeRepair = bindWorktreeRepair;
|
|
64
|
+
exports.destroyWorktree = destroyWorktree;
|
|
65
|
+
exports.mergeWorktree = mergeWorktree;
|
|
66
|
+
exports.listWorktreesPretty = listWorktreesPretty;
|
|
67
|
+
const child_process_1 = require("child_process");
|
|
68
|
+
const fs = __importStar(require("fs"));
|
|
69
|
+
const path = __importStar(require("path"));
|
|
70
|
+
const caws_kernel_1 = require("@paths.design/caws-kernel");
|
|
71
|
+
const apply_patch_1 = require("./apply-patch");
|
|
72
|
+
const specs_writer_1 = require("./specs-writer");
|
|
73
|
+
const specs_store_1 = require("./specs-store");
|
|
74
|
+
Object.defineProperty(exports, "loadSpecs", { enumerable: true, get: function () { return specs_store_1.loadSpecs; } });
|
|
75
|
+
const worktrees_store_1 = require("./worktrees-store");
|
|
76
|
+
const lifecycle_transaction_1 = require("./lifecycle-transaction");
|
|
77
|
+
const lifecycle_lock_1 = require("./lifecycle-lock");
|
|
78
|
+
const repo_root_1 = require("./repo-root");
|
|
79
|
+
const rules_1 = require("./rules");
|
|
80
|
+
const yaml_patch_1 = require("./yaml-patch");
|
|
81
|
+
const yaml_store_1 = require("./yaml-store");
|
|
82
|
+
// ─── Path helpers ────────────────────────────────────────────────────────
|
|
83
|
+
function specPath(cawsDir, id) {
|
|
84
|
+
return path.join(cawsDir, 'specs', `${id}.yaml`);
|
|
85
|
+
}
|
|
86
|
+
function worktreePathFor(cawsDir, name) {
|
|
87
|
+
return path.join(cawsDir, 'worktrees', name);
|
|
88
|
+
}
|
|
89
|
+
// ─── Git helpers ─────────────────────────────────────────────────────────
|
|
90
|
+
function runGit(args, cwd) {
|
|
91
|
+
try {
|
|
92
|
+
const stdout = (0, child_process_1.execFileSync)('git', [...args], {
|
|
93
|
+
cwd,
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
96
|
+
});
|
|
97
|
+
return { ok: true, stdout: stdout.toString() };
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
const cause = e;
|
|
101
|
+
const stderr = cause.stderr instanceof Buffer
|
|
102
|
+
? cause.stderr.toString()
|
|
103
|
+
: typeof cause.stderr === 'string'
|
|
104
|
+
? cause.stderr
|
|
105
|
+
: '';
|
|
106
|
+
const message = typeof cause.message === 'string' ? cause.message : '';
|
|
107
|
+
return { ok: false, reason: stderr || message || 'unknown git error' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function repoRootFromCawsDir(cawsDir) {
|
|
111
|
+
return path.dirname(cawsDir);
|
|
112
|
+
}
|
|
113
|
+
function getCurrentBranch(repoRoot) {
|
|
114
|
+
const r = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot);
|
|
115
|
+
if (!r.ok)
|
|
116
|
+
return null;
|
|
117
|
+
return r.stdout.trim();
|
|
118
|
+
}
|
|
119
|
+
function isWorkingTreeClean(worktreePath) {
|
|
120
|
+
const r = runGit(['status', '--porcelain'], worktreePath);
|
|
121
|
+
if (!r.ok)
|
|
122
|
+
return false;
|
|
123
|
+
return r.stdout.trim().length === 0;
|
|
124
|
+
}
|
|
125
|
+
function isBranchMerged(repoRoot, branch, base) {
|
|
126
|
+
const r = runGit(['merge-base', '--is-ancestor', branch, base], repoRoot);
|
|
127
|
+
// Git exits 0 when branch is ancestor of base (i.e., branch is fully merged).
|
|
128
|
+
return r.ok;
|
|
129
|
+
}
|
|
130
|
+
// ─── ID + name validation ────────────────────────────────────────────────
|
|
131
|
+
const WORKTREE_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
|
|
132
|
+
const SPEC_ID_PATTERN = /^[A-Z][A-Z0-9]*(-[A-Z0-9]+)*-\d+[a-z]*$/;
|
|
133
|
+
function validateWorktreeName(name) {
|
|
134
|
+
if (!WORKTREE_NAME_REGEX.test(name)) {
|
|
135
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree name "${name}" does not match the v11 pattern (alphanumeric, hyphen, underscore).`, { subject: name }));
|
|
136
|
+
}
|
|
137
|
+
return (0, caws_kernel_1.ok)(true);
|
|
138
|
+
}
|
|
139
|
+
function validateSpecId(id) {
|
|
140
|
+
if (!SPEC_ID_PATTERN.test(id)) {
|
|
141
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Spec id "${id}" does not match the v11 pattern.`, { subject: id }));
|
|
142
|
+
}
|
|
143
|
+
return (0, caws_kernel_1.ok)(true);
|
|
144
|
+
}
|
|
145
|
+
// ─── Spec lookup with strict active-only enforcement ─────────────────────
|
|
146
|
+
function loadSpecOrError(cawsDir, specId) {
|
|
147
|
+
const p = specPath(cawsDir, specId);
|
|
148
|
+
if (!fs.existsSync(p)) {
|
|
149
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Spec "${specId}" not found at ${p}.`, { subject: specId }));
|
|
150
|
+
}
|
|
151
|
+
const srcResult = (0, yaml_store_1.readYamlSource)(p);
|
|
152
|
+
if (!(0, caws_kernel_1.isOk)(srcResult))
|
|
153
|
+
return (0, caws_kernel_1.err)(srcResult.errors);
|
|
154
|
+
const parsed = (0, caws_kernel_1.parseAndValidateSpec)(srcResult.value);
|
|
155
|
+
if (!(0, caws_kernel_1.isOk)(parsed)) {
|
|
156
|
+
return (0, caws_kernel_1.err)(parsed.errors.map((d) => (0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, d.message, {
|
|
157
|
+
subject: d.subject ?? specId,
|
|
158
|
+
data: { source_rule: d.rule },
|
|
159
|
+
})));
|
|
160
|
+
}
|
|
161
|
+
const spec = parsed.value;
|
|
162
|
+
return (0, caws_kernel_1.ok)({
|
|
163
|
+
source: srcResult.value,
|
|
164
|
+
path: p,
|
|
165
|
+
spec: parsed.value,
|
|
166
|
+
lifecycleState: spec.lifecycle_state,
|
|
167
|
+
currentWorktree: spec.worktree,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// ─── Spec YAML mutation for worktree binding ─────────────────────────────
|
|
171
|
+
/** Set `worktree: <name>` on a spec via raw-byte patching. Inserts the
|
|
172
|
+
* field after `lifecycle_state` if absent. Returns patched bytes. */
|
|
173
|
+
function patchSpecSetWorktree(source, worktreeName) {
|
|
174
|
+
const hasField = /^worktree:/m.test(source);
|
|
175
|
+
if (hasField) {
|
|
176
|
+
return (0, yaml_patch_1.setTopLevelScalar)(source, 'worktree', worktreeName);
|
|
177
|
+
}
|
|
178
|
+
return (0, yaml_patch_1.insertTopLevelScalarAfter)(source, 'lifecycle_state', 'worktree', worktreeName);
|
|
179
|
+
}
|
|
180
|
+
/** Remove `worktree:` from a spec (sets to empty string via patch and
|
|
181
|
+
* trims). For destroy. */
|
|
182
|
+
function patchSpecClearWorktree(source) {
|
|
183
|
+
const hasField = /^worktree:/m.test(source);
|
|
184
|
+
if (!hasField)
|
|
185
|
+
return (0, caws_kernel_1.ok)(source);
|
|
186
|
+
// Replace with empty value to keep the surface minimal; future
|
|
187
|
+
// doctor logic may treat empty as "unset" or we may insert a
|
|
188
|
+
// remove operation later. For now, set to '' which the kernel
|
|
189
|
+
// tolerates as no binding.
|
|
190
|
+
return (0, yaml_patch_1.setTopLevelScalar)(source, 'worktree', "''");
|
|
191
|
+
}
|
|
192
|
+
// ─── createWorktree ──────────────────────────────────────────────────────
|
|
193
|
+
function createWorktree(cawsDir, input) {
|
|
194
|
+
// ─ Pre-flight validation (no git, no file writes) ─
|
|
195
|
+
const nameValidation = validateWorktreeName(input.name);
|
|
196
|
+
if (!nameValidation.ok)
|
|
197
|
+
return nameValidation;
|
|
198
|
+
const specValidation = validateSpecId(input.specId);
|
|
199
|
+
if (!specValidation.ok)
|
|
200
|
+
return specValidation;
|
|
201
|
+
const specInfo = loadSpecOrError(cawsDir, input.specId);
|
|
202
|
+
if (!(0, caws_kernel_1.isOk)(specInfo))
|
|
203
|
+
return (0, caws_kernel_1.err)(specInfo.errors);
|
|
204
|
+
if (specInfo.value.lifecycleState !== 'active') {
|
|
205
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Spec "${input.specId}" is in lifecycle_state "${specInfo.value.lifecycleState}"; only active specs can be bound to a new worktree.`, { subject: input.specId }));
|
|
206
|
+
}
|
|
207
|
+
if (specInfo.value.currentWorktree !== undefined &&
|
|
208
|
+
specInfo.value.currentWorktree.length > 0 &&
|
|
209
|
+
specInfo.value.currentWorktree !== input.name) {
|
|
210
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Spec "${input.specId}" is already bound to worktree "${specInfo.value.currentWorktree}".`, { subject: input.specId }));
|
|
211
|
+
}
|
|
212
|
+
// Refuse if a worktree with this name already exists in the registry.
|
|
213
|
+
const registry = (0, worktrees_store_1.loadWorktrees)(cawsDir);
|
|
214
|
+
if (!(0, caws_kernel_1.isOk)(registry))
|
|
215
|
+
return (0, caws_kernel_1.err)(registry.errors);
|
|
216
|
+
if (registry.value[input.name] !== undefined) {
|
|
217
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree "${input.name}" already exists in registry.`, { subject: input.name }));
|
|
218
|
+
}
|
|
219
|
+
const repoRoot = repoRootFromCawsDir(cawsDir);
|
|
220
|
+
const baseBranch = input.baseBranch ?? getCurrentBranch(repoRoot);
|
|
221
|
+
if (baseBranch === null) {
|
|
222
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Could not determine base branch for new worktree.`, { subject: input.name }));
|
|
223
|
+
}
|
|
224
|
+
const branch = input.branch ?? input.name;
|
|
225
|
+
const wtPath = worktreePathFor(cawsDir, input.name);
|
|
226
|
+
// ─ Git operation: outside lifecycle-transaction ─
|
|
227
|
+
const gitResult = runGit(['worktree', 'add', '-b', branch, wtPath, baseBranch], repoRoot);
|
|
228
|
+
if (!gitResult.ok) {
|
|
229
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `git worktree add failed: ${gitResult.reason}`, { subject: input.name, data: { git_stderr: gitResult.reason } }));
|
|
230
|
+
}
|
|
231
|
+
// ─ Lifecycle transaction: spec.worktree patch + worktrees.json patch
|
|
232
|
+
// + two events. If anything fails, run git worktree remove as
|
|
233
|
+
// compensation. ─
|
|
234
|
+
const now = (input.now ?? (() => new Date()))().toISOString();
|
|
235
|
+
const newSpecBytes = patchSpecSetWorktree(specInfo.value.source, input.name);
|
|
236
|
+
if (!(0, caws_kernel_1.isOk)(newSpecBytes)) {
|
|
237
|
+
rollbackGitWorktree(repoRoot, wtPath);
|
|
238
|
+
return (0, caws_kernel_1.err)(newSpecBytes.errors);
|
|
239
|
+
}
|
|
240
|
+
// Build the worktree_created event (no spec_id — binding is a
|
|
241
|
+
// separate fact emitted next).
|
|
242
|
+
const createdEvent = {
|
|
243
|
+
event: 'worktree_created',
|
|
244
|
+
ts: now,
|
|
245
|
+
actor: input.actor,
|
|
246
|
+
data: {
|
|
247
|
+
name: input.name,
|
|
248
|
+
branch,
|
|
249
|
+
base_branch: baseBranch,
|
|
250
|
+
path: wtPath,
|
|
251
|
+
owner_session_id: input.session.session_id,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const boundEvent = {
|
|
255
|
+
event: 'worktree_bound',
|
|
256
|
+
ts: now,
|
|
257
|
+
actor: input.actor,
|
|
258
|
+
spec_id: input.specId,
|
|
259
|
+
data: {
|
|
260
|
+
worktree_name: input.name,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
const txnOutcome = (0, lifecycle_lock_1.withLifecycleLock)(cawsDir, () => {
|
|
264
|
+
// Use kernel bindWorktree with the actual parsed Spec so it can
|
|
265
|
+
// verify lifecycle_state etc.
|
|
266
|
+
const bindResult = (0, caws_kernel_1.bindWorktree)(specInfo.value.spec, registry.value, input.name, input.session, { rebind: false }, new Date(now));
|
|
267
|
+
if (!(0, caws_kernel_1.isOk)(bindResult))
|
|
268
|
+
return (0, caws_kernel_1.err)(bindResult.errors);
|
|
269
|
+
// Apply the bind_worktree patch (writes worktrees.json with the
|
|
270
|
+
// kernel-modeled fields: specId, owner, last_heartbeat).
|
|
271
|
+
const applyResult = (0, apply_patch_1.applyRegistryPatch)(cawsDir, bindResult.value);
|
|
272
|
+
if (!(0, caws_kernel_1.isOk)(applyResult))
|
|
273
|
+
return (0, caws_kernel_1.err)(applyResult.errors);
|
|
274
|
+
// Augment the entry with descriptive metadata the kernel does NOT
|
|
275
|
+
// model (branch, baseBranch, path). These are governance metadata
|
|
276
|
+
// for merge/destroy decisions, not authority claims.
|
|
277
|
+
augmentRegistryEntry(cawsDir, input.name, { branch, baseBranch, path: wtPath });
|
|
278
|
+
// Then run the lifecycle transaction for spec YAML + events.
|
|
279
|
+
return (0, lifecycle_transaction_1.runLifecycleTransaction)({
|
|
280
|
+
cawsDir,
|
|
281
|
+
plannedWrites: [{ path: specInfo.value.path, contents: newSpecBytes.value }],
|
|
282
|
+
events: [createdEvent, boundEvent],
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
if (!txnOutcome.ok) {
|
|
286
|
+
// Compensation: remove the git worktree we created.
|
|
287
|
+
rollbackGitWorktree(repoRoot, wtPath);
|
|
288
|
+
// Also remove the registry entry that bind_worktree wrote.
|
|
289
|
+
rollbackRegistryEntry(cawsDir, input.name);
|
|
290
|
+
return (0, caws_kernel_1.err)(txnOutcome.errors);
|
|
291
|
+
}
|
|
292
|
+
if (txnOutcome.value.kind !== 'success') {
|
|
293
|
+
rollbackGitWorktree(repoRoot, wtPath);
|
|
294
|
+
rollbackRegistryEntry(cawsDir, input.name);
|
|
295
|
+
return (0, caws_kernel_1.ok)({
|
|
296
|
+
kind: 'partial_failure_recovered',
|
|
297
|
+
cause: txnOutcome.value.cause,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return (0, caws_kernel_1.ok)({
|
|
301
|
+
kind: 'success',
|
|
302
|
+
name: input.name,
|
|
303
|
+
action: 'created',
|
|
304
|
+
data: { branch, base_branch: baseBranch, path: wtPath, spec_id: input.specId },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function rollbackGitWorktree(repoRoot, wtPath) {
|
|
308
|
+
// Best-effort. We're already in an error path.
|
|
309
|
+
runGit(['worktree', 'remove', '--force', wtPath], repoRoot);
|
|
310
|
+
}
|
|
311
|
+
function rollbackRegistryEntry(cawsDir, name) {
|
|
312
|
+
// Direct file mutation for rollback — applyRegistryPatch has no
|
|
313
|
+
// "remove entry" mode. This is best-effort recovery.
|
|
314
|
+
const p = path.join(cawsDir, 'worktrees.json');
|
|
315
|
+
try {
|
|
316
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
317
|
+
const obj = JSON.parse(raw);
|
|
318
|
+
if (obj && typeof obj === 'object' && obj[name] !== undefined) {
|
|
319
|
+
delete obj[name];
|
|
320
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* best-effort */
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Augment a registry entry with descriptive metadata the kernel
|
|
328
|
+
* doesn't model (branch, baseBranch, path). These fields are used by
|
|
329
|
+
* merge/destroy for prerequisite checks but are not authority claims.
|
|
330
|
+
* applyRegistryPatch only touches the kernel-modeled fields, so we
|
|
331
|
+
* layer in the rest via a direct merge. Best-effort — read failure
|
|
332
|
+
* is logged but doesn't fail the caller. */
|
|
333
|
+
function augmentRegistryEntry(cawsDir, name, extra) {
|
|
334
|
+
const p = path.join(cawsDir, 'worktrees.json');
|
|
335
|
+
try {
|
|
336
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
337
|
+
const obj = JSON.parse(raw);
|
|
338
|
+
if (!obj || typeof obj !== 'object')
|
|
339
|
+
return;
|
|
340
|
+
const entry = obj[name];
|
|
341
|
+
if (!entry || typeof entry !== 'object')
|
|
342
|
+
return;
|
|
343
|
+
obj[name] = { ...entry, ...extra };
|
|
344
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
/* best-effort */
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ─── bindWorktree (repair) ───────────────────────────────────────────────
|
|
351
|
+
function bindWorktreeRepair(cawsDir, input) {
|
|
352
|
+
const nameValidation = validateWorktreeName(input.name);
|
|
353
|
+
if (!nameValidation.ok)
|
|
354
|
+
return nameValidation;
|
|
355
|
+
const specValidation = validateSpecId(input.specId);
|
|
356
|
+
if (!specValidation.ok)
|
|
357
|
+
return specValidation;
|
|
358
|
+
const specInfo = loadSpecOrError(cawsDir, input.specId);
|
|
359
|
+
if (!(0, caws_kernel_1.isOk)(specInfo))
|
|
360
|
+
return (0, caws_kernel_1.err)(specInfo.errors);
|
|
361
|
+
const registry = (0, worktrees_store_1.loadWorktrees)(cawsDir);
|
|
362
|
+
if (!(0, caws_kernel_1.isOk)(registry))
|
|
363
|
+
return (0, caws_kernel_1.err)(registry.errors);
|
|
364
|
+
const existingEntry = registry.value[input.name];
|
|
365
|
+
if (existingEntry === undefined) {
|
|
366
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree "${input.name}" has no registry entry. Use caws worktree create to create a new worktree.`, { subject: input.name }));
|
|
367
|
+
}
|
|
368
|
+
// Patch the spec YAML to set worktree: <name>.
|
|
369
|
+
const newSpecBytes = patchSpecSetWorktree(specInfo.value.source, input.name);
|
|
370
|
+
if (!(0, caws_kernel_1.isOk)(newSpecBytes))
|
|
371
|
+
return (0, caws_kernel_1.err)(newSpecBytes.errors);
|
|
372
|
+
// Apply registry patch to set specId on the entry. We use the
|
|
373
|
+
// kernel bindWorktree to get the right patch shape.
|
|
374
|
+
const now = (input.now ?? (() => new Date()))().toISOString();
|
|
375
|
+
const txnOutcome = (0, lifecycle_lock_1.withLifecycleLock)(cawsDir, () => {
|
|
376
|
+
const bindResult = (0, caws_kernel_1.bindWorktree)(specInfo.value.spec, registry.value, input.name, input.session, { rebind: existingEntry.specId !== undefined && existingEntry.specId !== input.specId }, new Date(now));
|
|
377
|
+
if (!(0, caws_kernel_1.isOk)(bindResult))
|
|
378
|
+
return (0, caws_kernel_1.err)(bindResult.errors);
|
|
379
|
+
const applyResult = (0, apply_patch_1.applyRegistryPatch)(cawsDir, bindResult.value);
|
|
380
|
+
if (!(0, caws_kernel_1.isOk)(applyResult))
|
|
381
|
+
return (0, caws_kernel_1.err)(applyResult.errors);
|
|
382
|
+
const eventData = { worktree_name: input.name };
|
|
383
|
+
if (existingEntry.specId !== undefined &&
|
|
384
|
+
existingEntry.specId !== input.specId) {
|
|
385
|
+
eventData.previously_bound_to = existingEntry.specId;
|
|
386
|
+
}
|
|
387
|
+
const event = {
|
|
388
|
+
event: 'worktree_bound',
|
|
389
|
+
ts: now,
|
|
390
|
+
actor: input.actor,
|
|
391
|
+
spec_id: input.specId,
|
|
392
|
+
data: eventData,
|
|
393
|
+
};
|
|
394
|
+
return (0, lifecycle_transaction_1.runLifecycleTransaction)({
|
|
395
|
+
cawsDir,
|
|
396
|
+
plannedWrites: [{ path: specInfo.value.path, contents: newSpecBytes.value }],
|
|
397
|
+
events: [event],
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
if (!txnOutcome.ok)
|
|
401
|
+
return (0, caws_kernel_1.err)(txnOutcome.errors);
|
|
402
|
+
if (txnOutcome.value.kind !== 'success') {
|
|
403
|
+
return (0, caws_kernel_1.ok)({ kind: 'partial_failure_recovered', cause: txnOutcome.value.cause });
|
|
404
|
+
}
|
|
405
|
+
return (0, caws_kernel_1.ok)({ kind: 'success', name: input.name, action: 'bound' });
|
|
406
|
+
}
|
|
407
|
+
// ─── destroyWorktree ─────────────────────────────────────────────────────
|
|
408
|
+
function destroyWorktree(cawsDir, input) {
|
|
409
|
+
const nameValidation = validateWorktreeName(input.name);
|
|
410
|
+
if (!nameValidation.ok)
|
|
411
|
+
return nameValidation;
|
|
412
|
+
const registry = (0, worktrees_store_1.loadWorktrees)(cawsDir);
|
|
413
|
+
if (!(0, caws_kernel_1.isOk)(registry))
|
|
414
|
+
return (0, caws_kernel_1.err)(registry.errors);
|
|
415
|
+
const entry = registry.value[input.name];
|
|
416
|
+
if (entry === undefined) {
|
|
417
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree "${input.name}" not found in registry.`, { subject: input.name }));
|
|
418
|
+
}
|
|
419
|
+
// Ownership check: refuse foreign session unless takeover already
|
|
420
|
+
// happened in a separate step (caws claim --takeover writes a
|
|
421
|
+
// prior_owners audit; ownership then matches and we proceed).
|
|
422
|
+
if (entry.owner !== undefined &&
|
|
423
|
+
entry.owner.session_id !== input.session.session_id) {
|
|
424
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree "${input.name}" is owned by a different session (${entry.owner.session_id}). Run 'caws claim ${input.name} --takeover' first if you need to take ownership.`, { subject: input.name }));
|
|
425
|
+
}
|
|
426
|
+
// Dirty-tree check.
|
|
427
|
+
const wtPath = entry.path ?? worktreePathFor(cawsDir, input.name);
|
|
428
|
+
if (fs.existsSync(wtPath) && !isWorkingTreeClean(wtPath)) {
|
|
429
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree "${input.name}" has uncommitted changes. Commit or stash before destroying.`, { subject: input.name }));
|
|
430
|
+
}
|
|
431
|
+
// Unmerged-branch check (skipped when --abandon-unmerged is passed).
|
|
432
|
+
const repoRoot = repoRootFromCawsDir(cawsDir);
|
|
433
|
+
if (entry.branch !== undefined &&
|
|
434
|
+
entry.baseBranch !== undefined &&
|
|
435
|
+
input.abandonUnmerged !== true &&
|
|
436
|
+
!isBranchMerged(repoRoot, entry.branch, entry.baseBranch)) {
|
|
437
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Branch "${entry.branch}" is not merged into "${entry.baseBranch}". Pass --abandon-unmerged to destroy anyway.`, { subject: input.name }));
|
|
438
|
+
}
|
|
439
|
+
// Run git worktree remove. Never rm -rf.
|
|
440
|
+
let removedGitWorktree = false;
|
|
441
|
+
if (fs.existsSync(wtPath)) {
|
|
442
|
+
const removeResult = runGit(['worktree', 'remove', wtPath], repoRoot);
|
|
443
|
+
if (!removeResult.ok) {
|
|
444
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_WRITE_FAILED, `git worktree remove failed: ${removeResult.reason}`, { subject: input.name }));
|
|
445
|
+
}
|
|
446
|
+
removedGitWorktree = true;
|
|
447
|
+
}
|
|
448
|
+
// Clear spec.worktree field if a spec was bound.
|
|
449
|
+
const now = (input.now ?? (() => new Date()))().toISOString();
|
|
450
|
+
const plannedWrites = [];
|
|
451
|
+
if (entry.specId !== undefined) {
|
|
452
|
+
const specInfo = loadSpecOrError(cawsDir, entry.specId);
|
|
453
|
+
if ((0, caws_kernel_1.isOk)(specInfo)) {
|
|
454
|
+
const newSpecBytes = patchSpecClearWorktree(specInfo.value.source);
|
|
455
|
+
if ((0, caws_kernel_1.isOk)(newSpecBytes) && newSpecBytes.value !== specInfo.value.source) {
|
|
456
|
+
plannedWrites.push({
|
|
457
|
+
path: specInfo.value.path,
|
|
458
|
+
contents: newSpecBytes.value,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const eventData = {
|
|
464
|
+
worktree_name: input.name,
|
|
465
|
+
branch: entry.branch ?? 'unknown',
|
|
466
|
+
path: wtPath,
|
|
467
|
+
removed_git_worktree: removedGitWorktree,
|
|
468
|
+
};
|
|
469
|
+
if (entry.specId !== undefined)
|
|
470
|
+
eventData.spec_id = entry.specId;
|
|
471
|
+
if (entry.owner !== undefined)
|
|
472
|
+
eventData.owner_session_id = entry.owner.session_id;
|
|
473
|
+
const event = {
|
|
474
|
+
event: 'worktree_destroyed',
|
|
475
|
+
ts: now,
|
|
476
|
+
actor: input.actor,
|
|
477
|
+
data: eventData,
|
|
478
|
+
};
|
|
479
|
+
const txnOutcome = (0, lifecycle_lock_1.withLifecycleLock)(cawsDir, () => {
|
|
480
|
+
// Remove the registry entry first.
|
|
481
|
+
rollbackRegistryEntry(cawsDir, input.name); // misnomer — also used here as the canonical remover
|
|
482
|
+
return (0, lifecycle_transaction_1.runLifecycleTransaction)({
|
|
483
|
+
cawsDir,
|
|
484
|
+
plannedWrites,
|
|
485
|
+
events: [event],
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
if (!txnOutcome.ok)
|
|
489
|
+
return (0, caws_kernel_1.err)(txnOutcome.errors);
|
|
490
|
+
if (txnOutcome.value.kind !== 'success') {
|
|
491
|
+
return (0, caws_kernel_1.ok)({ kind: 'partial_failure_recovered', cause: txnOutcome.value.cause });
|
|
492
|
+
}
|
|
493
|
+
return (0, caws_kernel_1.ok)({
|
|
494
|
+
kind: 'success',
|
|
495
|
+
name: input.name,
|
|
496
|
+
action: 'destroyed',
|
|
497
|
+
data: { removed_git_worktree: removedGitWorktree },
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// ─── mergeWorktree ───────────────────────────────────────────────────────
|
|
501
|
+
function mergeWorktree(cawsDir, input) {
|
|
502
|
+
const nameValidation = validateWorktreeName(input.name);
|
|
503
|
+
if (!nameValidation.ok)
|
|
504
|
+
return nameValidation;
|
|
505
|
+
const registry = (0, worktrees_store_1.loadWorktrees)(cawsDir);
|
|
506
|
+
if (!(0, caws_kernel_1.isOk)(registry))
|
|
507
|
+
return (0, caws_kernel_1.err)(registry.errors);
|
|
508
|
+
const entry = registry.value[input.name];
|
|
509
|
+
if (entry === undefined) {
|
|
510
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `Worktree "${input.name}" not found in registry.`, { subject: input.name }));
|
|
511
|
+
}
|
|
512
|
+
// Validate prerequisites.
|
|
513
|
+
const findings = [];
|
|
514
|
+
if (entry.owner !== undefined && entry.owner.session_id !== input.session.session_id) {
|
|
515
|
+
findings.push(`worktree is owned by a different session (${entry.owner.session_id})`);
|
|
516
|
+
}
|
|
517
|
+
const wtPath = entry.path ?? worktreePathFor(cawsDir, input.name);
|
|
518
|
+
if (fs.existsSync(wtPath) && !isWorkingTreeClean(wtPath)) {
|
|
519
|
+
findings.push('worktree has uncommitted changes');
|
|
520
|
+
}
|
|
521
|
+
if (entry.specId === undefined) {
|
|
522
|
+
findings.push('no spec_id binding on this worktree');
|
|
523
|
+
}
|
|
524
|
+
if (entry.branch === undefined || entry.baseBranch === undefined) {
|
|
525
|
+
findings.push('missing branch or base_branch on registry entry');
|
|
526
|
+
}
|
|
527
|
+
// Dry-run: report and return without mutation.
|
|
528
|
+
if (input.dryRun === true) {
|
|
529
|
+
return (0, caws_kernel_1.ok)({
|
|
530
|
+
kind: 'dry_run',
|
|
531
|
+
name: input.name,
|
|
532
|
+
canProceed: findings.length === 0,
|
|
533
|
+
findings,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (findings.length > 0) {
|
|
537
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PLAN_REJECTED, `caws worktree merge ${input.name}: prerequisites unmet (${findings.join('; ')}).`, { subject: input.name, data: { findings } }));
|
|
538
|
+
}
|
|
539
|
+
// Perform the merge: git checkout base + git merge --no-ff.
|
|
540
|
+
const repoRoot = repoRootFromCawsDir(cawsDir);
|
|
541
|
+
const baseBranch = entry.baseBranch;
|
|
542
|
+
const branch = entry.branch;
|
|
543
|
+
const specId = entry.specId;
|
|
544
|
+
const checkoutResult = runGit(['checkout', baseBranch], repoRoot);
|
|
545
|
+
if (!checkoutResult.ok) {
|
|
546
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_WRITE_FAILED, `git checkout ${baseBranch} failed: ${checkoutResult.reason}`, { subject: input.name }));
|
|
547
|
+
}
|
|
548
|
+
const message = input.message ?? `merge(worktree): ${input.name}`;
|
|
549
|
+
const mergeResult = runGit(['merge', '--no-ff', '-m', message, branch], repoRoot);
|
|
550
|
+
if (!mergeResult.ok) {
|
|
551
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_WRITE_FAILED, `git merge --no-ff ${branch} failed: ${mergeResult.reason}`, { subject: input.name }));
|
|
552
|
+
}
|
|
553
|
+
// Obtain the merge commit SHA.
|
|
554
|
+
const shaResult = runGit(['rev-parse', 'HEAD'], repoRoot);
|
|
555
|
+
if (!shaResult.ok) {
|
|
556
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_WRITE_FAILED, `git rev-parse HEAD failed: ${shaResult.reason}`, { subject: input.name }));
|
|
557
|
+
}
|
|
558
|
+
const mergeCommit = shaResult.stdout.trim();
|
|
559
|
+
if (!/^[0-9a-f]{7,40}$/.test(mergeCommit)) {
|
|
560
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_WRITE_FAILED, `Unexpected merge commit shape from git: ${mergeCommit}`, { subject: input.name }));
|
|
561
|
+
}
|
|
562
|
+
// Auto-close the bound spec through the canonical specs-writer
|
|
563
|
+
// path. This appends spec_closed. We then append worktree_merged
|
|
564
|
+
// with auto_closed_spec: true.
|
|
565
|
+
//
|
|
566
|
+
// `mergeNow` is captured once and reused for every sub-operation
|
|
567
|
+
// (close, worktree_merged append, destroy). Composed merge is one
|
|
568
|
+
// governance moment; emitted events must share that baseline so
|
|
569
|
+
// ts order matches seq order in the chain. Without this, sub-calls
|
|
570
|
+
// re-read the wall clock at append time and can produce timestamps
|
|
571
|
+
// that disagree with seq (seq remains the causal authority, but
|
|
572
|
+
// human-readable timestamps should not contradict it).
|
|
573
|
+
const mergeNow = new Date((input.now ?? (() => new Date()))().getTime());
|
|
574
|
+
const now = mergeNow.toISOString();
|
|
575
|
+
const sharedNowFactory = () => mergeNow;
|
|
576
|
+
const closeResult = (0, specs_writer_1.closeSpec)(cawsDir, {
|
|
577
|
+
id: specId,
|
|
578
|
+
resolution: 'completed',
|
|
579
|
+
reason: `Auto-closed by caws worktree merge ${input.name} at ${mergeCommit}`,
|
|
580
|
+
mergeCommit,
|
|
581
|
+
actor: input.actor,
|
|
582
|
+
now: sharedNowFactory,
|
|
583
|
+
});
|
|
584
|
+
if (!(0, caws_kernel_1.isOk)(closeResult)) {
|
|
585
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PARTIAL_FAILURE_UNRECOVERED, `Merge succeeded (commit ${mergeCommit}) but spec close failed. The bound spec remains active.`, {
|
|
586
|
+
subject: input.name,
|
|
587
|
+
data: {
|
|
588
|
+
merge_commit: mergeCommit,
|
|
589
|
+
spec_id: specId,
|
|
590
|
+
close_errors: closeResult.errors.map((d) => d.message),
|
|
591
|
+
recovery_instruction: `Manually run: caws specs close ${specId} --resolution completed --merge-commit ${mergeCommit}`,
|
|
592
|
+
},
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
// Append worktree_merged AFTER spec_closed so the chain reflects
|
|
596
|
+
// the actual order of state transitions.
|
|
597
|
+
const mergedEvent = {
|
|
598
|
+
event: 'worktree_merged',
|
|
599
|
+
ts: now,
|
|
600
|
+
actor: input.actor,
|
|
601
|
+
spec_id: specId,
|
|
602
|
+
data: {
|
|
603
|
+
worktree_name: input.name,
|
|
604
|
+
merge_commit: mergeCommit,
|
|
605
|
+
base_branch: baseBranch,
|
|
606
|
+
auto_closed_spec: true,
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
// The worktree_merged event is appended via runLifecycleTransaction
|
|
610
|
+
// even though we have no file writes for this step; the substrate's
|
|
611
|
+
// append path is the only sanctioned writer for events.jsonl.
|
|
612
|
+
const mergedTxn = (0, lifecycle_lock_1.withLifecycleLock)(cawsDir, () => (0, lifecycle_transaction_1.runLifecycleTransaction)({
|
|
613
|
+
cawsDir,
|
|
614
|
+
plannedWrites: [],
|
|
615
|
+
events: [mergedEvent],
|
|
616
|
+
}));
|
|
617
|
+
if (!mergedTxn.ok) {
|
|
618
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PARTIAL_FAILURE_UNRECOVERED, `Merge succeeded and spec_closed event appended, but worktree_merged event append failed. The worktree was not destroyed.`, {
|
|
619
|
+
subject: input.name,
|
|
620
|
+
data: {
|
|
621
|
+
merge_commit: mergeCommit,
|
|
622
|
+
recovery_instruction: `Manually destroy the worktree: caws worktree destroy ${input.name}`,
|
|
623
|
+
},
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
// Destroy the worktree last. Reuse the same merge-baseline clock
|
|
627
|
+
// so worktree_destroyed.ts matches the rest of the composed merge.
|
|
628
|
+
const destroyResult = destroyWorktree(cawsDir, {
|
|
629
|
+
name: input.name,
|
|
630
|
+
session: input.session,
|
|
631
|
+
actor: input.actor,
|
|
632
|
+
now: sharedNowFactory,
|
|
633
|
+
});
|
|
634
|
+
if (!(0, caws_kernel_1.isOk)(destroyResult)) {
|
|
635
|
+
// The merge + close + merged event all succeeded. The destroy
|
|
636
|
+
// failed. Surface as partial-failure with a manual recovery hint.
|
|
637
|
+
return (0, caws_kernel_1.err)((0, repo_root_1.storeDiagnostic)(rules_1.STORE_RULES.LIFECYCLE_PARTIAL_FAILURE_UNRECOVERED, `Merge succeeded but post-merge worktree destroy failed. Run caws worktree destroy ${input.name} manually.`, {
|
|
638
|
+
subject: input.name,
|
|
639
|
+
data: {
|
|
640
|
+
merge_commit: mergeCommit,
|
|
641
|
+
destroy_errors: destroyResult.errors.map((d) => d.message),
|
|
642
|
+
},
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
return (0, caws_kernel_1.ok)({
|
|
646
|
+
kind: 'success',
|
|
647
|
+
name: input.name,
|
|
648
|
+
action: 'merged',
|
|
649
|
+
data: { merge_commit: mergeCommit, spec_id: specId, auto_closed_spec: true },
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function listWorktreesPretty(cawsDir) {
|
|
653
|
+
const registry = (0, worktrees_store_1.loadWorktrees)(cawsDir);
|
|
654
|
+
if (!(0, caws_kernel_1.isOk)(registry))
|
|
655
|
+
return (0, caws_kernel_1.err)(registry.errors);
|
|
656
|
+
const entries = [];
|
|
657
|
+
for (const [name, record] of Object.entries(registry.value)) {
|
|
658
|
+
if (typeof record !== 'object' || record === null)
|
|
659
|
+
continue;
|
|
660
|
+
entries.push({
|
|
661
|
+
name,
|
|
662
|
+
path: record.path ?? worktreePathFor(cawsDir, name),
|
|
663
|
+
branch: record.branch ?? 'unknown',
|
|
664
|
+
baseBranch: record.baseBranch ?? 'unknown',
|
|
665
|
+
specId: record.specId ?? null,
|
|
666
|
+
owner: record.owner ?? null,
|
|
667
|
+
status: 'active',
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
// Sort for deterministic output.
|
|
671
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
672
|
+
return (0, caws_kernel_1.ok)({ entries });
|
|
673
|
+
}
|
|
674
|
+
//# sourceMappingURL=worktrees-writer.js.map
|