@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.
@@ -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,10 +24,10 @@ 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
29
  import { findProjectRoot } from "../lib/project-root.js";
30
- import { recordTokenEvent } from "../core/read-tracking.js";
30
+ import { emitInjectionEvent } from "../core/read-tracking.js";
31
31
  import {
32
32
  ensureDirectories as coreEnsureDirectories,
33
33
  loadConfig as coreLoadConfig,
@@ -389,32 +389,38 @@ async function main(): Promise<void> {
389
389
  installHudWrapper(log);
390
390
  debugLog(`installHudWrapper complete (${Date.now() - hookStart}ms)`);
391
391
 
392
- // Sync MCP server configs across assistants (FS only, fast)
393
- debugLog("mcpSync starting...");
394
- log.start("mcpSync");
395
- try {
396
- const mcpResult = syncMcpServers(detectPlatform(), cwd);
397
- const totalImported =
398
- mcpResult.user.imported + mcpResult.project.imported;
399
- const totalWritten =
400
- mcpResult.user.serversWritten + mcpResult.project.serversWritten;
401
- const totalSkipped = mcpResult.user.skipped + mcpResult.project.skipped;
402
- log.end("mcpSync", {
403
- userServers: mcpResult.user.serversWritten,
404
- projectServers: mcpResult.project.serversWritten,
405
- imported: totalImported,
406
- skipped: totalSkipped,
407
- });
408
- if (totalImported > 0) {
409
- debugLog(
410
- `mcp-sync: imported ${totalImported} server(s), ${totalWritten} total`,
411
- );
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) });
412
421
  }
413
- } catch (err) {
414
- log.warn("MCP sync failed (non-fatal)", err);
415
- log.end("mcpSync", { success: false, error: String(err) });
422
+ debugLog(`mcpSync complete (${Date.now() - hookStart}ms)`);
416
423
  }
417
- debugLog(`mcpSync complete (${Date.now() - hookStart}ms)`);
418
424
 
419
425
  // Check that aide binary is available (auto-downloads if missing/outdated)
420
426
  debugLog("checkAideBinary starting...");
@@ -483,26 +489,30 @@ async function main(): Promise<void> {
483
489
  log.end("buildWelcomeContext");
484
490
  debugLog(`buildWelcomeContext complete (${Date.now() - hookStart}ms)`);
485
491
 
486
- // Record token events for context injection
487
492
  try {
488
493
  const binary = findAideBinary(cwd);
489
- if (binary && context) {
490
- const memoryTokens = Math.round(
491
- ([...memories.static.global, ...memories.static.project, ...memories.dynamic.sessions]
492
- .join("").length) / 3.0
493
- );
494
- const decisionTokens = Math.round(
495
- memories.static.decisions.join("").length / 3.0
496
- );
497
- if (memoryTokens > 0) {
498
- recordTokenEvent(binary, cwd, "context_injected", "memory", "session-start", memoryTokens);
499
- }
500
- if (decisionTokens > 0) {
501
- 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
+ });
502
512
  }
503
513
  }
504
514
  } catch {
505
- // Non-fatal — don't break session start for token tracking
515
+ // Non-fatal — don't break session start for telemetry
506
516
  }
507
517
 
508
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));
@@ -12,6 +12,8 @@
12
12
  import { readStdin } from "../lib/hook-utils.js";
13
13
  import { debug } from "../lib/logger.js";
14
14
  import { checkWriteGuard } from "../core/write-guard.js";
15
+ import { findAideBinary } from "../core/aide-client.js";
16
+ import { emitInjectionEvent } from "../core/read-tracking.js";
15
17
 
16
18
  const SOURCE = "write-guard";
17
19
 
@@ -48,14 +50,35 @@ async function main(): Promise<void> {
48
50
  const toolName = data.tool_name || "";
49
51
  const toolInput = data.tool_input || {};
50
52
  const cwd = data.cwd || process.cwd();
53
+ const sessionId = data.session_id || "";
51
54
 
52
55
  const result = checkWriteGuard(toolName, toolInput, cwd);
53
56
 
54
57
  if (!result.allowed) {
55
- debug(
56
- SOURCE,
57
- `Advisory: Write to existing file: ${toolInput.file_path || toolInput.filePath || toolInput.path}`,
58
- );
58
+ const filePath =
59
+ (toolInput.file_path as string) ||
60
+ (toolInput.filePath as string) ||
61
+ (toolInput.path as string) ||
62
+ "";
63
+ debug(SOURCE, `Advisory: Write to existing file: ${filePath}`);
64
+ try {
65
+ const binary = findAideBinary({
66
+ cwd,
67
+ pluginRoot:
68
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
69
+ });
70
+ if (binary && result.message) {
71
+ emitInjectionEvent(binary, cwd, {
72
+ source: SOURCE,
73
+ subtype: "guard",
74
+ content: result.message,
75
+ sessionId,
76
+ attrs: { tool: toolName, ...(filePath ? { file: filePath } : {}) },
77
+ });
78
+ }
79
+ } catch {
80
+ // Non-fatal
81
+ }
59
82
  const output: HookOutput = {
60
83
  continue: true,
61
84
  hookSpecificOutput: {
@@ -93,6 +93,69 @@ export function detectPlatform(): "claude-code" | "codex" {
93
93
  return "claude-code";
94
94
  }
95
95
 
96
+ import { existsSync, readFileSync } from "fs";
97
+ import { join } from "path";
98
+ import { findProjectRoot } from "./project-root.js";
99
+
100
+ const TRUTHY = new Set(["1", "true", "on", "yes"]);
101
+ const FALSY = new Set(["0", "false", "off", "no"]);
102
+
103
+ /**
104
+ * isTruthy reports whether an env-var value should be treated as "on".
105
+ * Accepts the same set as the Go config helper: 1/true/on/yes
106
+ * (case-insensitive, whitespace-trimmed). Use for opt-in flags.
107
+ */
108
+ export function isTruthy(v: string | undefined): boolean {
109
+ if (!v) return false;
110
+ return TRUTHY.has(v.trim().toLowerCase());
111
+ }
112
+
113
+ /**
114
+ * isFalsy reports whether an env-var value was explicitly set to disable.
115
+ * Accepts 0/false/off/no. Unset/empty/unknown values return false so the
116
+ * caller's default-on behaviour wins. Use for opt-out flags.
117
+ */
118
+ export function isFalsy(v: string | undefined): boolean {
119
+ if (!v) return false;
120
+ return FALSY.has(v.trim().toLowerCase());
121
+ }
122
+
123
+ /**
124
+ * reflectEnabled mirrors the Go-side config.ResolveReflectEnabled precedence
125
+ * so TS hooks that need to gate on the reflect setting see the same answer
126
+ * as `aide reflect run`. Precedence:
127
+ *
128
+ * 1. AIDE_REFLECT env (recognised truthy/falsy values win)
129
+ * 2. .aide/config/aide.json `reflect.enabled` at the resolved project
130
+ * root (walks up from cwd via findProjectRoot — does NOT just look
131
+ * at cwd/.aide/)
132
+ * 3. default false
133
+ *
134
+ * Used by skill-injector.ts and opencode/hooks.ts to gate the user_prompt
135
+ * observe-event emit that convergence detection depends on.
136
+ */
137
+ export function reflectEnabled(cwd: string): boolean {
138
+ const env = process.env.AIDE_REFLECT;
139
+ if (env !== undefined && env !== "") {
140
+ const norm = env.trim().toLowerCase();
141
+ if (TRUTHY.has(norm)) return true;
142
+ if (FALSY.has(norm)) return false;
143
+ }
144
+ try {
145
+ const { root } = findProjectRoot(cwd);
146
+ const cfgPath = join(root, ".aide", "config", "aide.json");
147
+ if (existsSync(cfgPath)) {
148
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf-8")) as {
149
+ reflect?: { enabled?: boolean };
150
+ };
151
+ return cfg?.reflect?.enabled === true;
152
+ }
153
+ } catch {
154
+ // Unreadable / malformed config — treat as unset.
155
+ }
156
+ return false;
157
+ }
158
+
96
159
  /**
97
160
  * Get the plugin root directory from environment variables.
98
161
  */
package/src/lib/hud.ts CHANGED
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
9
9
  import { join } from "path";
10
10
  import { execFileSync } from "child_process";
11
11
  import { runAide, findAideBinary } from "./hook-utils.js";
12
+ import { findProjectRoot } from "./project-root.js";
12
13
 
13
14
  // Cache the aide version for the session (won't change)
14
15
  let aideVersionCache: string | null = null;
@@ -149,7 +150,8 @@ export function getAgentStates(cwd: string): AgentState[] {
149
150
  * Load HUD configuration
150
151
  */
151
152
  export function loadHudConfig(cwd: string): HudConfig {
152
- const configPath = join(cwd, ".aide", "config", "hud.json");
153
+ const { root } = findProjectRoot(cwd);
154
+ const configPath = join(root, ".aide", "config", "hud.json");
153
155
 
154
156
  if (existsSync(configPath)) {
155
157
  try {
@@ -444,7 +446,8 @@ export function formatHud(
444
446
  * Write HUD output to state file
445
447
  */
446
448
  export function writeHudOutput(cwd: string, output: string): void {
447
- const stateDir = join(cwd, ".aide", "state");
449
+ const { root } = findProjectRoot(cwd);
450
+ const stateDir = join(root, ".aide", "state");
448
451
  if (!existsSync(stateDir)) {
449
452
  try {
450
453
  mkdirSync(stateDir, { recursive: true });