@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/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/
|
|
@@ -186,23 +194,40 @@ export function getProjectName(cwd: string): string {
|
|
|
186
194
|
}
|
|
187
195
|
|
|
188
196
|
/**
|
|
189
|
-
* Load config from
|
|
190
|
-
*
|
|
197
|
+
* Load config from ~/.aide/config/aide.json (global).
|
|
198
|
+
* Used before a project root has been resolved (e.g. by the SessionStart
|
|
199
|
+
* hook deciding whether to honour `requireGit`).
|
|
200
|
+
*/
|
|
201
|
+
export function loadGlobalConfig(): AideConfig {
|
|
202
|
+
const configPath = join(homedir(), ".aide", "config", "aide.json");
|
|
203
|
+
if (!existsSync(configPath)) return DEFAULT_CONFIG;
|
|
204
|
+
try {
|
|
205
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(configPath, "utf-8")) };
|
|
206
|
+
} catch {
|
|
207
|
+
return DEFAULT_CONFIG;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Load config layered as: defaults → global (~/.aide/config/aide.json) →
|
|
213
|
+
* project (`<cwd>/.aide/config/aide.json`). Project values override global.
|
|
191
214
|
* Does NOT create a default config file — only user-set values are persisted.
|
|
192
215
|
*/
|
|
193
216
|
export function loadConfig(cwd: string): AideConfig {
|
|
194
|
-
const
|
|
217
|
+
const global = loadGlobalConfig();
|
|
218
|
+
const { root } = findProjectRoot(cwd);
|
|
219
|
+
const projectPath = join(root, ".aide", "config", "aide.json");
|
|
195
220
|
|
|
196
|
-
if (existsSync(
|
|
221
|
+
if (existsSync(projectPath)) {
|
|
197
222
|
try {
|
|
198
|
-
const
|
|
199
|
-
return { ...DEFAULT_CONFIG, ...
|
|
223
|
+
const project = JSON.parse(readFileSync(projectPath, "utf-8"));
|
|
224
|
+
return { ...DEFAULT_CONFIG, ...global, ...project };
|
|
200
225
|
} catch {
|
|
201
|
-
return
|
|
226
|
+
return global;
|
|
202
227
|
}
|
|
203
228
|
}
|
|
204
229
|
|
|
205
|
-
return
|
|
230
|
+
return global;
|
|
206
231
|
}
|
|
207
232
|
|
|
208
233
|
/**
|
|
@@ -236,7 +261,8 @@ export function cleanupStaleStateFiles(cwd: string): {
|
|
|
236
261
|
scanned: number;
|
|
237
262
|
deleted: number;
|
|
238
263
|
} {
|
|
239
|
-
const
|
|
264
|
+
const { root } = findProjectRoot(cwd);
|
|
265
|
+
const stateDir = join(root, ".aide", "state");
|
|
240
266
|
if (!existsSync(stateDir)) {
|
|
241
267
|
return { scanned: 0, deleted: 0 };
|
|
242
268
|
}
|
|
@@ -275,7 +301,8 @@ export function cleanupStaleStateFiles(cwd: string): {
|
|
|
275
301
|
* Reset HUD state file for clean session start
|
|
276
302
|
*/
|
|
277
303
|
export function resetHudState(cwd: string): void {
|
|
278
|
-
const
|
|
304
|
+
const { root } = findProjectRoot(cwd);
|
|
305
|
+
const hudPath = join(root, ".aide", "state", "hud.txt");
|
|
279
306
|
try {
|
|
280
307
|
if (existsSync(hudPath)) {
|
|
281
308
|
writeFileSync(hudPath, "mode:idle");
|
|
@@ -303,7 +330,7 @@ export function runSessionInit(
|
|
|
303
330
|
dynamic: { sessions: [] },
|
|
304
331
|
};
|
|
305
332
|
|
|
306
|
-
if (process.env.AIDE_MEMORY_INJECT
|
|
333
|
+
if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
|
|
307
334
|
return result;
|
|
308
335
|
}
|
|
309
336
|
|
|
@@ -318,7 +345,7 @@ export function runSessionInit(
|
|
|
318
345
|
// Add --share-import if configured or env var set
|
|
319
346
|
if (
|
|
320
347
|
config?.share?.autoImport ||
|
|
321
|
-
process.env.AIDE_SHARE_AUTO_IMPORT
|
|
348
|
+
isTruthy(process.env.AIDE_SHARE_AUTO_IMPORT)
|
|
322
349
|
) {
|
|
323
350
|
args.push("--share-import");
|
|
324
351
|
}
|
|
@@ -333,7 +360,7 @@ export function runSessionInit(
|
|
|
333
360
|
|
|
334
361
|
const data: SessionInitResult = JSON.parse(output);
|
|
335
362
|
|
|
336
|
-
if (process.env.AIDE_MEMORY_INJECT
|
|
363
|
+
if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
|
|
337
364
|
return result;
|
|
338
365
|
}
|
|
339
366
|
|
|
@@ -353,6 +380,55 @@ export function runSessionInit(
|
|
|
353
380
|
.join("\n");
|
|
354
381
|
result.dynamic.sessions.push(`${header}:\n${memories}`);
|
|
355
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;
|
|
356
432
|
} catch {
|
|
357
433
|
// Best effort
|
|
358
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
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
// =============================================================================
|
|
10
10
|
|
|
11
11
|
export interface AideConfig {
|
|
12
|
+
/**
|
|
13
|
+
* When true (default), AIDE refuses to bootstrap if no `.git/` or `.aide/`
|
|
14
|
+
* marker is found walking up from the launched cwd. This prevents the hook
|
|
15
|
+
* from planting an orphan `.aide/` folder in an arbitrary subdirectory of a
|
|
16
|
+
* git repo when `claude` is launched there. Set to false in
|
|
17
|
+
* `~/.aide/config/aide.json` to allow init in non-git directories.
|
|
18
|
+
* Only the global-config value is consulted (the project layer is moot
|
|
19
|
+
* because, if a project root was found, the gate has already passed).
|
|
20
|
+
*/
|
|
21
|
+
requireGit?: boolean;
|
|
12
22
|
share?: {
|
|
13
23
|
/** Auto-import shared data from .aide/shared/ on session start (default: false) */
|
|
14
24
|
autoImport?: boolean;
|
|
@@ -60,12 +70,14 @@ export interface SessionInitResult {
|
|
|
60
70
|
content: string;
|
|
61
71
|
category: string;
|
|
62
72
|
tags: string[];
|
|
73
|
+
score?: number;
|
|
63
74
|
}>;
|
|
64
75
|
project_memories: Array<{
|
|
65
76
|
id: string;
|
|
66
77
|
content: string;
|
|
67
78
|
category: string;
|
|
68
79
|
tags: string[];
|
|
80
|
+
score?: number;
|
|
69
81
|
}>;
|
|
70
82
|
project_memory_overflow?: boolean;
|
|
71
83
|
decisions: Array<{ topic: string; value: string; rationale?: string }>;
|
|
@@ -80,6 +92,18 @@ export interface SessionInitResult {
|
|
|
80
92
|
// Memory Injection
|
|
81
93
|
// =============================================================================
|
|
82
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
|
+
|
|
83
107
|
export interface MemoryInjection {
|
|
84
108
|
static: {
|
|
85
109
|
global: string[];
|
|
@@ -90,6 +114,7 @@ export interface MemoryInjection {
|
|
|
90
114
|
dynamic: {
|
|
91
115
|
sessions: string[];
|
|
92
116
|
};
|
|
117
|
+
sources?: InjectedSource[];
|
|
93
118
|
}
|
|
94
119
|
|
|
95
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: {
|