claude-recall 0.21.1 → 0.22.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.
@@ -108,7 +108,18 @@
108
108
  }
109
109
  ]
110
110
  }
111
+ ],
112
+ "SessionEnd": [
113
+ {
114
+ "hooks": [
115
+ {
116
+ "type": "command",
117
+ "command": "node /home/ebiarao/.nvm/versions/node/v20.19.3/lib/node_modules/claude-recall/dist/cli/claude-recall-cli.js hook run session-end-checkpoint",
118
+ "timeout": 5
119
+ }
120
+ ]
121
+ }
111
122
  ]
112
123
  },
113
- "hooksVersion": "12.0.0"
124
+ "hooksVersion": "13.0.0"
114
125
  }
@@ -12,10 +12,10 @@ Auto-generated from 5 memories. Last updated: 2026-04-11.
12
12
 
13
13
  ## Rules
14
14
 
15
- - Session test preference 1775896807164
16
- - Test preference 1775896807117-2
17
- - Test preference 1775896807117-1
18
- - Test preference 1775896807117-0
15
+ - Session test preference 1775902182248
16
+ - Test preference 1775902182184-2
17
+ - Test preference 1775902182184-1
18
+ - Test preference 1775902182184-0
19
19
  - Test memory content
20
20
 
21
21
  ---
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "topicId": "preferences",
3
- "sourceHash": "47589a75902fabe04a4aab7d8c89e3cdd26c4c1cf7b6e3086a7ea3186413abe4",
3
+ "sourceHash": "a383c0d6502023d06954eb49fcab8886dc5181d5e59666f6c74a381221e44f87",
4
4
  "memoryCount": 5,
5
- "generatedAt": "2026-04-11T08:40:07.182Z",
5
+ "generatedAt": "2026-04-11T10:09:42.271Z",
6
6
  "memoryKeys": [
7
- "memory_1775896807165_8gx2muuz6",
8
- "memory_1775896807142_2zmd3oc2x",
9
- "memory_1775896807130_nsc3d9wky",
10
- "memory_1775896807118_3qvm34ozn",
11
- "memory_1775896807081_saak1fkqp"
7
+ "memory_1775902182249_x5rzzep7s",
8
+ "memory_1775902182226_9uo2kaw57",
9
+ "memory_1775902182211_pl5fzrb85",
10
+ "memory_1775902182185_q6f9widp3",
11
+ "memory_1775902182147_olowsptz3"
12
12
  ]
13
13
  }
@@ -137,13 +137,16 @@ a SKILL.md file that Claude Code loads automatically.
137
137
 
138
138
  ## Automatic Capture Hooks
139
139
 
140
- Claude Recall registers hooks on three Claude Code events to capture memories automatically — no MCP tool call needed:
140
+ Claude Recall registers hooks on six Claude Code events for automatic capture, just-in-time rule injection, and outcome tracking — no MCP tool call needed:
141
141
 
142
- | Hook | Event | What it captures |
142
+ | Hook | Event | What it does |
143
143
  |------|-------|-----------------|
144
- | `correction-detector` | UserPromptSubmit | User corrections, preferences, and project knowledge from natural language |
145
- | `memory-stop` | Stop | Corrections, preferences, failures, and devops patterns from the last 6 transcript entries |
144
+ | `correction-detector` | UserPromptSubmit | Captures user corrections, preferences, and project knowledge from natural language |
145
+ | `memory-stop` | Stop | Captures corrections, preferences, failures, and devops patterns from the last 6 transcript entries |
146
146
  | `precompact-preserve` | PreCompact | Broader sweep of up to 50 transcript entries before context compression |
147
+ | `session-end-checkpoint` | SessionEnd | Auto-saves a `{completed, remaining, blockers}` task checkpoint when the session ends voluntarily (`clear`, `prompt_input_exit`, `logout`). Spawns a detached worker so it stays within Claude Code's 1.5s SessionEnd timeout. Pi has the equivalent via the `session_shutdown` event handler. |
148
+ | `rule-injector` | PreToolUse | **Just-in-time rule injection.** Before each tool call, searches active rules for matches against `tool_name + tool_input` and injects the top 3 (excluding raw failures) as a `<system-reminder>` block adjacent to the action. Closes the rule-loading gap: rules are surfaced at the moment of decision, not 50,000 tokens upstream from where attention has moved on. Each injection is logged to `rule_injection_events` for outcome correlation. Pi has the equivalent via per-turn injection in the `before_agent_start` handler. |
149
+ | `rule-injection-resolver` | PostToolUse / PostToolUseFailure | Resolves recorded `rule_injection_events` with the tool outcome (success/failure). Together with the injector, this becomes the new "is this rule actually helpful" signal — replacing the broken `(applied from memory: ...)` citation regex. |
147
150
 
148
151
  **Key behaviors:**
149
152
  - **LLM-first classification** via Claude Haiku — detects natural statements like "we use tabs here" or "tests go in \_\_tests\_\_/" that regex would miss
@@ -152,9 +155,10 @@ Claude Recall registers hooks on three Claude Code events to capture memories au
152
155
  - Batch classification: Stop and PreCompact hooks send all texts in a single API call
153
156
  - Near-duplicate detection via Jaccard similarity (55% threshold) prevents redundant storage
154
157
  - Per-event limits: 3 (Stop), 5 (PreCompact) to prevent DB flooding
158
+ - Auto-checkpoint quality gate: refuses to save when the LLM detects the task was already complete — manual checkpoints stay sticky
155
159
  - Always exits 0 — hooks never block Claude
156
160
 
157
- **Setup:** Run `npx claude-recall setup --install` to register hooks in `.claude/settings.json`.
161
+ **Setup:** Run `npx claude-recall setup --install` to register hooks in `.claude/settings.json`. After any upgrade, re-run `setup --install` in each project so newly-added hook events get registered (claude-recall uses a `hooksVersion` field to signal when registration has changed).
158
162
 
159
163
  ## Example Workflows
160
164
 
package/README.md CHANGED
@@ -69,8 +69,8 @@ Both agents use the same database (`~/.claude-recall/claude-recall.db`). Memorie
69
69
  npm install -g claude-recall
70
70
  claude-recall setup --install # run from each project directory
71
71
 
72
- # Pi
73
- pi update claude-recall
72
+ # Pi — must include the npm: prefix (matches the install command)
73
+ pi update npm:claude-recall
74
74
  ```
75
75
 
76
76
  The MCP server picks up the new version automatically. `setup --install` is needed to update hooks in `.claude/settings.json` (new hook events may have been added).
@@ -90,6 +90,8 @@ Once installed, Claude Recall works automatically in the background:
90
90
  7. **After context compression** (Claude Code only) — rules are automatically re-injected into context so they're not lost when the window shrinks
91
91
  8. **Sub-agent recall** (Claude Code only) — when sub-agents are spawned, active rules are injected into their context automatically. Sub-agent outcomes (completed/failed/killed) are captured as events
92
92
  9. **Rules sync** (Claude Code only) — top 30 rules are exported as typed `.md` files to Claude Code's native memory directory
93
+ 10. **Auto-checkpoint on session exit** — when a session ends (Pi shutdown or Claude Code's `SessionEnd` for `clear`/`prompt_input_exit`/`logout`), the most recent task is extracted via Haiku into a structured `{completed, remaining, blockers}` checkpoint and saved for the next session. Critical for Pi (which has no `--resume` flag); a useful safety net for Claude Code users who exit without resuming. Conservative quality gate refuses to save when the LLM detects the task was already complete — manual checkpoints are never clobbered with garbage
94
+ 11. **Just-in-time rule injection (JITRI)** — before each tool call (Claude Code) or each agent turn (Pi), the most relevant active rules are searched against `tool_name + tool_input + recent prompt` and injected as a `<system-reminder>` block immediately adjacent to the action. This closes the rule-loading gap: rules are no longer just loaded once at session start (where attention decays as context grows) — they're surfaced at the moment of decision. Each injection is recorded in `rule_injection_events` and resolved with the tool outcome via PostToolUse, replacing the broken citation-detection regex with direct measurement of "was the relevant rule present when the action happened?"
93
95
 
94
96
  Classification uses Claude Haiku (via `ANTHROPIC_API_KEY`) with silent regex fallback. No configuration needed.
95
97
 
@@ -198,6 +200,22 @@ claude-recall checkpoint clear # Delete the checkpoint
198
200
 
199
201
  Agents can also save/load checkpoints via MCP tools (`mcp__claude-recall__save_checkpoint` / `mcp__claude-recall__load_checkpoint`) or Pi tools (`recall_save_checkpoint` / `recall_load_checkpoint`).
200
202
 
203
+ #### Auto-checkpoint on session exit (v0.21.2+)
204
+
205
+ Manual `checkpoint save` is the explicit path. **Auto-checkpoint** is the safety net: when a session ends, the most recent task is extracted into a checkpoint automatically so the next session can resume.
206
+
207
+ - **Pi** — fires from the `session_shutdown` event handler. In-process synchronous call, runs as part of the existing session-end pipeline. **Critical for Pi: there is no `pi --resume` equivalent, so without this, restarting Pi loses all session context.**
208
+ - **Claude Code** — fires from the `SessionEnd` hook for voluntary exit reasons (`clear`, `prompt_input_exit`, `logout`). Spawns a detached background worker (fork+unref) so it stays well within Claude Code's tight 1.5s `SessionEnd` timeout. Skips `bypass_permissions_disabled` and `other` reasons (those are system-driven, not user intent). Useful for users who exit and start fresh instead of using `claude --resume`.
209
+
210
+ Both runtimes share the same Haiku-backed extraction (`extractCheckpointWithLLM`) and the same quality gate:
211
+
212
+ - **Quality gate**: refuses to save if the LLM returns an empty or trivially-short `remaining` field. The model is prompted to detect completion signals (assistant said "Done.", user said "thanks", no follow-up question) and return empty `remaining` when the task is finished. **An empty checkpoint is far better than a fabricated one** — manual checkpoints are never overwritten with garbage.
213
+ - **Notes tag**: auto-saved checkpoints include `[auto-saved on <pi|cc> session exit at <iso-timestamp>]` in the notes field, so you can tell auto from manual via `checkpoint load`.
214
+ - **Requires `ANTHROPIC_API_KEY`**. Without it, `extractCheckpointWithLLM` returns `null` (graceful fallback) and no auto-checkpoint is saved. Manual `checkpoint save` still works.
215
+ - **Disable**: remove the `SessionEnd` block from `.claude/settings.json` (Claude Code) or, for Pi, no per-project disable flag exists yet — open an issue if you need one.
216
+
217
+ The auto-checkpoint never clobbers a useful manual checkpoint because of the quality gate. If the LLM doesn't see clear unfinished work, it returns empty and the gate refuses the save. Manual checkpoints stay sticky until you explicitly save over them.
218
+
201
219
  ### Troubleshooting
202
220
 
203
221
  ```bash
@@ -809,7 +809,7 @@ async function main() {
809
809
  // This avoids registry lookups on every hook invocation.
810
810
  const cliScript = path.join(packageDir, 'dist', 'cli', 'claude-recall-cli.js');
811
811
  const hookCmd = `node ${cliScript} hook run`;
812
- settings.hooksVersion = '12.0.0'; // v12 = add SubagentStart/Stop for sub-agent recall integration
812
+ settings.hooksVersion = '14.0.0'; // v14 = add PreToolUse rule-injector + Post resolver for JITRI
813
813
  settings.hooks = {
814
814
  SubagentStart: [
815
815
  {
@@ -852,6 +852,11 @@ async function main() {
852
852
  type: "command",
853
853
  command: `${hookCmd} tool-outcome-watcher`,
854
854
  timeout: 3
855
+ },
856
+ {
857
+ type: "command",
858
+ command: `${hookCmd} rule-injection-resolver`,
859
+ timeout: 3
855
860
  }
856
861
  ]
857
862
  }
@@ -863,6 +868,11 @@ async function main() {
863
868
  type: "command",
864
869
  command: `${hookCmd} tool-failure`,
865
870
  timeout: 3
871
+ },
872
+ {
873
+ type: "command",
874
+ command: `${hookCmd} rule-injection-resolver`,
875
+ timeout: 3
866
876
  }
867
877
  ]
868
878
  }
@@ -874,6 +884,11 @@ async function main() {
874
884
  {
875
885
  type: "command",
876
886
  command: `python3 ${hookDest}`
887
+ },
888
+ {
889
+ type: "command",
890
+ command: `${hookCmd} rule-injector`,
891
+ timeout: 5
877
892
  }
878
893
  ]
879
894
  }
@@ -919,6 +934,20 @@ async function main() {
919
934
  }
920
935
  ]
921
936
  }
937
+ ],
938
+ // Auto-checkpoint on voluntary session exits. Worker is fire-and-forget,
939
+ // so the synchronous handler returns instantly (well within CC's tight
940
+ // 1.5s SessionEnd timeout). Symmetric with Pi's session_shutdown handler.
941
+ SessionEnd: [
942
+ {
943
+ hooks: [
944
+ {
945
+ type: "command",
946
+ command: `${hookCmd} session-end-checkpoint`,
947
+ timeout: 5
948
+ }
949
+ ]
950
+ }
922
951
  ]
923
952
  };
924
953
  if (!fs.existsSync(claudeDir)) {
@@ -106,9 +106,29 @@ class HookCommands {
106
106
  await handleBashFailureWatcher(input);
107
107
  break;
108
108
  }
109
+ case 'session-end-checkpoint': {
110
+ const { handleSessionEndCheckpoint } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint')));
111
+ await handleSessionEndCheckpoint(input);
112
+ break;
113
+ }
114
+ case 'session-end-checkpoint-worker': {
115
+ const { handleSessionEndCheckpointWorker } = await Promise.resolve().then(() => __importStar(require('../../hooks/session-end-checkpoint-worker')));
116
+ await handleSessionEndCheckpointWorker(input);
117
+ break;
118
+ }
119
+ case 'rule-injector': {
120
+ const { handleRuleInjector } = await Promise.resolve().then(() => __importStar(require('../../hooks/rule-injector')));
121
+ await handleRuleInjector(input);
122
+ break;
123
+ }
124
+ case 'rule-injection-resolver': {
125
+ const { handleRuleInjectionResolver } = await Promise.resolve().then(() => __importStar(require('../../hooks/rule-injection-resolver')));
126
+ await handleRuleInjectionResolver(input);
127
+ break;
128
+ }
109
129
  default:
110
130
  console.error(`Unknown hook: ${name}`);
111
- console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, tool-outcome-watcher');
131
+ console.error('Available: correction-detector, memory-stop, precompact-preserve, memory-sync, tool-outcome-watcher, session-end-checkpoint');
112
132
  }
113
133
  }
114
134
  catch {
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.classifyWithLLM = classifyWithLLM;
10
10
  exports.extractHindsightHint = extractHindsightHint;
11
11
  exports.extractSessionLearningsWithLLM = extractSessionLearningsWithLLM;
12
+ exports.extractCheckpointWithLLM = extractCheckpointWithLLM;
12
13
  exports.classifyBatchWithLLM = classifyBatchWithLLM;
13
14
  // Lazy singleton — avoid import cost when API key is absent
14
15
  let clientInstance; // undefined = not yet checked
@@ -217,6 +218,86 @@ async function extractSessionLearningsWithLLM(summary, existingMemories) {
217
218
  return null;
218
219
  }
219
220
  }
221
+ const CHECKPOINT_EXTRACTION_PROMPT = `You are extracting a "where I left off" checkpoint from a coding session that just ended. The next session — possibly minutes from now, possibly days later — needs a brief, accurate hint to resume from. Your output will overwrite any existing checkpoint, so it MUST be either accurate or empty. NEVER fabricate.
222
+
223
+ You will see the FINAL portion of a session transcript. Your job is to extract THREE fields:
224
+
225
+ - completed: what the user/agent finished in THIS recent task (concrete, brief, max 200 chars). Empty string if nothing was clearly completed.
226
+ - remaining: what was still in flight when the session ended — the actual hand-off (concrete, brief, max 300 chars). MUST be non-empty if there is real unfinished work. EMPTY STRING if the task is done.
227
+ - blockers: anything that was blocking progress (tools failing, decisions pending, dependencies). "none" if no blockers.
228
+
229
+ THE MOST IMPORTANT RULE — completion detection:
230
+ If the transcript ends with ANY signal that the task is finished, return remaining="". Completion signals include:
231
+ - assistant says "Done.", "All set.", "All done.", "Finished.", "That's it.", "Complete.", or similar terminal acknowledgement
232
+ - last user message is thanks/acknowledgement ("thanks", "perfect", "great") with no follow-up question
233
+ - tool calls succeeded and there is no explicit next step in the user's most recent prompt
234
+ - the conversation has reached a natural stopping point
235
+
236
+ When in doubt, prefer remaining="". An empty checkpoint is far better than a fabricated one.
237
+
238
+ THE SECOND MOST IMPORTANT RULE — no fabrication:
239
+ - ONLY use information present in the transcript. Do NOT extrapolate, do NOT invent follow-up work.
240
+ - If the most recent task is clearly complete, do NOT manufacture next steps from your imagination.
241
+ - If you cannot determine what was happening with high confidence, return all-empty: {"completed":"","remaining":"","blockers":""}
242
+ - "remaining" must be a SPECIFIC unfinished item visible in the transcript. Never generic ("continue work", "more testing", "documentation").
243
+
244
+ Other rules:
245
+ - Focus ONLY on the most recent coherent task. Ignore earlier work in the session.
246
+ - Return JSON: {"completed":"...","remaining":"...","blockers":"..."}
247
+ - Be terse and specific. Each field should help the future session pick up the thread.
248
+ - Do NOT include markdown fences. Respond with raw JSON only.
249
+
250
+ Examples of GOOD output:
251
+
252
+ Scenario A — task finished, agent said "Done.":
253
+ {"completed":"Copied cc-source-code into a dedicated dir under claude-recall/cc-source-code/","remaining":"","blockers":"none"}
254
+
255
+ Scenario B — task in progress, midway through implementation:
256
+ {"completed":"Added saveCheckpoint() to storage and MemoryService","remaining":"Wire CLI checkpoint command and add MCP/Pi tool wrappers","blockers":"none"}
257
+
258
+ Scenario C — task in progress, blocked on something:
259
+ {"completed":"Diagnosed [object Object] rendering bug in handleLoadRules","remaining":"Write failing test, extract formatRuleValue helper, replace 5 call sites","blockers":"none"}
260
+
261
+ Scenario D — uncertain, sparse context, can't tell what's happening:
262
+ {"completed":"","remaining":"","blockers":""}
263
+
264
+ Scenario E — just a question, no work done:
265
+ {"completed":"","remaining":"","blockers":""}
266
+
267
+ Examples of BAD output (DO NOT DO THIS):
268
+ {"completed":"various changes","remaining":"more work","blockers":"none"} # too vague
269
+ {"completed":"explored architecture","remaining":"document findings","blockers":"none"} # FABRICATED — there was no documentation task
270
+ {"completed":"finished everything","remaining":"finish everything","blockers":"none"} # nonsense filler`;
271
+ async function extractCheckpointWithLLM(conversationSummary) {
272
+ const client = getClient();
273
+ if (!client)
274
+ return null;
275
+ if (!conversationSummary || conversationSummary.trim().length < 30) {
276
+ return null;
277
+ }
278
+ try {
279
+ const response = await client.messages.create({
280
+ model: MODEL,
281
+ max_tokens: 600,
282
+ system: CHECKPOINT_EXTRACTION_PROMPT,
283
+ messages: [{ role: 'user', content: conversationSummary }],
284
+ });
285
+ const content = response.content?.[0];
286
+ if (content?.type !== 'text')
287
+ return null;
288
+ const result = parseJSON(content.text);
289
+ if (typeof result !== 'object' || result === null)
290
+ return null;
291
+ return {
292
+ completed: typeof result.completed === 'string' ? result.completed : '',
293
+ remaining: typeof result.remaining === 'string' ? result.remaining : '',
294
+ blockers: typeof result.blockers === 'string' ? result.blockers : '',
295
+ };
296
+ }
297
+ catch {
298
+ return null;
299
+ }
300
+ }
220
301
  async function classifyBatchWithLLM(texts) {
221
302
  if (texts.length === 0)
222
303
  return [];
@@ -174,9 +174,9 @@ async function handleMemoryStop(input) {
174
174
  // Prune old outcome data to prevent unbounded table growth
175
175
  try {
176
176
  const pruned = outcomeStorage.pruneOldData();
177
- const total = pruned.episodes + pruned.events + pruned.lessons + pruned.stats;
177
+ const total = pruned.episodes + pruned.events + pruned.lessons + pruned.stats + pruned.injections;
178
178
  if (total > 0) {
179
- (0, shared_1.hookLog)('memory-stop', `Pruned: ${pruned.episodes} episodes, ${pruned.events} events, ${pruned.lessons} lessons, ${pruned.stats} orphaned stats`);
179
+ (0, shared_1.hookLog)('memory-stop', `Pruned: ${pruned.episodes} episodes, ${pruned.events} events, ${pruned.lessons} lessons, ${pruned.stats} orphaned stats, ${pruned.injections} injections`);
180
180
  }
181
181
  }
182
182
  catch (err) {
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * rule-injection-resolver hook — fires on PostToolUse and PostToolUseFailure.
4
+ *
5
+ * Counterpart to rule-injector.ts. After a tool call completes (successfully
6
+ * or with failure), this hook resolves any rule_injection_events that were
7
+ * recorded for that tool_use_id with the actual outcome.
8
+ *
9
+ * The pair gives us a direct measurement of rule effectiveness:
10
+ * - Rule X was injected before Bash call Y
11
+ * - Bash call Y succeeded → rule X co-occurs with success
12
+ * - Bash call Y failed → rule X was either ignored, wrong, or unrelated
13
+ *
14
+ * Aggregated over time, this becomes the new "is this rule helpful" signal,
15
+ * replacing the broken citation-detection regex (.research/rule-loading-gap.md).
16
+ *
17
+ * Always exits cleanly with no stdout — this hook only writes to the DB,
18
+ * it doesn't influence tool execution.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.handleRuleInjectionResolver = handleRuleInjectionResolver;
22
+ const shared_1 = require("./shared");
23
+ const outcome_storage_1 = require("../services/outcome-storage");
24
+ async function handleRuleInjectionResolver(input) {
25
+ const toolUseId = input?.tool_use_id ?? '';
26
+ const eventName = input?.hook_event_name ?? '';
27
+ if (!toolUseId) {
28
+ return;
29
+ }
30
+ // Outcome inference: PostToolUseFailure means failure, anything else means success.
31
+ // (PostToolUse fires on success; PostToolUseFailure on tool errors.)
32
+ const outcome = eventName === 'PostToolUseFailure' ? 'failure' : 'success';
33
+ try {
34
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
35
+ const resolved = outcomeStorage.resolveRuleInjections(toolUseId, outcome);
36
+ if (resolved > 0) {
37
+ (0, shared_1.hookLog)('rule-injection-resolver', `Resolved ${resolved} rule injection(s) for ${toolUseId} as ${outcome}`);
38
+ }
39
+ }
40
+ catch (err) {
41
+ (0, shared_1.hookLog)('rule-injection-resolver', `Error: ${err.message}`);
42
+ }
43
+ }
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ /**
3
+ * rule-injector hook — fires on Claude Code's PreToolUse event.
4
+ *
5
+ * Just-in-time rule injection (JITRI). The core fix for the rule-loading gap
6
+ * documented in .research/rule-loading-gap.md: rules are loaded once at session
7
+ * start, then ignored when the agent acts because they're 50,000 tokens upstream
8
+ * by the time of the action. This hook closes that gap by searching active rules
9
+ * for matches against THIS specific tool call and injecting the top matches as
10
+ * a system-reminder block immediately adjacent to the tool action.
11
+ *
12
+ * Output mechanism (verified against cc-source-code/utils/hooks.ts:621 and
13
+ * services/tools/toolHooks.ts:565):
14
+ * - Hook prints JSON to stdout
15
+ * - JSON includes hookSpecificOutput.additionalContext
16
+ * - CC wraps that string in a <system-reminder> block via wrapInSystemReminder()
17
+ * and creates a meta user message at the moment of the tool call
18
+ * - The agent sees the rules adjacent to the action it's about to take
19
+ *
20
+ * No LLM call in the hot path — pure keyword-based ranking, ~10-30ms typical.
21
+ *
22
+ * Each injection is recorded as a rule_injection_event so we can later
23
+ * resolve it with the tool outcome (success/failure) and measure rule
24
+ * effectiveness directly. This is the meter that replaces the broken
25
+ * citation-detection regex.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.handleRuleInjector = handleRuleInjector;
29
+ const shared_1 = require("./shared");
30
+ const memory_1 = require("../services/memory");
31
+ const config_1 = require("../services/config");
32
+ const outcome_storage_1 = require("../services/outcome-storage");
33
+ const rule_retrieval_1 = require("../services/rule-retrieval");
34
+ const memory_tools_1 = require("../mcp/tools/memory-tools");
35
+ const TYPE_LABELS = {
36
+ correction: 'correction',
37
+ devops: 'devops',
38
+ preference: 'preference',
39
+ failure: 'avoid',
40
+ 'project-knowledge': 'project',
41
+ };
42
+ /**
43
+ * Render a rule's value for injection. Reuses the same formatRuleValue helper
44
+ * that handleLoadRules uses (memory-tools.ts), so the rule-injector and
45
+ * load_rules output stay consistent. handles all the historical value shapes
46
+ * including nested-content failures and stringified-JSON content.
47
+ */
48
+ function extractRuleSnippet(value) {
49
+ let snippet = (0, memory_tools_1.formatRuleValue)(value);
50
+ // formatRuleValue may return a stringified JSON for legacy shapes where
51
+ // value.content is a JSON string. Try one parse-and-extract pass to pull
52
+ // out a more readable summary.
53
+ if (snippet.startsWith('{') && snippet.includes('what_failed')) {
54
+ try {
55
+ const parsed = JSON.parse(snippet);
56
+ if (typeof parsed?.what_failed === 'string') {
57
+ snippet = parsed.what_failed;
58
+ }
59
+ }
60
+ catch { /* fall through with the stringified JSON */ }
61
+ }
62
+ return snippet;
63
+ }
64
+ function formatInjection(matches, toolName) {
65
+ if (matches.length === 0)
66
+ return '';
67
+ const lines = matches.map(m => {
68
+ const label = TYPE_LABELS[m.rule.type] ?? m.rule.type;
69
+ const snippet = extractRuleSnippet(m.rule.value).substring(0, 200).replace(/\s+/g, ' ').trim();
70
+ return `• [${label}] ${snippet}`;
71
+ });
72
+ return (`Recall: ${matches.length} rule${matches.length === 1 ? '' : 's'} relevant to this ${toolName} call. ` +
73
+ `Apply them or explicitly note why they don't fit:\n${lines.join('\n')}`);
74
+ }
75
+ async function handleRuleInjector(input) {
76
+ const toolName = input?.tool_name ?? '';
77
+ const toolInput = input?.tool_input ?? {};
78
+ const toolUseId = input?.tool_use_id ?? '';
79
+ if (!toolName) {
80
+ // Nothing to do — print empty JSON so CC parses it cleanly
81
+ process.stdout.write('{}\n');
82
+ return;
83
+ }
84
+ // Skip the hook for our own tools so we don't recursively inject rules
85
+ // about claude-recall into claude-recall calls. The agent already has
86
+ // claude-recall context when calling its own tools.
87
+ if (toolName.startsWith('mcp__claude-recall__') || toolName.startsWith('mcp__claude_recall')) {
88
+ process.stdout.write('{}\n');
89
+ return;
90
+ }
91
+ try {
92
+ const projectId = config_1.ConfigService.getInstance().getProjectId();
93
+ const memoryService = memory_1.MemoryService.getInstance();
94
+ // Fetch all active rules for this project. We pass them all to the ranker
95
+ // because the ranking function is fast and we want sticky rules to surface
96
+ // even when token overlap is low.
97
+ const activeRules = memoryService.loadActiveRules(projectId);
98
+ const allRules = [
99
+ ...activeRules.preferences,
100
+ ...activeRules.corrections,
101
+ ...activeRules.failures,
102
+ ...activeRules.devops,
103
+ ].map(m => ({
104
+ key: m.key,
105
+ type: m.type,
106
+ value: m.value,
107
+ is_active: m.is_active !== false,
108
+ timestamp: m.timestamp,
109
+ project_id: m.project_id,
110
+ }));
111
+ if (allRules.length === 0) {
112
+ (0, shared_1.hookLog)('rule-injector', `No active rules for project ${projectId} (tool=${toolName})`);
113
+ process.stdout.write('{}\n');
114
+ return;
115
+ }
116
+ const matches = (0, rule_retrieval_1.rankRulesForToolCall)(toolName, toolInput, allRules);
117
+ if (matches.length === 0) {
118
+ (0, shared_1.hookLog)('rule-injector', `No relevant rules for ${toolName} (scanned ${allRules.length})`);
119
+ process.stdout.write('{}\n');
120
+ return;
121
+ }
122
+ // Record each injection so PostToolUse can resolve it with the outcome
123
+ try {
124
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
125
+ for (const m of matches) {
126
+ outcomeStorage.recordRuleInjection({
127
+ rule_key: m.rule.key,
128
+ tool_name: toolName,
129
+ tool_use_id: toolUseId,
130
+ project_id: projectId,
131
+ match_score: m.score,
132
+ matched_tokens: m.matchedTokens,
133
+ });
134
+ }
135
+ }
136
+ catch (err) {
137
+ // Non-critical — failure to record shouldn't block the injection itself
138
+ (0, shared_1.hookLog)('rule-injector', `Failed to record injections: ${err.message}`);
139
+ }
140
+ const additionalContext = formatInjection(matches, toolName);
141
+ const output = {
142
+ hookSpecificOutput: {
143
+ hookEventName: 'PreToolUse',
144
+ additionalContext,
145
+ },
146
+ };
147
+ process.stdout.write(JSON.stringify(output) + '\n');
148
+ (0, shared_1.hookLog)('rule-injector', `Injected ${matches.length} rule(s) for ${toolName} (top score=${matches[0].score.toFixed(3)})`);
149
+ }
150
+ catch (err) {
151
+ (0, shared_1.hookLog)('rule-injector', `Error: ${err.message}`);
152
+ // Best-effort — never block the tool call
153
+ process.stdout.write('{}\n');
154
+ }
155
+ }