@jmylchreest/aide-plugin 0.0.66 → 0.1.1

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.
@@ -23,9 +23,12 @@ import type {
23
23
  SessionState,
24
24
  SessionInitResult,
25
25
  MemoryInjection,
26
+ InjectedSource,
26
27
  StartupNotices,
27
28
  } from "./types.js";
28
29
  import { DEFAULT_CONFIG } from "./types.js";
30
+ import { isTruthy, isFalsy } from "../lib/hook-utils.js";
31
+ import { findProjectRoot } from "../lib/project-root.js";
29
32
 
30
33
  /**
31
34
  * Ensure all .aide directories exist
@@ -34,14 +37,19 @@ export function ensureDirectories(cwd: string): {
34
37
  created: number;
35
38
  existed: number;
36
39
  } {
40
+ // Resolve to the canonical project root so we don't plant a stray .aide/
41
+ // in a subdirectory the harness happened to launch from. When no marker
42
+ // is found, fall back to cwd (caller's hasMarker gate elsewhere refuses
43
+ // bootstrap unless AIDE_FORCE_INIT is set).
44
+ const { root } = findProjectRoot(cwd);
37
45
  const dirs = [
38
- join(cwd, ".aide"),
39
- join(cwd, ".aide", "skills"),
40
- join(cwd, ".aide", "config"),
41
- join(cwd, ".aide", "state"),
42
- join(cwd, ".aide", "memory"),
43
- join(cwd, ".aide", "worktrees"),
44
- join(cwd, ".aide", "_logs"),
46
+ join(root, ".aide"),
47
+ join(root, ".aide", "skills"),
48
+ join(root, ".aide", "config"),
49
+ join(root, ".aide", "state"),
50
+ join(root, ".aide", "memory"),
51
+ join(root, ".aide", "worktrees"),
52
+ join(root, ".aide", "_logs"),
45
53
  join(homedir(), ".aide"),
46
54
  join(homedir(), ".aide", "skills"),
47
55
  join(homedir(), ".aide", "config"),
@@ -66,7 +74,7 @@ export function ensureDirectories(cwd: string): {
66
74
  // Ensure .gitignore exists in .aide directory.
67
75
  // Structure: exclude all local-only runtime data, allow shared/ and config/.
68
76
  // shared/ contains git-friendly markdown exports (decisions, memories).
69
- const gitignorePath = join(cwd, ".aide", ".gitignore");
77
+ const gitignorePath = join(root, ".aide", ".gitignore");
70
78
  const requiredGitignoreContent = `# AIDE local runtime files - do not commit
71
79
  # These are machine-specific and/or binary (non-mergeable)
72
80
  _logs/
@@ -207,7 +215,8 @@ export function loadGlobalConfig(): AideConfig {
207
215
  */
208
216
  export function loadConfig(cwd: string): AideConfig {
209
217
  const global = loadGlobalConfig();
210
- const projectPath = join(cwd, ".aide", "config", "aide.json");
218
+ const { root } = findProjectRoot(cwd);
219
+ const projectPath = join(root, ".aide", "config", "aide.json");
211
220
 
212
221
  if (existsSync(projectPath)) {
213
222
  try {
@@ -252,7 +261,8 @@ export function cleanupStaleStateFiles(cwd: string): {
252
261
  scanned: number;
253
262
  deleted: number;
254
263
  } {
255
- const stateDir = join(cwd, ".aide", "state");
264
+ const { root } = findProjectRoot(cwd);
265
+ const stateDir = join(root, ".aide", "state");
256
266
  if (!existsSync(stateDir)) {
257
267
  return { scanned: 0, deleted: 0 };
258
268
  }
@@ -291,7 +301,8 @@ export function cleanupStaleStateFiles(cwd: string): {
291
301
  * Reset HUD state file for clean session start
292
302
  */
293
303
  export function resetHudState(cwd: string): void {
294
- const hudPath = join(cwd, ".aide", "state", "hud.txt");
304
+ const { root } = findProjectRoot(cwd);
305
+ const hudPath = join(root, ".aide", "state", "hud.txt");
295
306
  try {
296
307
  if (existsSync(hudPath)) {
297
308
  writeFileSync(hudPath, "mode:idle");
@@ -319,7 +330,7 @@ export function runSessionInit(
319
330
  dynamic: { sessions: [] },
320
331
  };
321
332
 
322
- if (process.env.AIDE_MEMORY_INJECT === "0") {
333
+ if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
323
334
  return result;
324
335
  }
325
336
 
@@ -334,7 +345,7 @@ export function runSessionInit(
334
345
  // Add --share-import if configured or env var set
335
346
  if (
336
347
  config?.share?.autoImport ||
337
- process.env.AIDE_SHARE_AUTO_IMPORT === "1"
348
+ isTruthy(process.env.AIDE_SHARE_AUTO_IMPORT)
338
349
  ) {
339
350
  args.push("--share-import");
340
351
  }
@@ -349,7 +360,7 @@ export function runSessionInit(
349
360
 
350
361
  const data: SessionInitResult = JSON.parse(output);
351
362
 
352
- if (process.env.AIDE_MEMORY_INJECT === "0") {
363
+ if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
353
364
  return result;
354
365
  }
355
366
 
@@ -369,6 +380,55 @@ export function runSessionInit(
369
380
  .join("\n");
370
381
  result.dynamic.sessions.push(`${header}:\n${memories}`);
371
382
  }
383
+
384
+ const sources: InjectedSource[] = [];
385
+ for (const m of data.global_memories) {
386
+ sources.push({
387
+ kind: "memory",
388
+ scope: "global",
389
+ id: m.id,
390
+ name: m.tags?.[0] ?? m.category ?? "memory",
391
+ content: m.content,
392
+ category: m.category,
393
+ tags: m.tags,
394
+ score: m.score,
395
+ });
396
+ }
397
+ for (const m of data.project_memories) {
398
+ sources.push({
399
+ kind: "memory",
400
+ scope: "project",
401
+ id: m.id,
402
+ name: m.tags?.[0] ?? m.category ?? "memory",
403
+ content: m.content,
404
+ category: m.category,
405
+ tags: m.tags,
406
+ score: m.score,
407
+ });
408
+ }
409
+ for (const d of data.decisions) {
410
+ sources.push({
411
+ kind: "decision",
412
+ scope: "project",
413
+ id: d.topic,
414
+ name: d.topic,
415
+ content: `${d.value}${d.rationale ? ` (${d.rationale})` : ""}`,
416
+ });
417
+ }
418
+ for (const sess of data.recent_sessions) {
419
+ for (const m of sess.memories) {
420
+ sources.push({
421
+ kind: "session_memory",
422
+ scope: "session",
423
+ id: `${sess.session_id}:${m.content.slice(0, 32)}`,
424
+ name: m.category || "session",
425
+ content: m.content,
426
+ category: m.category,
427
+ sessionId: sess.session_id,
428
+ });
429
+ }
430
+ }
431
+ result.sources = sources;
372
432
  } catch {
373
433
  // Best effort
374
434
  }
@@ -229,40 +229,36 @@ export function loadSkill(path: string): Skill | null {
229
229
  export function discoverSkills(cwd: string, pluginRoot?: string): Skill[] {
230
230
  const skills: Skill[] = [];
231
231
  const seenPaths = new Set<string>();
232
+ // Dedupe by skill name too — the same skill (same `name:` frontmatter)
233
+ // is often present in both repo/skills/ and the plugin-install
234
+ // skills/ dir under different file paths. Without this, matchSkills
235
+ // returns the same skill twice and injects it twice.
236
+ const seenNames = new Set<string>();
237
+
238
+ const add = (file: string) => {
239
+ if (seenPaths.has(file)) return;
240
+ seenPaths.add(file);
241
+ const skill = loadSkill(file);
242
+ if (!skill) return;
243
+ if (seenNames.has(skill.name)) return;
244
+ seenNames.add(skill.name);
245
+ skills.push(skill);
246
+ };
232
247
 
233
248
  // Project-local skills (higher priority)
234
249
  for (const location of SKILL_LOCATIONS) {
235
250
  const dir = join(cwd, location);
236
- const files = findSkillFiles(dir);
237
- for (const file of files) {
238
- if (seenPaths.has(file)) continue;
239
- seenPaths.add(file);
240
- const skill = loadSkill(file);
241
- if (skill) skills.push(skill);
242
- }
251
+ for (const file of findSkillFiles(dir)) add(file);
243
252
  }
244
253
 
245
254
  // Plugin-bundled skills (if pluginRoot provided)
246
255
  if (pluginRoot) {
247
- const pluginSkillDir = join(pluginRoot, "skills");
248
- const files = findSkillFiles(pluginSkillDir);
249
- for (const file of files) {
250
- if (seenPaths.has(file)) continue;
251
- seenPaths.add(file);
252
- const skill = loadSkill(file);
253
- if (skill) skills.push(skill);
254
- }
256
+ for (const file of findSkillFiles(join(pluginRoot, "skills"))) add(file);
255
257
  }
256
258
 
257
259
  // Global skills (lower priority)
258
260
  for (const dir of GLOBAL_SKILL_LOCATIONS) {
259
- const files = findSkillFiles(dir);
260
- for (const file of files) {
261
- if (seenPaths.has(file)) continue;
262
- seenPaths.add(file);
263
- const skill = loadSkill(file);
264
- if (skill) skills.push(skill);
265
- }
261
+ for (const file of findSkillFiles(dir)) add(file);
266
262
  }
267
263
 
268
264
  return skills;
@@ -269,6 +269,18 @@ export function recordToolEvent(
269
269
  if (input.sessionId) args.push(`--session=${input.sessionId}`);
270
270
  if (startLine !== undefined) args.push(`--attr=start_line=${startLine}`);
271
271
  if (endLine !== undefined) args.push(`--attr=end_line=${endLine}`);
272
+ // Capture the command (Bash) or pattern (Grep) text so the repetition
273
+ // detector can group calls by canonical signature instead of lumping
274
+ // every Bash invocation under a single "Bash" bucket. Truncated to keep
275
+ // the attr cheap; the normaliser only uses the first token anyway.
276
+ const cmd = input.toolInput?.command;
277
+ if (typeof cmd === "string" && cmd.length > 0) {
278
+ args.push(`--attr=command=${cmd.slice(0, 500)}`);
279
+ }
280
+ const pattern = input.toolInput?.pattern;
281
+ if (typeof pattern === "string" && pattern.length > 0) {
282
+ args.push(`--attr=pattern=${pattern.slice(0, 200)}`);
283
+ }
272
284
  execFileSync(binary, args, {
273
285
  cwd,
274
286
  timeout: 3000,
package/src/core/types.ts CHANGED
@@ -70,12 +70,14 @@ export interface SessionInitResult {
70
70
  content: string;
71
71
  category: string;
72
72
  tags: string[];
73
+ score?: number;
73
74
  }>;
74
75
  project_memories: Array<{
75
76
  id: string;
76
77
  content: string;
77
78
  category: string;
78
79
  tags: string[];
80
+ score?: number;
79
81
  }>;
80
82
  project_memory_overflow?: boolean;
81
83
  decisions: Array<{ topic: string; value: string; rationale?: string }>;
@@ -90,6 +92,18 @@ export interface SessionInitResult {
90
92
  // Memory Injection
91
93
  // =============================================================================
92
94
 
95
+ export interface InjectedSource {
96
+ kind: "memory" | "decision" | "session_memory";
97
+ scope: "global" | "project" | "session";
98
+ id: string;
99
+ name: string;
100
+ content: string;
101
+ category?: string;
102
+ tags?: string[];
103
+ sessionId?: string;
104
+ score?: number;
105
+ }
106
+
93
107
  export interface MemoryInjection {
94
108
  static: {
95
109
  global: string[];
@@ -100,6 +114,7 @@ export interface MemoryInjection {
100
114
  dynamic: {
101
115
  sessions: string[];
102
116
  };
117
+ sources?: InjectedSource[];
103
118
  }
104
119
 
105
120
  // =============================================================================
@@ -14,6 +14,7 @@ import { readStdin } from "../lib/hook-utils.js";
14
14
  import { findAideBinary } from "../core/aide-client.js";
15
15
  import { cleanupAgent } from "../core/cleanup.js";
16
16
  import { debug } from "../lib/logger.js";
17
+ import { findProjectRoot } from "../lib/project-root.js";
17
18
 
18
19
  const SOURCE = "agent-cleanup";
19
20
 
@@ -48,7 +49,8 @@ async function main(): Promise<void> {
48
49
  if (binary) {
49
50
  const cleared = cleanupAgent(binary, cwd, agentId);
50
51
  if (cleared) {
51
- const logDir = join(cwd, ".aide", "_logs");
52
+ const { root } = findProjectRoot(cwd);
53
+ const logDir = join(root, ".aide", "_logs");
52
54
  if (existsSync(logDir)) {
53
55
  const logPath = join(logDir, "agent-cleanup.log");
54
56
  const timestamp = new Date().toISOString();
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Signals Hook (PreToolUse)
4
+ *
5
+ * Fires before every tool call. In subagent contexts (session has a parent
6
+ * recorded by SubagentStart) it checks for halt/pause flags and unread
7
+ * high-priority messages, blocking or injecting context accordingly.
8
+ *
9
+ * In orchestrator / solo sessions this hook is a no-op — gated on the
10
+ * session having a registered parent. Cost when gated out: one cheap
11
+ * `aide agent identify` call (~10ms).
12
+ */
13
+
14
+ import { execFileSync } from "child_process";
15
+ import { Logger } from "../lib/logger.js";
16
+ import { readStdin } from "../lib/hook-utils.js";
17
+ import { findAideBinary } from "../core/aide-client.js";
18
+ import { emitInjectionEvent } from "../core/read-tracking.js";
19
+
20
+ const SOURCE = "agent-signals";
21
+
22
+ interface PreToolUseInput {
23
+ hook_event_name: "PreToolUse";
24
+ session_id: string;
25
+ tool_name: string;
26
+ cwd: string;
27
+ }
28
+
29
+ interface HookOutput {
30
+ continue: boolean;
31
+ message?: string;
32
+ hookSpecificOutput?: {
33
+ hookEventName: string;
34
+ additionalContext?: string;
35
+ };
36
+ }
37
+
38
+ interface SignalsResponse {
39
+ agent: string;
40
+ halt?: boolean;
41
+ paused?: boolean;
42
+ reason?: string;
43
+ deadline?: string;
44
+ deadline_remaining_sec?: number;
45
+ high_priority_messages?: Array<{
46
+ id: number;
47
+ from: string;
48
+ content: string;
49
+ type?: string;
50
+ }>;
51
+ }
52
+
53
+ let log: Logger | null = null;
54
+
55
+ function passThrough(): void {
56
+ console.log(JSON.stringify({ continue: true }));
57
+ }
58
+
59
+ function block(message: string): void {
60
+ const out: HookOutput = { continue: false, message };
61
+ console.log(JSON.stringify(out));
62
+ }
63
+
64
+ function injectContext(context: string): void {
65
+ const out: HookOutput = {
66
+ continue: true,
67
+ hookSpecificOutput: {
68
+ hookEventName: "PreToolUse",
69
+ additionalContext: context,
70
+ },
71
+ };
72
+ console.log(JSON.stringify(out));
73
+ }
74
+
75
+ function runAide(binary: string, cwd: string, args: string[]): string | null {
76
+ try {
77
+ return execFileSync(binary, args, {
78
+ cwd,
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ timeout: 2000,
81
+ }).toString();
82
+ } catch (err) {
83
+ log?.debug(`aide ${args.join(" ")} failed: ${err}`);
84
+ return null;
85
+ }
86
+ }
87
+
88
+ // Tools the subagent is still allowed to call while paused — limited to
89
+ // communication primitives so it can report back / receive instruction.
90
+ const PAUSE_ALLOWLIST = new Set([
91
+ "mcp__plugin_aide_aide__message_send",
92
+ "mcp__plugin_aide_aide__message_list",
93
+ "mcp__plugin_aide_aide__message_ack",
94
+ "mcp__plugin_aide_aide__state_get",
95
+ ]);
96
+
97
+ async function main(): Promise<void> {
98
+ try {
99
+ const raw = await readStdin();
100
+ if (!raw.trim()) {
101
+ passThrough();
102
+ return;
103
+ }
104
+ const data: PreToolUseInput = JSON.parse(raw);
105
+ const cwd = data.cwd || process.cwd();
106
+ log = new Logger("agent-signals", cwd);
107
+
108
+ const binary = findAideBinary({
109
+ cwd,
110
+ pluginRoot:
111
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
112
+ });
113
+ if (!binary) {
114
+ passThrough();
115
+ return;
116
+ }
117
+
118
+ // Gate: is this session a registered subagent? identify returns JSON
119
+ // with parent_session if and only if SubagentStart recorded one.
120
+ const idRaw = runAide(binary, cwd, [
121
+ "agent",
122
+ "identify",
123
+ `--agent=${data.session_id}`,
124
+ ]);
125
+ if (!idRaw) {
126
+ passThrough();
127
+ return;
128
+ }
129
+ let identity: Record<string, string>;
130
+ try {
131
+ identity = JSON.parse(idRaw);
132
+ } catch {
133
+ passThrough();
134
+ return;
135
+ }
136
+ if (!identity.parent_session) {
137
+ // Not a subagent — orchestrator/solo session. No-op.
138
+ passThrough();
139
+ return;
140
+ }
141
+
142
+ // Pull signal snapshot.
143
+ const sigRaw = runAide(binary, cwd, [
144
+ "agent",
145
+ "signals",
146
+ `--agent=${data.session_id}`,
147
+ ]);
148
+ if (!sigRaw) {
149
+ passThrough();
150
+ return;
151
+ }
152
+ let sig: SignalsResponse;
153
+ try {
154
+ sig = JSON.parse(sigRaw);
155
+ } catch {
156
+ passThrough();
157
+ return;
158
+ }
159
+
160
+ // 1. Halt — hard stop with reason surfaced to model.
161
+ if (sig.halt) {
162
+ const reason = sig.reason || "halted by orchestrator";
163
+ log?.info(`halt active for ${data.session_id}: ${reason}`);
164
+ block(
165
+ `[aide] This subagent has been halted by the orchestrator. Reason: ${reason}\n` +
166
+ `Stop work. You may still send a final status message via aide message send --to=<orchestrator>.`,
167
+ );
168
+ return;
169
+ }
170
+
171
+ // 2. Pause — block all tools except the comm allowlist.
172
+ if (sig.paused && !PAUSE_ALLOWLIST.has(data.tool_name)) {
173
+ log?.info(`paused — blocking ${data.tool_name}`);
174
+ block(
175
+ `[aide] This subagent is paused by the orchestrator. Only messaging tools are allowed until resumed.`,
176
+ );
177
+ return;
178
+ }
179
+
180
+ // 3. Deadline approaching — inject warning at < 20% remaining.
181
+ let deadlineWarn = "";
182
+ if (sig.deadline_remaining_sec !== undefined) {
183
+ if (sig.deadline_remaining_sec <= 0) {
184
+ block(
185
+ `[aide] Soft deadline reached (${sig.deadline}). Halting per orchestrator policy.`,
186
+ );
187
+ return;
188
+ }
189
+ if (sig.deadline_remaining_sec < 60 * 5) {
190
+ deadlineWarn = `[aide] Deadline ${sig.deadline} — ~${sig.deadline_remaining_sec}s remaining.`;
191
+ }
192
+ }
193
+
194
+ // 4. High-priority messages — inject + ack so we don't re-inject next call.
195
+ const parts: string[] = [];
196
+ if (deadlineWarn) parts.push(deadlineWarn);
197
+ if (sig.high_priority_messages && sig.high_priority_messages.length > 0) {
198
+ parts.push("[aide] Mid-flight messages from orchestrator:");
199
+ for (const m of sig.high_priority_messages) {
200
+ parts.push(`- (${m.from}${m.type ? `, ${m.type}` : ""}): ${m.content}`);
201
+ runAide(binary, cwd, [
202
+ "message",
203
+ "ack",
204
+ String(m.id),
205
+ `--agent=${data.session_id}`,
206
+ ]);
207
+ }
208
+ }
209
+
210
+ if (parts.length > 0) {
211
+ const ctx = parts.join("\n");
212
+ try {
213
+ emitInjectionEvent(binary, cwd, {
214
+ source: SOURCE,
215
+ subtype: "signal",
216
+ content: ctx,
217
+ sessionId: data.session_id,
218
+ attrs: {
219
+ tool: data.tool_name,
220
+ ...(sig.deadline ? { deadline: sig.deadline } : {}),
221
+ high_priority_messages: String(
222
+ sig.high_priority_messages?.length ?? 0,
223
+ ),
224
+ },
225
+ });
226
+ } catch {
227
+ // Non-fatal
228
+ }
229
+ injectContext(ctx);
230
+ return;
231
+ }
232
+
233
+ passThrough();
234
+ } catch (err) {
235
+ log?.error(`agent-signals failed: ${err}`);
236
+ passThrough();
237
+ }
238
+ }
239
+
240
+ process.on("uncaughtException", () => {
241
+ passThrough();
242
+ process.exit(0);
243
+ });
244
+ process.on("unhandledRejection", () => {
245
+ passThrough();
246
+ process.exit(0);
247
+ });
248
+
249
+ main();
@@ -16,6 +16,8 @@ import {
16
16
  getCheckableFilePath,
17
17
  getContentToCheck,
18
18
  } from "../core/comment-checker.js";
19
+ import { findAideBinary } from "../core/aide-client.js";
20
+ import { emitInjectionEvent } from "../core/read-tracking.js";
19
21
 
20
22
  const SOURCE = "comment-checker";
21
23
 
@@ -53,6 +55,8 @@ async function main(): Promise<void> {
53
55
  const data: HookInput = JSON.parse(input);
54
56
  const toolName = data.tool_name || "";
55
57
  const toolInput = data.tool_input || {};
58
+ const cwd = data.cwd || process.cwd();
59
+ const sessionId = data.session_id || "";
56
60
 
57
61
  // Only check Write/Edit/MultiEdit tool calls
58
62
  const filePath = getCheckableFilePath(toolName, toolInput);
@@ -76,6 +80,28 @@ async function main(): Promise<void> {
76
80
  SOURCE,
77
81
  `Detected ${result.suspiciousCount} suspicious comments in ${filePath}`,
78
82
  );
83
+ try {
84
+ const binary = findAideBinary({
85
+ cwd,
86
+ pluginRoot:
87
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
88
+ });
89
+ if (binary && result.warning) {
90
+ emitInjectionEvent(binary, cwd, {
91
+ source: SOURCE,
92
+ subtype: "guard",
93
+ content: result.warning,
94
+ sessionId,
95
+ attrs: {
96
+ tool: toolName,
97
+ file: filePath,
98
+ suspicious_count: String(result.suspiciousCount),
99
+ },
100
+ });
101
+ }
102
+ } catch {
103
+ // Non-fatal
104
+ }
79
105
  const output: HookOutput = {
80
106
  continue: true,
81
107
  hookSpecificOutput: {
@@ -15,6 +15,7 @@ import { readStdin } from "../lib/hook-utils.js";
15
15
  import { debug } from "../lib/logger.js";
16
16
  import { checkContextGuard, checkSmartReadHint } from "../core/context-guard.js";
17
17
  import { findAideBinary } from "../core/aide-client.js";
18
+ import { emitInjectionEvent } from "../core/read-tracking.js";
18
19
 
19
20
  const SOURCE = "context-guard";
20
21
 
@@ -54,9 +55,28 @@ async function main(): Promise<void> {
54
55
  const sessionId = data.session_id || "unknown";
55
56
 
56
57
  const result = checkContextGuard(toolName, toolInput, cwd, sessionId);
58
+ const binary = findAideBinary({
59
+ cwd,
60
+ pluginRoot:
61
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
62
+ });
57
63
 
58
64
  if (result.shouldAdvise && result.advisory) {
59
65
  debug(SOURCE, `Advising on large file read`);
66
+ if (binary) {
67
+ try {
68
+ emitInjectionEvent(binary, cwd, {
69
+ source: SOURCE,
70
+ subtype: "guard",
71
+ name: "large-file-advisory",
72
+ content: result.advisory,
73
+ sessionId,
74
+ attrs: { tool: toolName },
75
+ });
76
+ } catch {
77
+ // Non-fatal
78
+ }
79
+ }
60
80
  const output: HookOutput = {
61
81
  continue: true,
62
82
  hookSpecificOutput: {
@@ -67,14 +87,23 @@ async function main(): Promise<void> {
67
87
  console.log(JSON.stringify(output));
68
88
  } else {
69
89
  // Smart read hint: suggest code index for re-reads of unchanged files
70
- const binary = findAideBinary({
71
- cwd,
72
- pluginRoot:
73
- process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
74
- });
75
90
  const hintResult = checkSmartReadHint(toolName, toolInput, cwd, binary);
76
91
  if (hintResult.shouldHint && hintResult.hint) {
77
92
  debug(SOURCE, `Smart read hint triggered`);
93
+ if (binary) {
94
+ try {
95
+ emitInjectionEvent(binary, cwd, {
96
+ source: SOURCE,
97
+ subtype: "guard",
98
+ name: "smart-read-hint",
99
+ content: hintResult.hint,
100
+ sessionId,
101
+ attrs: { tool: toolName },
102
+ });
103
+ } catch {
104
+ // Non-fatal
105
+ }
106
+ }
78
107
  const output: HookOutput = {
79
108
  continue: true,
80
109
  hookSpecificOutput: {
@@ -21,6 +21,8 @@ import type { ToolRecord } from "../core/context-pruning/types.js";
21
21
  import { tmpdir } from "os";
22
22
  import { join } from "path";
23
23
  import { existsSync, readFileSync, writeFileSync } from "fs";
24
+ import { findAideBinary } from "../core/aide-client.js";
25
+ import { emitInjectionEvent } from "../core/read-tracking.js";
24
26
 
25
27
  const SOURCE = "context-pruning";
26
28
 
@@ -184,6 +186,31 @@ async function main(): Promise<void> {
184
186
  saveHistory(sessionId, tracker.getHistory(), explained);
185
187
  }
186
188
 
189
+ try {
190
+ const binary = findAideBinary({
191
+ cwd,
192
+ pluginRoot:
193
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
194
+ });
195
+ const injected = output.hookSpecificOutput?.additionalContext;
196
+ if (binary && injected) {
197
+ emitInjectionEvent(binary, cwd, {
198
+ source: SOURCE,
199
+ subtype: "pruning",
200
+ name: result.strategy || "prune",
201
+ content: injected,
202
+ sessionId,
203
+ attrs: {
204
+ tool: toolName,
205
+ strategy: result.strategy ?? "",
206
+ bytes_saved: String(result.bytesSaved ?? 0),
207
+ },
208
+ });
209
+ }
210
+ } catch {
211
+ // Non-fatal
212
+ }
213
+
187
214
  console.log(JSON.stringify(output));
188
215
  } else {
189
216
  console.log(JSON.stringify({ continue: true }));