@jmylchreest/aide-plugin 0.0.57 → 0.0.58

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.
@@ -0,0 +1,525 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Subagent Tracker Hook (SubagentStart, SubagentStop)
4
+ *
5
+ * Tracks spawned subagents for HUD display and coordination.
6
+ * Registers agents in aide-memory with their type, model, and task.
7
+ * Also injects memory context for subagents (global preferences, decisions).
8
+ *
9
+ * SubagentStart data from Claude Code:
10
+ * - agent_id, agent_type, session_id, prompt
11
+ * - model, cwd, permission_mode
12
+ *
13
+ * SubagentStop data from Claude Code:
14
+ * - agent_id, agent_type, output, success
15
+ */
16
+
17
+ import { execFileSync } from "child_process";
18
+ import { basename } from "path";
19
+ import { Logger } from "../lib/logger.js";
20
+ import { readStdin, setMemoryState } from "../lib/hook-utils.js";
21
+ import { findAideBinary } from "../core/aide-client.js";
22
+ import { refreshHud } from "../lib/hud.js";
23
+ import {
24
+ getWorktreeForAgent,
25
+ markWorktreeComplete,
26
+ discoverWorktrees,
27
+ Worktree,
28
+ } from "../lib/worktree.js";
29
+
30
+ // Global logger instance
31
+ let log: Logger | null = null;
32
+
33
+ // Claude Code hook input format (uses hook_event_name, not event)
34
+ interface SubagentStartInput {
35
+ hook_event_name: "SubagentStart";
36
+ agent_id: string;
37
+ agent_type: string;
38
+ session_id: string;
39
+ transcript_path?: string;
40
+ cwd: string;
41
+ permission_mode?: string;
42
+ // Note: prompt and model are NOT provided by Claude Code
43
+ // We'll need to get these from PreToolUse if needed
44
+ }
45
+
46
+ interface SubagentStopInput {
47
+ hook_event_name: "SubagentStop";
48
+ agent_id: string;
49
+ agent_type: string;
50
+ session_id: string;
51
+ transcript_path?: string;
52
+ agent_transcript_path?: string;
53
+ stop_hook_active?: boolean;
54
+ cwd: string;
55
+ permission_mode?: string;
56
+ }
57
+
58
+ type HookInput = SubagentStartInput | SubagentStopInput;
59
+
60
+ interface HookOutput {
61
+ continue: boolean;
62
+ hookSpecificOutput?: {
63
+ hookEventName: string;
64
+ additionalContext?: string;
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Get project name from git remote or directory name
70
+ */
71
+ function getProjectName(cwd: string): string {
72
+ try {
73
+ // Try git remote first
74
+ const remoteUrl = execFileSync(
75
+ "git",
76
+ ["config", "--get", "remote.origin.url"],
77
+ {
78
+ cwd,
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ timeout: 2000,
81
+ },
82
+ )
83
+ .toString()
84
+ .trim();
85
+
86
+ // Extract repo name from URL
87
+ const match = remoteUrl.match(/[/:]([^/]+?)(?:\.git)?$/);
88
+ if (match) return match[1];
89
+ } catch (err) {
90
+ log?.debug(
91
+ `getProjectName: git remote failed (not a git repo or no remote): ${err}`,
92
+ );
93
+ }
94
+
95
+ // Fallback to directory name
96
+ return basename(cwd) || "unknown";
97
+ }
98
+
99
+ /**
100
+ * Fetch essential memories for subagent context injection
101
+ *
102
+ * Subagents get:
103
+ * - Global preferences (scope:global)
104
+ * - Project memories (project:<name>)
105
+ * - Project decisions
106
+ *
107
+ * This ensures subagents respect user preferences, project context,
108
+ * and architectural decisions.
109
+ */
110
+ function fetchSubagentMemories(cwd: string): {
111
+ global: string[];
112
+ project: string[];
113
+ decisions: string[];
114
+ } {
115
+ const result = {
116
+ global: [] as string[],
117
+ project: [] as string[],
118
+ decisions: [] as string[],
119
+ };
120
+
121
+ // Check for disable flag
122
+ if (process.env.AIDE_MEMORY_INJECT === "0") {
123
+ return result;
124
+ }
125
+
126
+ const binary = findAideBinary({
127
+ cwd,
128
+ pluginRoot: process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
129
+ });
130
+ if (!binary) {
131
+ return result;
132
+ }
133
+
134
+ const projectName = getProjectName(cwd);
135
+
136
+ // Fetch global memories (scope:global)
137
+ try {
138
+ const globalOutput = execFileSync(
139
+ binary,
140
+ [
141
+ "memory",
142
+ "list",
143
+ "--category=global",
144
+ "--tags=scope:global",
145
+ "--format=json",
146
+ ],
147
+ { cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
148
+ )
149
+ .toString()
150
+ .trim();
151
+
152
+ if (globalOutput && globalOutput !== "[]") {
153
+ const memories = JSON.parse(globalOutput);
154
+ result.global = memories.map((m: { content: string }) => m.content);
155
+ }
156
+ } catch (err) {
157
+ log?.debug(
158
+ `fetchSubagentMemories: global memory fetch failed (optional): ${err}`,
159
+ );
160
+ }
161
+
162
+ // Fetch project memories (project:<name>)
163
+ try {
164
+ const projectOutput = execFileSync(
165
+ binary,
166
+ ["memory", "list", `--tags=project:${projectName}`, "--format=json"],
167
+ { cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
168
+ )
169
+ .toString()
170
+ .trim();
171
+
172
+ if (projectOutput && projectOutput !== "[]") {
173
+ const memories = JSON.parse(projectOutput);
174
+ result.project = memories.map((m: { content: string }) => m.content);
175
+ }
176
+ } catch (err) {
177
+ log?.debug(
178
+ `fetchSubagentMemories: project memory fetch failed (optional): ${err}`,
179
+ );
180
+ }
181
+
182
+ // Fetch project decisions
183
+ try {
184
+ const decisionsOutput = execFileSync(
185
+ binary,
186
+ ["decision", "list", "--format=json"],
187
+ { cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 3000 },
188
+ )
189
+ .toString()
190
+ .trim();
191
+
192
+ if (decisionsOutput && decisionsOutput !== "[]") {
193
+ const decisions = JSON.parse(decisionsOutput);
194
+ result.decisions = decisions.map(
195
+ (d: { topic: string; value: string }) => `**${d.topic}**: ${d.value}`,
196
+ );
197
+ }
198
+ } catch (err) {
199
+ log?.debug(
200
+ `fetchSubagentMemories: decision fetch failed (optional): ${err}`,
201
+ );
202
+ }
203
+
204
+ return result;
205
+ }
206
+
207
+ /**
208
+ * Build context for subagent injection
209
+ */
210
+ function buildSubagentContext(
211
+ memories: {
212
+ global: string[];
213
+ project: string[];
214
+ decisions: string[];
215
+ },
216
+ worktree?: Worktree,
217
+ ): string {
218
+ const lines: string[] = [];
219
+
220
+ lines.push("<aide-subagent-context>");
221
+
222
+ // Inject worktree information if this is a swarm agent
223
+ if (worktree) {
224
+ lines.push("");
225
+ lines.push("## Swarm Worktree");
226
+ lines.push("");
227
+ lines.push(`You are working in an isolated git worktree for swarm mode.`);
228
+ lines.push(`- **Worktree Path**: ${worktree.path}`);
229
+ lines.push(`- **Branch**: ${worktree.branch}`);
230
+ lines.push(`- **Story ID**: ${worktree.taskId || "unknown"}`);
231
+ lines.push("");
232
+ lines.push(
233
+ `**IMPORTANT**: All file operations should be performed in: ${worktree.path}`,
234
+ );
235
+ lines.push(
236
+ `Commit your changes to the ${worktree.branch} branch when complete.`,
237
+ );
238
+ }
239
+
240
+ if (memories.global.length > 0) {
241
+ lines.push("");
242
+ lines.push("## User Preferences");
243
+ lines.push("");
244
+ for (const mem of memories.global) {
245
+ lines.push(`- ${mem}`);
246
+ }
247
+ }
248
+
249
+ if (memories.project.length > 0) {
250
+ lines.push("");
251
+ lines.push("## Project Context");
252
+ lines.push("");
253
+ for (const mem of memories.project) {
254
+ lines.push(`- ${mem}`);
255
+ }
256
+ }
257
+
258
+ if (memories.decisions.length > 0) {
259
+ lines.push("");
260
+ lines.push("## Project Decisions");
261
+ lines.push("");
262
+ for (const decision of memories.decisions) {
263
+ lines.push(`- ${decision}`);
264
+ }
265
+ }
266
+
267
+ // Always inject messaging protocol for agent coordination
268
+ lines.push("");
269
+ lines.push("## Agent Communication");
270
+ lines.push("");
271
+ lines.push("Use aide MCP messaging tools to coordinate with other agents:");
272
+ lines.push("");
273
+ lines.push("**Send messages** via `mcp__plugin_aide_aide__message_send`:");
274
+ lines.push("- `from`: Your agent ID (required)");
275
+ lines.push("- `to`: Target agent ID (omit to broadcast)");
276
+ lines.push("- `content`: Message text");
277
+ lines.push(
278
+ "- `type`: One of `status`, `request`, `response`, `blocker`, `completion`, `handoff`",
279
+ );
280
+ lines.push("");
281
+ lines.push("**Check messages** via `mcp__plugin_aide_aide__message_list`:");
282
+ lines.push("- `agent_id`: Your agent ID");
283
+ lines.push("");
284
+ lines.push("**Acknowledge** via `mcp__plugin_aide_aide__message_ack`:");
285
+ lines.push("- `message_id`: ID from message_list");
286
+ lines.push("- `agent_id`: Your agent ID");
287
+ lines.push("");
288
+ lines.push("**Protocol:**");
289
+ lines.push("- Send `status` message at each SDLC stage transition");
290
+ lines.push("- Send `blocker` when stuck and need help from another agent");
291
+ lines.push("- Send `completion` when your story/task is done");
292
+ lines.push("- Check messages at the start of each SDLC stage");
293
+
294
+ lines.push("");
295
+ lines.push("</aide-subagent-context>");
296
+ return lines.join("\n");
297
+ }
298
+
299
+ /**
300
+ * Set state in aide-memory for an agent (wrapper with logging)
301
+ */
302
+ function setAgentState(
303
+ cwd: string,
304
+ agentId: string,
305
+ key: string,
306
+ value: string,
307
+ ): boolean {
308
+ const truncatedValue = value.replace(/\n/g, " ").slice(0, 500);
309
+ log?.debug(
310
+ `setAgentState: setting ${key}="${truncatedValue}" for agent ${agentId}`,
311
+ );
312
+ const result = setMemoryState(cwd, key, truncatedValue, agentId);
313
+ if (!result) {
314
+ log?.warn(`setAgentState: failed to set ${key} for agent ${agentId}`);
315
+ }
316
+ return result;
317
+ }
318
+
319
+ /**
320
+ * Handle SubagentStart event
321
+ * Returns context to inject into the subagent
322
+ */
323
+ async function processSubagentStart(
324
+ data: SubagentStartInput,
325
+ ): Promise<string | undefined> {
326
+ const { agent_id, agent_type, session_id, cwd } = data;
327
+
328
+ log?.info(
329
+ `SubagentStart: agent_id=${agent_id}, type=${agent_type}, session=${session_id}`,
330
+ );
331
+
332
+ // Claude Code doesn't provide prompt/model in SubagentStart
333
+ // Use agent_type directly as the type
334
+ const type = agent_type;
335
+
336
+ log?.debug(`SubagentStart: registering type=${type}`);
337
+
338
+ // Register agent in aide-memory
339
+ // Note: modelTier is NOT stored - model instructions are injected into context instead
340
+ log?.start("registerAgent");
341
+ setAgentState(cwd, agent_id, "status", "running");
342
+ setAgentState(cwd, agent_id, "type", type);
343
+ setAgentState(cwd, agent_id, "startedAt", new Date().toISOString());
344
+ setAgentState(cwd, agent_id, "session", session_id); // Track which session owns this agent
345
+ log?.end("registerAgent");
346
+
347
+ // Refresh HUD to show the new running agent
348
+ log?.start("refreshHud");
349
+ refreshHud(cwd, session_id);
350
+ log?.end("refreshHud");
351
+
352
+ // Auto-discover any worktrees created by the orchestrator via git commands
353
+ // This ensures we track worktrees even if they weren't created via our library
354
+ log?.start("discoverWorktrees");
355
+ const discovered = discoverWorktrees(cwd);
356
+ if (discovered.length > 0) {
357
+ log?.info(`Auto-discovered ${discovered.length} worktrees`);
358
+ }
359
+ log?.end("discoverWorktrees", { discovered: discovered.length });
360
+
361
+ // Check if this agent has an associated worktree (swarm mode)
362
+ // Match by agent_id or by pattern in worktree name
363
+ log?.start("checkWorktree");
364
+ let worktree = getWorktreeForAgent(cwd, agent_id);
365
+
366
+ // If no direct match, try to match by agent_id pattern in worktree name
367
+ // This handles cases where worktree was created before agent_id was known
368
+ if (!worktree) {
369
+ const { loadWorktreeState } = await import("../lib/worktree.js");
370
+ const state = loadWorktreeState(cwd);
371
+ // Look for worktree with matching name pattern (e.g., "story-auth" matches "agent-auth")
372
+ const agentPattern = agent_id.replace(/^agent-/, "");
373
+ worktree = state.active.find(
374
+ (w) => w.name.includes(agentPattern) && !w.agentId,
375
+ );
376
+ if (worktree) {
377
+ // Assign this agent to the worktree
378
+ worktree.agentId = agent_id;
379
+ const { saveWorktreeState } = await import("../lib/worktree.js");
380
+ saveWorktreeState(cwd, state);
381
+ log?.info(`Assigned worktree ${worktree.name} to agent ${agent_id}`);
382
+ }
383
+ }
384
+
385
+ if (worktree) {
386
+ log?.info(
387
+ `Found worktree for agent ${agent_id}: ${worktree.path} (branch: ${worktree.branch})`,
388
+ );
389
+ }
390
+ log?.end("checkWorktree", { hasWorktree: !!worktree });
391
+
392
+ // Fetch memories for subagent context injection
393
+ log?.start("fetchMemories");
394
+ const memories = fetchSubagentMemories(cwd);
395
+ log?.end("fetchMemories", {
396
+ globalCount: memories.global.length,
397
+ projectCount: memories.project.length,
398
+ decisionCount: memories.decisions.length,
399
+ });
400
+
401
+ // Always build and inject context (messaging section is unconditional)
402
+ const context = buildSubagentContext(memories, worktree);
403
+ log?.info(
404
+ `Injecting context for subagent: ${memories.global.length} preferences, ${memories.project.length} project, ${memories.decisions.length} decisions, worktree=${!!worktree}`,
405
+ );
406
+ return context;
407
+ }
408
+
409
+ /**
410
+ * Handle SubagentStop event
411
+ */
412
+ async function processSubagentStop(data: SubagentStopInput): Promise<void> {
413
+ const { agent_id, session_id, cwd, stop_hook_active } = data;
414
+
415
+ log?.info(
416
+ `SubagentStop: agent_id=${agent_id}, session=${session_id}, stop_hook_active=${stop_hook_active}`,
417
+ );
418
+
419
+ // Mark as completed (Claude Code doesn't provide success/failure status)
420
+ log?.start("updateAgentStatus");
421
+ setAgentState(cwd, agent_id, "status", "completed");
422
+ setAgentState(cwd, agent_id, "endedAt", new Date().toISOString());
423
+ log?.end("updateAgentStatus");
424
+
425
+ // Mark worktree as agent-complete if this agent had one (swarm mode)
426
+ // The worktree stays for merge review - cleanup happens after worktree-resolve
427
+ log?.start("checkWorktreeComplete");
428
+ const worktreeMarked = markWorktreeComplete(cwd, agent_id);
429
+ if (worktreeMarked) {
430
+ log?.info(
431
+ `Marked worktree as agent-complete for ${agent_id} - ready for merge review`,
432
+ );
433
+ }
434
+ log?.end("checkWorktreeComplete", { worktreeMarked });
435
+
436
+ // Refresh HUD to remove the completed agent
437
+ log?.start("refreshHud");
438
+ refreshHud(cwd, session_id);
439
+ log?.end("refreshHud");
440
+
441
+ log?.debug(`SubagentStop: agent ${agent_id} marked as completed`);
442
+ }
443
+
444
+ async function main(): Promise<void> {
445
+ try {
446
+ const input = await readStdin();
447
+ if (!input.trim()) {
448
+ console.log(JSON.stringify({ continue: true }));
449
+ return;
450
+ }
451
+
452
+ const data: HookInput = JSON.parse(input);
453
+ const cwd = data.cwd || process.cwd();
454
+
455
+ // Initialize logger
456
+ log = new Logger("subagent-tracker", cwd);
457
+ log.start("total");
458
+ log.info(`Received event: ${data.hook_event_name}`);
459
+ log.debug(`Full input: ${JSON.stringify(data, null, 2)}`);
460
+
461
+ let additionalContext: string | undefined;
462
+
463
+ // Dispatch based on event type from input (Claude Code uses hook_event_name)
464
+ if (data.hook_event_name === "SubagentStart") {
465
+ additionalContext = await processSubagentStart(
466
+ data as SubagentStartInput,
467
+ );
468
+ } else if (data.hook_event_name === "SubagentStop") {
469
+ await processSubagentStop(data as SubagentStopInput);
470
+ } else {
471
+ // TypeScript narrows to never here, but handle unexpected events gracefully
472
+ log.warn(
473
+ `Unknown event type: ${(data as { hook_event_name?: string }).hook_event_name || "undefined"}`,
474
+ );
475
+ }
476
+
477
+ log.end("total");
478
+ log.flush();
479
+
480
+ // Output with optional context injection for subagents
481
+ const output: HookOutput = { continue: true };
482
+ if (additionalContext) {
483
+ output.hookSpecificOutput = {
484
+ hookEventName: "SubagentStart",
485
+ additionalContext,
486
+ };
487
+ }
488
+
489
+ console.log(JSON.stringify(output));
490
+ } catch (error) {
491
+ // Log error but don't block
492
+ if (log) {
493
+ log.error("Subagent tracker failed", error);
494
+ log.flush();
495
+ }
496
+ console.log(JSON.stringify({ continue: true }));
497
+ }
498
+ }
499
+
500
+ process.on("uncaughtException", (err) => {
501
+ if (log) {
502
+ log.error(`UNCAUGHT EXCEPTION: ${err}`);
503
+ log.flush();
504
+ }
505
+ try {
506
+ console.log(JSON.stringify({ continue: true }));
507
+ } catch {
508
+ console.log('{"continue":true}');
509
+ }
510
+ process.exit(0);
511
+ });
512
+ process.on("unhandledRejection", (reason) => {
513
+ if (log) {
514
+ log.error(`UNHANDLED REJECTION: ${reason}`);
515
+ log.flush();
516
+ }
517
+ try {
518
+ console.log(JSON.stringify({ continue: true }));
519
+ } catch {
520
+ console.log('{"continue":true}');
521
+ }
522
+ process.exit(0);
523
+ });
524
+
525
+ main();