chapterhouse 0.3.12 → 0.3.14

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.
Files changed (53) hide show
  1. package/README.md +2 -69
  2. package/dist/api/server.js +15 -157
  3. package/dist/api/server.test.js +1 -1
  4. package/dist/api/turn-sse.integration.test.js +36 -0
  5. package/dist/cli.js +0 -30
  6. package/dist/config.js +0 -3
  7. package/dist/copilot/agent-event-bus.js +41 -0
  8. package/dist/copilot/agent-event-bus.test.js +23 -0
  9. package/dist/copilot/agents.js +4 -59
  10. package/dist/copilot/orchestrator.js +60 -65
  11. package/dist/copilot/orchestrator.test.js +73 -158
  12. package/dist/copilot/task-event-log.js +5 -5
  13. package/dist/copilot/task-event-log.test.js +68 -142
  14. package/dist/copilot/tools.js +9 -85
  15. package/dist/daemon.js +0 -22
  16. package/dist/store/db.js +2 -50
  17. package/dist/store/db.test.js +0 -45
  18. package/package.json +1 -3
  19. package/web/dist/assets/index-BlIWCM11.js +217 -0
  20. package/web/dist/assets/index-BlIWCM11.js.map +1 -0
  21. package/web/dist/assets/{index-BtAcw3EP.css → index-lvHFM_ut.css} +1 -1
  22. package/web/dist/index.html +2 -2
  23. package/dist/api/ralph.js +0 -153
  24. package/dist/api/ralph.test.js +0 -101
  25. package/dist/copilot/agents.squad.test.js +0 -72
  26. package/dist/copilot/hooks.js +0 -157
  27. package/dist/copilot/hooks.test.js +0 -315
  28. package/dist/copilot/squad-event-bus.js +0 -27
  29. package/dist/copilot/tools.squad.test.js +0 -168
  30. package/dist/squad/charter.js +0 -125
  31. package/dist/squad/charter.test.js +0 -89
  32. package/dist/squad/context.js +0 -48
  33. package/dist/squad/context.test.js +0 -59
  34. package/dist/squad/discovery.js +0 -268
  35. package/dist/squad/discovery.test.js +0 -154
  36. package/dist/squad/index.js +0 -9
  37. package/dist/squad/init-cli.js +0 -109
  38. package/dist/squad/init.js +0 -395
  39. package/dist/squad/init.test.js +0 -351
  40. package/dist/squad/mirror.js +0 -83
  41. package/dist/squad/mirror.scheduler.js +0 -80
  42. package/dist/squad/mirror.scheduler.test.js +0 -197
  43. package/dist/squad/mirror.test.js +0 -172
  44. package/dist/squad/registry.js +0 -162
  45. package/dist/squad/registry.test.js +0 -31
  46. package/dist/squad/squad-coordinator-system-message.test.js +0 -190
  47. package/dist/squad/squad-session-routing.test.js +0 -260
  48. package/dist/squad/types.js +0 -4
  49. package/dist/squad/worktree.js +0 -295
  50. package/dist/squad/worktree.test.js +0 -189
  51. package/dist/store/squad-sessions.test.js +0 -341
  52. package/web/dist/assets/index-BR2cks94.js +0 -219
  53. package/web/dist/assets/index-BR2cks94.js.map +0 -1
@@ -1,295 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
3
- import { join, relative } from 'node:path';
4
- // ---------------------------------------------------------------------------
5
- // Path convention
6
- // ---------------------------------------------------------------------------
7
- /**
8
- * Location: `.worktrees/{agent}-{issueNum}/` inside the repo root.
9
- * This keeps worktrees discoverable (they're adjacent to the code) and
10
- * `git worktree list` always surfaces them. The directory is gitignored.
11
- *
12
- * Tradeoff vs `~/.cache/chapterhouse-worktrees/<repo>/`:
13
- * + Visible without knowing a cache path
14
- * + `chapterhouse squad worktree list` doesn't need to guess the home
15
- * - Must be added to .gitignore (done once, documented here)
16
- */
17
- export function getWorktreePath(repoRoot, agent, issueNum) {
18
- return join(repoRoot, '.worktrees', `${agent}-${issueNum}`);
19
- }
20
- export function getBranchName(issueNum, slug) {
21
- const safeslug = slug ? `-${slug.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}` : '';
22
- return `squad/${issueNum}${safeslug}`;
23
- }
24
- // ---------------------------------------------------------------------------
25
- // Git helpers
26
- // ---------------------------------------------------------------------------
27
- function git(args, cwd) {
28
- const result = spawnSync('git', args, { cwd, encoding: 'utf-8' });
29
- return {
30
- ok: result.status === 0,
31
- stdout: (result.stdout ?? '').trim(),
32
- stderr: (result.stderr ?? '').trim(),
33
- };
34
- }
35
- function hasUncommittedChanges(worktreePath) {
36
- const r = git(['status', '--porcelain'], worktreePath);
37
- return r.ok && r.stdout.length > 0;
38
- }
39
- function isBranchMerged(repoRoot, branch, base = 'main') {
40
- // merged if branch tip is reachable from base
41
- const r = git(['branch', '--merged', base, '--list', branch], repoRoot);
42
- return r.ok && r.stdout.length > 0;
43
- }
44
- function getRepoRoot(cwd) {
45
- const r = git(['rev-parse', '--show-toplevel'], cwd);
46
- if (!r.ok)
47
- throw new Error(`Not inside a git repo: ${r.stderr}`);
48
- return r.stdout;
49
- }
50
- /**
51
- * Create a dedicated worktree for an agent + issue pair.
52
- * Branch: `squad/{issueNum}-{agent}` (off `main` by default).
53
- * Returns the worktree path.
54
- */
55
- export function createWorktree(repoRoot, agent, issueNum, opts = {}) {
56
- const baseBranch = opts.baseBranch ?? 'main';
57
- const branch = getBranchName(issueNum, opts.slug ?? agent);
58
- const worktreePath = getWorktreePath(repoRoot, agent, issueNum);
59
- if (existsSync(worktreePath)) {
60
- // Reuse existing worktree
61
- console.log(`↩ Reusing existing worktree: ${relative(repoRoot, worktreePath)}`);
62
- return worktreePath;
63
- }
64
- // Check if branch already exists (another agent on same issue)
65
- const branchExists = git(['show-ref', '--verify', `refs/heads/${branch}`], repoRoot);
66
- let result;
67
- if (branchExists.ok) {
68
- result = git(['worktree', 'add', worktreePath, branch], repoRoot);
69
- }
70
- else {
71
- result = git(['worktree', 'add', '-b', branch, worktreePath, baseBranch], repoRoot);
72
- }
73
- if (!result.ok) {
74
- throw new Error(`Failed to create worktree: ${result.stderr}`);
75
- }
76
- console.log(`✅ Worktree created: ${relative(repoRoot, worktreePath)} (branch: ${branch})`);
77
- return worktreePath;
78
- }
79
- /**
80
- * List all squad worktrees (worktrees under `.worktrees/` with `squad/` branches).
81
- */
82
- export function listWorktrees(repoRoot) {
83
- const r = git(['worktree', 'list', '--porcelain'], repoRoot);
84
- if (!r.ok)
85
- throw new Error(`git worktree list failed: ${r.stderr}`);
86
- const entries = parseWorktreePorcelain(r.stdout);
87
- const squadEntries = entries.filter(e => e.path.includes('/.worktrees/'));
88
- return squadEntries.map(e => {
89
- const dirName = e.path.split('/.worktrees/')[1] ?? '';
90
- const match = dirName.match(/^([^-]+)-(\d+)/);
91
- const agent = match ? match[1] : dirName;
92
- const issueNum = match ? match[2] : '?';
93
- const dirty = existsSync(e.path) && hasUncommittedChanges(e.path);
94
- return {
95
- agent,
96
- issueNum,
97
- path: e.path,
98
- branch: e.branch ?? '(detached)',
99
- exists: existsSync(e.path),
100
- dirty,
101
- head: e.head ?? '',
102
- };
103
- });
104
- }
105
- /**
106
- * Remove a specific worktree. Warns but refuses if the worktree has
107
- * uncommitted changes (pass `force: true` to override).
108
- */
109
- export function removeWorktree(repoRoot, agent, issueNum, opts = {}) {
110
- const worktreePath = getWorktreePath(repoRoot, agent, issueNum);
111
- if (!existsSync(worktreePath)) {
112
- console.log(`ℹ Worktree not found: ${worktreePath} (nothing to remove)`);
113
- return;
114
- }
115
- if (!opts.force && hasUncommittedChanges(worktreePath)) {
116
- console.error(`⚠ Worktree ${relative(repoRoot, worktreePath)} has uncommitted changes.\n` +
117
- ` Commit or stash your work before removing, or pass --force to discard.`);
118
- process.exitCode = 1;
119
- return;
120
- }
121
- const removeArgs = ['worktree', 'remove', worktreePath];
122
- if (opts.force)
123
- removeArgs.push('--force');
124
- const r = git(removeArgs, repoRoot);
125
- if (!r.ok)
126
- throw new Error(`Failed to remove worktree: ${r.stderr}`);
127
- if (opts.deleteBranch) {
128
- const branch = getBranchName(issueNum, agent);
129
- const del = git(['branch', '-d', branch], repoRoot);
130
- if (!del.ok) {
131
- console.warn(`⚠ Could not delete branch ${branch}: ${del.stderr}`);
132
- }
133
- }
134
- console.log(`🗑 Removed worktree: ${relative(repoRoot, worktreePath)}`);
135
- }
136
- /**
137
- * Remove all squad worktrees whose branch has been merged into `main`.
138
- * Warns and skips worktrees with uncommitted changes.
139
- */
140
- export function pruneWorktrees(repoRoot, opts = {}) {
141
- const base = opts.base ?? 'main';
142
- const worktrees = listWorktrees(repoRoot);
143
- if (worktrees.length === 0) {
144
- console.log('ℹ No squad worktrees found.');
145
- return;
146
- }
147
- for (const wt of worktrees) {
148
- const merged = isBranchMerged(repoRoot, wt.branch, base);
149
- if (!merged) {
150
- console.log(`↷ Skipping ${wt.agent}-${wt.issueNum} (branch not merged into ${base})`);
151
- continue;
152
- }
153
- if (wt.dirty) {
154
- console.warn(`⚠ Skipping ${wt.agent}-${wt.issueNum} (has uncommitted changes — clean up manually)`);
155
- continue;
156
- }
157
- if (opts.dryRun) {
158
- console.log(`[dry-run] Would remove: ${wt.agent}-${wt.issueNum} (branch: ${wt.branch})`);
159
- continue;
160
- }
161
- removeWorktree(repoRoot, wt.agent, Number(wt.issueNum), { deleteBranch: true });
162
- }
163
- }
164
- // ---------------------------------------------------------------------------
165
- // Print helpers
166
- // ---------------------------------------------------------------------------
167
- export function printWorktreeList(repoRoot) {
168
- const worktrees = listWorktrees(repoRoot);
169
- if (worktrees.length === 0) {
170
- console.log('No squad worktrees found.');
171
- return;
172
- }
173
- const header = ['AGENT', 'ISSUE', 'BRANCH', 'STATUS', 'PATH'];
174
- const rows = worktrees.map(wt => [
175
- wt.agent,
176
- wt.issueNum,
177
- wt.branch,
178
- wt.dirty ? '⚠ dirty' : '✓ clean',
179
- wt.path,
180
- ]);
181
- const widths = header.map((h, i) => Math.max(h.length, ...rows.map(r => r[i].length)));
182
- const fmt = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(' ');
183
- console.log(fmt(header));
184
- console.log(widths.map(w => '-'.repeat(w)).join(' '));
185
- for (const row of rows)
186
- console.log(fmt(row));
187
- }
188
- export function printWorktreeHelp() {
189
- console.log(`
190
- chapterhouse squad worktree — manage per-agent git worktrees
191
-
192
- Usage:
193
- chapterhouse squad worktree <subcommand>
194
-
195
- Subcommands:
196
- create <agent> <issue> [--base <branch>] [--slug <slug>]
197
- Create a dedicated worktree for an agent + issue.
198
- Path: .worktrees/{agent}-{issue}/ Branch: squad/{issue}-{agent}
199
-
200
- list
201
- List all squad worktrees with agent, branch, and status.
202
-
203
- remove <agent> <issue> [--force] [--delete-branch]
204
- Remove a worktree. Refuses if there are uncommitted changes
205
- unless --force is passed (which discards the changes).
206
-
207
- prune [--base <branch>] [--dry-run]
208
- Remove all worktrees whose branch has been merged into main
209
- (or the specified --base branch). Skips dirty worktrees.
210
- `.trim());
211
- }
212
- function parseWorktreePorcelain(output) {
213
- const entries = [];
214
- let current = null;
215
- for (const line of output.split('\n')) {
216
- if (line.startsWith('worktree ')) {
217
- if (current?.path)
218
- entries.push(current);
219
- current = { path: line.slice('worktree '.length) };
220
- }
221
- else if (line.startsWith('HEAD ') && current) {
222
- current.head = line.slice('HEAD '.length);
223
- }
224
- else if (line.startsWith('branch ') && current) {
225
- current.branch = line.slice('branch refs/heads/'.length);
226
- }
227
- else if (line === '' && current?.path) {
228
- entries.push(current);
229
- current = null;
230
- }
231
- }
232
- if (current?.path)
233
- entries.push(current);
234
- return entries;
235
- }
236
- // ---------------------------------------------------------------------------
237
- // CLI entry point
238
- // ---------------------------------------------------------------------------
239
- export async function runWorktreeCli(args) {
240
- let repoRoot;
241
- try {
242
- repoRoot = getRepoRoot(process.cwd());
243
- }
244
- catch {
245
- console.error('❌ Not inside a git repository.');
246
- process.exit(1);
247
- }
248
- const subcommand = args[0];
249
- switch (subcommand) {
250
- case 'create': {
251
- const agent = args[1];
252
- const issueNum = args[2];
253
- if (!agent || !issueNum) {
254
- console.error('Usage: chapterhouse squad worktree create <agent> <issue> [--base <branch>] [--slug <slug>]');
255
- process.exit(1);
256
- }
257
- const baseIdx = args.indexOf('--base');
258
- const baseBranch = baseIdx !== -1 ? args[baseIdx + 1] : undefined;
259
- const slugIdx = args.indexOf('--slug');
260
- const slug = slugIdx !== -1 ? args[slugIdx + 1] : undefined;
261
- const path = createWorktree(repoRoot, agent, issueNum, { baseBranch, slug });
262
- console.log(path);
263
- break;
264
- }
265
- case 'list':
266
- printWorktreeList(repoRoot);
267
- break;
268
- case 'remove': {
269
- const agent = args[1];
270
- const issueNum = args[2];
271
- if (!agent || !issueNum) {
272
- console.error('Usage: chapterhouse squad worktree remove <agent> <issue> [--force] [--delete-branch]');
273
- process.exit(1);
274
- }
275
- removeWorktree(repoRoot, agent, issueNum, {
276
- force: args.includes('--force'),
277
- deleteBranch: args.includes('--delete-branch'),
278
- });
279
- break;
280
- }
281
- case 'prune': {
282
- const baseIdx = args.indexOf('--base');
283
- const base = baseIdx !== -1 ? args[baseIdx + 1] : undefined;
284
- pruneWorktrees(repoRoot, { base, dryRun: args.includes('--dry-run') });
285
- break;
286
- }
287
- default:
288
- if (subcommand)
289
- console.error(`Unknown worktree subcommand: ${subcommand}\n`);
290
- printWorktreeHelp();
291
- if (subcommand)
292
- process.exit(1);
293
- }
294
- }
295
- //# sourceMappingURL=worktree.js.map
@@ -1,189 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { execSync } from 'node:child_process';
5
- import test from 'node:test';
6
- // ---------------------------------------------------------------------------
7
- // Test fixture: minimal git repo
8
- // ---------------------------------------------------------------------------
9
- const repoRoot = process.cwd();
10
- const sandboxRoot = join(repoRoot, '.test-work', `worktree-${process.pid}`);
11
- function initRepo(dir) {
12
- mkdirSync(dir, { recursive: true });
13
- // --initial-branch=main ensures the branch is named 'main' regardless of
14
- // the runner's global init.defaultBranch setting (older git may default to 'master').
15
- execSync('git init --initial-branch=main', { cwd: dir, stdio: 'pipe' });
16
- execSync('git config user.email "test@example.com"', { cwd: dir, stdio: 'pipe' });
17
- execSync('git config user.name "Test CI"', { cwd: dir, stdio: 'pipe' });
18
- writeFileSync(join(dir, 'README.md'), '# test\n');
19
- execSync('git add .', { cwd: dir, stdio: 'pipe' });
20
- // An initial commit is required so that 'main' resolves as a ref.
21
- // Without it, `git worktree add <path> -b <branch> main` fails with
22
- // "fatal: invalid reference: main" because the branch has no commits.
23
- execSync('git commit -m "chore: initial commit"', { cwd: dir, stdio: 'pipe' });
24
- }
25
- // We need a separate repo because the main repo's worktrees would interfere.
26
- // Each test suite gets its own isolated sandbox git repo.
27
- const testRepo = join(sandboxRoot, 'repo');
28
- test.before(() => {
29
- mkdirSync(sandboxRoot, { recursive: true });
30
- initRepo(testRepo);
31
- });
32
- test.after(() => {
33
- // Clean up any worktrees first (git worktree remove) then the sandbox
34
- try {
35
- execSync('git worktree prune', { cwd: testRepo });
36
- }
37
- catch { /* ignore */ }
38
- rmSync(sandboxRoot, { recursive: true, force: true });
39
- });
40
- async function loadWorktree() {
41
- return await import(new URL(`./worktree.js?v=${Date.now()}-${Math.random()}`, import.meta.url).href);
42
- }
43
- // ---------------------------------------------------------------------------
44
- // getWorktreePath
45
- // ---------------------------------------------------------------------------
46
- test('getWorktreePath returns correct path inside .worktrees/', async () => {
47
- const m = await loadWorktree();
48
- const result = m.getWorktreePath('/repo', 'kaylee', 42);
49
- assert.equal(result, '/repo/.worktrees/kaylee-42');
50
- });
51
- test('getWorktreePath works with string issue number', async () => {
52
- const m = await loadWorktree();
53
- const result = m.getWorktreePath('/repo', 'wash', '17');
54
- assert.equal(result, '/repo/.worktrees/wash-17');
55
- });
56
- // ---------------------------------------------------------------------------
57
- // getBranchName
58
- // ---------------------------------------------------------------------------
59
- test('getBranchName without slug uses squad/{issue} format', async () => {
60
- const m = await loadWorktree();
61
- assert.equal(m.getBranchName(49), 'squad/49');
62
- });
63
- test('getBranchName with slug appends kebab-case slug', async () => {
64
- const m = await loadWorktree();
65
- assert.equal(m.getBranchName(49, 'git worktrees'), 'squad/49-git-worktrees');
66
- });
67
- test('getBranchName normalises special characters in slug', async () => {
68
- const m = await loadWorktree();
69
- assert.equal(m.getBranchName(10, 'Fix Login!!'), 'squad/10-fix-login');
70
- });
71
- // ---------------------------------------------------------------------------
72
- // createWorktree
73
- // ---------------------------------------------------------------------------
74
- test('createWorktree creates a worktree at .worktrees/{agent}-{issue}/', async () => {
75
- const m = await loadWorktree();
76
- const { existsSync } = await import('node:fs');
77
- const wt = m.createWorktree(testRepo, 'kaylee', 1);
78
- assert.ok(existsSync(wt), `Worktree path should exist: ${wt}`);
79
- assert.ok(wt.endsWith('.worktrees/kaylee-1'), `Path should end with .worktrees/kaylee-1, got: ${wt}`);
80
- // Verify branch was created
81
- const { spawnSync } = await import('node:child_process');
82
- const r = spawnSync('git', ['branch', '--list', 'squad/1-kaylee'], { cwd: testRepo, encoding: 'utf-8' });
83
- assert.ok(r.stdout.trim().length > 0, 'Branch squad/1-kaylee should exist');
84
- });
85
- test('createWorktree reuses existing worktree without error', async () => {
86
- const m = await loadWorktree();
87
- // create again (already exists from above)
88
- assert.doesNotThrow(() => {
89
- m.createWorktree(testRepo, 'kaylee', 1);
90
- });
91
- });
92
- test('createWorktree respects --base option', async () => {
93
- const m = await loadWorktree();
94
- const { existsSync } = await import('node:fs');
95
- // We guarantee 'main' exists via --initial-branch=main in initRepo
96
- const wt = m.createWorktree(testRepo, 'wash', 2, { baseBranch: 'main' });
97
- assert.ok(existsSync(wt), `Worktree should be created at ${wt}`);
98
- });
99
- test('createWorktree with slug uses slug in branch name', async () => {
100
- const m = await loadWorktree();
101
- const wt = m.createWorktree(testRepo, 'zoe', 3, { slug: 'test-isolation' });
102
- const { spawnSync } = await import('node:child_process');
103
- const r = spawnSync('git', ['branch', '--list', 'squad/3-test-isolation'], { cwd: testRepo, encoding: 'utf-8' });
104
- assert.ok(r.stdout.trim().length > 0, 'Branch squad/3-test-isolation should exist');
105
- void wt;
106
- });
107
- // ---------------------------------------------------------------------------
108
- // listWorktrees
109
- // ---------------------------------------------------------------------------
110
- test('listWorktrees returns entries for squad worktrees', async () => {
111
- const m = await loadWorktree();
112
- const list = m.listWorktrees(testRepo);
113
- // We created kaylee-1 above; at minimum it should appear
114
- assert.ok(list.length >= 1, 'Should have at least 1 squad worktree');
115
- const kaylee = list.find(w => w.agent === 'kaylee' && w.issueNum === '1');
116
- assert.ok(kaylee, 'Should include kaylee-1 worktree');
117
- assert.equal(kaylee?.branch, 'squad/1-kaylee');
118
- assert.equal(typeof kaylee?.dirty, 'boolean');
119
- });
120
- test('listWorktrees does not include the main checkout', async () => {
121
- const m = await loadWorktree();
122
- const list = m.listWorktrees(testRepo);
123
- const hasMain = list.some(w => !w.path.includes('/.worktrees/'));
124
- assert.ok(!hasMain, 'Main checkout should not appear in squad worktree list');
125
- });
126
- // ---------------------------------------------------------------------------
127
- // removeWorktree
128
- // ---------------------------------------------------------------------------
129
- test('removeWorktree removes a clean worktree', async () => {
130
- const m = await loadWorktree();
131
- // Create a fresh worktree to remove
132
- m.createWorktree(testRepo, 'scribe', 5);
133
- const wtPath = m.getWorktreePath(testRepo, 'scribe', 5);
134
- const { existsSync } = await import('node:fs');
135
- assert.ok(existsSync(wtPath));
136
- m.removeWorktree(testRepo, 'scribe', 5);
137
- assert.ok(!existsSync(wtPath), 'Worktree should be removed');
138
- });
139
- test('removeWorktree is a no-op for non-existent worktree', async () => {
140
- const m = await loadWorktree();
141
- assert.doesNotThrow(() => {
142
- m.removeWorktree(testRepo, 'ghost', 999);
143
- });
144
- });
145
- test('removeWorktree refuses to remove dirty worktree without --force', async () => {
146
- const m = await loadWorktree();
147
- const { writeFileSync } = await import('node:fs');
148
- // Create worktree, then add an uncommitted file
149
- m.createWorktree(testRepo, 'mal', 6);
150
- const wtPath = m.getWorktreePath(testRepo, 'mal', 6);
151
- writeFileSync(join(wtPath, 'uncommitted.txt'), 'dirty\n');
152
- // Should set exitCode=1 but not throw
153
- const prev = process.exitCode;
154
- process.exitCode = 0;
155
- m.removeWorktree(testRepo, 'mal', 6, { force: false });
156
- const rejected = process.exitCode === 1;
157
- process.exitCode = prev; // restore
158
- assert.ok(rejected, 'Should set exitCode=1 for dirty worktree');
159
- // cleanup with force
160
- m.removeWorktree(testRepo, 'mal', 6, { force: true });
161
- });
162
- // ---------------------------------------------------------------------------
163
- // pruneWorktrees
164
- // ---------------------------------------------------------------------------
165
- test('pruneWorktrees dry-run lists worktrees that would be removed', async () => {
166
- const m = await loadWorktree();
167
- // Capture stdout (basic test — just ensure it doesn't throw)
168
- assert.doesNotThrow(() => {
169
- m.pruneWorktrees(testRepo, { dryRun: true });
170
- });
171
- });
172
- test('pruneWorktrees skips worktrees on unmerged branches', async () => {
173
- const m = await loadWorktree();
174
- const { existsSync, writeFileSync } = await import('node:fs');
175
- const { spawnSync } = await import('node:child_process');
176
- const { join } = await import('node:path');
177
- // zoe-3 worktree should exist (created in earlier test)
178
- const wt = m.getWorktreePath(testRepo, 'zoe', 3);
179
- assert.ok(existsSync(wt), 'zoe-3 worktree should exist');
180
- // Add a commit so the branch diverges from base — otherwise git considers
181
- // a branch with no extra commits as already merged into its parent.
182
- writeFileSync(join(wt, 'feature.txt'), 'in progress\n');
183
- spawnSync('git', ['add', 'feature.txt'], { cwd: wt, stdio: 'pipe' });
184
- spawnSync('git', ['commit', '-m', 'feat: in-progress work'], { cwd: wt, stdio: 'pipe' });
185
- // We guarantee 'main' via --initial-branch=main in initRepo
186
- m.pruneWorktrees(testRepo, { base: 'main' });
187
- assert.ok(existsSync(wt), 'Unmerged worktree should NOT be pruned');
188
- });
189
- //# sourceMappingURL=worktree.test.js.map