@jmylchreest/aide-plugin 0.0.66 → 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.
package/src/lib/logger.ts CHANGED
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { existsSync, mkdirSync, appendFileSync } from "fs";
23
23
  import { join } from "path";
24
+ import { findProjectRoot } from "./project-root.js";
24
25
 
25
26
  export type LogLevel = "debug" | "info" | "warn" | "error";
26
27
 
@@ -51,10 +52,14 @@ export class Logger {
51
52
  constructor(source: string, cwd?: string) {
52
53
  this.source = source;
53
54
  this.cwd = cwd || process.cwd();
54
- // Set debugLogCwd so isDebugEnabled() can check the sentinel file
55
- if (cwd) setDebugCwd(cwd);
55
+ // Resolve to the canonical project root so logs land alongside the
56
+ // real .aide/ store, not in a stray subdir the harness launched from.
57
+ const { root } = findProjectRoot(this.cwd);
58
+ // Set debugLogCwd so isDebugEnabled() can check the sentinel file at
59
+ // the resolved root.
60
+ setDebugCwd(root);
56
61
  this.enabled = isDebugEnabled();
57
- this.logDir = join(this.cwd, ".aide", "_logs");
62
+ this.logDir = join(root, ".aide", "_logs");
58
63
  this.logFile = join(this.logDir, "startup.log");
59
64
  this.sessionStart = Date.now();
60
65
 
@@ -288,11 +293,13 @@ export function isDebugEnabled(): boolean {
288
293
  const debugEnv = process.env.AIDE_DEBUG || "";
289
294
  if (debugEnv === "1" || debugEnv === "true") return true;
290
295
 
291
- // Check sentinel file (cached per cwd)
296
+ // Check sentinel file (cached per cwd). Resolve to project root so the
297
+ // sentinel works regardless of which subdir the hook fired from.
292
298
  if (debugLogCwd !== debugSentinelCwd) {
293
299
  debugSentinelCwd = debugLogCwd;
294
300
  try {
295
- debugSentinelResult = existsSync(join(debugLogCwd, ".aide", ".debug"));
301
+ const { root } = findProjectRoot(debugLogCwd);
302
+ debugSentinelResult = existsSync(join(root, ".aide", ".debug"));
296
303
  } catch {
297
304
  debugSentinelResult = false;
298
305
  }
@@ -303,6 +310,10 @@ export function isDebugEnabled(): boolean {
303
310
  /**
304
311
  * Set the working directory for debug logging.
305
312
  * Call this after parsing stdin to use project-local logs.
313
+ *
314
+ * Note: callers may pass either the raw cwd or an already-resolved project
315
+ * root. Logger's constructor resolves cwd → root before calling this; bare
316
+ * callers of debug() may pass cwd and we'll resolve at use-time.
306
317
  */
307
318
  export function setDebugCwd(cwd: string): void {
308
319
  debugLogCwd = cwd;
@@ -326,7 +337,8 @@ export function setDebugCwd(cwd: string): void {
326
337
  export function debug(source: string, msg: string): void {
327
338
  if (!isDebugEnabled()) return;
328
339
 
329
- const logDir = join(debugLogCwd, ".aide", "_logs");
340
+ const { root } = findProjectRoot(debugLogCwd);
341
+ const logDir = join(root, ".aide", "_logs");
330
342
  try {
331
343
  if (!existsSync(logDir)) {
332
344
  mkdirSync(logDir, { recursive: true });
@@ -9,11 +9,22 @@
9
9
  *
10
10
  * Resolution order, matching main.go:findProjectRoot:
11
11
  * 1. AIDE_PROJECT_ROOT env override (must be an existing directory).
12
- * 2. Walk up from cwd. At each level:
13
- * a. .aide/ — return this dir. Skip ~/.aide/ unless cwd === $HOME.
14
- * b. .git/ directory return this dir.
15
- * c. .git/ file (worktree pointer) — resolve to the main repo root.
12
+ * 2. Walk the full ancestry from cwd to /, collecting candidates, then
13
+ * prefer:
14
+ * a. Closest ancestor with BOTH .aide/ and a VCS marker
15
+ * (.git/.hg/.svn/.bzr/.fossil) — handles the common case where
16
+ * the canonical root sits at the git repo root.
17
+ * b. Closest ancestor with a VCS marker only — .aide/ will be
18
+ * created there if needed.
19
+ * c. Closest ancestor with .aide/ only — standalone projects with
20
+ * no VCS.
21
+ * ~/.aide/ is skipped as a project marker unless cwd is $HOME.
16
22
  * 3. No marker found: return { root: cwd, hasMarker: false }.
23
+ *
24
+ * The "both markers wins" priority is what stops a stray child .aide/ from
25
+ * shadowing the real project root: a sibling .aide/ created by an
26
+ * accidental CLI invocation lives in a subdir with no .git/, so the walk
27
+ * keeps going until it finds the parent that has both.
17
28
  */
18
29
 
19
30
  import { basename, dirname, join, resolve } from "path";
@@ -53,42 +64,68 @@ export function findProjectRoot(cwd: string): ProjectRootResult {
53
64
  const startCwd = resolve(cwd);
54
65
  const home = homedir();
55
66
 
67
+ interface Candidate {
68
+ dir: string;
69
+ hasAide: boolean;
70
+ hasVCS: boolean;
71
+ vcsResolved: string;
72
+ }
73
+ const path: Candidate[] = [];
74
+
56
75
  let dir = startCwd;
57
76
  for (;;) {
58
- const aidePath = join(dir, ".aide");
59
- if (existsSync(aidePath)) {
60
- // Skip ~/.aide/ unless cwd is $HOME itself. ~/.aide/ is the global
61
- // config dir, not a project marker.
62
- if (!(home && dir === home && startCwd !== home)) {
63
- return { root: dir, hasMarker: true };
64
- }
77
+ const cand: Candidate = { dir, hasAide: false, hasVCS: false, vcsResolved: "" };
78
+
79
+ if (existsSync(join(dir, ".aide"))) {
80
+ // Skip ~/.aide/ unless cwd is $HOME itself.
81
+ const isHomeAide = home && dir === home && startCwd !== home;
82
+ if (!isHomeAide) cand.hasAide = true;
65
83
  }
66
84
 
67
- const gitPath = join(dir, ".git");
68
- if (existsSync(gitPath)) {
69
- try {
70
- const stat = statSync(gitPath);
71
- if (stat.isDirectory()) {
72
- return { root: dir, hasMarker: true };
73
- }
74
- if (stat.isFile()) {
75
- const mainRoot = resolveWorktreeGitFile(gitPath);
76
- if (mainRoot) {
77
- return { root: mainRoot, hasMarker: true };
78
- }
79
- return { root: dir, hasMarker: true };
80
- }
81
- } catch {
82
- return { root: dir, hasMarker: true };
83
- }
85
+ const vcs = vcsMarkerAt(dir);
86
+ if (vcs.ok) {
87
+ cand.hasVCS = true;
88
+ cand.vcsResolved = vcs.resolved || dir;
84
89
  }
85
90
 
91
+ if (cand.hasAide || cand.hasVCS) path.push(cand);
92
+
86
93
  const parent = dirname(dir);
87
- if (parent === dir) {
88
- return { root: startCwd, hasMarker: false };
89
- }
94
+ if (parent === dir) break;
90
95
  dir = parent;
91
96
  }
97
+
98
+ for (const c of path) {
99
+ if (c.hasAide && c.hasVCS) return { root: c.vcsResolved, hasMarker: true };
100
+ }
101
+ for (const c of path) {
102
+ if (c.hasVCS) return { root: c.vcsResolved, hasMarker: true };
103
+ }
104
+ for (const c of path) {
105
+ if (c.hasAide) return { root: c.dir, hasMarker: true };
106
+ }
107
+ return { root: startCwd, hasMarker: false };
108
+ }
109
+
110
+ function vcsMarkerAt(dir: string): { ok: boolean; resolved: string } {
111
+ const gitPath = join(dir, ".git");
112
+ if (existsSync(gitPath)) {
113
+ try {
114
+ const st = statSync(gitPath);
115
+ if (st.isDirectory()) return { ok: true, resolved: dir };
116
+ if (st.isFile()) {
117
+ const mainRoot = resolveWorktreeGitFile(gitPath);
118
+ return { ok: true, resolved: mainRoot || dir };
119
+ }
120
+ return { ok: true, resolved: dir };
121
+ } catch {
122
+ return { ok: true, resolved: dir };
123
+ }
124
+ }
125
+ for (const marker of [".hg", ".svn", ".bzr", ".fossil"]) {
126
+ if (existsSync(join(dir, marker))) return { ok: true, resolved: dir };
127
+ }
128
+ return { ok: false, resolved: "" };
92
129
  }
93
130
 
94
131
  /**
@@ -52,12 +52,14 @@ import { checkWriteGuard } from "../core/write-guard.js";
52
52
  import { checkSmartReadHint } from "../core/context-guard.js";
53
53
  import { checkSearchEnrichment } from "../core/search-enrichment.js";
54
54
  import { recordToolEvent } from "../core/tool-observe.js";
55
+ import { recordObserveEvent, previewContent } from "../core/read-tracking.js";
55
56
  import {
56
57
  checkComments,
57
58
  getCheckableFilePath,
58
59
  getContentToCheck,
59
60
  } from "../core/comment-checker.js";
60
61
  import { getState, setState } from "../core/aide-client.js";
62
+ import { isFalsy, reflectEnabled } from "../lib/hook-utils.js";
61
63
  import { saveStateSnapshot } from "../core/pre-compact-logic.js";
62
64
  import { cleanupSession } from "../core/cleanup.js";
63
65
  import {
@@ -335,11 +337,16 @@ function initializeAide(state: AideState): void {
335
337
 
336
338
  ensureDirectories(state.cwd);
337
339
 
338
- // Sync MCP server configs across assistants (FS only, fast)
339
- try {
340
- syncMcpServers("opencode", state.cwd);
341
- } catch (err) {
342
- debug(SOURCE, `MCP sync failed (non-fatal): ${err}`);
340
+ // Sync MCP server configs across assistants (FS only, fast).
341
+ // Opt-out via AIDE_MCP_SYNC=0 (defaults to enabled).
342
+ if (isFalsy(process.env.AIDE_MCP_SYNC)) {
343
+ debug(SOURCE, "MCP sync disabled (AIDE_MCP_SYNC falsy)");
344
+ } else {
345
+ try {
346
+ syncMcpServers("opencode", state.cwd);
347
+ } catch (err) {
348
+ debug(SOURCE, `MCP sync failed (non-fatal): ${err}`);
349
+ }
343
350
  }
344
351
 
345
352
  const config = loadConfig(state.cwd);
@@ -592,6 +599,22 @@ async function handleSessionIdle(
592
599
  cleanupPartials(state.binary, state.cwd, sessionId);
593
600
  }
594
601
  }
602
+
603
+ // Reflect (extract instinct proposals). The CLI itself checks env +
604
+ // .aide/config/aide.json reflect.enabled and no-ops when disabled, so
605
+ // invoke unconditionally. Mirrors the Claude Code Stop hook.
606
+ if (state.binary) {
607
+ try {
608
+ execFileSync(state.binary, ["reflect", "run", `--session=${sessionId}`], {
609
+ cwd: state.cwd,
610
+ timeout: 10000,
611
+ stdio: ["pipe", "pipe", "pipe"],
612
+ });
613
+ debug(SOURCE, `reflect run session=${sessionId.slice(0, 8)} ok`);
614
+ } catch (err) {
615
+ debug(SOURCE, `reflect run failed (non-fatal): ${err}`);
616
+ }
617
+ }
595
618
  }
596
619
 
597
620
  async function handleSessionDeleted(
@@ -647,6 +670,24 @@ async function handleMessagePartUpdated(
647
670
  // this event fires after the transform (defensive against ordering).
648
671
  state.lastUserPrompt = prompt;
649
672
 
673
+ // Emit a user-prompt observe event so the convergence detector can find
674
+ // corrective markers near edit sequences. Gated behind AIDE_REFLECT,
675
+ // mirroring src/hooks/skill-injector.ts for cross-harness parity.
676
+ if (state.binary && reflectEnabled(state.cwd)) {
677
+ try {
678
+ recordObserveEvent(state.binary, state.cwd, {
679
+ kind: "hook",
680
+ name: "user_prompt",
681
+ category: "input",
682
+ tokens: Math.round(prompt.length / 3.0),
683
+ session: extractSessionId(event),
684
+ attrs: { text: previewContent(prompt, 2000) },
685
+ });
686
+ } catch {
687
+ // Non-fatal
688
+ }
689
+ }
690
+
650
691
  const skills = discoverSkills(state.cwd, state.pluginRoot ?? undefined);
651
692
  const matched = matchSkills(prompt, skills, 3, "opencode");
652
693