@jmylchreest/aide-plugin 0.0.59 → 0.0.61

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.
@@ -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 { recordFileRead } from "../core/read-tracking.js";
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 file reads for smart-read-hint feature
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
- if (
792
- input.tool.toLowerCase() === "read" &&
793
- toolArgs.file_path &&
794
- state.binary
795
- ) {
796
- recordFileRead(
797
- state.binary,
798
- state.cwd,
799
- toolArgs.file_path as string,
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, `Read tracking failed (non-fatal): ${err}`);
825
+ debug(SOURCE, `Tool observe recording failed (non-fatal): ${err}`);
804
826
  }
805
827
 
806
828
  // Context pruning: dedup/supersede/purge tool outputs
@@ -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
- }