@jmylchreest/aide-plugin 0.0.65 → 0.1.0

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.
@@ -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 }));
@@ -14,6 +14,7 @@ import { readStdin } from "../lib/hook-utils.js";
14
14
  import { debug } from "../lib/logger.js";
15
15
  import { evaluateToolUse } from "../core/tool-enforcement.js";
16
16
  import { findAideBinary, getState } from "../core/aide-client.js";
17
+ import { emitInjectionEvent } from "../core/read-tracking.js";
17
18
 
18
19
  const SOURCE = "pre-tool-enforcer";
19
20
 
@@ -49,17 +50,19 @@ async function main(): Promise<void> {
49
50
  const toolName = data.tool_name || "";
50
51
  const agentName = data.agent_name || "";
51
52
  const cwd = data.cwd || process.cwd();
53
+ const sessionId = data.session_id || "";
52
54
 
53
55
  // Resolve active mode from aide binary (source of truth: BBolt store)
54
56
  let activeMode: string | null = null;
57
+ let aideBinary: string | null = null;
55
58
  try {
56
- const binary = findAideBinary({
59
+ aideBinary = findAideBinary({
57
60
  cwd,
58
61
  pluginRoot:
59
62
  process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
60
63
  });
61
- if (binary) {
62
- activeMode = getState(binary, cwd, "mode");
64
+ if (aideBinary) {
65
+ activeMode = getState(aideBinary, cwd, "mode");
63
66
  }
64
67
  } catch (err) {
65
68
  debug(SOURCE, `Failed to resolve active mode (non-fatal): ${err}`);
@@ -81,6 +84,23 @@ async function main(): Promise<void> {
81
84
  }
82
85
 
83
86
  if (result.reminder) {
87
+ if (aideBinary) {
88
+ try {
89
+ emitInjectionEvent(aideBinary, cwd, {
90
+ source: SOURCE,
91
+ subtype: "guard",
92
+ content: result.reminder,
93
+ sessionId,
94
+ attrs: {
95
+ tool: toolName,
96
+ ...(agentName ? { agent: agentName } : {}),
97
+ ...(activeMode ? { mode: activeMode } : {}),
98
+ },
99
+ });
100
+ } catch {
101
+ // Non-fatal — telemetry must not block tool use
102
+ }
103
+ }
84
104
  const output: HookOutput = {
85
105
  continue: true,
86
106
  hookSpecificOutput: {
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reflect Hook (Stop)
4
+ *
5
+ * Runs the instinct parser catalogue against the session's observe events
6
+ * when AIDE_REFLECT is truthy (1/true/on/yes). Off by default.
7
+ *
8
+ * Fire-and-forget: never blocks Stop, never returns an error to the harness.
9
+ */
10
+
11
+ import { execFileSync } from "child_process";
12
+ import { readStdin } from "../lib/hook-utils.js";
13
+ import { findAideBinary } from "../core/aide-client.js";
14
+ import { debug } from "../lib/logger.js";
15
+
16
+ const SOURCE = "reflect";
17
+
18
+ interface HookInput {
19
+ hook_event_name: string;
20
+ session_id: string;
21
+ cwd: string;
22
+ }
23
+
24
+ async function main(): Promise<void> {
25
+ try {
26
+ // The CLI itself checks env + .aide/config/aide.json reflect.enabled
27
+ // and no-ops when disabled, so the hook can invoke unconditionally. This
28
+ // is a 1-process spawn at session end — negligible overhead even when
29
+ // disabled.
30
+
31
+ const input = await readStdin();
32
+ if (!input.trim()) {
33
+ console.log(JSON.stringify({}));
34
+ return;
35
+ }
36
+
37
+ const data: HookInput = JSON.parse(input);
38
+ const cwd = data.cwd || process.cwd();
39
+ const sessionID = data.session_id;
40
+ if (!sessionID) {
41
+ console.log(JSON.stringify({}));
42
+ return;
43
+ }
44
+
45
+ const binary = findAideBinary({ cwd });
46
+ if (!binary) {
47
+ console.log(JSON.stringify({}));
48
+ return;
49
+ }
50
+
51
+ try {
52
+ execFileSync(binary, ["reflect", "run", `--session=${sessionID}`], {
53
+ cwd,
54
+ timeout: 10000,
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ });
57
+ debug(SOURCE, `reflect run session=${sessionID} ok`);
58
+ } catch (err) {
59
+ debug(SOURCE, `reflect run failed (non-fatal): ${err}`);
60
+ }
61
+
62
+ console.log(JSON.stringify({}));
63
+ } catch (err) {
64
+ debug(SOURCE, `error: ${err}`);
65
+ console.log(JSON.stringify({}));
66
+ }
67
+ }
68
+
69
+ void main();
@@ -15,7 +15,7 @@ import { readStdin } from "../lib/hook-utils.js";
15
15
  import { debug } from "../lib/logger.js";
16
16
  import { checkSearchEnrichment } from "../core/search-enrichment.js";
17
17
  import { findAideBinary } from "../core/aide-client.js";
18
- import { recordTokenEvent } from "../core/read-tracking.js";
18
+ import { emitInjectionEvent } from "../core/read-tracking.js";
19
19
 
20
20
  const SOURCE = "search-enrichment";
21
21
 
@@ -50,6 +50,7 @@ async function main(): Promise<void> {
50
50
  const toolName = data.tool_name || "";
51
51
  const toolInput = data.tool_input || {};
52
52
  const cwd = data.cwd || process.cwd();
53
+ const sessionId = data.session_id || "";
53
54
 
54
55
  const binary = findAideBinary({
55
56
  cwd,
@@ -62,11 +63,18 @@ async function main(): Promise<void> {
62
63
  if (result.shouldEnrich && result.enrichment) {
63
64
  debug(SOURCE, `Enriching grep with code index context`);
64
65
 
65
- // Record token event for search enrichment
66
66
  if (binary) {
67
67
  try {
68
- const tokens = Math.round(result.enrichment.length / 3.0);
69
- recordTokenEvent(binary, cwd, "context_injected", "enrichment", "search-enrichment", tokens);
68
+ // `name: "enrichment"` is load-bearing: the TokensPage by_delivery
69
+ // rollup keys on Event.Name via observeToTokenEvent.
70
+ emitInjectionEvent(binary, cwd, {
71
+ source: SOURCE,
72
+ subtype: "enrichment",
73
+ name: "enrichment",
74
+ content: result.enrichment,
75
+ sessionId,
76
+ attrs: { tool: toolName },
77
+ });
70
78
  } catch {
71
79
  // Non-fatal
72
80
  }
@@ -27,10 +27,41 @@ const T0 = performance.now();
27
27
  console.log(JSON.stringify({ continue: true }));
28
28
 
29
29
  const { spawn, execFileSync } = require("child_process") as typeof import("child_process");
30
- const { existsSync, realpathSync, appendFileSync, mkdirSync, readFileSync } = require("fs") as typeof import("fs");
31
- const { join } = require("path") as typeof import("path");
30
+ const { existsSync, realpathSync, appendFileSync, mkdirSync, readFileSync, statSync } = require("fs") as typeof import("fs");
31
+ const { join, dirname } = require("path") as typeof import("path");
32
32
  const whichSync = (require("which") as typeof import("which")).sync;
33
33
 
34
+ /**
35
+ * Inline walk-up for project root — mirrors lib/project-root.ts logic
36
+ * (priority: both markers > VCS > .aide/-only > cwd). Inlined here to keep
37
+ * this hook's startup cheap (no extra ES imports).
38
+ */
39
+ function resolveRoot(cwd: string): string {
40
+ const override = process.env.AIDE_PROJECT_ROOT;
41
+ if (override) {
42
+ try {
43
+ if (statSync(override).isDirectory()) return override;
44
+ } catch { /* fall through */ }
45
+ }
46
+ const candidates: { dir: string; hasAide: boolean; hasVCS: boolean }[] = [];
47
+ let dir = cwd;
48
+ for (;;) {
49
+ const hasAide = existsSync(join(dir, ".aide"));
50
+ let hasVCS = false;
51
+ for (const m of [".git", ".hg", ".svn", ".bzr", ".fossil"]) {
52
+ if (existsSync(join(dir, m))) { hasVCS = true; break; }
53
+ }
54
+ if (hasAide || hasVCS) candidates.push({ dir, hasAide, hasVCS });
55
+ const parent = dirname(dir);
56
+ if (parent === dir) break;
57
+ dir = parent;
58
+ }
59
+ for (const c of candidates) if (c.hasAide && c.hasVCS) return c.dir;
60
+ for (const c of candidates) if (c.hasVCS) return c.dir;
61
+ for (const c of candidates) if (c.hasAide) return c.dir;
62
+ return cwd;
63
+ }
64
+
34
65
  const SESSION_ID_RE = /^[a-zA-Z0-9_-]{1,128}$/;
35
66
 
36
67
  /** Elapsed ms since T0. */
@@ -44,7 +75,7 @@ function ms(): string {
44
75
  */
45
76
  function log(cwd: string, msg: string): void {
46
77
  try {
47
- const logDir = join(cwd, ".aide", "_logs");
78
+ const logDir = join(resolveRoot(cwd), ".aide", "_logs");
48
79
  if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
49
80
  const line = `[${new Date().toISOString()}] [session-end] ${ms()} ${msg}\n`;
50
81
  appendFileSync(join(logDir, "session-end.log"), line);
@@ -61,7 +92,7 @@ function findBinary(cwd?: string): string | null {
61
92
  if (existsSync(p)) return p;
62
93
  }
63
94
  if (cwd) {
64
- const p = join(cwd, ".aide", "bin", "aide");
95
+ const p = join(resolveRoot(cwd), ".aide", "bin", "aide");
65
96
  if (existsSync(p)) return p;
66
97
  }
67
98
  try {
@@ -24,12 +24,14 @@ 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 } from "../lib/hook-utils.js";
27
+ import { readStdin, detectPlatform, isFalsy } from "../lib/hook-utils.js";
28
28
  import { findAideBinary, ensureAideBinary } from "../lib/aide-downloader.js";
29
- import { recordTokenEvent } from "../core/read-tracking.js";
29
+ import { findProjectRoot } from "../lib/project-root.js";
30
+ import { emitInjectionEvent } from "../core/read-tracking.js";
30
31
  import {
31
32
  ensureDirectories as coreEnsureDirectories,
32
33
  loadConfig as coreLoadConfig,
34
+ loadGlobalConfig as coreLoadGlobalConfig,
33
35
  initializeSession as coreInitializeSession,
34
36
  cleanupStaleStateFiles as coreCleanupStaleStateFiles,
35
37
  resetHudState as coreResetHudState,
@@ -340,13 +342,35 @@ async function main(): Promise<void> {
340
342
  }
341
343
 
342
344
  const data: HookInput = JSON.parse(input);
343
- const cwd = data.cwd || process.cwd();
345
+ const launchedCwd = data.cwd || process.cwd();
344
346
  const sessionId = data.session_id || "unknown";
345
347
 
348
+ // Resolve the project root so we never plant a sibling .aide/ in a
349
+ // subdirectory of a git repo. Mirrors the Go binary's findProjectRoot().
350
+ const { root: resolvedRoot, hasMarker } = findProjectRoot(launchedCwd);
351
+ if (!hasMarker) {
352
+ const requireGit = coreLoadGlobalConfig().requireGit ?? true;
353
+ if (requireGit) {
354
+ process.stderr.write(
355
+ `[aide] No .git/ or .aide/ found walking up from ${launchedCwd}. ` +
356
+ `Set \`requireGit\`: false in ~/.aide/config/aide.json to allow ` +
357
+ `init in arbitrary directories. Skipping AIDE bootstrap.\n`,
358
+ );
359
+ console.log(JSON.stringify({ continue: true }));
360
+ return;
361
+ }
362
+ process.stderr.write(
363
+ `[aide] No project root found, falling back to ${launchedCwd} (requireGit=false).\n`,
364
+ );
365
+ }
366
+ const cwd = hasMarker ? resolvedRoot : launchedCwd;
367
+
346
368
  // Switch debug logging to project-local logs
347
369
  setDebugCwd(cwd);
348
370
 
349
- debugLog(`Parsed input: cwd=${cwd}, sessionId=${sessionId.slice(0, 8)}`);
371
+ debugLog(
372
+ `Parsed input: cwd=${cwd}, launchedCwd=${launchedCwd}, sessionId=${sessionId.slice(0, 8)}`,
373
+ );
350
374
 
351
375
  // Initialize logger
352
376
  log = new Logger("session-start", cwd);
@@ -365,32 +389,38 @@ async function main(): Promise<void> {
365
389
  installHudWrapper(log);
366
390
  debugLog(`installHudWrapper complete (${Date.now() - hookStart}ms)`);
367
391
 
368
- // Sync MCP server configs across assistants (FS only, fast)
369
- debugLog("mcpSync starting...");
370
- log.start("mcpSync");
371
- try {
372
- const mcpResult = syncMcpServers(detectPlatform(), cwd);
373
- const totalImported =
374
- mcpResult.user.imported + mcpResult.project.imported;
375
- const totalWritten =
376
- mcpResult.user.serversWritten + mcpResult.project.serversWritten;
377
- const totalSkipped = mcpResult.user.skipped + mcpResult.project.skipped;
378
- log.end("mcpSync", {
379
- userServers: mcpResult.user.serversWritten,
380
- projectServers: mcpResult.project.serversWritten,
381
- imported: totalImported,
382
- skipped: totalSkipped,
383
- });
384
- if (totalImported > 0) {
385
- debugLog(
386
- `mcp-sync: imported ${totalImported} server(s), ${totalWritten} total`,
387
- );
392
+ // Sync MCP server configs across assistants (FS only, fast).
393
+ // Opt-out via AIDE_MCP_SYNC=0 (defaults to enabled).
394
+ if (isFalsy(process.env.AIDE_MCP_SYNC)) {
395
+ debugLog("mcpSync disabled via AIDE_MCP_SYNC");
396
+ log.info("MCP sync disabled (AIDE_MCP_SYNC falsy)");
397
+ } else {
398
+ debugLog("mcpSync starting...");
399
+ log.start("mcpSync");
400
+ try {
401
+ const mcpResult = syncMcpServers(detectPlatform(), cwd);
402
+ const totalImported =
403
+ mcpResult.user.imported + mcpResult.project.imported;
404
+ const totalWritten =
405
+ mcpResult.user.serversWritten + mcpResult.project.serversWritten;
406
+ const totalSkipped = mcpResult.user.skipped + mcpResult.project.skipped;
407
+ log.end("mcpSync", {
408
+ userServers: mcpResult.user.serversWritten,
409
+ projectServers: mcpResult.project.serversWritten,
410
+ imported: totalImported,
411
+ skipped: totalSkipped,
412
+ });
413
+ if (totalImported > 0) {
414
+ debugLog(
415
+ `mcp-sync: imported ${totalImported} server(s), ${totalWritten} total`,
416
+ );
417
+ }
418
+ } catch (err) {
419
+ log.warn("MCP sync failed (non-fatal)", err);
420
+ log.end("mcpSync", { success: false, error: String(err) });
388
421
  }
389
- } catch (err) {
390
- log.warn("MCP sync failed (non-fatal)", err);
391
- log.end("mcpSync", { success: false, error: String(err) });
422
+ debugLog(`mcpSync complete (${Date.now() - hookStart}ms)`);
392
423
  }
393
- debugLog(`mcpSync complete (${Date.now() - hookStart}ms)`);
394
424
 
395
425
  // Check that aide binary is available (auto-downloads if missing/outdated)
396
426
  debugLog("checkAideBinary starting...");
@@ -459,26 +489,30 @@ async function main(): Promise<void> {
459
489
  log.end("buildWelcomeContext");
460
490
  debugLog(`buildWelcomeContext complete (${Date.now() - hookStart}ms)`);
461
491
 
462
- // Record token events for context injection
463
492
  try {
464
493
  const binary = findAideBinary(cwd);
465
- if (binary && context) {
466
- const memoryTokens = Math.round(
467
- ([...memories.static.global, ...memories.static.project, ...memories.dynamic.sessions]
468
- .join("").length) / 3.0
469
- );
470
- const decisionTokens = Math.round(
471
- memories.static.decisions.join("").length / 3.0
472
- );
473
- if (memoryTokens > 0) {
474
- recordTokenEvent(binary, cwd, "context_injected", "memory", "session-start", memoryTokens);
475
- }
476
- if (decisionTokens > 0) {
477
- recordTokenEvent(binary, cwd, "context_injected", "decision", "session-start", decisionTokens);
494
+ if (binary && context && memories.sources) {
495
+ for (const src of memories.sources) {
496
+ const attrs: Record<string, string> = { scope: src.scope };
497
+ if (src.category) attrs.category = src.category;
498
+ if (src.tags && src.tags.length > 0) attrs.tags = src.tags.join(",");
499
+ if (src.sessionId) attrs.source_session_id = src.sessionId;
500
+ if (typeof src.score === "number") {
501
+ attrs.score_at_injection = src.score.toFixed(2);
502
+ }
503
+
504
+ emitInjectionEvent(binary, cwd, {
505
+ source: SOURCE,
506
+ subtype: src.kind,
507
+ name: src.name,
508
+ content: src.content,
509
+ sessionId,
510
+ attrs: { ...attrs, source_id: src.id },
511
+ });
478
512
  }
479
513
  }
480
514
  } catch {
481
- // Non-fatal — don't break session start for token tracking
515
+ // Non-fatal — don't break session start for telemetry
482
516
  }
483
517
 
484
518
  log.end("total");
@@ -19,6 +19,7 @@ import { existsSync, mkdirSync } from "fs";
19
19
  import { join } from "path";
20
20
  import { Logger, debug, setDebugCwd } from "../lib/logger.js";
21
21
  import { readStdin, detectPlatform } from "../lib/hook-utils.js";
22
+ import { findProjectRoot } from "../lib/project-root.js";
22
23
  import {
23
24
  discoverSkills as coreDiscoverSkills,
24
25
  matchSkills as coreMatchSkills,
@@ -26,7 +27,12 @@ import {
26
27
  } from "../core/skill-matcher.js";
27
28
  import type { Skill } from "../core/types.js";
28
29
  import { findAideBinary } from "../core/aide-client.js";
29
- import { recordObserveEvent } from "../core/read-tracking.js";
30
+ import {
31
+ recordObserveEvent,
32
+ previewContent,
33
+ emitInjectionEvent,
34
+ } from "../core/read-tracking.js";
35
+ import { reflectEnabled } from "../lib/hook-utils.js";
30
36
 
31
37
  const SOURCE = "skill-injector";
32
38
 
@@ -57,12 +63,15 @@ let log: Logger | null = null;
57
63
  * Ensure .aide directories exist (minimal version for skill-injector)
58
64
  */
59
65
  function ensureDirectories(cwd: string): void {
66
+ // Resolve to the canonical project root so we don't plant a stray .aide/
67
+ // in a subdir the harness happened to launch from. See lib/project-root.ts.
68
+ const { root } = findProjectRoot(cwd);
60
69
  const dirs = [
61
- join(cwd, ".aide"),
62
- join(cwd, ".aide", "skills"),
63
- join(cwd, ".aide", "config"),
64
- join(cwd, ".aide", "state"),
65
- join(cwd, ".aide", "memory"),
70
+ join(root, ".aide"),
71
+ join(root, ".aide", "skills"),
72
+ join(root, ".aide", "config"),
73
+ join(root, ".aide", "state"),
74
+ join(root, ".aide", "memory"),
66
75
  ];
67
76
 
68
77
  for (const dir of dirs) {
@@ -156,6 +165,7 @@ async function main(): Promise<void> {
156
165
  const data: HookInput = JSON.parse(input);
157
166
  const prompt = data.prompt || "";
158
167
  const cwd = data.cwd || process.cwd();
168
+ const sessionId = data.session_id || "";
159
169
 
160
170
  // Switch debug logging to project-local logs
161
171
  setDebugCwd(cwd);
@@ -175,6 +185,27 @@ async function main(): Promise<void> {
175
185
  log.end("ensureDirectories");
176
186
  debugLog(`ensureDirectories complete (${Date.now() - hookStart}ms)`);
177
187
 
188
+ // Emit a user-prompt observe event so the convergence detector can find
189
+ // corrective markers near edit sequences. Gated behind AIDE_REFLECT so
190
+ // sessions that aren't running reflect don't accrue extra events.
191
+ if (prompt && reflectEnabled(cwd)) {
192
+ try {
193
+ const binary = findAideBinary({ cwd });
194
+ if (binary) {
195
+ recordObserveEvent(binary, cwd, {
196
+ kind: "hook",
197
+ name: "user_prompt",
198
+ category: "input",
199
+ tokens: Math.round(prompt.length / 3.0),
200
+ session: sessionId,
201
+ attrs: { text: previewContent(prompt, 2000) },
202
+ });
203
+ }
204
+ } catch {
205
+ // Non-fatal
206
+ }
207
+ }
208
+
178
209
  if (!prompt) {
179
210
  debugLog("No prompt provided, exiting");
180
211
  log.info("No prompt provided");
@@ -215,14 +246,12 @@ async function main(): Promise<void> {
215
246
  if (binary) {
216
247
  for (const skill of matched) {
217
248
  const text = `### ${skill.name}\n${skill.description ?? ""}\n${skill.content}`;
218
- const tokens = Math.round(text.length / 3.0);
219
- recordObserveEvent(binary, cwd, {
220
- kind: "injection",
221
- name: skill.name,
222
- category: "inject",
249
+ emitInjectionEvent(binary, cwd, {
250
+ source: SOURCE,
223
251
  subtype: "skill",
224
- tokens,
225
- file: SOURCE,
252
+ name: skill.name,
253
+ content: text,
254
+ sessionId,
226
255
  });
227
256
  }
228
257
  }
@@ -17,8 +17,9 @@
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 } from "../lib/hook-utils.js";
20
+ import { readStdin, setMemoryState, isFalsy } from "../lib/hook-utils.js";
21
21
  import { findAideBinary } from "../core/aide-client.js";
22
+ import { emitInjectionEvent } from "../core/read-tracking.js";
22
23
  import { refreshHud } from "../lib/hud.js";
23
24
 
24
25
  // Global logger instance
@@ -112,8 +113,7 @@ function fetchSubagentMemories(cwd: string): {
112
113
  decisions: [] as string[],
113
114
  };
114
115
 
115
- // Check for disable flag
116
- if (process.env.AIDE_MEMORY_INJECT === "0") {
116
+ if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
117
117
  return result;
118
118
  }
119
119
 
@@ -315,6 +315,11 @@ async function processSubagentStart(
315
315
  setAgentState(cwd, agent_id, "type", type);
316
316
  setAgentState(cwd, agent_id, "startedAt", new Date().toISOString());
317
317
  setAgentState(cwd, agent_id, "session", session_id); // Track which session owns this agent
318
+ // Parent linkage for swarm queries: the orchestrator (session_id) spawned
319
+ // this subagent (agent_id). Watch filters key off parent_session.
320
+ // namespace stamps swarm-scoped memories the subagent creates.
321
+ setAgentState(cwd, agent_id, "parent_session", session_id);
322
+ setAgentState(cwd, agent_id, "namespace", `swarm:${session_id}`);
318
323
  log?.end("registerAgent");
319
324
 
320
325
  // Refresh HUD to show the new running agent
@@ -406,6 +411,31 @@ async function main(): Promise<void> {
406
411
  hookEventName: "SubagentStart",
407
412
  additionalContext,
408
413
  };
414
+ try {
415
+ const binary = findAideBinary({
416
+ cwd,
417
+ pluginRoot:
418
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
419
+ });
420
+ if (binary) {
421
+ const startData = data as SubagentStartInput;
422
+ emitInjectionEvent(binary, cwd, {
423
+ source: "subagent-tracker",
424
+ subtype: "signal",
425
+ name: "subagent-priming",
426
+ content: additionalContext,
427
+ sessionId: startData.session_id,
428
+ attrs: {
429
+ ...(startData.agent_id ? { agent_id: startData.agent_id } : {}),
430
+ ...(startData.agent_type
431
+ ? { agent_type: startData.agent_type }
432
+ : {}),
433
+ },
434
+ });
435
+ }
436
+ } catch {
437
+ // Non-fatal
438
+ }
409
439
  }
410
440
 
411
441
  console.log(JSON.stringify(output));