@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.
- package/package.json +3 -3
- package/skills/reflect/SKILL.md +289 -0
- package/skills/swarm/SKILL.md +26 -1
- package/skills/swarm-status/SKILL.md +107 -0
- package/src/core/context-guard.ts +2 -1
- package/src/core/mcp-sync.ts +5 -2
- package/src/core/read-tracking.ts +54 -31
- package/src/core/search-enrichment.ts +2 -1
- package/src/core/session-init.ts +97 -21
- package/src/core/skill-matcher.ts +18 -22
- package/src/core/tool-observe.ts +12 -0
- package/src/core/types.ts +25 -0
- package/src/hooks/agent-cleanup.ts +3 -1
- package/src/hooks/agent-signals.ts +249 -0
- package/src/hooks/comment-checker.ts +26 -0
- package/src/hooks/context-guard.ts +34 -5
- package/src/hooks/context-pruning.ts +27 -0
- package/src/hooks/pre-tool-enforcer.ts +23 -3
- package/src/hooks/reflect.ts +69 -0
- package/src/hooks/search-enrichment.ts +12 -4
- package/src/hooks/session-end.ts +35 -4
- package/src/hooks/session-start.ts +77 -43
- package/src/hooks/skill-injector.ts +42 -13
- package/src/hooks/subagent-tracker.ts +33 -3
- package/src/hooks/write-guard.ts +27 -4
- package/src/lib/aide-downloader.ts +20 -2
- package/src/lib/hook-utils.ts +63 -0
- package/src/lib/hud.ts +5 -2
- package/src/lib/logger.ts +18 -6
- package/src/lib/project-root.ts +174 -0
- package/src/opencode/hooks.ts +46 -5
- package/src/opencode/index.ts +2 -80
package/src/hooks/write-guard.ts
CHANGED
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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: {
|
|
@@ -28,6 +28,8 @@ import { Readable, Transform } from "stream";
|
|
|
28
28
|
import { pipeline } from "stream/promises";
|
|
29
29
|
// Canonical binary finder — import for local use, re-export for backward compat
|
|
30
30
|
import { findAideBinary } from "./hook-utils.js";
|
|
31
|
+
import { findProjectRoot } from "./project-root.js";
|
|
32
|
+
import { loadGlobalConfig } from "../core/session-init.js";
|
|
31
33
|
export { findAideBinary };
|
|
32
34
|
|
|
33
35
|
export interface DownloadResult {
|
|
@@ -544,8 +546,24 @@ Downloads the aide binary from GitHub releases.
|
|
|
544
546
|
}
|
|
545
547
|
destDir = join(pluginRoot, "bin");
|
|
546
548
|
} else if (!destDir) {
|
|
547
|
-
// Default to
|
|
548
|
-
|
|
549
|
+
// Default: resolve to the project root rather than blindly using cwd.
|
|
550
|
+
// Matches the SessionStart hook so the CLI fallback never plants an
|
|
551
|
+
// orphan .aide/bin/ in a subdirectory of a git repo.
|
|
552
|
+
const { root, hasMarker } = findProjectRoot(process.cwd());
|
|
553
|
+
if (!hasMarker) {
|
|
554
|
+
const requireGit = loadGlobalConfig().requireGit ?? true;
|
|
555
|
+
if (requireGit) {
|
|
556
|
+
console.error(
|
|
557
|
+
`[aide] No .git/ or .aide/ found walking up from ${process.cwd()}. ` +
|
|
558
|
+
`Set \`requireGit\`: false in ~/.aide/config/aide.json or pass --cwd / --dest to install anyway.`,
|
|
559
|
+
);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
console.error(
|
|
563
|
+
`[aide] No project root found, installing into ${process.cwd()} (requireGit=false).`,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
destDir = join(hasMarker ? root : process.cwd(), ".aide", "bin");
|
|
549
567
|
}
|
|
550
568
|
|
|
551
569
|
const result = await downloadAideBinary(destDir, { force, quiet: false });
|
package/src/lib/hook-utils.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 });
|
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
|
-
//
|
|
55
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 });
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project root resolution for the AIDE plugin.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the Go binary's findProjectRoot() in aide/cmd/aide/main.go so that
|
|
5
|
+
* the TypeScript hook layer and the Go binary always agree on where `.aide/`
|
|
6
|
+
* lives. Without this, the hook would plant a sibling `.aide/` in whatever
|
|
7
|
+
* subdirectory `claude` was launched from, while the Go binary would walk up
|
|
8
|
+
* and use the real one at the repo root.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order, matching main.go:findProjectRoot:
|
|
11
|
+
* 1. AIDE_PROJECT_ROOT env override (must be an existing directory).
|
|
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.
|
|
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.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { basename, dirname, join, resolve } from "path";
|
|
31
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
32
|
+
import { homedir } from "os";
|
|
33
|
+
|
|
34
|
+
export interface ProjectRootResult {
|
|
35
|
+
root: string;
|
|
36
|
+
hasMarker: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the AIDE project root for a given cwd.
|
|
41
|
+
*
|
|
42
|
+
* `hasMarker` is true when an actual `.aide/` or `.git/` marker was found
|
|
43
|
+
* (or when AIDE_PROJECT_ROOT is set to an existing directory). When false,
|
|
44
|
+
* `root` is just the input cwd — callers should decide whether to fall
|
|
45
|
+
* back to it (e.g. via the `requireGit` config) or refuse to bootstrap.
|
|
46
|
+
*/
|
|
47
|
+
export function findProjectRoot(cwd: string): ProjectRootResult {
|
|
48
|
+
const override = process.env.AIDE_PROJECT_ROOT;
|
|
49
|
+
if (override) {
|
|
50
|
+
try {
|
|
51
|
+
const abs = resolve(override);
|
|
52
|
+
const stat = statSync(abs);
|
|
53
|
+
if (stat.isDirectory()) {
|
|
54
|
+
return { root: abs, hasMarker: true };
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall through to the walk-up.
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`aide: AIDE_PROJECT_ROOT=${JSON.stringify(override)} is not a directory; falling back to walk-up\n`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const startCwd = resolve(cwd);
|
|
65
|
+
const home = homedir();
|
|
66
|
+
|
|
67
|
+
interface Candidate {
|
|
68
|
+
dir: string;
|
|
69
|
+
hasAide: boolean;
|
|
70
|
+
hasVCS: boolean;
|
|
71
|
+
vcsResolved: string;
|
|
72
|
+
}
|
|
73
|
+
const path: Candidate[] = [];
|
|
74
|
+
|
|
75
|
+
let dir = startCwd;
|
|
76
|
+
for (;;) {
|
|
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;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const vcs = vcsMarkerAt(dir);
|
|
86
|
+
if (vcs.ok) {
|
|
87
|
+
cand.hasVCS = true;
|
|
88
|
+
cand.vcsResolved = vcs.resolved || dir;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (cand.hasAide || cand.hasVCS) path.push(cand);
|
|
92
|
+
|
|
93
|
+
const parent = dirname(dir);
|
|
94
|
+
if (parent === dir) break;
|
|
95
|
+
dir = parent;
|
|
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: "" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Walk up from `startDir` looking for `.aide/` or `.git/` markers.
|
|
133
|
+
* Returns the resolved root directory, or null when nothing is found.
|
|
134
|
+
*
|
|
135
|
+
* Thin wrapper around findProjectRoot for callers that want a nullable
|
|
136
|
+
* result rather than the {root,hasMarker} shape (e.g. the OpenCode plugin
|
|
137
|
+
* which has its own fallback chain).
|
|
138
|
+
*/
|
|
139
|
+
export function walkUpForProjectRoot(startDir: string): string | null {
|
|
140
|
+
const { root, hasMarker } = findProjectRoot(startDir);
|
|
141
|
+
return hasMarker ? root : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read a .git worktree file ("gitdir: <path>") and return the main repo root.
|
|
146
|
+
*
|
|
147
|
+
* Mirrors aide/cmd/aide/main.go:resolveWorktreeRoot(). The file's gitdir
|
|
148
|
+
* normally points at "<main>/.git/worktrees/<name>"; we walk up that path
|
|
149
|
+
* until we find a component named ".git" and return its parent.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveWorktreeGitFile(gitFilePath: string): string | null {
|
|
152
|
+
try {
|
|
153
|
+
const content = readFileSync(gitFilePath, "utf-8").trim();
|
|
154
|
+
if (!content.startsWith("gitdir:")) return null;
|
|
155
|
+
|
|
156
|
+
let gitdir = content.slice("gitdir:".length).trim();
|
|
157
|
+
if (!gitdir.startsWith("/")) {
|
|
158
|
+
gitdir = resolve(dirname(gitFilePath), gitdir);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let candidate = gitdir;
|
|
162
|
+
for (;;) {
|
|
163
|
+
const parent = dirname(candidate);
|
|
164
|
+
if (parent === candidate) break;
|
|
165
|
+
if (basename(candidate) === ".git") {
|
|
166
|
+
return parent;
|
|
167
|
+
}
|
|
168
|
+
candidate = parent;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/opencode/hooks.ts
CHANGED
|
@@ -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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
package/src/opencode/index.ts
CHANGED
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import {
|
|
39
|
+
import { dirname, join, resolve } from "path";
|
|
40
40
|
import { fileURLToPath } from "url";
|
|
41
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
42
41
|
import { createHooks } from "./hooks.js";
|
|
43
42
|
import { isDebugEnabled } from "../lib/logger.js";
|
|
43
|
+
import { walkUpForProjectRoot } from "../lib/project-root.js";
|
|
44
44
|
import type { Plugin, PluginInput, Hooks } from "./types.js";
|
|
45
45
|
|
|
46
46
|
// Resolve the plugin package root so we can find bundled skills.
|
|
@@ -121,84 +121,6 @@ function resolveProjectRoot(ctx: PluginInput): {
|
|
|
121
121
|
return { root: directory || "/", hasProjectRoot: false };
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
* Walk up from `startDir` looking for .aide/ or .git/ directories.
|
|
126
|
-
* Returns the project root path, or null if none found.
|
|
127
|
-
*
|
|
128
|
-
* For git worktrees, .git is a file containing "gitdir: <path>".
|
|
129
|
-
* We follow it to the main repo root, matching the Go binary's
|
|
130
|
-
* resolveWorktreeRoot() behavior.
|
|
131
|
-
*/
|
|
132
|
-
function walkUpForProjectRoot(startDir: string): string | null {
|
|
133
|
-
let dir = resolve(startDir);
|
|
134
|
-
for (;;) {
|
|
135
|
-
if (existsSync(join(dir, ".aide"))) {
|
|
136
|
-
return dir;
|
|
137
|
-
}
|
|
138
|
-
const gitPath = join(dir, ".git");
|
|
139
|
-
if (existsSync(gitPath)) {
|
|
140
|
-
try {
|
|
141
|
-
const stat = statSync(gitPath);
|
|
142
|
-
if (stat.isDirectory()) {
|
|
143
|
-
// Normal git repo
|
|
144
|
-
return dir;
|
|
145
|
-
}
|
|
146
|
-
if (stat.isFile()) {
|
|
147
|
-
// Worktree: .git is a file containing "gitdir: <path>"
|
|
148
|
-
// Follow it to the main repo root.
|
|
149
|
-
const mainRoot = resolveWorktreeGitFile(gitPath);
|
|
150
|
-
if (mainRoot) return mainRoot;
|
|
151
|
-
// Fallback to current dir if resolution fails
|
|
152
|
-
return dir;
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
return dir;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const parent = resolve(dir, "..");
|
|
159
|
-
if (parent === dir) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
dir = parent;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Read a .git worktree file and resolve to the main repository root.
|
|
168
|
-
* Mirrors the Go binary's resolveWorktreeRoot() in main.go.
|
|
169
|
-
*
|
|
170
|
-
* The file contains "gitdir: /path/to/repo/.git/worktrees/<name>".
|
|
171
|
-
* We walk up from that gitdir path to find the .git directory,
|
|
172
|
-
* then return its parent.
|
|
173
|
-
*/
|
|
174
|
-
function resolveWorktreeGitFile(gitFilePath: string): string | null {
|
|
175
|
-
try {
|
|
176
|
-
const content = readFileSync(gitFilePath, "utf-8").trim();
|
|
177
|
-
if (!content.startsWith("gitdir:")) return null;
|
|
178
|
-
|
|
179
|
-
let gitdir = content.slice("gitdir:".length).trim();
|
|
180
|
-
// Make absolute if relative
|
|
181
|
-
if (!gitdir.startsWith("/")) {
|
|
182
|
-
gitdir = resolve(dirname(gitFilePath), gitdir);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Walk up from .git/worktrees/<name> to find the .git directory,
|
|
186
|
-
// then return its parent as the repo root.
|
|
187
|
-
let candidate = gitdir;
|
|
188
|
-
for (;;) {
|
|
189
|
-
const parent = dirname(candidate);
|
|
190
|
-
if (parent === candidate) break;
|
|
191
|
-
if (basename(candidate) === ".git") {
|
|
192
|
-
return parent;
|
|
193
|
-
}
|
|
194
|
-
candidate = parent;
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
} catch {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
124
|
export const AidePlugin: Plugin = async (ctx: PluginInput): Promise<Hooks> => {
|
|
203
125
|
// Log raw plugin input BEFORE any resolution for diagnostics.
|
|
204
126
|
// This is the key to understanding what OpenCode actually passes.
|