@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.
- package/package.json +1 -1
- 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 +74 -14
- package/src/core/skill-matcher.ts +18 -22
- package/src/core/tool-observe.ts +12 -0
- package/src/core/types.ts +15 -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 +51 -41
- 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/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 +68 -31
- package/src/opencode/hooks.ts +46 -5
package/src/core/session-init.ts
CHANGED
|
@@ -23,9 +23,12 @@ import type {
|
|
|
23
23
|
SessionState,
|
|
24
24
|
SessionInitResult,
|
|
25
25
|
MemoryInjection,
|
|
26
|
+
InjectedSource,
|
|
26
27
|
StartupNotices,
|
|
27
28
|
} from "./types.js";
|
|
28
29
|
import { DEFAULT_CONFIG } from "./types.js";
|
|
30
|
+
import { isTruthy, isFalsy } from "../lib/hook-utils.js";
|
|
31
|
+
import { findProjectRoot } from "../lib/project-root.js";
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Ensure all .aide directories exist
|
|
@@ -34,14 +37,19 @@ export function ensureDirectories(cwd: string): {
|
|
|
34
37
|
created: number;
|
|
35
38
|
existed: number;
|
|
36
39
|
} {
|
|
40
|
+
// Resolve to the canonical project root so we don't plant a stray .aide/
|
|
41
|
+
// in a subdirectory the harness happened to launch from. When no marker
|
|
42
|
+
// is found, fall back to cwd (caller's hasMarker gate elsewhere refuses
|
|
43
|
+
// bootstrap unless AIDE_FORCE_INIT is set).
|
|
44
|
+
const { root } = findProjectRoot(cwd);
|
|
37
45
|
const dirs = [
|
|
38
|
-
join(
|
|
39
|
-
join(
|
|
40
|
-
join(
|
|
41
|
-
join(
|
|
42
|
-
join(
|
|
43
|
-
join(
|
|
44
|
-
join(
|
|
46
|
+
join(root, ".aide"),
|
|
47
|
+
join(root, ".aide", "skills"),
|
|
48
|
+
join(root, ".aide", "config"),
|
|
49
|
+
join(root, ".aide", "state"),
|
|
50
|
+
join(root, ".aide", "memory"),
|
|
51
|
+
join(root, ".aide", "worktrees"),
|
|
52
|
+
join(root, ".aide", "_logs"),
|
|
45
53
|
join(homedir(), ".aide"),
|
|
46
54
|
join(homedir(), ".aide", "skills"),
|
|
47
55
|
join(homedir(), ".aide", "config"),
|
|
@@ -66,7 +74,7 @@ export function ensureDirectories(cwd: string): {
|
|
|
66
74
|
// Ensure .gitignore exists in .aide directory.
|
|
67
75
|
// Structure: exclude all local-only runtime data, allow shared/ and config/.
|
|
68
76
|
// shared/ contains git-friendly markdown exports (decisions, memories).
|
|
69
|
-
const gitignorePath = join(
|
|
77
|
+
const gitignorePath = join(root, ".aide", ".gitignore");
|
|
70
78
|
const requiredGitignoreContent = `# AIDE local runtime files - do not commit
|
|
71
79
|
# These are machine-specific and/or binary (non-mergeable)
|
|
72
80
|
_logs/
|
|
@@ -207,7 +215,8 @@ export function loadGlobalConfig(): AideConfig {
|
|
|
207
215
|
*/
|
|
208
216
|
export function loadConfig(cwd: string): AideConfig {
|
|
209
217
|
const global = loadGlobalConfig();
|
|
210
|
-
const
|
|
218
|
+
const { root } = findProjectRoot(cwd);
|
|
219
|
+
const projectPath = join(root, ".aide", "config", "aide.json");
|
|
211
220
|
|
|
212
221
|
if (existsSync(projectPath)) {
|
|
213
222
|
try {
|
|
@@ -252,7 +261,8 @@ export function cleanupStaleStateFiles(cwd: string): {
|
|
|
252
261
|
scanned: number;
|
|
253
262
|
deleted: number;
|
|
254
263
|
} {
|
|
255
|
-
const
|
|
264
|
+
const { root } = findProjectRoot(cwd);
|
|
265
|
+
const stateDir = join(root, ".aide", "state");
|
|
256
266
|
if (!existsSync(stateDir)) {
|
|
257
267
|
return { scanned: 0, deleted: 0 };
|
|
258
268
|
}
|
|
@@ -291,7 +301,8 @@ export function cleanupStaleStateFiles(cwd: string): {
|
|
|
291
301
|
* Reset HUD state file for clean session start
|
|
292
302
|
*/
|
|
293
303
|
export function resetHudState(cwd: string): void {
|
|
294
|
-
const
|
|
304
|
+
const { root } = findProjectRoot(cwd);
|
|
305
|
+
const hudPath = join(root, ".aide", "state", "hud.txt");
|
|
295
306
|
try {
|
|
296
307
|
if (existsSync(hudPath)) {
|
|
297
308
|
writeFileSync(hudPath, "mode:idle");
|
|
@@ -319,7 +330,7 @@ export function runSessionInit(
|
|
|
319
330
|
dynamic: { sessions: [] },
|
|
320
331
|
};
|
|
321
332
|
|
|
322
|
-
if (process.env.AIDE_MEMORY_INJECT
|
|
333
|
+
if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
|
|
323
334
|
return result;
|
|
324
335
|
}
|
|
325
336
|
|
|
@@ -334,7 +345,7 @@ export function runSessionInit(
|
|
|
334
345
|
// Add --share-import if configured or env var set
|
|
335
346
|
if (
|
|
336
347
|
config?.share?.autoImport ||
|
|
337
|
-
process.env.AIDE_SHARE_AUTO_IMPORT
|
|
348
|
+
isTruthy(process.env.AIDE_SHARE_AUTO_IMPORT)
|
|
338
349
|
) {
|
|
339
350
|
args.push("--share-import");
|
|
340
351
|
}
|
|
@@ -349,7 +360,7 @@ export function runSessionInit(
|
|
|
349
360
|
|
|
350
361
|
const data: SessionInitResult = JSON.parse(output);
|
|
351
362
|
|
|
352
|
-
if (process.env.AIDE_MEMORY_INJECT
|
|
363
|
+
if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
|
|
353
364
|
return result;
|
|
354
365
|
}
|
|
355
366
|
|
|
@@ -369,6 +380,55 @@ export function runSessionInit(
|
|
|
369
380
|
.join("\n");
|
|
370
381
|
result.dynamic.sessions.push(`${header}:\n${memories}`);
|
|
371
382
|
}
|
|
383
|
+
|
|
384
|
+
const sources: InjectedSource[] = [];
|
|
385
|
+
for (const m of data.global_memories) {
|
|
386
|
+
sources.push({
|
|
387
|
+
kind: "memory",
|
|
388
|
+
scope: "global",
|
|
389
|
+
id: m.id,
|
|
390
|
+
name: m.tags?.[0] ?? m.category ?? "memory",
|
|
391
|
+
content: m.content,
|
|
392
|
+
category: m.category,
|
|
393
|
+
tags: m.tags,
|
|
394
|
+
score: m.score,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
for (const m of data.project_memories) {
|
|
398
|
+
sources.push({
|
|
399
|
+
kind: "memory",
|
|
400
|
+
scope: "project",
|
|
401
|
+
id: m.id,
|
|
402
|
+
name: m.tags?.[0] ?? m.category ?? "memory",
|
|
403
|
+
content: m.content,
|
|
404
|
+
category: m.category,
|
|
405
|
+
tags: m.tags,
|
|
406
|
+
score: m.score,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
for (const d of data.decisions) {
|
|
410
|
+
sources.push({
|
|
411
|
+
kind: "decision",
|
|
412
|
+
scope: "project",
|
|
413
|
+
id: d.topic,
|
|
414
|
+
name: d.topic,
|
|
415
|
+
content: `${d.value}${d.rationale ? ` (${d.rationale})` : ""}`,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
for (const sess of data.recent_sessions) {
|
|
419
|
+
for (const m of sess.memories) {
|
|
420
|
+
sources.push({
|
|
421
|
+
kind: "session_memory",
|
|
422
|
+
scope: "session",
|
|
423
|
+
id: `${sess.session_id}:${m.content.slice(0, 32)}`,
|
|
424
|
+
name: m.category || "session",
|
|
425
|
+
content: m.content,
|
|
426
|
+
category: m.category,
|
|
427
|
+
sessionId: sess.session_id,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
result.sources = sources;
|
|
372
432
|
} catch {
|
|
373
433
|
// Best effort
|
|
374
434
|
}
|
|
@@ -229,40 +229,36 @@ export function loadSkill(path: string): Skill | null {
|
|
|
229
229
|
export function discoverSkills(cwd: string, pluginRoot?: string): Skill[] {
|
|
230
230
|
const skills: Skill[] = [];
|
|
231
231
|
const seenPaths = new Set<string>();
|
|
232
|
+
// Dedupe by skill name too — the same skill (same `name:` frontmatter)
|
|
233
|
+
// is often present in both repo/skills/ and the plugin-install
|
|
234
|
+
// skills/ dir under different file paths. Without this, matchSkills
|
|
235
|
+
// returns the same skill twice and injects it twice.
|
|
236
|
+
const seenNames = new Set<string>();
|
|
237
|
+
|
|
238
|
+
const add = (file: string) => {
|
|
239
|
+
if (seenPaths.has(file)) return;
|
|
240
|
+
seenPaths.add(file);
|
|
241
|
+
const skill = loadSkill(file);
|
|
242
|
+
if (!skill) return;
|
|
243
|
+
if (seenNames.has(skill.name)) return;
|
|
244
|
+
seenNames.add(skill.name);
|
|
245
|
+
skills.push(skill);
|
|
246
|
+
};
|
|
232
247
|
|
|
233
248
|
// Project-local skills (higher priority)
|
|
234
249
|
for (const location of SKILL_LOCATIONS) {
|
|
235
250
|
const dir = join(cwd, location);
|
|
236
|
-
const
|
|
237
|
-
for (const file of files) {
|
|
238
|
-
if (seenPaths.has(file)) continue;
|
|
239
|
-
seenPaths.add(file);
|
|
240
|
-
const skill = loadSkill(file);
|
|
241
|
-
if (skill) skills.push(skill);
|
|
242
|
-
}
|
|
251
|
+
for (const file of findSkillFiles(dir)) add(file);
|
|
243
252
|
}
|
|
244
253
|
|
|
245
254
|
// Plugin-bundled skills (if pluginRoot provided)
|
|
246
255
|
if (pluginRoot) {
|
|
247
|
-
const
|
|
248
|
-
const files = findSkillFiles(pluginSkillDir);
|
|
249
|
-
for (const file of files) {
|
|
250
|
-
if (seenPaths.has(file)) continue;
|
|
251
|
-
seenPaths.add(file);
|
|
252
|
-
const skill = loadSkill(file);
|
|
253
|
-
if (skill) skills.push(skill);
|
|
254
|
-
}
|
|
256
|
+
for (const file of findSkillFiles(join(pluginRoot, "skills"))) add(file);
|
|
255
257
|
}
|
|
256
258
|
|
|
257
259
|
// Global skills (lower priority)
|
|
258
260
|
for (const dir of GLOBAL_SKILL_LOCATIONS) {
|
|
259
|
-
const
|
|
260
|
-
for (const file of files) {
|
|
261
|
-
if (seenPaths.has(file)) continue;
|
|
262
|
-
seenPaths.add(file);
|
|
263
|
-
const skill = loadSkill(file);
|
|
264
|
-
if (skill) skills.push(skill);
|
|
265
|
-
}
|
|
261
|
+
for (const file of findSkillFiles(dir)) add(file);
|
|
266
262
|
}
|
|
267
263
|
|
|
268
264
|
return skills;
|
package/src/core/tool-observe.ts
CHANGED
|
@@ -269,6 +269,18 @@ export function recordToolEvent(
|
|
|
269
269
|
if (input.sessionId) args.push(`--session=${input.sessionId}`);
|
|
270
270
|
if (startLine !== undefined) args.push(`--attr=start_line=${startLine}`);
|
|
271
271
|
if (endLine !== undefined) args.push(`--attr=end_line=${endLine}`);
|
|
272
|
+
// Capture the command (Bash) or pattern (Grep) text so the repetition
|
|
273
|
+
// detector can group calls by canonical signature instead of lumping
|
|
274
|
+
// every Bash invocation under a single "Bash" bucket. Truncated to keep
|
|
275
|
+
// the attr cheap; the normaliser only uses the first token anyway.
|
|
276
|
+
const cmd = input.toolInput?.command;
|
|
277
|
+
if (typeof cmd === "string" && cmd.length > 0) {
|
|
278
|
+
args.push(`--attr=command=${cmd.slice(0, 500)}`);
|
|
279
|
+
}
|
|
280
|
+
const pattern = input.toolInput?.pattern;
|
|
281
|
+
if (typeof pattern === "string" && pattern.length > 0) {
|
|
282
|
+
args.push(`--attr=pattern=${pattern.slice(0, 200)}`);
|
|
283
|
+
}
|
|
272
284
|
execFileSync(binary, args, {
|
|
273
285
|
cwd,
|
|
274
286
|
timeout: 3000,
|
package/src/core/types.ts
CHANGED
|
@@ -70,12 +70,14 @@ export interface SessionInitResult {
|
|
|
70
70
|
content: string;
|
|
71
71
|
category: string;
|
|
72
72
|
tags: string[];
|
|
73
|
+
score?: number;
|
|
73
74
|
}>;
|
|
74
75
|
project_memories: Array<{
|
|
75
76
|
id: string;
|
|
76
77
|
content: string;
|
|
77
78
|
category: string;
|
|
78
79
|
tags: string[];
|
|
80
|
+
score?: number;
|
|
79
81
|
}>;
|
|
80
82
|
project_memory_overflow?: boolean;
|
|
81
83
|
decisions: Array<{ topic: string; value: string; rationale?: string }>;
|
|
@@ -90,6 +92,18 @@ export interface SessionInitResult {
|
|
|
90
92
|
// Memory Injection
|
|
91
93
|
// =============================================================================
|
|
92
94
|
|
|
95
|
+
export interface InjectedSource {
|
|
96
|
+
kind: "memory" | "decision" | "session_memory";
|
|
97
|
+
scope: "global" | "project" | "session";
|
|
98
|
+
id: string;
|
|
99
|
+
name: string;
|
|
100
|
+
content: string;
|
|
101
|
+
category?: string;
|
|
102
|
+
tags?: string[];
|
|
103
|
+
sessionId?: string;
|
|
104
|
+
score?: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
export interface MemoryInjection {
|
|
94
108
|
static: {
|
|
95
109
|
global: string[];
|
|
@@ -100,6 +114,7 @@ export interface MemoryInjection {
|
|
|
100
114
|
dynamic: {
|
|
101
115
|
sessions: string[];
|
|
102
116
|
};
|
|
117
|
+
sources?: InjectedSource[];
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
// =============================================================================
|
|
@@ -14,6 +14,7 @@ import { readStdin } from "../lib/hook-utils.js";
|
|
|
14
14
|
import { findAideBinary } from "../core/aide-client.js";
|
|
15
15
|
import { cleanupAgent } from "../core/cleanup.js";
|
|
16
16
|
import { debug } from "../lib/logger.js";
|
|
17
|
+
import { findProjectRoot } from "../lib/project-root.js";
|
|
17
18
|
|
|
18
19
|
const SOURCE = "agent-cleanup";
|
|
19
20
|
|
|
@@ -48,7 +49,8 @@ async function main(): Promise<void> {
|
|
|
48
49
|
if (binary) {
|
|
49
50
|
const cleared = cleanupAgent(binary, cwd, agentId);
|
|
50
51
|
if (cleared) {
|
|
51
|
-
const
|
|
52
|
+
const { root } = findProjectRoot(cwd);
|
|
53
|
+
const logDir = join(root, ".aide", "_logs");
|
|
52
54
|
if (existsSync(logDir)) {
|
|
53
55
|
const logPath = join(logDir, "agent-cleanup.log");
|
|
54
56
|
const timestamp = new Date().toISOString();
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Agent Signals Hook (PreToolUse)
|
|
4
|
+
*
|
|
5
|
+
* Fires before every tool call. In subagent contexts (session has a parent
|
|
6
|
+
* recorded by SubagentStart) it checks for halt/pause flags and unread
|
|
7
|
+
* high-priority messages, blocking or injecting context accordingly.
|
|
8
|
+
*
|
|
9
|
+
* In orchestrator / solo sessions this hook is a no-op — gated on the
|
|
10
|
+
* session having a registered parent. Cost when gated out: one cheap
|
|
11
|
+
* `aide agent identify` call (~10ms).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execFileSync } from "child_process";
|
|
15
|
+
import { Logger } from "../lib/logger.js";
|
|
16
|
+
import { readStdin } from "../lib/hook-utils.js";
|
|
17
|
+
import { findAideBinary } from "../core/aide-client.js";
|
|
18
|
+
import { emitInjectionEvent } from "../core/read-tracking.js";
|
|
19
|
+
|
|
20
|
+
const SOURCE = "agent-signals";
|
|
21
|
+
|
|
22
|
+
interface PreToolUseInput {
|
|
23
|
+
hook_event_name: "PreToolUse";
|
|
24
|
+
session_id: string;
|
|
25
|
+
tool_name: string;
|
|
26
|
+
cwd: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HookOutput {
|
|
30
|
+
continue: boolean;
|
|
31
|
+
message?: string;
|
|
32
|
+
hookSpecificOutput?: {
|
|
33
|
+
hookEventName: string;
|
|
34
|
+
additionalContext?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SignalsResponse {
|
|
39
|
+
agent: string;
|
|
40
|
+
halt?: boolean;
|
|
41
|
+
paused?: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
deadline?: string;
|
|
44
|
+
deadline_remaining_sec?: number;
|
|
45
|
+
high_priority_messages?: Array<{
|
|
46
|
+
id: number;
|
|
47
|
+
from: string;
|
|
48
|
+
content: string;
|
|
49
|
+
type?: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let log: Logger | null = null;
|
|
54
|
+
|
|
55
|
+
function passThrough(): void {
|
|
56
|
+
console.log(JSON.stringify({ continue: true }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function block(message: string): void {
|
|
60
|
+
const out: HookOutput = { continue: false, message };
|
|
61
|
+
console.log(JSON.stringify(out));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function injectContext(context: string): void {
|
|
65
|
+
const out: HookOutput = {
|
|
66
|
+
continue: true,
|
|
67
|
+
hookSpecificOutput: {
|
|
68
|
+
hookEventName: "PreToolUse",
|
|
69
|
+
additionalContext: context,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
console.log(JSON.stringify(out));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runAide(binary: string, cwd: string, args: string[]): string | null {
|
|
76
|
+
try {
|
|
77
|
+
return execFileSync(binary, args, {
|
|
78
|
+
cwd,
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
+
timeout: 2000,
|
|
81
|
+
}).toString();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log?.debug(`aide ${args.join(" ")} failed: ${err}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Tools the subagent is still allowed to call while paused — limited to
|
|
89
|
+
// communication primitives so it can report back / receive instruction.
|
|
90
|
+
const PAUSE_ALLOWLIST = new Set([
|
|
91
|
+
"mcp__plugin_aide_aide__message_send",
|
|
92
|
+
"mcp__plugin_aide_aide__message_list",
|
|
93
|
+
"mcp__plugin_aide_aide__message_ack",
|
|
94
|
+
"mcp__plugin_aide_aide__state_get",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
async function main(): Promise<void> {
|
|
98
|
+
try {
|
|
99
|
+
const raw = await readStdin();
|
|
100
|
+
if (!raw.trim()) {
|
|
101
|
+
passThrough();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const data: PreToolUseInput = JSON.parse(raw);
|
|
105
|
+
const cwd = data.cwd || process.cwd();
|
|
106
|
+
log = new Logger("agent-signals", cwd);
|
|
107
|
+
|
|
108
|
+
const binary = findAideBinary({
|
|
109
|
+
cwd,
|
|
110
|
+
pluginRoot:
|
|
111
|
+
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
112
|
+
});
|
|
113
|
+
if (!binary) {
|
|
114
|
+
passThrough();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Gate: is this session a registered subagent? identify returns JSON
|
|
119
|
+
// with parent_session if and only if SubagentStart recorded one.
|
|
120
|
+
const idRaw = runAide(binary, cwd, [
|
|
121
|
+
"agent",
|
|
122
|
+
"identify",
|
|
123
|
+
`--agent=${data.session_id}`,
|
|
124
|
+
]);
|
|
125
|
+
if (!idRaw) {
|
|
126
|
+
passThrough();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let identity: Record<string, string>;
|
|
130
|
+
try {
|
|
131
|
+
identity = JSON.parse(idRaw);
|
|
132
|
+
} catch {
|
|
133
|
+
passThrough();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!identity.parent_session) {
|
|
137
|
+
// Not a subagent — orchestrator/solo session. No-op.
|
|
138
|
+
passThrough();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pull signal snapshot.
|
|
143
|
+
const sigRaw = runAide(binary, cwd, [
|
|
144
|
+
"agent",
|
|
145
|
+
"signals",
|
|
146
|
+
`--agent=${data.session_id}`,
|
|
147
|
+
]);
|
|
148
|
+
if (!sigRaw) {
|
|
149
|
+
passThrough();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let sig: SignalsResponse;
|
|
153
|
+
try {
|
|
154
|
+
sig = JSON.parse(sigRaw);
|
|
155
|
+
} catch {
|
|
156
|
+
passThrough();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 1. Halt — hard stop with reason surfaced to model.
|
|
161
|
+
if (sig.halt) {
|
|
162
|
+
const reason = sig.reason || "halted by orchestrator";
|
|
163
|
+
log?.info(`halt active for ${data.session_id}: ${reason}`);
|
|
164
|
+
block(
|
|
165
|
+
`[aide] This subagent has been halted by the orchestrator. Reason: ${reason}\n` +
|
|
166
|
+
`Stop work. You may still send a final status message via aide message send --to=<orchestrator>.`,
|
|
167
|
+
);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Pause — block all tools except the comm allowlist.
|
|
172
|
+
if (sig.paused && !PAUSE_ALLOWLIST.has(data.tool_name)) {
|
|
173
|
+
log?.info(`paused — blocking ${data.tool_name}`);
|
|
174
|
+
block(
|
|
175
|
+
`[aide] This subagent is paused by the orchestrator. Only messaging tools are allowed until resumed.`,
|
|
176
|
+
);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 3. Deadline approaching — inject warning at < 20% remaining.
|
|
181
|
+
let deadlineWarn = "";
|
|
182
|
+
if (sig.deadline_remaining_sec !== undefined) {
|
|
183
|
+
if (sig.deadline_remaining_sec <= 0) {
|
|
184
|
+
block(
|
|
185
|
+
`[aide] Soft deadline reached (${sig.deadline}). Halting per orchestrator policy.`,
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (sig.deadline_remaining_sec < 60 * 5) {
|
|
190
|
+
deadlineWarn = `[aide] Deadline ${sig.deadline} — ~${sig.deadline_remaining_sec}s remaining.`;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 4. High-priority messages — inject + ack so we don't re-inject next call.
|
|
195
|
+
const parts: string[] = [];
|
|
196
|
+
if (deadlineWarn) parts.push(deadlineWarn);
|
|
197
|
+
if (sig.high_priority_messages && sig.high_priority_messages.length > 0) {
|
|
198
|
+
parts.push("[aide] Mid-flight messages from orchestrator:");
|
|
199
|
+
for (const m of sig.high_priority_messages) {
|
|
200
|
+
parts.push(`- (${m.from}${m.type ? `, ${m.type}` : ""}): ${m.content}`);
|
|
201
|
+
runAide(binary, cwd, [
|
|
202
|
+
"message",
|
|
203
|
+
"ack",
|
|
204
|
+
String(m.id),
|
|
205
|
+
`--agent=${data.session_id}`,
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (parts.length > 0) {
|
|
211
|
+
const ctx = parts.join("\n");
|
|
212
|
+
try {
|
|
213
|
+
emitInjectionEvent(binary, cwd, {
|
|
214
|
+
source: SOURCE,
|
|
215
|
+
subtype: "signal",
|
|
216
|
+
content: ctx,
|
|
217
|
+
sessionId: data.session_id,
|
|
218
|
+
attrs: {
|
|
219
|
+
tool: data.tool_name,
|
|
220
|
+
...(sig.deadline ? { deadline: sig.deadline } : {}),
|
|
221
|
+
high_priority_messages: String(
|
|
222
|
+
sig.high_priority_messages?.length ?? 0,
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
} catch {
|
|
227
|
+
// Non-fatal
|
|
228
|
+
}
|
|
229
|
+
injectContext(ctx);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
passThrough();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
log?.error(`agent-signals failed: ${err}`);
|
|
236
|
+
passThrough();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.on("uncaughtException", () => {
|
|
241
|
+
passThrough();
|
|
242
|
+
process.exit(0);
|
|
243
|
+
});
|
|
244
|
+
process.on("unhandledRejection", () => {
|
|
245
|
+
passThrough();
|
|
246
|
+
process.exit(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
main();
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
getCheckableFilePath,
|
|
17
17
|
getContentToCheck,
|
|
18
18
|
} from "../core/comment-checker.js";
|
|
19
|
+
import { findAideBinary } from "../core/aide-client.js";
|
|
20
|
+
import { emitInjectionEvent } from "../core/read-tracking.js";
|
|
19
21
|
|
|
20
22
|
const SOURCE = "comment-checker";
|
|
21
23
|
|
|
@@ -53,6 +55,8 @@ async function main(): Promise<void> {
|
|
|
53
55
|
const data: HookInput = JSON.parse(input);
|
|
54
56
|
const toolName = data.tool_name || "";
|
|
55
57
|
const toolInput = data.tool_input || {};
|
|
58
|
+
const cwd = data.cwd || process.cwd();
|
|
59
|
+
const sessionId = data.session_id || "";
|
|
56
60
|
|
|
57
61
|
// Only check Write/Edit/MultiEdit tool calls
|
|
58
62
|
const filePath = getCheckableFilePath(toolName, toolInput);
|
|
@@ -76,6 +80,28 @@ async function main(): Promise<void> {
|
|
|
76
80
|
SOURCE,
|
|
77
81
|
`Detected ${result.suspiciousCount} suspicious comments in ${filePath}`,
|
|
78
82
|
);
|
|
83
|
+
try {
|
|
84
|
+
const binary = findAideBinary({
|
|
85
|
+
cwd,
|
|
86
|
+
pluginRoot:
|
|
87
|
+
process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
|
|
88
|
+
});
|
|
89
|
+
if (binary && result.warning) {
|
|
90
|
+
emitInjectionEvent(binary, cwd, {
|
|
91
|
+
source: SOURCE,
|
|
92
|
+
subtype: "guard",
|
|
93
|
+
content: result.warning,
|
|
94
|
+
sessionId,
|
|
95
|
+
attrs: {
|
|
96
|
+
tool: toolName,
|
|
97
|
+
file: filePath,
|
|
98
|
+
suspicious_count: String(result.suspiciousCount),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// Non-fatal
|
|
104
|
+
}
|
|
79
105
|
const output: HookOutput = {
|
|
80
106
|
continue: true,
|
|
81
107
|
hookSpecificOutput: {
|
|
@@ -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 }));
|