memorix 1.2.2 → 1.2.3

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 (139) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +34 -18
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/index.js +17 -8
  6. package/dist/index.js.map +1 -1
  7. package/dist/maintenance-runner.js.map +1 -1
  8. package/dist/memcode-runtime/CHANGELOG.md +7 -0
  9. package/dist/sdk.js +17 -8
  10. package/dist/sdk.js.map +1 -1
  11. package/docs/DESIGN_DECISIONS.md +357 -357
  12. package/docs/dev-log/progress.txt +18 -8
  13. package/package.json +1 -1
  14. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  15. package/src/audit/index.ts +156 -156
  16. package/src/cli/commands/audit-list.ts +89 -89
  17. package/src/cli/commands/background.ts +659 -659
  18. package/src/cli/commands/formation.ts +48 -48
  19. package/src/cli/commands/git-hook-install.ts +111 -111
  20. package/src/cli/commands/handoff.ts +54 -54
  21. package/src/cli/commands/hooks-status.ts +63 -63
  22. package/src/cli/commands/ingest-commit.ts +153 -153
  23. package/src/cli/commands/ingest-image.ts +66 -66
  24. package/src/cli/commands/ingest-log.ts +180 -180
  25. package/src/cli/commands/ingest.ts +44 -44
  26. package/src/cli/commands/integrate-shared.ts +15 -15
  27. package/src/cli/commands/lock.ts +82 -82
  28. package/src/cli/commands/message.ts +104 -104
  29. package/src/cli/commands/poll.ts +58 -58
  30. package/src/cli/commands/purge-all-memory.ts +85 -85
  31. package/src/cli/commands/purge-project-memory.ts +83 -83
  32. package/src/cli/commands/reasoning.ts +118 -118
  33. package/src/cli/commands/serve-shared.ts +118 -118
  34. package/src/cli/commands/session.ts +15 -7
  35. package/src/cli/commands/skills.ts +114 -114
  36. package/src/cli/commands/task.ts +167 -167
  37. package/src/cli/commands/transfer.ts +47 -47
  38. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  39. package/src/cli/tui/ChatView.tsx +234 -234
  40. package/src/cli/tui/CommandBar.tsx +312 -312
  41. package/src/cli/tui/ContextRail.tsx +118 -118
  42. package/src/cli/tui/HeaderBar.tsx +72 -72
  43. package/src/cli/tui/LogoBanner.tsx +51 -51
  44. package/src/cli/tui/Sidebar.tsx +179 -179
  45. package/src/cli/tui/index.ts +41 -41
  46. package/src/cli/tui/markdown-render.tsx +371 -371
  47. package/src/cli/tui/session-service.ts +3 -2
  48. package/src/cli/tui/use-mouse.ts +157 -157
  49. package/src/cli/tui/useNavigation.ts +56 -56
  50. package/src/cli/update-checker.ts +211 -211
  51. package/src/cli/version.ts +7 -7
  52. package/src/cli/workbench.ts +1 -1
  53. package/src/compact/token-budget.ts +74 -74
  54. package/src/dashboard/project-classification.ts +64 -64
  55. package/src/embedding/fastembed-provider.ts +142 -142
  56. package/src/embedding/transformers-provider.ts +111 -111
  57. package/src/git/extractor.ts +209 -209
  58. package/src/git/hooks-path.ts +85 -85
  59. package/src/hooks/pattern-detector.ts +173 -173
  60. package/src/hooks/significance-filter.ts +250 -250
  61. package/src/llm/memory-manager.ts +328 -328
  62. package/src/llm/provider.ts +885 -885
  63. package/src/llm/quality.ts +248 -248
  64. package/src/memory/attribution-guard.ts +249 -249
  65. package/src/memory/disclosure-policy.ts +135 -135
  66. package/src/memory/entity-extractor.ts +197 -197
  67. package/src/memory/formation/evaluate.ts +217 -217
  68. package/src/memory/formation/extract.ts +361 -361
  69. package/src/memory/formation/index.ts +417 -417
  70. package/src/memory/formation/resolve.ts +344 -344
  71. package/src/memory/formation/types.ts +315 -315
  72. package/src/memory/freshness.ts +122 -122
  73. package/src/memory/graph.ts +197 -197
  74. package/src/memory/refs.ts +94 -94
  75. package/src/memory/secret-filter.ts +79 -79
  76. package/src/memory/session.ts +24 -9
  77. package/src/multimodal/image-loader.ts +143 -143
  78. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  79. package/src/orchestrate/adapters/claude.ts +111 -111
  80. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  81. package/src/orchestrate/adapters/codex.ts +41 -41
  82. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  83. package/src/orchestrate/adapters/gemini.ts +42 -42
  84. package/src/orchestrate/adapters/index.ts +73 -73
  85. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  86. package/src/orchestrate/adapters/opencode.ts +47 -47
  87. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  88. package/src/orchestrate/adapters/types.ts +77 -77
  89. package/src/orchestrate/capability-router.ts +284 -284
  90. package/src/orchestrate/context-compact.ts +188 -188
  91. package/src/orchestrate/cost-tracker.ts +219 -219
  92. package/src/orchestrate/error-recovery.ts +191 -191
  93. package/src/orchestrate/evidence.ts +140 -140
  94. package/src/orchestrate/ledger.ts +110 -110
  95. package/src/orchestrate/memorix-bridge.ts +343 -343
  96. package/src/orchestrate/output-budget.ts +80 -80
  97. package/src/orchestrate/permission.ts +152 -152
  98. package/src/orchestrate/pipeline-trace.ts +131 -131
  99. package/src/orchestrate/prompt-builder.ts +155 -155
  100. package/src/orchestrate/ring-buffer.ts +37 -37
  101. package/src/orchestrate/task-graph.ts +389 -389
  102. package/src/orchestrate/worktree.ts +232 -232
  103. package/src/project/aliases.ts +374 -374
  104. package/src/project/detector.ts +268 -268
  105. package/src/rules/adapters/claude-code.ts +99 -99
  106. package/src/rules/adapters/codex.ts +97 -97
  107. package/src/rules/adapters/copilot.ts +124 -124
  108. package/src/rules/adapters/cursor.ts +114 -114
  109. package/src/rules/adapters/kiro.ts +126 -126
  110. package/src/rules/adapters/trae.ts +56 -56
  111. package/src/rules/adapters/windsurf.ts +83 -83
  112. package/src/rules/syncer.ts +235 -235
  113. package/src/sdk.ts +299 -299
  114. package/src/search/intent-detector.ts +289 -289
  115. package/src/search/query-expansion.ts +52 -52
  116. package/src/server/formation-timeout.ts +27 -27
  117. package/src/server.ts +7 -2
  118. package/src/skills/mini-skills.ts +386 -386
  119. package/src/store/chat-store.ts +119 -119
  120. package/src/store/graph-store.ts +249 -249
  121. package/src/store/mini-skill-store.ts +349 -349
  122. package/src/store/persistence-json.ts +212 -212
  123. package/src/store/persistence.ts +291 -291
  124. package/src/store/project-affinity.ts +195 -195
  125. package/src/team/event-bus.ts +76 -76
  126. package/src/team/file-locks.ts +173 -173
  127. package/src/team/handoff.ts +161 -161
  128. package/src/team/messages.ts +203 -203
  129. package/src/team/poll.ts +132 -132
  130. package/src/team/tasks.ts +211 -211
  131. package/src/workspace/mcp-adapters/codex.ts +191 -191
  132. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  133. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  134. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  135. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  136. package/src/workspace/mcp-adapters/trae.ts +134 -134
  137. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  138. package/src/workspace/sanitizer.ts +60 -60
  139. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,232 +1,232 @@
1
- /**
2
- * Git Worktree — Phase 6i: Parallel agent isolation.
3
- *
4
- * Each parallel agent gets its own working directory + branch via
5
- * git worktree. Prevents file conflicts during concurrent execution.
6
- * Startup cleanup handles orphaned worktrees (pays D6 debt).
7
- */
8
-
9
- import { execSync } from 'node:child_process';
10
- import { existsSync, rmSync } from 'node:fs';
11
- import { join, basename } from 'node:path';
12
-
13
- // ── Types ──────────────────────────────────────────────────────────
14
-
15
- export interface WorktreeInfo {
16
- worktreePath: string;
17
- branch: string;
18
- }
19
-
20
- // ── Constants ──────────────────────────────────────────────────────
21
-
22
- const WORKTREE_DIR = '.worktrees';
23
-
24
- // ── Create ─────────────────────────────────────────────────────────
25
-
26
- /**
27
- * Create an isolated git worktree for a task.
28
- * The worktree lives at `<projectDir>/.worktrees/task-<shortId>/`
29
- * on branch `pipeline/<pipelineId>/task-<shortId>`.
30
- */
31
- export function createWorktree(
32
- projectDir: string,
33
- taskId: string,
34
- pipelineId: string,
35
- ): WorktreeInfo {
36
- const shortId = taskId.slice(0, 8);
37
- const shortPipeline = pipelineId.slice(0, 8);
38
- const worktreePath = join(projectDir, WORKTREE_DIR, `task-${shortId}`);
39
- const branch = `pipeline/${shortPipeline}/task-${shortId}`;
40
-
41
- // Ensure .worktrees directory exists (git worktree add handles the rest)
42
- execSync(`git worktree add "${worktreePath}" -b "${branch}"`, {
43
- cwd: projectDir,
44
- encoding: 'utf-8',
45
- timeout: 30_000,
46
- });
47
-
48
- return { worktreePath, branch };
49
- }
50
-
51
- // ── Merge ──────────────────────────────────────────────────────────
52
-
53
- export interface MergeResult {
54
- success: boolean;
55
- conflicts?: string;
56
- }
57
-
58
- /**
59
- * Commit any uncommitted changes in the worktree, then merge the worktree
60
- * branch back into the current branch in projectDir.
61
- * Returns success=false with conflict details if merge fails.
62
- *
63
- * @param worktreePath - path to the worktree directory (for add+commit)
64
- * @param projectDir - path to the main repo (for merge)
65
- * @param branch - the worktree branch name
66
- */
67
- export function mergeWorktree(
68
- projectDir: string,
69
- branch: string,
70
- worktreePath?: string,
71
- ): MergeResult {
72
- // Step 1: Commit any agent-produced changes in the worktree.
73
- // Without this, files written by the agent are untracked and the merge
74
- // will be a no-op (no commits on the branch beyond the checkout base).
75
- if (worktreePath) {
76
- try {
77
- execSync('git add -A', {
78
- cwd: worktreePath,
79
- encoding: 'utf-8',
80
- timeout: 15_000,
81
- });
82
- // Check if there's anything to commit
83
- const status = execSync('git status --porcelain', {
84
- cwd: worktreePath,
85
- encoding: 'utf-8',
86
- timeout: 5_000,
87
- }).trim();
88
- if (status) {
89
- execSync('git commit -m "task: agent work" --no-verify', {
90
- cwd: worktreePath,
91
- encoding: 'utf-8',
92
- timeout: 15_000,
93
- });
94
- }
95
- // else: nothing to commit — merge will be a no-op
96
- } catch {
97
- // "nothing to commit" is fine — the merge will just be a no-op.
98
- // Any other git error here (e.g., invalid index) is also non-fatal;
99
- // we still attempt the merge which will correctly report as no-op.
100
- }
101
- }
102
-
103
- // Step 2: Merge the branch into the main repo.
104
- try {
105
- execSync(`git merge --no-ff "${branch}" -m "merge: ${branch}"`, {
106
- cwd: projectDir,
107
- encoding: 'utf-8',
108
- timeout: 30_000,
109
- });
110
- return { success: true };
111
- } catch (err) {
112
- // Abort the failed merge to leave working tree clean
113
- try {
114
- execSync('git merge --abort', { cwd: projectDir, encoding: 'utf-8', timeout: 5_000 });
115
- } catch { /* best-effort */ }
116
-
117
- const msg = err instanceof Error ? err.message : String(err);
118
- return { success: false, conflicts: msg };
119
- }
120
- }
121
-
122
- // ── Remove ─────────────────────────────────────────────────────────
123
-
124
- /**
125
- * Remove a worktree and optionally delete the branch.
126
- */
127
- export function removeWorktree(
128
- projectDir: string,
129
- worktreePath: string,
130
- branch?: string,
131
- ): void {
132
- try {
133
- execSync(`git worktree remove "${worktreePath}" --force`, {
134
- cwd: projectDir,
135
- encoding: 'utf-8',
136
- timeout: 10_000,
137
- });
138
- } catch {
139
- // If git worktree remove fails, try manual cleanup
140
- if (existsSync(worktreePath)) {
141
- try { rmSync(worktreePath, { recursive: true, force: true }); } catch { /* best-effort */ }
142
- }
143
- try {
144
- execSync('git worktree prune', { cwd: projectDir, encoding: 'utf-8', timeout: 5_000 });
145
- } catch { /* best-effort */ }
146
- }
147
-
148
- // Delete the branch (best-effort)
149
- if (branch) {
150
- try {
151
- execSync(`git branch -D "${branch}"`, {
152
- cwd: projectDir,
153
- encoding: 'utf-8',
154
- timeout: 5_000,
155
- });
156
- } catch { /* may already be deleted or is current branch */ }
157
- }
158
- }
159
-
160
- // ── Cleanup Orphans ────────────────────────────────────────────────
161
-
162
- /**
163
- * List existing worktrees under .worktrees/ directory.
164
- * Returns array of { path, branch } from `git worktree list --porcelain`.
165
- */
166
- export function listWorktrees(projectDir: string): Array<{ path: string; branch: string | null }> {
167
- try {
168
- const raw = execSync('git worktree list --porcelain', {
169
- cwd: projectDir,
170
- encoding: 'utf-8',
171
- timeout: 5_000,
172
- });
173
-
174
- const worktrees: Array<{ path: string; branch: string | null }> = [];
175
- let current: { path: string; branch: string | null } | null = null;
176
-
177
- for (const line of raw.split('\n')) {
178
- if (line.startsWith('worktree ')) {
179
- if (current) worktrees.push(current);
180
- current = { path: line.slice('worktree '.length).trim(), branch: null };
181
- } else if (line.startsWith('branch ') && current) {
182
- // branch refs/heads/pipeline/xxx/task-yyy
183
- const ref = line.slice('branch '.length).trim();
184
- current.branch = ref.replace('refs/heads/', '');
185
- }
186
- }
187
- if (current) worktrees.push(current);
188
-
189
- // Filter to only our managed worktrees (normalize slashes for Windows compat)
190
- const worktreeBase = join(projectDir, WORKTREE_DIR).replace(/\\/g, '/');
191
- return worktrees.filter(w => w.path.replace(/\\/g, '/').startsWith(worktreeBase));
192
- } catch {
193
- return [];
194
- }
195
- }
196
-
197
- /**
198
- * Extract taskId from a worktree path.
199
- * Path format: .../task-<shortId>
200
- */
201
- export function extractTaskIdFromPath(worktreePath: string): string | null {
202
- const name = basename(worktreePath);
203
- const match = name.match(/^task-([a-f0-9]+)$/);
204
- return match ? match[1] : null;
205
- }
206
-
207
- /**
208
- * Clean up orphaned worktrees — those whose tasks no longer exist or are terminal.
209
- * Call this at coordinator startup.
210
- *
211
- * @param isTaskTerminal - callback to check if a task (by short ID prefix) is done/nonexistent
212
- * @returns number of worktrees removed
213
- */
214
- export function cleanupOrphanWorktrees(
215
- projectDir: string,
216
- isTaskTerminal: (shortId: string) => boolean,
217
- ): number {
218
- const worktrees = listWorktrees(projectDir);
219
- let removed = 0;
220
-
221
- for (const wt of worktrees) {
222
- const shortId = extractTaskIdFromPath(wt.path);
223
- if (!shortId) continue;
224
-
225
- if (isTaskTerminal(shortId)) {
226
- removeWorktree(projectDir, wt.path, wt.branch ?? undefined);
227
- removed++;
228
- }
229
- }
230
-
231
- return removed;
232
- }
1
+ /**
2
+ * Git Worktree — Phase 6i: Parallel agent isolation.
3
+ *
4
+ * Each parallel agent gets its own working directory + branch via
5
+ * git worktree. Prevents file conflicts during concurrent execution.
6
+ * Startup cleanup handles orphaned worktrees (pays D6 debt).
7
+ */
8
+
9
+ import { execSync } from 'node:child_process';
10
+ import { existsSync, rmSync } from 'node:fs';
11
+ import { join, basename } from 'node:path';
12
+
13
+ // ── Types ──────────────────────────────────────────────────────────
14
+
15
+ export interface WorktreeInfo {
16
+ worktreePath: string;
17
+ branch: string;
18
+ }
19
+
20
+ // ── Constants ──────────────────────────────────────────────────────
21
+
22
+ const WORKTREE_DIR = '.worktrees';
23
+
24
+ // ── Create ─────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Create an isolated git worktree for a task.
28
+ * The worktree lives at `<projectDir>/.worktrees/task-<shortId>/`
29
+ * on branch `pipeline/<pipelineId>/task-<shortId>`.
30
+ */
31
+ export function createWorktree(
32
+ projectDir: string,
33
+ taskId: string,
34
+ pipelineId: string,
35
+ ): WorktreeInfo {
36
+ const shortId = taskId.slice(0, 8);
37
+ const shortPipeline = pipelineId.slice(0, 8);
38
+ const worktreePath = join(projectDir, WORKTREE_DIR, `task-${shortId}`);
39
+ const branch = `pipeline/${shortPipeline}/task-${shortId}`;
40
+
41
+ // Ensure .worktrees directory exists (git worktree add handles the rest)
42
+ execSync(`git worktree add "${worktreePath}" -b "${branch}"`, {
43
+ cwd: projectDir,
44
+ encoding: 'utf-8',
45
+ timeout: 30_000,
46
+ });
47
+
48
+ return { worktreePath, branch };
49
+ }
50
+
51
+ // ── Merge ──────────────────────────────────────────────────────────
52
+
53
+ export interface MergeResult {
54
+ success: boolean;
55
+ conflicts?: string;
56
+ }
57
+
58
+ /**
59
+ * Commit any uncommitted changes in the worktree, then merge the worktree
60
+ * branch back into the current branch in projectDir.
61
+ * Returns success=false with conflict details if merge fails.
62
+ *
63
+ * @param worktreePath - path to the worktree directory (for add+commit)
64
+ * @param projectDir - path to the main repo (for merge)
65
+ * @param branch - the worktree branch name
66
+ */
67
+ export function mergeWorktree(
68
+ projectDir: string,
69
+ branch: string,
70
+ worktreePath?: string,
71
+ ): MergeResult {
72
+ // Step 1: Commit any agent-produced changes in the worktree.
73
+ // Without this, files written by the agent are untracked and the merge
74
+ // will be a no-op (no commits on the branch beyond the checkout base).
75
+ if (worktreePath) {
76
+ try {
77
+ execSync('git add -A', {
78
+ cwd: worktreePath,
79
+ encoding: 'utf-8',
80
+ timeout: 15_000,
81
+ });
82
+ // Check if there's anything to commit
83
+ const status = execSync('git status --porcelain', {
84
+ cwd: worktreePath,
85
+ encoding: 'utf-8',
86
+ timeout: 5_000,
87
+ }).trim();
88
+ if (status) {
89
+ execSync('git commit -m "task: agent work" --no-verify', {
90
+ cwd: worktreePath,
91
+ encoding: 'utf-8',
92
+ timeout: 15_000,
93
+ });
94
+ }
95
+ // else: nothing to commit — merge will be a no-op
96
+ } catch {
97
+ // "nothing to commit" is fine — the merge will just be a no-op.
98
+ // Any other git error here (e.g., invalid index) is also non-fatal;
99
+ // we still attempt the merge which will correctly report as no-op.
100
+ }
101
+ }
102
+
103
+ // Step 2: Merge the branch into the main repo.
104
+ try {
105
+ execSync(`git merge --no-ff "${branch}" -m "merge: ${branch}"`, {
106
+ cwd: projectDir,
107
+ encoding: 'utf-8',
108
+ timeout: 30_000,
109
+ });
110
+ return { success: true };
111
+ } catch (err) {
112
+ // Abort the failed merge to leave working tree clean
113
+ try {
114
+ execSync('git merge --abort', { cwd: projectDir, encoding: 'utf-8', timeout: 5_000 });
115
+ } catch { /* best-effort */ }
116
+
117
+ const msg = err instanceof Error ? err.message : String(err);
118
+ return { success: false, conflicts: msg };
119
+ }
120
+ }
121
+
122
+ // ── Remove ─────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Remove a worktree and optionally delete the branch.
126
+ */
127
+ export function removeWorktree(
128
+ projectDir: string,
129
+ worktreePath: string,
130
+ branch?: string,
131
+ ): void {
132
+ try {
133
+ execSync(`git worktree remove "${worktreePath}" --force`, {
134
+ cwd: projectDir,
135
+ encoding: 'utf-8',
136
+ timeout: 10_000,
137
+ });
138
+ } catch {
139
+ // If git worktree remove fails, try manual cleanup
140
+ if (existsSync(worktreePath)) {
141
+ try { rmSync(worktreePath, { recursive: true, force: true }); } catch { /* best-effort */ }
142
+ }
143
+ try {
144
+ execSync('git worktree prune', { cwd: projectDir, encoding: 'utf-8', timeout: 5_000 });
145
+ } catch { /* best-effort */ }
146
+ }
147
+
148
+ // Delete the branch (best-effort)
149
+ if (branch) {
150
+ try {
151
+ execSync(`git branch -D "${branch}"`, {
152
+ cwd: projectDir,
153
+ encoding: 'utf-8',
154
+ timeout: 5_000,
155
+ });
156
+ } catch { /* may already be deleted or is current branch */ }
157
+ }
158
+ }
159
+
160
+ // ── Cleanup Orphans ────────────────────────────────────────────────
161
+
162
+ /**
163
+ * List existing worktrees under .worktrees/ directory.
164
+ * Returns array of { path, branch } from `git worktree list --porcelain`.
165
+ */
166
+ export function listWorktrees(projectDir: string): Array<{ path: string; branch: string | null }> {
167
+ try {
168
+ const raw = execSync('git worktree list --porcelain', {
169
+ cwd: projectDir,
170
+ encoding: 'utf-8',
171
+ timeout: 5_000,
172
+ });
173
+
174
+ const worktrees: Array<{ path: string; branch: string | null }> = [];
175
+ let current: { path: string; branch: string | null } | null = null;
176
+
177
+ for (const line of raw.split('\n')) {
178
+ if (line.startsWith('worktree ')) {
179
+ if (current) worktrees.push(current);
180
+ current = { path: line.slice('worktree '.length).trim(), branch: null };
181
+ } else if (line.startsWith('branch ') && current) {
182
+ // branch refs/heads/pipeline/xxx/task-yyy
183
+ const ref = line.slice('branch '.length).trim();
184
+ current.branch = ref.replace('refs/heads/', '');
185
+ }
186
+ }
187
+ if (current) worktrees.push(current);
188
+
189
+ // Filter to only our managed worktrees (normalize slashes for Windows compat)
190
+ const worktreeBase = join(projectDir, WORKTREE_DIR).replace(/\\/g, '/');
191
+ return worktrees.filter(w => w.path.replace(/\\/g, '/').startsWith(worktreeBase));
192
+ } catch {
193
+ return [];
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Extract taskId from a worktree path.
199
+ * Path format: .../task-<shortId>
200
+ */
201
+ export function extractTaskIdFromPath(worktreePath: string): string | null {
202
+ const name = basename(worktreePath);
203
+ const match = name.match(/^task-([a-f0-9]+)$/);
204
+ return match ? match[1] : null;
205
+ }
206
+
207
+ /**
208
+ * Clean up orphaned worktrees — those whose tasks no longer exist or are terminal.
209
+ * Call this at coordinator startup.
210
+ *
211
+ * @param isTaskTerminal - callback to check if a task (by short ID prefix) is done/nonexistent
212
+ * @returns number of worktrees removed
213
+ */
214
+ export function cleanupOrphanWorktrees(
215
+ projectDir: string,
216
+ isTaskTerminal: (shortId: string) => boolean,
217
+ ): number {
218
+ const worktrees = listWorktrees(projectDir);
219
+ let removed = 0;
220
+
221
+ for (const wt of worktrees) {
222
+ const shortId = extractTaskIdFromPath(wt.path);
223
+ if (!shortId) continue;
224
+
225
+ if (isTaskTerminal(shortId)) {
226
+ removeWorktree(projectDir, wt.path, wt.branch ?? undefined);
227
+ removed++;
228
+ }
229
+ }
230
+
231
+ return removed;
232
+ }