@jmylchreest/aide-plugin 0.0.60 → 0.0.62
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/package.json +1 -1
- package/src/cli/codex-config.ts +15 -0
- package/src/cli/hook.ts +2 -0
- package/src/core/mcp-sync.ts +0 -16
- package/src/core/read-tracking.ts +38 -4
- package/src/core/search-enrichment.ts +208 -0
- package/src/core/tool-observe.ts +284 -0
- package/src/hooks/hud-updater.ts +0 -23
- package/src/hooks/search-enrichment.ts +111 -0
- package/src/hooks/session-start.ts +23 -0
- package/src/hooks/skill-injector.ts +30 -1
- package/src/hooks/subagent-tracker.ts +7 -85
- package/src/hooks/task-completed.ts +0 -17
- package/src/hooks/tool-observe.ts +97 -0
- package/src/lib/aide-downloader.ts +0 -8
- package/src/lib/hook-utils.ts +0 -13
- package/src/lib/logger.ts +1 -68
- package/src/lib/usage.ts +1 -30
- package/src/opencode/hooks.ts +36 -14
- package/src/lib/worktree.ts +0 -457
package/src/opencode/hooks.ts
CHANGED
|
@@ -50,7 +50,8 @@ import { evaluateToolUse, isToolDenied } from "../core/tool-enforcement.js";
|
|
|
50
50
|
import { checkPersistence, getActiveMode } from "../core/persistence-logic.js";
|
|
51
51
|
import { checkWriteGuard } from "../core/write-guard.js";
|
|
52
52
|
import { checkSmartReadHint } from "../core/context-guard.js";
|
|
53
|
-
import {
|
|
53
|
+
import { checkSearchEnrichment } from "../core/search-enrichment.js";
|
|
54
|
+
import { recordToolEvent } from "../core/tool-observe.js";
|
|
54
55
|
import {
|
|
55
56
|
checkComments,
|
|
56
57
|
getCheckableFilePath,
|
|
@@ -739,6 +740,21 @@ function createToolBeforeHandler(
|
|
|
739
740
|
debug(SOURCE, `Smart read hint check failed (non-fatal): ${err}`);
|
|
740
741
|
}
|
|
741
742
|
|
|
743
|
+
// Search enrichment: append code index context for grep calls
|
|
744
|
+
try {
|
|
745
|
+
const enrichResult = checkSearchEnrichment(
|
|
746
|
+
input.tool,
|
|
747
|
+
(_output.args || {}) as Record<string, unknown>,
|
|
748
|
+
state.cwd,
|
|
749
|
+
state.binary,
|
|
750
|
+
);
|
|
751
|
+
if (enrichResult.shouldEnrich && enrichResult.enrichment) {
|
|
752
|
+
debug(SOURCE, `Search enrichment for ${input.tool}: ${enrichResult.enrichment.length} chars`);
|
|
753
|
+
}
|
|
754
|
+
} catch (err) {
|
|
755
|
+
debug(SOURCE, `Search enrichment check failed (non-fatal): ${err}`);
|
|
756
|
+
}
|
|
757
|
+
|
|
742
758
|
// Track tool use
|
|
743
759
|
if (!state.binary) return;
|
|
744
760
|
|
|
@@ -782,25 +798,31 @@ function createToolAfterHandler(
|
|
|
782
798
|
debug(SOURCE, `Partial memory write failed (non-fatal): ${err}`);
|
|
783
799
|
}
|
|
784
800
|
|
|
785
|
-
// Record
|
|
801
|
+
// Record tool call as an observe event (mirror of MCP middleware on the
|
|
802
|
+
// Go side — together they give complete tool-call coverage). Also handles
|
|
803
|
+
// smart-read-hint state via recordFileRead inside the core module.
|
|
786
804
|
try {
|
|
787
805
|
const toolArgs = (_output.metadata?.args || {}) as Record<
|
|
788
806
|
string,
|
|
789
807
|
unknown
|
|
790
808
|
>;
|
|
791
|
-
|
|
792
|
-
input.tool
|
|
793
|
-
toolArgs
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
809
|
+
recordToolEvent(state.binary, state.cwd, {
|
|
810
|
+
toolName: input.tool,
|
|
811
|
+
toolInput: toolArgs as {
|
|
812
|
+
file_path?: string;
|
|
813
|
+
offset?: number;
|
|
814
|
+
limit?: number;
|
|
815
|
+
command?: string;
|
|
816
|
+
pattern?: string;
|
|
817
|
+
},
|
|
818
|
+
// OpenCode's tool.execute.after gives us the rendered output text
|
|
819
|
+
// directly — that's what gets fed back to the model, so it's the
|
|
820
|
+
// right thing to estimate output-sized tool cost from.
|
|
821
|
+
toolResponse: _output.output,
|
|
822
|
+
sessionId: input.sessionID,
|
|
823
|
+
});
|
|
802
824
|
} catch (err) {
|
|
803
|
-
debug(SOURCE, `
|
|
825
|
+
debug(SOURCE, `Tool observe recording failed (non-fatal): ${err}`);
|
|
804
826
|
}
|
|
805
827
|
|
|
806
828
|
// Context pruning: dedup/supersede/purge tool outputs
|
package/src/lib/worktree.ts
DELETED
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git Worktree Manager
|
|
3
|
-
*
|
|
4
|
-
* STATUS: UTILITY LIBRARY - Not yet integrated into hooks
|
|
5
|
-
*
|
|
6
|
-
* This library manages git worktrees for parallel agent execution in swarm mode.
|
|
7
|
-
* Each agent gets its own worktree to avoid file conflicts when multiple agents
|
|
8
|
-
* work on different tasks simultaneously.
|
|
9
|
-
*
|
|
10
|
-
* Currently, worktree management is handled by the aide Go binary directly,
|
|
11
|
-
* called via the CLI from swarm mode orchestration. This TypeScript library
|
|
12
|
-
* provides an alternative implementation for hooks or plugins that need
|
|
13
|
-
* worktree management without the aide binary.
|
|
14
|
-
*
|
|
15
|
-
* Future integration:
|
|
16
|
-
* - swarm skill could use this for TypeScript-native worktree management
|
|
17
|
-
* - Automated cleanup of stale worktrees on session start
|
|
18
|
-
* - Integration with subagent-tracker for per-agent worktree assignment
|
|
19
|
-
*
|
|
20
|
-
* The Go implementation is currently preferred because:
|
|
21
|
-
* 1. It integrates with aide's task and memory systems
|
|
22
|
-
* 2. Git operations are faster from native code
|
|
23
|
-
* 3. Error handling and edge cases are better tested
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { execFileSync } from "child_process";
|
|
27
|
-
import {
|
|
28
|
-
existsSync,
|
|
29
|
-
mkdirSync,
|
|
30
|
-
readFileSync,
|
|
31
|
-
writeFileSync,
|
|
32
|
-
rmSync,
|
|
33
|
-
readdirSync,
|
|
34
|
-
statSync,
|
|
35
|
-
} from "fs";
|
|
36
|
-
import { join } from "path";
|
|
37
|
-
import { debug } from "./logger.js";
|
|
38
|
-
|
|
39
|
-
const SOURCE = "worktree";
|
|
40
|
-
|
|
41
|
-
export type WorktreeStatus = "active" | "agent-complete" | "merged";
|
|
42
|
-
|
|
43
|
-
export interface Worktree {
|
|
44
|
-
name: string;
|
|
45
|
-
path: string;
|
|
46
|
-
branch: string;
|
|
47
|
-
taskId?: string;
|
|
48
|
-
agentId?: string;
|
|
49
|
-
status: WorktreeStatus;
|
|
50
|
-
createdAt: string;
|
|
51
|
-
completedAt?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface WorktreeState {
|
|
55
|
-
active: Worktree[];
|
|
56
|
-
baseBranch: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const WORKTREE_DIR = ".aide/worktrees";
|
|
60
|
-
const STATE_FILE = ".aide/state/worktrees.json";
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Validate and sanitize an ID (taskId, agentId, branch name)
|
|
64
|
-
* Only allows alphanumeric characters, hyphens, and underscores
|
|
65
|
-
*/
|
|
66
|
-
function sanitizeId(id: string): string {
|
|
67
|
-
// Remove any characters that aren't alphanumeric, hyphens, or underscores
|
|
68
|
-
return id.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Load worktree state
|
|
73
|
-
*/
|
|
74
|
-
export function loadWorktreeState(cwd: string): WorktreeState {
|
|
75
|
-
const statePath = join(cwd, STATE_FILE);
|
|
76
|
-
if (existsSync(statePath)) {
|
|
77
|
-
try {
|
|
78
|
-
return JSON.parse(readFileSync(statePath, "utf-8"));
|
|
79
|
-
} catch {
|
|
80
|
-
// Return default
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
active: [],
|
|
85
|
-
baseBranch: getCurrentBranch(cwd),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Save worktree state
|
|
91
|
-
*/
|
|
92
|
-
export function saveWorktreeState(cwd: string, state: WorktreeState): void {
|
|
93
|
-
const stateDir = join(cwd, ".aide", "state");
|
|
94
|
-
if (!existsSync(stateDir)) {
|
|
95
|
-
mkdirSync(stateDir, { recursive: true });
|
|
96
|
-
}
|
|
97
|
-
writeFileSync(join(cwd, STATE_FILE), JSON.stringify(state, null, 2));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Get current git branch
|
|
102
|
-
*/
|
|
103
|
-
export function getCurrentBranch(cwd: string): string {
|
|
104
|
-
try {
|
|
105
|
-
return execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
106
|
-
cwd,
|
|
107
|
-
encoding: "utf-8",
|
|
108
|
-
}).trim();
|
|
109
|
-
} catch {
|
|
110
|
-
return "main";
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Check if we're in a git repository
|
|
116
|
-
*/
|
|
117
|
-
export function isGitRepo(cwd: string): boolean {
|
|
118
|
-
try {
|
|
119
|
-
execFileSync("git", ["rev-parse", "--git-dir"], { cwd, stdio: "ignore" });
|
|
120
|
-
return true;
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Create a new worktree for an agent
|
|
128
|
-
*/
|
|
129
|
-
export function createWorktree(
|
|
130
|
-
cwd: string,
|
|
131
|
-
taskId: string,
|
|
132
|
-
agentId: string,
|
|
133
|
-
): Worktree | null {
|
|
134
|
-
if (!isGitRepo(cwd)) {
|
|
135
|
-
debug(SOURCE, "Not a git repository");
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const state = loadWorktreeState(cwd);
|
|
140
|
-
const worktreeDir = join(cwd, WORKTREE_DIR);
|
|
141
|
-
|
|
142
|
-
// Ensure worktree directory exists
|
|
143
|
-
if (!existsSync(worktreeDir)) {
|
|
144
|
-
mkdirSync(worktreeDir, { recursive: true });
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Generate unique names with sanitized IDs
|
|
148
|
-
// Use feat/<task>-<agent> branch naming for cleaner git history
|
|
149
|
-
const safeTaskId = sanitizeId(taskId).slice(0, 8);
|
|
150
|
-
const safeAgentId = sanitizeId(agentId);
|
|
151
|
-
const name = `${safeTaskId}-${safeAgentId}`;
|
|
152
|
-
const branch = `feat/${name}`;
|
|
153
|
-
const worktreePath = join(worktreeDir, name);
|
|
154
|
-
|
|
155
|
-
// Check if worktree already exists
|
|
156
|
-
if (existsSync(worktreePath)) {
|
|
157
|
-
const existing = state.active.find((w) => w.path === worktreePath);
|
|
158
|
-
if (existing) {
|
|
159
|
-
return existing;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
// Create branch and worktree using execFileSync with argument array
|
|
165
|
-
execFileSync(
|
|
166
|
-
"git",
|
|
167
|
-
["worktree", "add", "-b", branch, worktreePath, "HEAD"],
|
|
168
|
-
{
|
|
169
|
-
cwd,
|
|
170
|
-
stdio: "pipe",
|
|
171
|
-
},
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
const worktree: Worktree = {
|
|
175
|
-
name,
|
|
176
|
-
path: worktreePath,
|
|
177
|
-
branch,
|
|
178
|
-
taskId,
|
|
179
|
-
agentId,
|
|
180
|
-
status: "active",
|
|
181
|
-
createdAt: new Date().toISOString(),
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
// Update state
|
|
185
|
-
state.active.push(worktree);
|
|
186
|
-
saveWorktreeState(cwd, state);
|
|
187
|
-
|
|
188
|
-
return worktree;
|
|
189
|
-
} catch (error) {
|
|
190
|
-
debug(SOURCE, `Failed to create worktree: ${error}`);
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Remove a worktree
|
|
197
|
-
*/
|
|
198
|
-
export function removeWorktree(cwd: string, name: string): boolean {
|
|
199
|
-
const state = loadWorktreeState(cwd);
|
|
200
|
-
const worktree = state.active.find((w) => w.name === name);
|
|
201
|
-
|
|
202
|
-
if (!worktree) {
|
|
203
|
-
debug(SOURCE, `Worktree not found: ${name}`);
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
// Remove worktree using execFileSync with argument array
|
|
209
|
-
execFileSync("git", ["worktree", "remove", worktree.path, "--force"], {
|
|
210
|
-
cwd,
|
|
211
|
-
stdio: "pipe",
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Delete branch using execFileSync with argument array
|
|
215
|
-
execFileSync("git", ["branch", "-D", worktree.branch], {
|
|
216
|
-
cwd,
|
|
217
|
-
stdio: "pipe",
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// Update state
|
|
221
|
-
state.active = state.active.filter((w) => w.name !== name);
|
|
222
|
-
saveWorktreeState(cwd, state);
|
|
223
|
-
|
|
224
|
-
return true;
|
|
225
|
-
} catch (error) {
|
|
226
|
-
debug(SOURCE, `Failed to remove worktree: ${error}`);
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Merge worktree changes back to base branch
|
|
233
|
-
*/
|
|
234
|
-
export function mergeWorktree(cwd: string, name: string): boolean {
|
|
235
|
-
const state = loadWorktreeState(cwd);
|
|
236
|
-
const worktree = state.active.find((w) => w.name === name);
|
|
237
|
-
|
|
238
|
-
if (!worktree) {
|
|
239
|
-
debug(SOURCE, `Worktree not found: ${name}`);
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
// Checkout base branch - sanitize branch name just in case
|
|
245
|
-
const safeBranch = sanitizeId(state.baseBranch);
|
|
246
|
-
execFileSync("git", ["checkout", safeBranch], { cwd, stdio: "pipe" });
|
|
247
|
-
|
|
248
|
-
// Merge worktree branch - branch name was sanitized at creation
|
|
249
|
-
execFileSync("git", ["merge", worktree.branch, "--no-edit"], {
|
|
250
|
-
cwd,
|
|
251
|
-
stdio: "pipe",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return true;
|
|
255
|
-
} catch (error) {
|
|
256
|
-
debug(SOURCE, `Failed to merge worktree: ${error}`);
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Cleanup all worktrees
|
|
263
|
-
*/
|
|
264
|
-
export function cleanupWorktrees(cwd: string): void {
|
|
265
|
-
const state = loadWorktreeState(cwd);
|
|
266
|
-
|
|
267
|
-
for (const worktree of [...state.active]) {
|
|
268
|
-
removeWorktree(cwd, worktree.name);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Prune any orphaned worktrees
|
|
272
|
-
try {
|
|
273
|
-
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe" });
|
|
274
|
-
} catch {
|
|
275
|
-
// Ignore errors
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* List active worktrees
|
|
281
|
-
*/
|
|
282
|
-
export function listWorktrees(cwd: string): Worktree[] {
|
|
283
|
-
const state = loadWorktreeState(cwd);
|
|
284
|
-
return state.active;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Get worktree for a specific task
|
|
289
|
-
*/
|
|
290
|
-
export function getWorktreeForTask(
|
|
291
|
-
cwd: string,
|
|
292
|
-
taskId: string,
|
|
293
|
-
): Worktree | undefined {
|
|
294
|
-
const state = loadWorktreeState(cwd);
|
|
295
|
-
return state.active.find((w) => w.taskId === taskId);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Get worktree for a specific agent
|
|
300
|
-
*/
|
|
301
|
-
export function getWorktreeForAgent(
|
|
302
|
-
cwd: string,
|
|
303
|
-
agentId: string,
|
|
304
|
-
): Worktree | undefined {
|
|
305
|
-
const state = loadWorktreeState(cwd);
|
|
306
|
-
return state.active.find((w) => w.agentId === agentId);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Register an existing worktree that was created externally (e.g., via git CLI)
|
|
311
|
-
* This allows the hooks to track worktrees created by the orchestrator
|
|
312
|
-
*/
|
|
313
|
-
export function registerWorktree(
|
|
314
|
-
cwd: string,
|
|
315
|
-
worktreePath: string,
|
|
316
|
-
branch: string,
|
|
317
|
-
storyId: string,
|
|
318
|
-
agentId: string,
|
|
319
|
-
): Worktree | null {
|
|
320
|
-
if (!existsSync(worktreePath)) {
|
|
321
|
-
debug(SOURCE, `Worktree path does not exist: ${worktreePath}`);
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const state = loadWorktreeState(cwd);
|
|
326
|
-
|
|
327
|
-
// Check if already registered
|
|
328
|
-
const existing = state.active.find((w) => w.path === worktreePath);
|
|
329
|
-
if (existing) {
|
|
330
|
-
// Update agentId if different (agent may have been assigned later)
|
|
331
|
-
if (existing.agentId !== agentId) {
|
|
332
|
-
existing.agentId = agentId;
|
|
333
|
-
saveWorktreeState(cwd, state);
|
|
334
|
-
}
|
|
335
|
-
return existing;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Extract name from path
|
|
339
|
-
const name = worktreePath.split("/").pop() || storyId;
|
|
340
|
-
|
|
341
|
-
const worktree: Worktree = {
|
|
342
|
-
name,
|
|
343
|
-
path: worktreePath,
|
|
344
|
-
branch,
|
|
345
|
-
taskId: storyId,
|
|
346
|
-
agentId,
|
|
347
|
-
status: "active",
|
|
348
|
-
createdAt: new Date().toISOString(),
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
state.active.push(worktree);
|
|
352
|
-
saveWorktreeState(cwd, state);
|
|
353
|
-
|
|
354
|
-
return worktree;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Auto-discover worktrees in the standard location that aren't registered
|
|
359
|
-
* Scans .aide/worktrees/ for directories and registers them
|
|
360
|
-
*/
|
|
361
|
-
export function discoverWorktrees(cwd: string): Worktree[] {
|
|
362
|
-
const worktreeDir = join(cwd, WORKTREE_DIR);
|
|
363
|
-
if (!existsSync(worktreeDir)) {
|
|
364
|
-
return [];
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const state = loadWorktreeState(cwd);
|
|
368
|
-
const discovered: Worktree[] = [];
|
|
369
|
-
|
|
370
|
-
try {
|
|
371
|
-
const entries = readdirSync(worktreeDir);
|
|
372
|
-
|
|
373
|
-
for (const entry of entries) {
|
|
374
|
-
const entryPath = join(worktreeDir, entry);
|
|
375
|
-
if (!statSync(entryPath).isDirectory()) continue;
|
|
376
|
-
|
|
377
|
-
// Skip if already registered
|
|
378
|
-
if (state.active.find((w) => w.path === entryPath)) continue;
|
|
379
|
-
|
|
380
|
-
// Try to get the branch name from the worktree
|
|
381
|
-
try {
|
|
382
|
-
const branch = execFileSync(
|
|
383
|
-
"git",
|
|
384
|
-
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
385
|
-
{ cwd: entryPath, encoding: "utf-8" },
|
|
386
|
-
).trim();
|
|
387
|
-
|
|
388
|
-
const worktree: Worktree = {
|
|
389
|
-
name: entry,
|
|
390
|
-
path: entryPath,
|
|
391
|
-
branch,
|
|
392
|
-
taskId: entry, // Use directory name as story ID
|
|
393
|
-
status: "active",
|
|
394
|
-
createdAt: new Date().toISOString(),
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
state.active.push(worktree);
|
|
398
|
-
discovered.push(worktree);
|
|
399
|
-
} catch {
|
|
400
|
-
// Not a valid git worktree, skip
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (discovered.length > 0) {
|
|
405
|
-
saveWorktreeState(cwd, state);
|
|
406
|
-
}
|
|
407
|
-
} catch {
|
|
408
|
-
// Directory read failed
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return discovered;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Mark a worktree as agent-complete (ready for merge review)
|
|
416
|
-
* Called when the subagent finishes its work
|
|
417
|
-
*/
|
|
418
|
-
export function markWorktreeComplete(cwd: string, agentId: string): boolean {
|
|
419
|
-
const state = loadWorktreeState(cwd);
|
|
420
|
-
const worktree = state.active.find((w) => w.agentId === agentId);
|
|
421
|
-
|
|
422
|
-
if (!worktree) {
|
|
423
|
-
return false;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
worktree.status = "agent-complete";
|
|
427
|
-
worktree.completedAt = new Date().toISOString();
|
|
428
|
-
saveWorktreeState(cwd, state);
|
|
429
|
-
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Mark a worktree as merged
|
|
435
|
-
* Called after successful merge to main branch
|
|
436
|
-
*/
|
|
437
|
-
export function markWorktreeMerged(cwd: string, name: string): boolean {
|
|
438
|
-
const state = loadWorktreeState(cwd);
|
|
439
|
-
const worktree = state.active.find((w) => w.name === name);
|
|
440
|
-
|
|
441
|
-
if (!worktree) {
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
worktree.status = "merged";
|
|
446
|
-
saveWorktreeState(cwd, state);
|
|
447
|
-
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Get all worktrees ready for merge (status: agent-complete)
|
|
453
|
-
*/
|
|
454
|
-
export function getWorktreesReadyForMerge(cwd: string): Worktree[] {
|
|
455
|
-
const state = loadWorktreeState(cwd);
|
|
456
|
-
return state.active.filter((w) => w.status === "agent-complete");
|
|
457
|
-
}
|