@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmylchreest/aide-plugin",
3
- "version": "0.0.65",
3
+ "version": "0.1.0",
4
4
  "description": "aide plugin for OpenCode and Codex CLI — multi-agent orchestration, memory, skills, and persistence",
5
5
  "type": "module",
6
6
  "main": "./src/opencode/index.ts",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "cross-spawn": "^7.0.6",
55
- "smol-toml": "^1.3.1",
56
- "which": "^6.0.1"
55
+ "smol-toml": "^1.6.1",
56
+ "which": "^7.0.0"
57
57
  }
58
58
  }
@@ -0,0 +1,289 @@
1
+ ---
2
+ name: reflect
3
+ description: Run the instinct parser catalogue against this session's observe events to surface candidate patterns for promotion to memories. Two-pass: gather candidates, judge intent, write proposals.
4
+ triggers:
5
+ - reflect on this
6
+ - reflect on the session
7
+ - find instincts
8
+ - extract instincts
9
+ - propose instincts
10
+ ---
11
+
12
+ # Reflect
13
+
14
+ Extract candidate **instincts** — patterns repeatedly observed in this session
15
+ that might be worth promoting to durable memories.
16
+
17
+ ## Your role as the agent running this skill
18
+
19
+ You **propose, the user approves**. Nothing this skill does writes a memory
20
+ or marks anything superseded without an explicit yes from the user.
21
+
22
+ Detectors emit proposals into a holding bucket (they're never auto-promoted
23
+ to memories). When this skill runs, your job is to make those proposals
24
+ *reviewable* — to add the judgement that mechanical matching can't:
25
+
26
+ 1. **Classify intent** of user prompts in convergence windows — was the
27
+ user actually correcting the previous edit, or just commenting?
28
+ 2. **Rewrite the content into a useful memory** — see below. This is the
29
+ most important step; the detector's content is a `[DRAFT — rewrite…]`
30
+ placeholder, never the finished memory.
31
+ 3. **Judge semantic conflicts** — which existing memories (if any) does
32
+ this proposal supersede?
33
+ 4. **Recommend an action** — accept (with rewritten content + supersedes),
34
+ reject (why), or leave open for the user to think about.
35
+
36
+ Then **stop and ask**. Surface each proposal to the user with your
37
+ recommendation. Wait for explicit approval before running
38
+ `aide reflect accept|reject`. The CLI is the write surface; your role is
39
+ to make the user's approval decision as well-informed as possible, not to
40
+ make it for them.
41
+
42
+ Concretely: do not chain "list proposals → accept proposals" in the same
43
+ turn. List, summarise, recommend, **wait**, then act on user instruction.
44
+
45
+ ## Why rewriting is the default, not the exception
46
+
47
+ The detector emits an **observation** ("`cat` was run 5 times in 1 minute").
48
+ That's a structural signal, not a useful memory. A useful memory captures:
49
+
50
+ - **Why** the repetition / convergence happened (the underlying need or
51
+ mistake the agent kept circling around).
52
+ - **What** the canonical alternative is (a specific file path, command,
53
+ pattern, or piece of project knowledge).
54
+ - **Scope** (this codebase / this kind of task / this directory).
55
+
56
+ The agent reviewing the proposal does this synthesis by reading the
57
+ evidence snapshot. The detector can't — it only knows the count.
58
+
59
+ Bad memory (the raw template): *"In this project, `cat` is run repeatedly.
60
+ Cache its output."*
61
+
62
+ Good memory (after agent synthesis): *"When investigating the aide
63
+ README for plugin/config questions, the file is stable per commit and the
64
+ config table sits around lines 11-25 — inject the slice via Read with
65
+ offset/limit instead of re-`cat`-ing the whole file."*
66
+
67
+ The proposal's content field literally starts with `[DRAFT — rewrite with
68
+ concrete context before accepting]` to make this obvious. If you accept
69
+ without rewriting, you've stored noise.
70
+
71
+ ## How this skill differs from the automatic Stop hook
72
+
73
+ - The **Stop hook** (`AIDE_REFLECT=1` env or `reflect.enabled=true` in
74
+ `.aide/config/aide.json`) runs `aide reflect run` automatically at session
75
+ end. Deterministic-only: marker matching for convergence, pure counting
76
+ for repetition. No supersession beyond structural `instinct_key:*` matches.
77
+ - **This skill** adds a second pass: it lists user-prompt candidates that
78
+ fall in convergence-relevant windows, you judge intent in-context, you
79
+ search for semantically conflicting memories, then you feed everything
80
+ back. Higher-quality output, no extra API cost — uses tokens you'd be
81
+ spending anyway.
82
+
83
+ ## Session resolution
84
+
85
+ All commands below default to the **current session** — `aide` finds it by
86
+ checking, in order: an explicit `--session=<id>` flag, the `AIDE_SESSION_ID`
87
+ env var (set by OpenCode automatically; not by Claude Code), then the
88
+ session of the most recent observe event. Pass `--session=<id>` to target
89
+ a specific session, or run `./.aide/bin/aide reflect current-session` to
90
+ see what would be resolved.
91
+
92
+ ## Steps
93
+
94
+ ### 1. Get candidate prompts
95
+
96
+ ```bash
97
+ ./.aide/bin/aide reflect candidates
98
+ ```
99
+
100
+ Returns JSON like:
101
+
102
+ ```json
103
+ {
104
+ "session_id": "abc123",
105
+ "guidance": "For each prompt, judge whether it was correcting...",
106
+ "asks": ["intent"],
107
+ "prompts": [
108
+ {
109
+ "id": "01JF...A",
110
+ "timestamp": "...",
111
+ "text": "no don't add async — it should stay sync",
112
+ "preceding_edit": "Edit src/api/users.ts",
113
+ "following_edit": "Edit src/api/users.ts",
114
+ "file_path": "src/api/users.ts"
115
+ }
116
+ ]
117
+ }
118
+ ```
119
+
120
+ If `prompts` is empty, skip to step 4 (deterministic run only).
121
+
122
+ ### 2. Classify each prompt
123
+
124
+ For each prompt, judge its `intent` using the surrounding edit context:
125
+
126
+ - `corrective` — the user was redirecting the assistant's last action
127
+ - `positive` — the user was affirming the assistant's last action ("perfect", "ship it", "lgtm")
128
+ - `neutral` — neither corrective nor affirming (e.g. a new task, a question)
129
+
130
+ Build a JSON array:
131
+
132
+ ```json
133
+ [
134
+ {"id": "01JF...A", "intent": "corrective", "confidence": 0.95},
135
+ {"id": "01JF...B", "intent": "neutral"}
136
+ ]
137
+ ```
138
+
139
+ ### 3. Run with classifications
140
+
141
+ ```bash
142
+ ./.aide/bin/aide reflect run \
143
+ --classifications-json='[{"id":"01JF...A","intent":"corrective"}]'
144
+ ```
145
+
146
+ Returns a JSON summary: `{"proposals_written": N, "shapes": {...}}`.
147
+
148
+ ### 4. (Deterministic fallback) Run without classifications
149
+
150
+ If step 1 returned no candidates, just run deterministically:
151
+
152
+ ```bash
153
+ ./.aide/bin/aide reflect run
154
+ ```
155
+
156
+ ### 5. List proposals and summarise for the user
157
+
158
+ ```
159
+ mcp__plugin_aide_aide__instinct_proposals_list { "status": "open" }
160
+ ```
161
+
162
+ Summarise each new proposal's `summary` field for the user. Don't act on
163
+ anything yet — let them decide.
164
+
165
+ ### 6. You decide what gets superseded
166
+
167
+ This is where your judgement matters most. The structural auto-supersession
168
+ (same `instinct_key:*` tag) only catches instinct-on-instinct dedup. The
169
+ interesting case is when a new instinct contradicts a **manually-set**
170
+ memory the user wrote earlier — e.g. a "documentation standard: always run
171
+ rustdoc" memory being superseded by a new "rustdoc runs repeatedly, cache
172
+ it" instinct.
173
+
174
+ Mechanical matching can't catch that. You can. Process:
175
+
176
+ 1. Extract the key subject from the proposal (e.g. "rustdoc", a file path,
177
+ a command name).
178
+ 2. `mcp__plugin_aide_aide__memory_search { "query": "<subject>" }`
179
+ 3. Read each result. Judge — **you are the judge**:
180
+ - Does the new instinct's guidance *contradict* this memory's guidance?
181
+ - Does it *replace* it with a better recommendation?
182
+ - Or does it just touch the same topic without conflicting?
183
+ 4. Only collect IDs that genuinely conflict. False positives create silent
184
+ memory loss; bias toward "leave it" when uncertain.
185
+ 5. Surface your reasoning to the user before acting: "I think proposal X
186
+ supersedes memory Y because Z — accept with `--supersedes=Y`?"
187
+
188
+ ### 7. Rewrite the content, then present recommendations — do not auto-act
189
+
190
+ For each proposal:
191
+
192
+ 1. **Read the evidence snapshot** via `mcp__plugin_aide_aide__instinct_inspect`.
193
+ 2. **Synthesise the underlying lesson**. What was the agent (you, or a past
194
+ you) actually trying to achieve? Why did the pattern repeat / converge?
195
+ What's the canonical alternative for this codebase?
196
+ 3. **Draft the rewritten memory content**. Skip the `[DRAFT — …]` template
197
+ text from the proposal; write a useful instinct from scratch.
198
+ 4. **Present to the user** with the rewrite inline:
199
+
200
+ > Proposal `01JF…` (repetition, `rustdoc` × 7 in 5 min). Reading the
201
+ > evidence: 6 of the 7 calls were `rustdoc --no-deps` checking the same
202
+ > public crate while iterating on a doctest. I'd accept with this
203
+ > rewritten content and supersede memory `01JD…` ("always run rustdoc")
204
+ > because the new guidance refines, not contradicts, that one:
205
+ >
206
+ > > "When iterating on a single doctest, `cargo test --doc <module>`
207
+ > > re-runs only that doctest in ~1s; reserve `rustdoc --no-deps` for
208
+ > > final pre-commit verification across all crates."
209
+ >
210
+ > Command:
211
+ > `aide reflect accept 01JF… --supersedes=01JD… \
212
+ > --content="When iterating on a single doctest…"`
213
+
214
+ Then **wait**. The user might say:
215
+ - "yes" → run the command verbatim
216
+ - "yes but tweak the wording to …" → adjust `--content=` and re-present
217
+ - "accept but don't supersede" → drop `--supersedes`
218
+ - "reject — that's not actually a pattern, I asked you to repeat for testing"
219
+ → run reject with that reason
220
+ - "leave them open, I'll review later" → do nothing, you're done
221
+
222
+ If the evidence doesn't support a meaningful rewrite — e.g. the events are
223
+ a test trigger, a one-off, a data artifact — **recommend reject**. A
224
+ proposal with no useful synthesis is noise; promoting it pollutes the
225
+ memory store.
226
+
227
+ ### 8. Execute the user's decision via the CLI
228
+
229
+ Only after the user has chosen — writes are CLI-only per aide convention:
230
+
231
+ ```bash
232
+ # Accept with rewritten content (the default — see step 7):
233
+ ./.aide/bin/aide reflect accept <proposal-id> --content="<your synthesised memory>"
234
+
235
+ # Accept with rewritten content AND supersession:
236
+ ./.aide/bin/aide reflect accept <proposal-id> \
237
+ --content="<your synthesis>" \
238
+ --supersedes=<mem-id1>,<mem-id2>
239
+
240
+ # Accept verbatim (the [DRAFT…] template lands as the memory — almost
241
+ # never what you want, included only for completeness):
242
+ ./.aide/bin/aide reflect accept <proposal-id>
243
+
244
+ # Reject (with reason for the audit trail):
245
+ ./.aide/bin/aide reflect reject <proposal-id> --reason="not useful"
246
+ ```
247
+
248
+ Supersession unions two sources:
249
+
250
+ 1. **Structural (auto)** — instinct memories sharing the same
251
+ `instinct_key:*` tag (cheap dedup; same Bash command for repetition,
252
+ same file path for convergence).
253
+ 2. **Semantic (via `--supersedes`)** — IDs from step 6. Works for any
254
+ memory including manually-set ones with no instinct tags.
255
+
256
+ Each superseded record gets `superseded` + `superseded_by:<new-id>` tags;
257
+ the new memory gets `supersedes:<csv>` pointing back. Superseded records
258
+ stay in the bucket for audit but are filtered out of default queries.
259
+
260
+ ## Inspecting evidence
261
+
262
+ ```
263
+ mcp__plugin_aide_aide__instinct_inspect { "id": "<ulid>" }
264
+ ```
265
+
266
+ The `evidence.snapshot` array contains the observe events that triggered the
267
+ pattern.
268
+
269
+ ## Opt-in toggle for the Stop hook
270
+
271
+ ```bash
272
+ export AIDE_REFLECT=1 # also: true, on, yes — any truthy value
273
+ ```
274
+
275
+ When unset or set to a falsy value (`0`, `false`, `off`, `no`), the Stop hook
276
+ is a no-op. This skill still works regardless — it's manually invoked, not
277
+ gated by the env var.
278
+
279
+ ## Shape catalogue
280
+
281
+ - **repetition** — Bash commands run > N times in a session, suggesting the
282
+ agent forgot it already had the answer. Pure counting, no LLM input needed.
283
+ - **convergence** — `Edit A` → user corrective marker → `Edit B` on the same
284
+ file → optional positive signal. Marker-based by default, upgrades to
285
+ LLM-classified when intent labels are provided via step 3.
286
+
287
+ Detectors declare a `RequiresLLM` capability. The CLI Stop hook
288
+ (`aide reflect run`) runs only `RequiresLLM=false` detectors automatically;
289
+ LLM-required detectors are skipped unless this skill provides classifications.
@@ -6,6 +6,10 @@ triggers:
6
6
  - parallel agents
7
7
  - spawn agents
8
8
  - multi-agent
9
+ - halt agent
10
+ - pause swarm
11
+ - stop agent
12
+ - resume agent
9
13
  ---
10
14
 
11
15
  # Swarm Mode
@@ -221,7 +225,28 @@ When all 5 stages are complete:
221
225
  });
222
226
  ```
223
227
 
224
- ### 4. Monitor Progress
228
+ ### Mid-flight control
229
+
230
+ Once stories are launched, you can intervene without killing/restarting agents.
231
+ All control writes go through `aide agent` and reach the subagent on its
232
+ **next tool call** (worst case ≈ duration of an in-flight Bash/Edit). The
233
+ signal hook (`src/hooks/agent-signals.ts`) gates on a subagent's
234
+ `parent_session` being set — orchestrator/solo sessions see zero overhead.
235
+
236
+ - **Inspect**: `./.aide/bin/aide agent list --parent=$(./.aide/bin/aide reflect current-session) --json`
237
+ - **Halt cleanly**: `./.aide/bin/aide agent halt <agent-id> --reason="repeated rustdoc — see new instinct"`
238
+ Next tool call is blocked with the reason surfaced to the model.
239
+ - **Pause / resume**: `./.aide/bin/aide agent pause <agent-id>` then `./.aide/bin/aide agent resume <agent-id>`.
240
+ Paused agents can only call `message_send`/`message_list`/`message_ack`/`state_get`.
241
+ - **Mid-flight instruction**: `./.aide/bin/aide message send --from=orchestrator --to=<agent-id> --priority=high "scope drifted — focus on auth.ts only"`.
242
+ Surfaced as `additionalContext` on the subagent's next tool call.
243
+ - **Soft deadline**: `./.aide/bin/aide agent deadline <agent-id> 30m` — warns at < 5min remaining; halts at 0.
244
+
245
+ When to intervene vs. let it run: prefer letting agents finish a stage and
246
+ review at the next checkpoint. Use mid-flight halt only for clearly-wrong
247
+ directions (infinite loops, scope drift, depleted budget with no progress).
248
+
249
+ ### Monitor Progress
225
250
 
226
251
  Use TaskList to see all story progress:
227
252
 
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: swarm-status
3
+ description: Inspect a running swarm — show the agent tree, current tools, halts/pauses, and recent task/message activity for the orchestrator's own swarm.
4
+ triggers:
5
+ - swarm status
6
+ - what are the agents doing
7
+ - show swarm
8
+ - agent status
9
+ - check agents
10
+ ---
11
+
12
+ # Swarm Status
13
+
14
+ Quick orchestrator-side inspection of a live swarm. Run this when you want
15
+ to see what your spawned subagents are doing right now without halting them
16
+ or opening the aide-web dashboard.
17
+
18
+ ## Steps
19
+
20
+ ### 1. Resolve current session (your own)
21
+
22
+ ```bash
23
+ ./.aide/bin/aide reflect current-session
24
+ ```
25
+
26
+ Use that ID as `<parent>` below — it's the orchestrator session that owns
27
+ the swarm.
28
+
29
+ ### 2. List agents in this swarm
30
+
31
+ ```bash
32
+ ./.aide/bin/aide agent list --parent=<parent> --json
33
+ ```
34
+
35
+ Returns one record per registered subagent with `parent_session`,
36
+ `namespace`, `status`, `halt`, `paused`, `deadline`.
37
+
38
+ ### 3. Active tasks for this swarm
39
+
40
+ ```bash
41
+ ./.aide/bin/aide task list --parent-session=<parent> --json
42
+ ```
43
+
44
+ Filters the project-wide task bucket to ones tagged with this swarm's
45
+ parent. Look for tasks stuck in `claimed` without progress.
46
+
47
+ ### 4. Recent messages
48
+
49
+ ```bash
50
+ ./.aide/bin/aide message list --parent-session=<parent>
51
+ ```
52
+
53
+ Cross-agent comms within this swarm. High-priority messages from the
54
+ orchestrator are surfaced to subagents on their next tool call by the
55
+ signals hook.
56
+
57
+ ### 5. Summarise for the user
58
+
59
+ Group by agent. For each, report:
60
+
61
+ - Status (running / paused / halted)
62
+ - Current tool (read `agent:<id>:currentTool` via `aide state get currentTool --agent=<id>` if you want this)
63
+ - Any flags (halt with reason, pause)
64
+ - Open tasks they hold
65
+ - Deadline if set
66
+
67
+ Example output:
68
+
69
+ ```
70
+ Swarm <parent-id-short> — 3 agents
71
+
72
+ agent-auth (running):
73
+ current: Edit src/auth/handler.ts
74
+ tasks: 2 in_progress
75
+ no flags
76
+
77
+ agent-payments (paused):
78
+ reason: scope drift — investigating
79
+ tasks: 1 claimed (#42)
80
+ paused 3m ago
81
+
82
+ agent-docs (halted):
83
+ reason: repeated rustdoc — see new instinct
84
+ halted 12m ago
85
+ ```
86
+
87
+ ### When to use halts vs. messages
88
+
89
+ - **Halt** if the agent is clearly off-track or burning budget with no
90
+ progress. The halt blocks tool calls; the model will respond once with
91
+ the halt reason then stop.
92
+ - **High-priority message** if you want to redirect without stopping —
93
+ arrives as `additionalContext` on the next tool call so the model can
94
+ read it and adjust.
95
+
96
+ Both are sent via `aide` CLI:
97
+
98
+ ```bash
99
+ ./.aide/bin/aide agent halt <agent-id> --reason="..."
100
+ ./.aide/bin/aide message send --from=orchestrator --to=<agent-id> \
101
+ --priority=high "redirect: focus on auth.ts only"
102
+ ```
103
+
104
+ ## Not for
105
+
106
+ This skill is read-only orchestration. Use the `swarm` skill itself to
107
+ launch new agents or resolve worktrees at the end.
@@ -19,6 +19,7 @@ import { resolve, isAbsolute, normalize, extname } from "path";
19
19
  import { tmpdir } from "os";
20
20
  import { join } from "path";
21
21
  import { debug } from "../lib/logger.js";
22
+ import { isTruthy } from "../lib/hook-utils.js";
22
23
  import { getPreviousRead, checkFileReadFreshness } from "./read-tracking.js";
23
24
 
24
25
  const SOURCE = "context-guard";
@@ -248,7 +249,7 @@ export function checkSmartReadHint(
248
249
  }
249
250
 
250
251
  // Require code watcher to be enabled
251
- if (process.env.AIDE_CODE_WATCH !== "1") {
252
+ if (!isTruthy(process.env.AIDE_CODE_WATCH)) {
252
253
  return { shouldHint: false };
253
254
  }
254
255
 
@@ -26,6 +26,7 @@ import {
26
26
  import { join, dirname } from "path";
27
27
  import { homedir } from "os";
28
28
  import * as TOML from "smol-toml";
29
+ import { findProjectRoot } from "../lib/project-root.js";
29
30
 
30
31
  // =============================================================================
31
32
  // Types
@@ -178,11 +179,13 @@ function aideUserMcpPath(): string {
178
179
  }
179
180
 
180
181
  function aideProjectMcpPath(cwd: string): string {
181
- return join(cwd, ".aide", "config", "mcp.json");
182
+ const { root } = findProjectRoot(cwd);
183
+ return join(root, ".aide", "config", "mcp.json");
182
184
  }
183
185
 
184
186
  function journalPath(cwd: string): string {
185
- return join(cwd, ".aide", "config", "mcp-sync.journal.json");
187
+ const { root } = findProjectRoot(cwd);
188
+ return join(root, ".aide", "config", "mcp-sync.journal.json");
186
189
  }
187
190
 
188
191
  function userJournalPath(): string {
@@ -13,6 +13,7 @@ import { execFileSync } from "child_process";
13
13
  import { isAbsolute, relative, resolve } from "path";
14
14
  import { setState, getState } from "./aide-client.js";
15
15
  import { debug } from "../lib/logger.js";
16
+ import { isTruthy } from "../lib/hook-utils.js";
16
17
 
17
18
  const SOURCE = "read-tracking";
18
19
 
@@ -50,7 +51,7 @@ export function recordFileRead(
50
51
  cwd: string,
51
52
  filePath: string,
52
53
  ): void {
53
- if (process.env.AIDE_CODE_WATCH !== "1") return;
54
+ if (!isTruthy(process.env.AIDE_CODE_WATCH)) return;
54
55
 
55
56
  try {
56
57
  const relPath = toRelativePath(cwd, filePath);
@@ -73,7 +74,7 @@ export function getPreviousRead(
73
74
  cwd: string,
74
75
  filePath: string,
75
76
  ): string | null {
76
- if (process.env.AIDE_CODE_WATCH !== "1") return null;
77
+ if (!isTruthy(process.env.AIDE_CODE_WATCH)) return null;
77
78
 
78
79
  try {
79
80
  const relPath = toRelativePath(cwd, filePath);
@@ -113,39 +114,17 @@ export function checkFileReadFreshness(
113
114
  }
114
115
  }
115
116
 
116
- /**
117
- * Record a token event via `aide token record`.
118
- * Fire-and-forget errors are logged but not propagated.
119
- */
120
- export function recordTokenEvent(
121
- binary: string,
122
- cwd: string,
123
- eventType: string,
124
- tool: string,
125
- filePath: string,
126
- tokens: number,
127
- tokensSaved: number = 0,
128
- ): void {
129
- try {
130
- const args = ["token", "record", eventType, tool, filePath, String(tokens)];
131
- if (tokensSaved > 0) {
132
- args.push(String(tokensSaved));
133
- }
134
- execFileSync(binary, args, {
135
- cwd,
136
- timeout: 3000,
137
- stdio: ["pipe", "pipe", "pipe"],
138
- });
139
- debug(SOURCE, `Token event: ${eventType} ${tool} ${filePath} tokens=${tokens} saved=${tokensSaved}`);
140
- } catch (err) {
141
- debug(SOURCE, `Failed to record token event: ${err}`);
142
- }
117
+ export function previewContent(text: string, maxChars = 300): string {
118
+ const collapsed = text.replace(/\s+/g, " ").trim();
119
+ if (collapsed.length <= maxChars) return collapsed;
120
+ return collapsed.slice(0, maxChars - 1) + "…";
143
121
  }
144
122
 
145
123
  /**
146
124
  * Record an arbitrary observe event via `aide observe record`.
147
- * Use when you need richer fields than recordTokenEvent (per-skill name with
148
- * a stable subtype, attrs, etc.). Fire-and-forget.
125
+ * Prefer `emitInjectionEvent` for `kind=injection` callers this raw
126
+ * recorder is reserved for non-injection kinds (e.g. `hook` user_prompt
127
+ * events). Fire-and-forget.
149
128
  */
150
129
  export function recordObserveEvent(
151
130
  binary: string,
@@ -183,3 +162,47 @@ export function recordObserveEvent(
183
162
  debug(SOURCE, `Failed to record observe event: ${err}`);
184
163
  }
185
164
  }
165
+
166
+ /**
167
+ * Emit a `kind=injection` observe event for any hook that pushes
168
+ * `additionalContext` back to the harness. Centralises field naming so the
169
+ * Injections page can group/colour consistently.
170
+ *
171
+ * `subtype` should come from a small fixed taxonomy:
172
+ * memory | decision | session_memory | skill | enrichment | guard |
173
+ * signal | pruning
174
+ *
175
+ * `source` is the emitting hook name (e.g. "search-enrichment"); it lands in
176
+ * both `file` and `name` so the UI can show "who injected this" without
177
+ * forcing every caller to invent a unique `name`.
178
+ *
179
+ * Fire-and-forget; failures are logged at debug level and never thrown.
180
+ */
181
+ export function emitInjectionEvent(
182
+ binary: string,
183
+ cwd: string,
184
+ opts: {
185
+ source: string;
186
+ subtype: string;
187
+ content: string;
188
+ sessionId?: string;
189
+ name?: string;
190
+ attrs?: Record<string, string>;
191
+ },
192
+ ): void {
193
+ const baseAttrs: Record<string, string> = {
194
+ source_id: opts.source,
195
+ source_kind: opts.subtype,
196
+ content_preview: previewContent(opts.content, 2000),
197
+ };
198
+ recordObserveEvent(binary, cwd, {
199
+ kind: "injection",
200
+ name: opts.name ?? opts.source,
201
+ category: "inject",
202
+ subtype: opts.subtype,
203
+ tokens: Math.round(opts.content.length / 3.0),
204
+ file: opts.source,
205
+ session: opts.sessionId,
206
+ attrs: { ...baseAttrs, ...(opts.attrs ?? {}) },
207
+ });
208
+ }
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { execFileSync } from "child_process";
23
23
  import { debug } from "../lib/logger.js";
24
+ import { isTruthy } from "../lib/hook-utils.js";
24
25
 
25
26
  const SOURCE = "search-enrichment";
26
27
 
@@ -73,7 +74,7 @@ export function checkSearchEnrichment(
73
74
  }
74
75
 
75
76
  // Require code watcher to be enabled (implies code index exists)
76
- if (process.env.AIDE_CODE_WATCH !== "1") {
77
+ if (!isTruthy(process.env.AIDE_CODE_WATCH)) {
77
78
  return { shouldEnrich: false };
78
79
  }
79
80