@jmylchreest/aide-plugin 0.1.1 → 0.1.2
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 +1 -1
- package/skills/reflect/SKILL.md +24 -8
- package/src/cli/codex-config.ts +15 -0
- package/src/core/context-pruning/dedup.ts +7 -2
- package/src/core/context-pruning/tracker.ts +0 -23
- package/src/core/memory-query.ts +63 -0
- package/src/core/partial-memory.ts +23 -105
- package/src/core/session-checkpoint-logic.ts +170 -0
- package/src/core/session-resume-logic.ts +104 -0
- package/src/core/session-summary-logic.ts +16 -41
- package/src/core/session-text.ts +60 -0
- package/src/core/tool-observe.ts +53 -14
- package/src/hooks/agent-cleanup.ts +11 -28
- package/src/hooks/agent-signals.ts +11 -18
- package/src/hooks/comment-checker.ts +14 -31
- package/src/hooks/context-guard.ts +17 -31
- package/src/hooks/context-pruning.ts +13 -30
- package/src/hooks/hud-updater.ts +11 -29
- package/src/hooks/permission-handler.ts +14 -25
- package/src/hooks/persistence.ts +15 -31
- package/src/hooks/pre-compact.ts +52 -38
- package/src/hooks/pre-tool-enforcer.ts +14 -30
- package/src/hooks/reflect.ts +15 -8
- package/src/hooks/search-enrichment.ts +12 -29
- package/src/hooks/session-end.ts +60 -11
- package/src/hooks/session-start.ts +68 -30
- package/src/hooks/session-summary.ts +11 -27
- package/src/hooks/skill-injector.ts +15 -30
- package/src/hooks/subagent-tracker.ts +44 -39
- package/src/hooks/task-completed.ts +6 -9
- package/src/hooks/tool-observe.ts +27 -32
- package/src/hooks/tool-tracker.ts +11 -28
- package/src/hooks/write-guard.ts +12 -29
- package/src/lib/hook-utils.ts +48 -0
- package/src/lib/project-root.ts +30 -15
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Tool Observe Hook (PostToolUse)
|
|
3
|
+
* Tool Observe Hook (PostToolUse + PostToolUseFailure)
|
|
4
4
|
*
|
|
5
5
|
* Single-purpose: record every Claude-native tool invocation as an
|
|
6
6
|
* observe.KindToolCall event. Mirror image of the MCP middleware on the Go
|
|
7
7
|
* side — together they give the dashboard complete tool-call coverage.
|
|
8
8
|
*
|
|
9
|
+
* Claude Code fires PostToolUse only on success and PostToolUseFailure on
|
|
10
|
+
* failure (the latter carries top-level is_error/error/exit_code). This hook is
|
|
11
|
+
* registered for BOTH so failed tool calls are recorded with their error text —
|
|
12
|
+
* the signal the friction detector reads.
|
|
13
|
+
*
|
|
9
14
|
* All taxonomy (tool → category/subtype) and the recording itself live in
|
|
10
15
|
* src/core/tool-observe.ts so the OpenCode tool.execute.after handler can
|
|
11
16
|
* reuse the same logic.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
19
|
+
import {
|
|
20
|
+
readStdin,
|
|
21
|
+
emitHookResult,
|
|
22
|
+
installHookSafetyNet,
|
|
23
|
+
findAideBinary,
|
|
24
|
+
} from "../lib/hook-utils.js";
|
|
16
25
|
import { recordToolEvent } from "../core/tool-observe.js";
|
|
17
26
|
import { debug } from "../lib/logger.js";
|
|
18
27
|
|
|
@@ -28,50 +37,46 @@ interface HookInput {
|
|
|
28
37
|
// Claude Code passes the tool's actual output payload as tool_response.
|
|
29
38
|
// Shape varies per tool (string for Bash, object for others).
|
|
30
39
|
tool_response?: unknown;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
} catch {
|
|
37
|
-
console.log('{"continue":true}');
|
|
38
|
-
}
|
|
40
|
+
// PostToolUseFailure-only fields: the harness flags the failure at the top
|
|
41
|
+
// level (PostToolUse never sets these — it only fires on success).
|
|
42
|
+
is_error?: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
exit_code?: number;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
async function main(): Promise<void> {
|
|
42
48
|
try {
|
|
43
49
|
const input = await readStdin();
|
|
44
50
|
if (!input.trim()) {
|
|
45
|
-
|
|
51
|
+
emitHookResult();
|
|
46
52
|
return;
|
|
47
53
|
}
|
|
48
54
|
const data: HookInput = JSON.parse(input);
|
|
49
55
|
const cwd = data.cwd || process.cwd();
|
|
50
56
|
const toolName = data.tool_name;
|
|
51
57
|
if (!toolName) {
|
|
52
|
-
|
|
58
|
+
emitHookResult();
|
|
53
59
|
return;
|
|
54
60
|
}
|
|
55
|
-
const binary = findAideBinary(
|
|
56
|
-
cwd,
|
|
57
|
-
pluginRoot:
|
|
58
|
-
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
59
|
-
});
|
|
61
|
+
const binary = findAideBinary(cwd);
|
|
60
62
|
if (!binary) {
|
|
61
|
-
|
|
63
|
+
emitHookResult();
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
recordToolEvent(binary, cwd, {
|
|
65
67
|
toolName,
|
|
66
68
|
toolInput: data.tool_input as ToolInput,
|
|
67
69
|
toolResponse: data.tool_response,
|
|
68
|
-
|
|
70
|
+
// PostToolUseFailure marks failure at the top level; PostToolUse omits
|
|
71
|
+
// these (success only). Map both into the shared recorder.
|
|
72
|
+
success: data.is_error === true ? false : data.tool_result?.success,
|
|
73
|
+
errorText: data.error,
|
|
69
74
|
sessionId: data.session_id,
|
|
70
75
|
});
|
|
71
76
|
} catch (err) {
|
|
72
77
|
debug(SOURCE, `Hook error: ${err}`);
|
|
73
78
|
}
|
|
74
|
-
|
|
79
|
+
emitHookResult();
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
type ToolInput = {
|
|
@@ -82,16 +87,6 @@ type ToolInput = {
|
|
|
82
87
|
pattern?: string;
|
|
83
88
|
};
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
87
|
-
outputContinue();
|
|
88
|
-
process.exit(0);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
process.on("unhandledRejection", (reason) => {
|
|
92
|
-
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
93
|
-
outputContinue();
|
|
94
|
-
process.exit(0);
|
|
95
|
-
});
|
|
90
|
+
installHookSafetyNet(SOURCE);
|
|
96
91
|
|
|
97
92
|
main();
|
|
@@ -6,9 +6,13 @@
|
|
|
6
6
|
* Sets currentTool in aide-memory before tool execution.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
readStdin,
|
|
11
|
+
emitHookResult,
|
|
12
|
+
installHookSafetyNet,
|
|
13
|
+
findAideBinary,
|
|
14
|
+
} from "../lib/hook-utils.js";
|
|
10
15
|
import { trackToolUse, formatToolDescription } from "../core/tool-tracking.js";
|
|
11
|
-
import { findAideBinary } from "../core/aide-client.js";
|
|
12
16
|
import { debug } from "../lib/logger.js";
|
|
13
17
|
|
|
14
18
|
const SOURCE = "tool-tracker";
|
|
@@ -35,7 +39,7 @@ async function main(): Promise<void> {
|
|
|
35
39
|
try {
|
|
36
40
|
const input = await readStdin();
|
|
37
41
|
if (!input.trim()) {
|
|
38
|
-
|
|
42
|
+
emitHookResult({ continue: true });
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -45,11 +49,7 @@ async function main(): Promise<void> {
|
|
|
45
49
|
const toolName = data.tool_name || "";
|
|
46
50
|
|
|
47
51
|
if (agentId && toolName) {
|
|
48
|
-
const binary = findAideBinary(
|
|
49
|
-
cwd,
|
|
50
|
-
pluginRoot:
|
|
51
|
-
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
52
|
-
});
|
|
52
|
+
const binary = findAideBinary(cwd);
|
|
53
53
|
|
|
54
54
|
if (binary) {
|
|
55
55
|
trackToolUse(binary, cwd, {
|
|
@@ -60,30 +60,13 @@ async function main(): Promise<void> {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
emitHookResult({ continue: true });
|
|
64
64
|
} catch (error) {
|
|
65
65
|
debug(SOURCE, `Hook error: ${error}`);
|
|
66
|
-
|
|
66
|
+
emitHookResult({ continue: true });
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
72
|
-
try {
|
|
73
|
-
console.log(JSON.stringify({ continue: true }));
|
|
74
|
-
} catch {
|
|
75
|
-
console.log('{"continue":true}');
|
|
76
|
-
}
|
|
77
|
-
process.exit(0);
|
|
78
|
-
});
|
|
79
|
-
process.on("unhandledRejection", (reason) => {
|
|
80
|
-
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
81
|
-
try {
|
|
82
|
-
console.log(JSON.stringify({ continue: true }));
|
|
83
|
-
} catch {
|
|
84
|
-
console.log('{"continue":true}');
|
|
85
|
-
}
|
|
86
|
-
process.exit(0);
|
|
87
|
-
});
|
|
70
|
+
installHookSafetyNet(SOURCE);
|
|
88
71
|
|
|
89
72
|
main();
|
package/src/hooks/write-guard.ts
CHANGED
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* Core logic is in src/core/write-guard.ts for cross-platform reuse.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
readStdin,
|
|
14
|
+
emitHookResult,
|
|
15
|
+
installHookSafetyNet,
|
|
16
|
+
findAideBinary,
|
|
17
|
+
} from "../lib/hook-utils.js";
|
|
13
18
|
import { debug } from "../lib/logger.js";
|
|
14
19
|
import { checkWriteGuard } from "../core/write-guard.js";
|
|
15
|
-
import { findAideBinary } from "../core/aide-client.js";
|
|
16
20
|
import { emitInjectionEvent } from "../core/read-tracking.js";
|
|
17
21
|
|
|
18
22
|
const SOURCE = "write-guard";
|
|
@@ -42,7 +46,7 @@ async function main(): Promise<void> {
|
|
|
42
46
|
try {
|
|
43
47
|
const input = await readStdin();
|
|
44
48
|
if (!input.trim()) {
|
|
45
|
-
|
|
49
|
+
emitHookResult({ continue: true });
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -62,11 +66,7 @@ async function main(): Promise<void> {
|
|
|
62
66
|
"";
|
|
63
67
|
debug(SOURCE, `Advisory: Write to existing file: ${filePath}`);
|
|
64
68
|
try {
|
|
65
|
-
const binary = findAideBinary(
|
|
66
|
-
cwd,
|
|
67
|
-
pluginRoot:
|
|
68
|
-
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
69
|
-
});
|
|
69
|
+
const binary = findAideBinary(cwd);
|
|
70
70
|
if (binary && result.message) {
|
|
71
71
|
emitInjectionEvent(binary, cwd, {
|
|
72
72
|
source: SOURCE,
|
|
@@ -86,33 +86,16 @@ async function main(): Promise<void> {
|
|
|
86
86
|
additionalContext: result.message,
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
|
-
|
|
89
|
+
emitHookResult(output);
|
|
90
90
|
} else {
|
|
91
|
-
|
|
91
|
+
emitHookResult({ continue: true });
|
|
92
92
|
}
|
|
93
93
|
} catch (error) {
|
|
94
94
|
debug(SOURCE, `Hook error: ${error}`);
|
|
95
|
-
|
|
95
|
+
emitHookResult({ continue: true });
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
debug(SOURCE, `UNCAUGHT EXCEPTION: ${err}`);
|
|
101
|
-
try {
|
|
102
|
-
console.log(JSON.stringify({ continue: true }));
|
|
103
|
-
} catch {
|
|
104
|
-
console.log('{"continue":true}');
|
|
105
|
-
}
|
|
106
|
-
process.exit(0);
|
|
107
|
-
});
|
|
108
|
-
process.on("unhandledRejection", (reason) => {
|
|
109
|
-
debug(SOURCE, `UNHANDLED REJECTION: ${reason}`);
|
|
110
|
-
try {
|
|
111
|
-
console.log(JSON.stringify({ continue: true }));
|
|
112
|
-
} catch {
|
|
113
|
-
console.log('{"continue":true}');
|
|
114
|
-
}
|
|
115
|
-
process.exit(0);
|
|
116
|
-
});
|
|
99
|
+
installHookSafetyNet(SOURCE);
|
|
117
100
|
|
|
118
101
|
main();
|
package/src/lib/hook-utils.ts
CHANGED
|
@@ -16,9 +16,57 @@ import {
|
|
|
16
16
|
sanitizeForLog,
|
|
17
17
|
shellEscape,
|
|
18
18
|
} from "../core/aide-client.js";
|
|
19
|
+
import { debug } from "./logger.js";
|
|
19
20
|
|
|
20
21
|
export { sanitizeForLog, shellEscape };
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A hook's JSON result object (printed to stdout for the harness). Typed as
|
|
25
|
+
* `object` rather than `Record<string, unknown>` so callers can pass their own
|
|
26
|
+
* named result interfaces (interfaces lack the implicit index signature
|
|
27
|
+
* `Record` requires).
|
|
28
|
+
*/
|
|
29
|
+
export type HookResult = object;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialise and print a hook result as exactly one JSON line on stdout, with a
|
|
33
|
+
* raw-string fallback if serialisation itself throws. Defaults to
|
|
34
|
+
* `{continue:true}` — the fail-open result a hook emits when it has nothing
|
|
35
|
+
* specific to say. Callers needing a different shape (e.g. `{}` or a permission
|
|
36
|
+
* decision) pass it explicitly: the output decision stays with the caller, this
|
|
37
|
+
* helper only guarantees a single, always-valid line.
|
|
38
|
+
*/
|
|
39
|
+
export function emitHookResult(result: HookResult = { continue: true }): void {
|
|
40
|
+
try {
|
|
41
|
+
console.log(JSON.stringify(result));
|
|
42
|
+
} catch {
|
|
43
|
+
console.log('{"continue":true}');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Register process-level last-resort handlers so an uncaught error or rejection
|
|
49
|
+
* still emits a valid hook result (fail-open) instead of crashing with no
|
|
50
|
+
* output. `fallback` is the result emitted on crash (default `{continue:true}`).
|
|
51
|
+
* Replaces the identical uncaughtException/unhandledRejection block that was
|
|
52
|
+
* copy-pasted across every hook.
|
|
53
|
+
*/
|
|
54
|
+
export function installHookSafetyNet(
|
|
55
|
+
source: string,
|
|
56
|
+
fallback: HookResult = { continue: true },
|
|
57
|
+
): void {
|
|
58
|
+
process.on("uncaughtException", (err) => {
|
|
59
|
+
debug(source, `UNCAUGHT EXCEPTION: ${err}`);
|
|
60
|
+
emitHookResult(fallback);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
});
|
|
63
|
+
process.on("unhandledRejection", (reason) => {
|
|
64
|
+
debug(source, `UNHANDLED REJECTION: ${reason}`);
|
|
65
|
+
emitHookResult(fallback);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
22
70
|
/** Maximum stdin payload size: 50 MiB. Prevents unbounded memory allocation. */
|
|
23
71
|
const MAX_STDIN_BYTES = 50 * 1024 * 1024;
|
|
24
72
|
|
package/src/lib/project-root.ts
CHANGED
|
@@ -11,23 +11,24 @@
|
|
|
11
11
|
* 1. AIDE_PROJECT_ROOT env override (must be an existing directory).
|
|
12
12
|
* 2. Walk the full ancestry from cwd to /, collecting candidates, then
|
|
13
13
|
* prefer:
|
|
14
|
-
* a. Closest ancestor with
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* a. Closest ancestor with a VCS marker (.git/.hg/.svn/.bzr/.fossil).
|
|
15
|
+
* A VCS boundary — including a submodule, which is its own
|
|
16
|
+
* repository — is the project boundary: starting inside a
|
|
17
|
+
* submodule anchors the submodule, starting in the superproject
|
|
18
|
+
* anchors the superproject. Worktree .git files resolve to the
|
|
19
|
+
* main repo root (worktrees share one store); submodule .git
|
|
20
|
+
* files anchor the submodule directory itself.
|
|
21
|
+
* b. Closest ancestor with .aide/ only — standalone projects with
|
|
20
22
|
* no VCS.
|
|
21
23
|
* ~/.aide/ is skipped as a project marker unless cwd is $HOME.
|
|
22
24
|
* 3. No marker found: return { root: cwd, hasMarker: false }.
|
|
23
25
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* keeps going until it finds the parent that has both.
|
|
26
|
+
* A stray child .aide/ (created by an accidental CLI invocation from a
|
|
27
|
+
* subdir) cannot shadow the real project root: it has no VCS marker, so
|
|
28
|
+
* any real repository above it takes priority.
|
|
28
29
|
*/
|
|
29
30
|
|
|
30
|
-
import { basename, dirname, join, resolve } from "path";
|
|
31
|
+
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
|
31
32
|
import { existsSync, readFileSync, statSync } from "fs";
|
|
32
33
|
import { homedir } from "os";
|
|
33
34
|
|
|
@@ -95,9 +96,6 @@ export function findProjectRoot(cwd: string): ProjectRootResult {
|
|
|
95
96
|
dir = parent;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
for (const c of path) {
|
|
99
|
-
if (c.hasAide && c.hasVCS) return { root: c.vcsResolved, hasMarker: true };
|
|
100
|
-
}
|
|
101
99
|
for (const c of path) {
|
|
102
100
|
if (c.hasVCS) return { root: c.vcsResolved, hasMarker: true };
|
|
103
101
|
}
|
|
@@ -141,12 +139,27 @@ export function walkUpForProjectRoot(startDir: string): string | null {
|
|
|
141
139
|
return hasMarker ? root : null;
|
|
142
140
|
}
|
|
143
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Reports whether a resolved gitdir path points into a superproject's
|
|
144
|
+
* .git/modules/ tree, i.e. the .git file belongs to a submodule checkout
|
|
145
|
+
* rather than a linked worktree.
|
|
146
|
+
*/
|
|
147
|
+
function isSubmoduleGitdir(gitdir: string): boolean {
|
|
148
|
+
const parts = gitdir.split(/[\\/]+/);
|
|
149
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
150
|
+
if (parts[i] === ".git" && parts[i + 1] === "modules") return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
/**
|
|
145
156
|
* Read a .git worktree file ("gitdir: <path>") and return the main repo root.
|
|
146
157
|
*
|
|
147
158
|
* Mirrors aide/cmd/aide/main.go:resolveWorktreeRoot(). The file's gitdir
|
|
148
159
|
* normally points at "<main>/.git/worktrees/<name>"; we walk up that path
|
|
149
160
|
* until we find a component named ".git" and return its parent.
|
|
161
|
+
* Submodule .git files (gitdir under .git/modules/) return null so the
|
|
162
|
+
* submodule directory itself becomes the root.
|
|
150
163
|
*/
|
|
151
164
|
export function resolveWorktreeGitFile(gitFilePath: string): string | null {
|
|
152
165
|
try {
|
|
@@ -154,10 +167,12 @@ export function resolveWorktreeGitFile(gitFilePath: string): string | null {
|
|
|
154
167
|
if (!content.startsWith("gitdir:")) return null;
|
|
155
168
|
|
|
156
169
|
let gitdir = content.slice("gitdir:".length).trim();
|
|
157
|
-
if (!gitdir
|
|
170
|
+
if (!isAbsolute(gitdir)) {
|
|
158
171
|
gitdir = resolve(dirname(gitFilePath), gitdir);
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
if (isSubmoduleGitdir(gitdir)) return null;
|
|
175
|
+
|
|
161
176
|
let candidate = gitdir;
|
|
162
177
|
for (;;) {
|
|
163
178
|
const parent = dirname(candidate);
|