@jmylchreest/aide-plugin 0.1.1 → 0.1.2

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 (35) hide show
  1. package/package.json +1 -1
  2. package/skills/reflect/SKILL.md +24 -8
  3. package/src/cli/codex-config.ts +15 -0
  4. package/src/core/context-pruning/dedup.ts +7 -2
  5. package/src/core/context-pruning/tracker.ts +0 -23
  6. package/src/core/memory-query.ts +63 -0
  7. package/src/core/partial-memory.ts +23 -105
  8. package/src/core/session-checkpoint-logic.ts +170 -0
  9. package/src/core/session-resume-logic.ts +104 -0
  10. package/src/core/session-summary-logic.ts +16 -41
  11. package/src/core/session-text.ts +60 -0
  12. package/src/core/tool-observe.ts +53 -14
  13. package/src/hooks/agent-cleanup.ts +11 -28
  14. package/src/hooks/agent-signals.ts +11 -18
  15. package/src/hooks/comment-checker.ts +14 -31
  16. package/src/hooks/context-guard.ts +17 -31
  17. package/src/hooks/context-pruning.ts +13 -30
  18. package/src/hooks/hud-updater.ts +11 -29
  19. package/src/hooks/permission-handler.ts +14 -25
  20. package/src/hooks/persistence.ts +15 -31
  21. package/src/hooks/pre-compact.ts +52 -38
  22. package/src/hooks/pre-tool-enforcer.ts +14 -30
  23. package/src/hooks/reflect.ts +15 -8
  24. package/src/hooks/search-enrichment.ts +12 -29
  25. package/src/hooks/session-end.ts +60 -11
  26. package/src/hooks/session-start.ts +68 -30
  27. package/src/hooks/session-summary.ts +11 -27
  28. package/src/hooks/skill-injector.ts +15 -30
  29. package/src/hooks/subagent-tracker.ts +44 -39
  30. package/src/hooks/task-completed.ts +6 -9
  31. package/src/hooks/tool-observe.ts +27 -32
  32. package/src/hooks/tool-tracker.ts +11 -28
  33. package/src/hooks/write-guard.ts +12 -29
  34. package/src/lib/hook-utils.ts +48 -0
  35. package/src/lib/project-root.ts +30 -15
@@ -26,8 +26,16 @@ const T0 = performance.now();
26
26
  // Output continue IMMEDIATELY — before require(), before anything.
27
27
  console.log(JSON.stringify({ continue: true }));
28
28
 
29
- const { spawn, execFileSync } = require("child_process") as typeof import("child_process");
30
- const { existsSync, realpathSync, appendFileSync, mkdirSync, readFileSync, statSync } = require("fs") as typeof import("fs");
29
+ const { spawn, execFileSync } =
30
+ require("child_process") as typeof import("child_process");
31
+ const {
32
+ existsSync,
33
+ realpathSync,
34
+ appendFileSync,
35
+ mkdirSync,
36
+ readFileSync,
37
+ statSync,
38
+ } = require("fs") as typeof import("fs");
31
39
  const { join, dirname } = require("path") as typeof import("path");
32
40
  const whichSync = (require("which") as typeof import("which")).sync;
33
41
 
@@ -41,7 +49,9 @@ function resolveRoot(cwd: string): string {
41
49
  if (override) {
42
50
  try {
43
51
  if (statSync(override).isDirectory()) return override;
44
- } catch { /* fall through */ }
52
+ } catch {
53
+ /* fall through */
54
+ }
45
55
  }
46
56
  const candidates: { dir: string; hasAide: boolean; hasVCS: boolean }[] = [];
47
57
  let dir = cwd;
@@ -49,7 +59,10 @@ function resolveRoot(cwd: string): string {
49
59
  const hasAide = existsSync(join(dir, ".aide"));
50
60
  let hasVCS = false;
51
61
  for (const m of [".git", ".hg", ".svn", ".bzr", ".fossil"]) {
52
- if (existsSync(join(dir, m))) { hasVCS = true; break; }
62
+ if (existsSync(join(dir, m))) {
63
+ hasVCS = true;
64
+ break;
65
+ }
53
66
  }
54
67
  if (hasAide || hasVCS) candidates.push({ dir, hasAide, hasVCS });
55
68
  const parent = dirname(dir);
@@ -79,15 +92,22 @@ function log(cwd: string, msg: string): void {
79
92
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
80
93
  const line = `[${new Date().toISOString()}] [session-end] ${ms()} ${msg}\n`;
81
94
  appendFileSync(join(logDir, "session-end.log"), line);
82
- } catch { /* best effort */ }
95
+ } catch {
96
+ /* best effort */
97
+ }
83
98
  }
84
99
 
85
100
  /** Find aide binary — inline, no external module imports. */
86
101
  function findBinary(cwd?: string): string | null {
87
- const pluginRoot = process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
102
+ const pluginRoot =
103
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT;
88
104
  let resolvedRoot = pluginRoot;
89
105
  if (resolvedRoot) {
90
- try { resolvedRoot = realpathSync(resolvedRoot); } catch { /* symlink may not resolve */ }
106
+ try {
107
+ resolvedRoot = realpathSync(resolvedRoot);
108
+ } catch {
109
+ /* symlink may not resolve */
110
+ }
91
111
  const p = join(resolvedRoot, "bin", "aide");
92
112
  if (existsSync(p)) return p;
93
113
  }
@@ -97,7 +117,9 @@ function findBinary(cwd?: string): string | null {
97
117
  }
98
118
  try {
99
119
  return whichSync("aide", { nothrow: true });
100
- } catch { return null; }
120
+ } catch {
121
+ return null;
122
+ }
101
123
  }
102
124
 
103
125
  function main(): void {
@@ -143,15 +165,40 @@ function main(): void {
143
165
  return;
144
166
  }
145
167
 
168
+ // Emit a lifecycle trigger so SessionEnd is traceable in the dashboard,
169
+ // symmetric with session-start and subagent-start/stop. Inlined (no ES
170
+ // import) to keep this hook's startup cheap. Fire-and-forget.
171
+ try {
172
+ execFileSync(
173
+ binary,
174
+ [
175
+ "observe",
176
+ "record",
177
+ "--kind=session",
178
+ "--name=session-end",
179
+ "--category=lifecycle",
180
+ `--session=${sessionId}`,
181
+ ],
182
+ { cwd, timeout: 3000, stdio: ["pipe", "pipe", "pipe"] },
183
+ );
184
+ } catch {
185
+ // Non-fatal telemetry — never block session end.
186
+ }
187
+
146
188
  // Mode guard: skip cleanup if autopilot is active (session is continuing,
147
189
  // not ending). This matters when Codex CLI invokes session-end from Stop
148
190
  // hook since there's no separate SessionEnd event.
149
191
  try {
150
192
  const mode = execFileSync(binary, ["state", "get", "mode"], {
151
- cwd, timeout: 500, encoding: "utf-8",
193
+ cwd,
194
+ timeout: 500,
195
+ encoding: "utf-8",
152
196
  }).trim();
153
197
  if (mode === "autopilot") {
154
- log(cwd, "autopilot mode active, skipping cleanup (session continuing)");
198
+ log(
199
+ cwd,
200
+ "autopilot mode active, skipping cleanup (session continuing)",
201
+ );
155
202
  return;
156
203
  }
157
204
  } catch {
@@ -160,7 +207,9 @@ function main(): void {
160
207
 
161
208
  log(cwd, `spawning cleanup: session end --session=${sessionId}`);
162
209
  const child = spawn(binary, ["session", "end", `--session=${sessionId}`], {
163
- cwd, detached: true, stdio: "ignore",
210
+ cwd,
211
+ detached: true,
212
+ stdio: "ignore",
164
213
  });
165
214
  child.unref();
166
215
  log(cwd, "cleanup spawned (detached)");
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  /**
3
3
  * Session Start Hook (SessionStart)
4
4
  *
@@ -24,10 +24,20 @@ import { join, dirname } from "path";
24
24
  import { fileURLToPath } from "url";
25
25
  import { homedir } from "os";
26
26
  import { Logger, debug, setDebugCwd } from "../lib/logger.js";
27
- import { readStdin, detectPlatform, isFalsy } from "../lib/hook-utils.js";
27
+ import {
28
+ readStdin,
29
+ detectPlatform,
30
+ isFalsy,
31
+ emitHookResult,
32
+ installHookSafetyNet,
33
+ } from "../lib/hook-utils.js";
28
34
  import { findAideBinary, ensureAideBinary } from "../lib/aide-downloader.js";
29
35
  import { findProjectRoot } from "../lib/project-root.js";
30
- import { emitInjectionEvent } from "../core/read-tracking.js";
36
+ import {
37
+ emitInjectionEvent,
38
+ recordObserveEvent,
39
+ } from "../core/read-tracking.js";
40
+ import { buildResumeContext } from "../core/session-resume-logic.js";
31
41
  import {
32
42
  ensureDirectories as coreEnsureDirectories,
33
43
  loadConfig as coreLoadConfig,
@@ -58,10 +68,13 @@ interface HookInput {
58
68
  cwd: string;
59
69
  transcript_path?: string;
60
70
  permission_mode?: string;
71
+ /** How the session began: startup | resume | clear | compact */
72
+ source?: string;
61
73
  }
62
74
 
63
75
  interface HookOutput {
64
76
  continue: boolean;
77
+ suppressOutput?: boolean;
65
78
  hookSpecificOutput?: {
66
79
  hookEventName: string;
67
80
  additionalContext?: string;
@@ -189,7 +202,11 @@ function installHudWrapper(log: Logger): void {
189
202
 
190
203
  // Copy wrapper script
191
204
  copyFileSync(wrapperSrc, wrapperDest);
192
- try { chmodSync(wrapperDest, 0o755); } catch { /* no-op on Windows */ }
205
+ try {
206
+ chmodSync(wrapperDest, 0o755);
207
+ } catch {
208
+ /* no-op on Windows */
209
+ }
193
210
 
194
211
  log.info(`Installed HUD wrapper to ${wrapperDest}`);
195
212
  log.end("installHudWrapper", { success: true, path: wrapperDest });
@@ -301,28 +318,8 @@ function debugLog(msg: string): void {
301
318
  debug(SOURCE, msg);
302
319
  }
303
320
 
304
- // Ensure we always output valid JSON, even on catastrophic errors
305
- function outputContinue(): void {
306
- try {
307
- console.log(JSON.stringify({ continue: true }));
308
- } catch {
309
- // Last resort - raw JSON string
310
- console.log('{"continue":true}');
311
- }
312
- }
313
-
314
321
  // Global error handlers to prevent hook crashes without JSON output
315
- process.on("uncaughtException", (err) => {
316
- debugLog(`UNCAUGHT EXCEPTION: ${err}`);
317
- outputContinue();
318
- process.exit(0);
319
- });
320
-
321
- process.on("unhandledRejection", (reason) => {
322
- debugLog(`UNHANDLED REJECTION: ${reason}`);
323
- outputContinue();
324
- process.exit(0);
325
- });
322
+ installHookSafetyNet(SOURCE);
326
323
 
327
324
  async function main(): Promise<void> {
328
325
  let log: Logger | null = null;
@@ -337,7 +334,7 @@ async function main(): Promise<void> {
337
334
 
338
335
  if (!input.trim()) {
339
336
  debugLog("Empty input, exiting");
340
- console.log(JSON.stringify({ continue: true }));
337
+ emitHookResult();
341
338
  return;
342
339
  }
343
340
 
@@ -356,7 +353,7 @@ async function main(): Promise<void> {
356
353
  `Set \`requireGit\`: false in ~/.aide/config/aide.json to allow ` +
357
354
  `init in arbitrary directories. Skipping AIDE bootstrap.\n`,
358
355
  );
359
- console.log(JSON.stringify({ continue: true }));
356
+ emitHookResult();
360
357
  return;
361
358
  }
362
359
  process.stderr.write(
@@ -485,12 +482,52 @@ async function main(): Promise<void> {
485
482
  // Build welcome context with injected memories
486
483
  debugLog("buildWelcomeContext starting...");
487
484
  log.start("buildWelcomeContext");
488
- const context = buildWelcomeContext(state, memories, notices);
485
+ let context = buildWelcomeContext(state, memories, notices);
489
486
  log.end("buildWelcomeContext");
490
487
  debugLog(`buildWelcomeContext complete (${Date.now() - hookStart}ms)`);
491
488
 
489
+ // On resume/compact, re-inject the last session checkpoint so the agent
490
+ // rebuilds working context instead of relearning it (MiMo-style context
491
+ // reconstruction, done at the SessionStart boundary).
492
+ try {
493
+ const resumeBinary = findAideBinary(cwd);
494
+ if (resumeBinary) {
495
+ const resume = buildResumeContext(
496
+ resumeBinary,
497
+ cwd,
498
+ sessionId,
499
+ data.source,
500
+ );
501
+ if (resume) {
502
+ context = `${context}\n\n${resume}`;
503
+ debugLog(`Injected resume checkpoint (source=${data.source})`);
504
+ recordObserveEvent(resumeBinary, cwd, {
505
+ kind: "injection",
506
+ name: "resume-checkpoint",
507
+ category: "resume",
508
+ subtype: data.source || "resume",
509
+ session: sessionId,
510
+ });
511
+ }
512
+ }
513
+ } catch (err) {
514
+ debugLog(`Resume checkpoint injection failed (non-fatal): ${err}`);
515
+ }
516
+
492
517
  try {
493
518
  const binary = findAideBinary(cwd);
519
+ if (binary) {
520
+ // Emit a lifecycle trigger so SessionStart is traceable in the
521
+ // dashboard, symmetric with subagent-start/stop. Fires regardless of
522
+ // whether any memories were injected.
523
+ recordObserveEvent(binary, cwd, {
524
+ kind: "session",
525
+ name: "session-start",
526
+ category: "lifecycle",
527
+ subtype: data.source || "startup",
528
+ session: sessionId,
529
+ });
530
+ }
494
531
  if (binary && context && memories.sources) {
495
532
  for (const src of memories.sources) {
496
533
  const attrs: Record<string, string> = { scope: src.scope };
@@ -523,13 +560,14 @@ async function main(): Promise<void> {
523
560
 
524
561
  const output: HookOutput = {
525
562
  continue: true,
563
+ ...(process.env.AIDE_DEBUG ? {} : { suppressOutput: true }),
526
564
  hookSpecificOutput: {
527
565
  hookEventName: "SessionStart",
528
566
  additionalContext: context,
529
567
  },
530
568
  };
531
569
 
532
- console.log(JSON.stringify(output));
570
+ emitHookResult(output);
533
571
  } catch (error) {
534
572
  debugLog(`ERROR: ${error}`);
535
573
  // Log error if logger is available
@@ -538,7 +576,7 @@ async function main(): Promise<void> {
538
576
  log.flush();
539
577
  }
540
578
  // On error, allow continuation without context
541
- console.log(JSON.stringify({ continue: true }));
579
+ emitHookResult();
542
580
  }
543
581
  }
544
582
 
@@ -10,8 +10,12 @@
10
10
  */
11
11
 
12
12
  import { debug, setDebugCwd } from "../lib/logger.js";
13
- import { readStdin } from "../lib/hook-utils.js";
14
- import { findAideBinary } from "../core/aide-client.js";
13
+ import {
14
+ readStdin,
15
+ emitHookResult,
16
+ installHookSafetyNet,
17
+ findAideBinary,
18
+ } from "../lib/hook-utils.js";
15
19
  import {
16
20
  buildSessionSummary,
17
21
  getSessionCommits,
@@ -45,10 +49,7 @@ function captureSessionSummary(
45
49
  sessionId: string,
46
50
  transcriptPath: string,
47
51
  ): boolean {
48
- const binary = findAideBinary({
49
- cwd,
50
- pluginRoot: process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
51
- });
52
+ const binary = findAideBinary(cwd);
52
53
  if (!binary) {
53
54
  debug(SOURCE, "aide binary not found, cannot capture session summary");
54
55
  return false;
@@ -98,7 +99,7 @@ async function main(): Promise<void> {
98
99
  try {
99
100
  const input = await readStdin();
100
101
  if (!input.trim()) {
101
- console.log(JSON.stringify({ continue: true }));
102
+ emitHookResult({ continue: true });
102
103
  return;
103
104
  }
104
105
 
@@ -118,30 +119,13 @@ async function main(): Promise<void> {
118
119
  }
119
120
  }
120
121
 
121
- console.log(JSON.stringify({ continue: true }));
122
+ emitHookResult({ continue: true });
122
123
  } catch (err) {
123
124
  debug(SOURCE, `Error: ${err}`);
124
- console.log(JSON.stringify({ continue: true }));
125
+ emitHookResult({ continue: true });
125
126
  }
126
127
  }
127
128
 
128
- process.on("uncaughtException", (err) => {
129
- debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
130
- try {
131
- console.log(JSON.stringify({ continue: true }));
132
- } catch {
133
- console.log('{"continue":true}');
134
- }
135
- process.exit(0);
136
- });
137
- process.on("unhandledRejection", (reason) => {
138
- debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
139
- try {
140
- console.log(JSON.stringify({ continue: true }));
141
- } catch {
142
- console.log('{"continue":true}');
143
- }
144
- process.exit(0);
145
- });
129
+ installHookSafetyNet(SOURCE);
146
130
 
147
131
  main();
@@ -18,7 +18,13 @@
18
18
  import { existsSync, mkdirSync } from "fs";
19
19
  import { join } from "path";
20
20
  import { Logger, debug, setDebugCwd } from "../lib/logger.js";
21
- import { readStdin, detectPlatform } from "../lib/hook-utils.js";
21
+ import {
22
+ readStdin,
23
+ detectPlatform,
24
+ emitHookResult,
25
+ installHookSafetyNet,
26
+ findAideBinary,
27
+ } from "../lib/hook-utils.js";
22
28
  import { findProjectRoot } from "../lib/project-root.js";
23
29
  import {
24
30
  discoverSkills as coreDiscoverSkills,
@@ -26,7 +32,6 @@ import {
26
32
  formatSkillsContext as coreFormatSkillsContext,
27
33
  } from "../core/skill-matcher.js";
28
34
  import type { Skill } from "../core/types.js";
29
- import { findAideBinary } from "../core/aide-client.js";
30
35
  import {
31
36
  recordObserveEvent,
32
37
  previewContent,
@@ -124,28 +129,8 @@ function debugLog(msg: string): void {
124
129
  debug(SOURCE, msg);
125
130
  }
126
131
 
127
- // Ensure we always output valid JSON, even on catastrophic errors
128
- function outputContinue(): void {
129
- try {
130
- console.log(JSON.stringify({ continue: true }));
131
- } catch {
132
- // Last resort - raw JSON string
133
- console.log('{"continue":true}');
134
- }
135
- }
136
-
137
132
  // Global error handlers to prevent hook crashes without JSON output
138
- process.on("uncaughtException", (err) => {
139
- debugLog(`UNCAUGHT EXCEPTION: ${err}`);
140
- outputContinue();
141
- process.exit(0);
142
- });
143
-
144
- process.on("unhandledRejection", (reason) => {
145
- debugLog(`UNHANDLED REJECTION: ${reason}`);
146
- outputContinue();
147
- process.exit(0);
148
- });
133
+ installHookSafetyNet(SOURCE);
149
134
 
150
135
  async function main(): Promise<void> {
151
136
  const hookStart = Date.now();
@@ -158,7 +143,7 @@ async function main(): Promise<void> {
158
143
 
159
144
  if (!input.trim()) {
160
145
  debugLog("Empty input, exiting");
161
- console.log(JSON.stringify({ continue: true }));
146
+ emitHookResult({ continue: true });
162
147
  return;
163
148
  }
164
149
 
@@ -190,7 +175,7 @@ async function main(): Promise<void> {
190
175
  // sessions that aren't running reflect don't accrue extra events.
191
176
  if (prompt && reflectEnabled(cwd)) {
192
177
  try {
193
- const binary = findAideBinary({ cwd });
178
+ const binary = findAideBinary(cwd);
194
179
  if (binary) {
195
180
  recordObserveEvent(binary, cwd, {
196
181
  kind: "hook",
@@ -211,7 +196,7 @@ async function main(): Promise<void> {
211
196
  log.info("No prompt provided");
212
197
  log.end("total");
213
198
  log.flush();
214
- console.log(JSON.stringify({ continue: true }));
199
+ emitHookResult({ continue: true });
215
200
  return;
216
201
  }
217
202
 
@@ -242,7 +227,7 @@ async function main(): Promise<void> {
242
227
  // single "skill-injector" aggregate). Subtype="skill" keeps the
243
228
  // category roll-up; Name carries the per-skill identifier.
244
229
  try {
245
- const binary = findAideBinary({ cwd });
230
+ const binary = findAideBinary(cwd);
246
231
  if (binary) {
247
232
  for (const skill of matched) {
248
233
  const text = `### ${skill.name}\n${skill.description ?? ""}\n${skill.content}`;
@@ -270,13 +255,13 @@ async function main(): Promise<void> {
270
255
  additionalContext: skillContext,
271
256
  },
272
257
  };
273
- console.log(JSON.stringify(output));
258
+ emitHookResult(output);
274
259
  } else {
275
260
  log.info("No matching skills");
276
261
  debugLog(`Flushing logs...`);
277
262
  log.flush();
278
263
  debugLog(`Hook complete (${Date.now() - hookStart}ms total)`);
279
- console.log(JSON.stringify({ continue: true }));
264
+ emitHookResult({ continue: true });
280
265
  }
281
266
  } catch (error) {
282
267
  debugLog(`ERROR: ${error}`);
@@ -286,7 +271,7 @@ async function main(): Promise<void> {
286
271
  log.flush();
287
272
  }
288
273
  // On error, allow continuation
289
- console.log(JSON.stringify({ continue: true }));
274
+ emitHookResult({ continue: true });
290
275
  }
291
276
  }
292
277
 
@@ -17,9 +17,18 @@
17
17
  import { execFileSync } from "child_process";
18
18
  import { basename } from "path";
19
19
  import { Logger } from "../lib/logger.js";
20
- import { readStdin, setMemoryState, isFalsy } from "../lib/hook-utils.js";
21
- import { findAideBinary } from "../core/aide-client.js";
22
- import { emitInjectionEvent } from "../core/read-tracking.js";
20
+ import {
21
+ readStdin,
22
+ setMemoryState,
23
+ isFalsy,
24
+ emitHookResult,
25
+ installHookSafetyNet,
26
+ findAideBinary,
27
+ } from "../lib/hook-utils.js";
28
+ import {
29
+ emitInjectionEvent,
30
+ recordObserveEvent,
31
+ } from "../core/read-tracking.js";
23
32
  import { refreshHud } from "../lib/hud.js";
24
33
 
25
34
  // Global logger instance
@@ -117,10 +126,7 @@ function fetchSubagentMemories(cwd: string): {
117
126
  return result;
118
127
  }
119
128
 
120
- const binary = findAideBinary({
121
- cwd,
122
- pluginRoot: process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
123
- });
129
+ const binary = findAideBinary(cwd);
124
130
  if (!binary) {
125
131
  return result;
126
132
  }
@@ -341,6 +347,20 @@ async function processSubagentStart(
341
347
  log?.info(
342
348
  `Injecting context for subagent: ${memories.global.length} preferences, ${memories.project.length} project, ${memories.decisions.length} decisions`,
343
349
  );
350
+
351
+ // Emit session observe event so SubagentStart is traceable in the dashboard
352
+ const binary = findAideBinary(cwd);
353
+ if (binary) {
354
+ recordObserveEvent(binary, cwd, {
355
+ kind: "session",
356
+ name: "subagent-start",
357
+ category: "lifecycle",
358
+ subtype: type,
359
+ session: session_id,
360
+ attrs: { agent_id, agent_type: type },
361
+ });
362
+ }
363
+
344
364
  return context;
345
365
  }
346
366
 
@@ -365,6 +385,18 @@ async function processSubagentStop(data: SubagentStopInput): Promise<void> {
365
385
  refreshHud(cwd, session_id);
366
386
  log?.end("refreshHud");
367
387
 
388
+ // Emit session observe event so SubagentStop is traceable in the dashboard
389
+ const binary = findAideBinary(cwd);
390
+ if (binary) {
391
+ recordObserveEvent(binary, cwd, {
392
+ kind: "session",
393
+ name: "subagent-stop",
394
+ category: "lifecycle",
395
+ session: session_id,
396
+ attrs: { agent_id, agent_type: data.agent_type ?? "" },
397
+ });
398
+ }
399
+
368
400
  log?.debug(`SubagentStop: agent ${agent_id} marked as completed`);
369
401
  }
370
402
 
@@ -372,7 +404,7 @@ async function main(): Promise<void> {
372
404
  try {
373
405
  const input = await readStdin();
374
406
  if (!input.trim()) {
375
- console.log(JSON.stringify({ continue: true }));
407
+ emitHookResult({ continue: true });
376
408
  return;
377
409
  }
378
410
 
@@ -412,11 +444,7 @@ async function main(): Promise<void> {
412
444
  additionalContext,
413
445
  };
414
446
  try {
415
- const binary = findAideBinary({
416
- cwd,
417
- pluginRoot:
418
- process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
419
- });
447
+ const binary = findAideBinary(cwd);
420
448
  if (binary) {
421
449
  const startData = data as SubagentStartInput;
422
450
  emitInjectionEvent(binary, cwd, {
@@ -438,40 +466,17 @@ async function main(): Promise<void> {
438
466
  }
439
467
  }
440
468
 
441
- console.log(JSON.stringify(output));
469
+ emitHookResult(output);
442
470
  } catch (error) {
443
471
  // Log error but don't block
444
472
  if (log) {
445
473
  log.error("Subagent tracker failed", error);
446
474
  log.flush();
447
475
  }
448
- console.log(JSON.stringify({ continue: true }));
476
+ emitHookResult({ continue: true });
449
477
  }
450
478
  }
451
479
 
452
- process.on("uncaughtException", (err) => {
453
- if (log) {
454
- log.error(`UNCAUGHT EXCEPTION: ${err}`);
455
- log.flush();
456
- }
457
- try {
458
- console.log(JSON.stringify({ continue: true }));
459
- } catch {
460
- console.log('{"continue":true}');
461
- }
462
- process.exit(0);
463
- });
464
- process.on("unhandledRejection", (reason) => {
465
- if (log) {
466
- log.error(`UNHANDLED REJECTION: ${reason}`);
467
- log.flush();
468
- }
469
- try {
470
- console.log(JSON.stringify({ continue: true }));
471
- } catch {
472
- console.log('{"continue":true}');
473
- }
474
- process.exit(0);
475
- });
480
+ installHookSafetyNet("subagent-tracker");
476
481
 
477
482
  main();
@@ -26,7 +26,11 @@ import { existsSync, readFileSync } from "fs";
26
26
  import { join } from "path";
27
27
  import which from "which";
28
28
  import { debug, setDebugCwd } from "../lib/logger.js";
29
- import { readStdin } from "../lib/hook-utils.js";
29
+ import {
30
+ readStdin,
31
+ emitHookResult,
32
+ installHookSafetyNet,
33
+ } from "../lib/hook-utils.js";
30
34
 
31
35
  const SOURCE = "task-completed";
32
36
 
@@ -416,13 +420,6 @@ async function main(): Promise<void> {
416
420
  }
417
421
  }
418
422
 
419
- process.on("uncaughtException", (err) => {
420
- debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
421
- process.exit(0);
422
- });
423
- process.on("unhandledRejection", (reason) => {
424
- debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
425
- process.exit(0);
426
- });
423
+ installHookSafetyNet(SOURCE);
427
424
 
428
425
  main();