phronesis 1.0.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.
Files changed (142) hide show
  1. package/README.md +66 -0
  2. package/package.json +34 -0
  3. package/src/act.js +61 -0
  4. package/src/active-context.js +56 -0
  5. package/src/adapter.js +386 -0
  6. package/src/cli.js +1893 -0
  7. package/src/codex-hooks-schema.js +92 -0
  8. package/src/compile.js +513 -0
  9. package/src/doctor.js +303 -0
  10. package/src/due.js +271 -0
  11. package/src/eval.js +467 -0
  12. package/src/event.js +212 -0
  13. package/src/export.js +136 -0
  14. package/src/guard.js +89 -0
  15. package/src/hooks.js +230 -0
  16. package/src/ingest.js +965 -0
  17. package/src/init.js +150 -0
  18. package/src/layout.js +394 -0
  19. package/src/links.js +83 -0
  20. package/src/lint.js +58 -0
  21. package/src/profile.js +116 -0
  22. package/src/prompts.js +26 -0
  23. package/src/registry.js +190 -0
  24. package/src/scaffold.js +134 -0
  25. package/src/search.js +139 -0
  26. package/src/skill-discovery.js +117 -0
  27. package/src/skills-bump.js +885 -0
  28. package/src/skills.js +355 -0
  29. package/src/subscription.js +5 -0
  30. package/src/validate.js +152 -0
  31. package/templates/claude/settings.json +86 -0
  32. package/templates/codex/INDEX.md +68 -0
  33. package/templates/codex/seed/ai-practice/compile-dont-just-capture.md +42 -0
  34. package/templates/codex/seed/ai-practice/design-for-the-distribution.md +42 -0
  35. package/templates/codex/seed/ai-practice/from-spec-author-to-outcome-curator.md +43 -0
  36. package/templates/codex/seed/ai-practice/recall-builds-trust-when-calibrated.md +70 -0
  37. package/templates/codex/seed/ai-practice/the-eval-is-the-spec.md +42 -0
  38. package/templates/codex/seed/ai-practice/the-gold-dust-leaves-no-trace.md +72 -0
  39. package/templates/codex/seed/ai-practice/the-model-is-not-the-moat.md +40 -0
  40. package/templates/codex/seed/ai-practice/when-generation-is-cheap-judgment-is-the-work.md +39 -0
  41. package/templates/codex/seed/judgment/contextual-stewardship-is-the-senior-skill.md +50 -0
  42. package/templates/codex/seed/judgment/decision-quality-is-distinct-from-outcome-quality.md +39 -0
  43. package/templates/codex/seed/judgment/embrace-reality-ego-and-blind-spots.md +38 -0
  44. package/templates/codex/seed/judgment/groups-seek-consensus-individuals-seek-truth.md +44 -0
  45. package/templates/codex/seed/judgment/heresies-are-where-the-edge-lives.md +42 -0
  46. package/templates/codex/seed/judgment/judgment-compounds-through-honest-reflection.md +38 -0
  47. package/templates/codex/seed/judgment/phronesis-is-judgment-in-the-particular.md +67 -0
  48. package/templates/codex/seed/judgment/show-me-the-apparatus.md +40 -0
  49. package/templates/codex/seed/judgment/small-judgment-gaps-compound-under-leverage.md +37 -0
  50. package/templates/codex/seed/judgment/taste-is-compressed-judgment.md +38 -0
  51. package/templates/codex/seed/judgment/trust-taste-only-where-feedback-is-valid.md +42 -0
  52. package/templates/codex/seed/judgment/weigh-the-reasoning-not-the-recommendation.md +43 -0
  53. package/templates/codex/seed/leadership/a-managers-output-is-the-teams-output.md +35 -0
  54. package/templates/codex/seed/leadership/most-team-problems-are-structural.md +40 -0
  55. package/templates/codex/seed/leadership/next-play.md +36 -0
  56. package/templates/codex/seed/leadership/psychological-safety.md +39 -0
  57. package/templates/codex/seed/leadership/right-size-your-teams.md +38 -0
  58. package/templates/codex/seed/leadership/spend-time-on-high-leverage-activities.md +36 -0
  59. package/templates/codex/seed/leadership/talent-density-is-the-master-variable.md +39 -0
  60. package/templates/codex/seed/leadership/under-pressure-calm-is-contagious.md +36 -0
  61. package/templates/codex/seed/leadership/your-best-alternative-is-your-leverage.md +37 -0
  62. package/templates/codex/seed/pm/a-problem-well-stated-is-half-solved.md +39 -0
  63. package/templates/codex/seed/pm/a-real-strategy-makes-planning-trivial.md +40 -0
  64. package/templates/codex/seed/pm/barrels-and-ammunition.md +39 -0
  65. package/templates/codex/seed/pm/develop-taste-in-your-beliefs.md +38 -0
  66. package/templates/codex/seed/pm/hire-for-progress.md +44 -0
  67. package/templates/codex/seed/pm/jobs-are-stable-solutions-churn.md +39 -0
  68. package/templates/codex/seed/pm/level-by-the-ambiguity-someone-can-own.md +39 -0
  69. package/templates/codex/seed/pm/listen-before-you-prescribe.md +38 -0
  70. package/templates/codex/seed/pm/live-in-the-future-and-build-whats-missing.md +38 -0
  71. package/templates/codex/seed/pm/match-effort-to-leverage.md +37 -0
  72. package/templates/codex/seed/pm/not-my-fault-but-my-problem.md +40 -0
  73. package/templates/codex/seed/pm/pick-the-job-then-refuse-everything-outside-it.md +37 -0
  74. package/templates/codex/seed/pm/retention-sets-the-ceiling.md +38 -0
  75. package/templates/codex/seed/pm/scale-with-leaders-not-process.md +38 -0
  76. package/templates/codex/seed/pm/solution-discovery-over-problem-validation.md +38 -0
  77. package/templates/codex/seed/pm/tenfold-better-on-a-painful-problem.md +40 -0
  78. package/templates/codex-surface/README.md +73 -0
  79. package/templates/codex-surface/hooks/_resolve.sh +36 -0
  80. package/templates/codex-surface/hooks/recall-guard.sh +30 -0
  81. package/templates/codex-surface/hooks/session-start.sh +18 -0
  82. package/templates/codex-surface/hooks/session-sweep-precompact.sh +20 -0
  83. package/templates/codex-surface/hooks/session-sweep-subagent.sh +23 -0
  84. package/templates/codex-surface/hooks/session-sweep.sh +19 -0
  85. package/templates/codex-surface/hooks.json +69 -0
  86. package/templates/domains/pm/AGENTS.md +111 -0
  87. package/templates/domains/pm/MEMORY.md +7 -0
  88. package/templates/domains/pm/USER.md +8 -0
  89. package/templates/phronesis-hooks/compile-active-context.sh +38 -0
  90. package/templates/phronesis-hooks/recall-guard.sh +103 -0
  91. package/templates/phronesis-hooks/session-start.sh +90 -0
  92. package/templates/phronesis-hooks/session-sweep.sh +140 -0
  93. package/templates/phronesis-hooks/skill-lifecycle.sh +37 -0
  94. package/templates/skills/compile/SKILL.md +279 -0
  95. package/templates/skills/compile/evals/rubric.md +75 -0
  96. package/templates/skills/decision-log/SKILL.md +156 -0
  97. package/templates/skills/decision-log/evals/rubric.md +97 -0
  98. package/templates/skills/extract-gold-dust/SKILL.md +311 -0
  99. package/templates/skills/extract-gold-dust/evals/rubric.md +52 -0
  100. package/templates/skills/ingest/SKILL.md +204 -0
  101. package/templates/skills/ingest/evals/rubric.md +117 -0
  102. package/templates/skills/lint/SKILL.md +246 -0
  103. package/templates/skills/lint/evals/rubric.md +139 -0
  104. package/templates/skills/meeting-prep/SKILL.md +195 -0
  105. package/templates/skills/meeting-prep/evals/rubric.md +93 -0
  106. package/templates/skills/onboard/SKILL.md +258 -0
  107. package/templates/skills/onboard/evals/rubric.md +60 -0
  108. package/templates/skills/prd-draft/SKILL.md +193 -0
  109. package/templates/skills/prd-draft/evals/rubric.md +38 -0
  110. package/templates/skills/prep-extraction/SKILL.md +217 -0
  111. package/templates/skills/prep-extraction/evals/rubric.md +45 -0
  112. package/templates/skills/recall/SKILL.md +155 -0
  113. package/templates/skills/recall/evals/rubric.md +37 -0
  114. package/templates/skills/research/SKILL.md +247 -0
  115. package/templates/skills/research/evals/rubric.md +139 -0
  116. package/templates/skills/stakeholder-update/SKILL.md +163 -0
  117. package/templates/skills/stakeholder-update/evals/rubric.md +111 -0
  118. package/vendor/core/package.json +25 -0
  119. package/vendor/core/src/action-registry.js +459 -0
  120. package/vendor/core/src/action-writers.js +201 -0
  121. package/vendor/core/src/actions.js +2140 -0
  122. package/vendor/core/src/active-context.js +167 -0
  123. package/vendor/core/src/artifacts.js +301 -0
  124. package/vendor/core/src/content-digest.js +21 -0
  125. package/vendor/core/src/contract.js +245 -0
  126. package/vendor/core/src/event-log.js +338 -0
  127. package/vendor/core/src/guard.js +398 -0
  128. package/vendor/core/src/hook-executor.js +527 -0
  129. package/vendor/core/src/index.js +180 -0
  130. package/vendor/core/src/ingest.js +286 -0
  131. package/vendor/core/src/install-root.js +42 -0
  132. package/vendor/core/src/layout.js +13 -0
  133. package/vendor/core/src/link-index.js +198 -0
  134. package/vendor/core/src/lint.js +368 -0
  135. package/vendor/core/src/schedule.js +535 -0
  136. package/vendor/core/src/search.js +534 -0
  137. package/vendor/core/src/session-hooks.js +129 -0
  138. package/vendor/core/src/skill-lifecycle.js +479 -0
  139. package/vendor/core/src/skill-registry.js +524 -0
  140. package/vendor/core/src/subscription.js +84 -0
  141. package/vendor/core/src/validate-object.js +62 -0
  142. package/vendor/core/src/workspace-scan.js +125 -0
@@ -0,0 +1,398 @@
1
+ // Context relevance guard — pre-answer recall for owned-context advice/action prompts
2
+ // (core-0019 / T5.2a). The guard brain lives in @phronesis/core: surfaces pass a
3
+ // prompt plus execution context, core detects, emits recall.candidate, composes the
4
+ // in-process retrieval packet, then emits recall.injected. CLI and hooks are glue.
5
+
6
+ import { createHash } from 'node:crypto';
7
+ import { join, relative, resolve, sep } from 'node:path';
8
+ import { readFile } from 'node:fs/promises';
9
+ import { appendEvent } from './event-log.js';
10
+ import { appendAuditEntry } from './hook-executor.js';
11
+ import { resolveInstall } from './install-root.js';
12
+ import { loadSearchIndex } from './search.js';
13
+
14
+ export const GUARD_ID = 'on_recall_candidate';
15
+ export const GUARD_RULESET_VERSION = 'guard-rules-v2';
16
+
17
+ const STALE_STATUSES = new Set(['archived', 'superseded']);
18
+ const DEFAULT_LIMIT = 10;
19
+ const DEFAULT_HOPS = 1;
20
+
21
+ const POSSESSIVE_RE =
22
+ /\b(my|our|ours|mine|this|that|current|the current|at my company|for my team|for our team|my company|our company)\b/i;
23
+ const ADVICE_RE =
24
+ /\b(should i|should we|what should i|what should we|what do i do|what do we do|what's next|what is next|can i|can we|can i wait|can we wait|is it safe to|do we|do i|which option|how should i|how should we|need to)\b/i;
25
+ const NAMED_CONTEXT_RE =
26
+ /\b(tell me about|status of|update on|give me an update on|how's my|how is my|how's our|how is our|what's the status of|what is the status of|where are we on|where are we with)\b/i;
27
+
28
+ function sha256(s) {
29
+ return createHash('sha256').update(String(s ?? ''), 'utf8').digest('hex');
30
+ }
31
+
32
+ function normalizePhrase(s) {
33
+ return String(s ?? '')
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9]+/g, ' ')
36
+ .replace(/\s+/g, ' ')
37
+ .trim();
38
+ }
39
+
40
+ function phraseMatchesPrompt(phrase, normalizedPrompt) {
41
+ const normalized = normalizePhrase(phrase);
42
+ if (normalized.length < 4) return false;
43
+ return ` ${normalizedPrompt} `.includes(` ${normalized} `);
44
+ }
45
+
46
+ function ruleMatches(prompt) {
47
+ const text = String(prompt ?? '');
48
+ const rules = [];
49
+ let confidence = 0;
50
+
51
+ if (POSSESSIVE_RE.test(text)) {
52
+ rules.push('possessive');
53
+ confidence += 0.65;
54
+ }
55
+ if (ADVICE_RE.test(text)) {
56
+ rules.push('advice-verb');
57
+ confidence += 0.7;
58
+ }
59
+
60
+ return { rules, confidence };
61
+ }
62
+
63
+ function shouldCheckNameMatch(prompt) {
64
+ const text = String(prompt ?? '');
65
+ const titleLike = /\b[A-Z][a-z0-9]+(?:\s+[A-Z][a-z0-9]+)+\b/.test(text);
66
+ const slugLike = /\b[a-z0-9]+-[a-z0-9-]+\b/i.test(text);
67
+ return titleLike || slugLike || NAMED_CONTEXT_RE.test(text);
68
+ }
69
+
70
+ function objectNameMatch(prompt, docs = []) {
71
+ const normalizedPrompt = normalizePhrase(prompt);
72
+ if (!normalizedPrompt) return false;
73
+ return docs.some((doc) => {
74
+ const candidates = [doc.name, doc.title].filter(Boolean);
75
+ return candidates.some((candidate) => phraseMatchesPrompt(candidate, normalizedPrompt));
76
+ });
77
+ }
78
+
79
+ function triggerFor(rules, confidence) {
80
+ return {
81
+ category: 'owned-context-advice',
82
+ rules,
83
+ ruleset_version: GUARD_RULESET_VERSION,
84
+ confidence,
85
+ };
86
+ }
87
+
88
+ export function detectContextRelevance(prompt, docs = []) {
89
+ const { rules, confidence: baseConfidence } = ruleMatches(prompt);
90
+ let confidence = baseConfidence;
91
+ if (objectNameMatch(prompt, docs)) {
92
+ rules.push('name-match');
93
+ confidence += 0.8;
94
+ }
95
+
96
+ confidence = Math.min(0.99, Number(confidence.toFixed(2)));
97
+ const hasNameMatch = rules.includes('name-match');
98
+ const hasOwnedAdvice = rules.includes('possessive') && rules.includes('advice-verb');
99
+ return {
100
+ fired: hasNameMatch || hasOwnedAdvice,
101
+ confidence,
102
+ trigger: triggerFor(rules, confidence),
103
+ };
104
+ }
105
+
106
+ function parseSkipList(env = {}) {
107
+ const raw = env.PHRONESIS_SKIP;
108
+ if (typeof raw !== 'string' || raw.trim() === '') return new Set();
109
+ return new Set(raw.split(',').map((s) => s.trim()).filter(Boolean));
110
+ }
111
+
112
+ function isSkipped(env) {
113
+ const skip = parseSkipList(env);
114
+ return skip.has(GUARD_ID) || skip.has('context-relevance-guard');
115
+ }
116
+
117
+ async function auditGuard(installRoot, entry) {
118
+ await appendAuditEntry(installRoot, {
119
+ hook: GUARD_ID,
120
+ kind: 'ambient',
121
+ classification: 'silent',
122
+ ...entry,
123
+ });
124
+ }
125
+
126
+ function activeResults(results) {
127
+ return results.filter((r) => !STALE_STATUSES.has(r.status));
128
+ }
129
+
130
+ function staleResults(results) {
131
+ return results.filter((r) => STALE_STATUSES.has(r.status));
132
+ }
133
+
134
+ function activeLinkedNodes(result) {
135
+ const nodes = result.subgraph?.nodes ?? [];
136
+ return nodes.filter(
137
+ (n) =>
138
+ n.kind === 'object' &&
139
+ n.path !== result.path &&
140
+ !STALE_STATUSES.has(n.status),
141
+ );
142
+ }
143
+
144
+ function formatSources(result) {
145
+ const sources = Array.isArray(result.sources) ? result.sources : [];
146
+ return sources.length ? sources.join(', ') : 'none cited';
147
+ }
148
+
149
+ function formatRecallPacket({ results, stale, trigger }) {
150
+ const lines = [
151
+ 'Phronesis recall packet',
152
+ '',
153
+ `Trigger: ${trigger.category} (${trigger.rules.join(', ') || 'none'}; ${trigger.ruleset_version})`,
154
+ '',
155
+ 'Current workspace state:',
156
+ ];
157
+
158
+ for (const r of results) {
159
+ lines.push(
160
+ `- ${r.title ?? r.name ?? r.path} (${r.type ?? 'object'}, status: ${r.status ?? 'active'}, exposure: ${r.exposure ?? 'unknown'})`,
161
+ ` Path: ${r.path}`,
162
+ );
163
+ if (r.snippet) lines.push(` Snippet: ${r.snippet}`);
164
+ lines.push(` Sources: ${formatSources(r)}`);
165
+ const linked = activeLinkedNodes(r);
166
+ if (linked.length) {
167
+ lines.push(' Linked context:');
168
+ for (const n of linked) {
169
+ lines.push(
170
+ ` - ${n.title ?? n.name ?? n.path} (${n.type ?? 'object'}, status: ${n.status ?? 'active'}, exposure: ${n.exposure ?? 'unknown'})`,
171
+ ` Path: ${n.path}`,
172
+ );
173
+ }
174
+ }
175
+ }
176
+
177
+ if (stale.length) {
178
+ lines.push('', 'Excluded stale diagnostics:');
179
+ for (const r of stale) {
180
+ lines.push(`- ${r.path} (status: ${r.status})`);
181
+ }
182
+ }
183
+
184
+ lines.push(
185
+ '',
186
+ 'Use this workspace state before answering. Preserve the cited paths and exposure tiers in the response.',
187
+ );
188
+ return lines.join('\n');
189
+ }
190
+
191
+ function formatHonestNegative({ stale = [] } = {}) {
192
+ const lines = [
193
+ 'No existing compiled/codex state found for this owned-context prompt.',
194
+ 'Say that plainly before answering from general knowledge, and consider capture if this should become workspace state.',
195
+ ];
196
+ if (stale.length) {
197
+ lines.push('', 'Excluded stale diagnostics:');
198
+ for (const r of stale) lines.push(`- ${r.path} (status: ${r.status})`);
199
+ }
200
+ return lines.join('\n');
201
+ }
202
+
203
+ function normalizeResult({ promptHash, trigger, retrieval, startedAt }) {
204
+ const current = activeResults(retrieval.results);
205
+ const stale = staleResults(retrieval.results);
206
+ const empty = current.length === 0;
207
+ const packet = empty
208
+ ? formatHonestNegative({ stale })
209
+ : formatRecallPacket({ results: current, stale, trigger });
210
+ return {
211
+ fired: true,
212
+ empty_result: empty,
213
+ packet,
214
+ result_paths: current.map((r) => r.path),
215
+ result_count: current.length,
216
+ prompt_hash: promptHash,
217
+ trigger,
218
+ retrieval_latency_ms: retrieval.latency_ms,
219
+ latency_ms: Math.round(performance.now() - startedAt),
220
+ excluded_stale_paths: stale.map((r) => r.path),
221
+ };
222
+ }
223
+
224
+ async function emitCandidate({ installRoot, session, actor, surface, prompt, promptHash, trigger, confidence, now }) {
225
+ return appendEvent({
226
+ installRoot,
227
+ type: 'recall.candidate',
228
+ session_id: session,
229
+ actor,
230
+ surface,
231
+ payload: {
232
+ prompt_hash: promptHash,
233
+ trigger,
234
+ query: String(prompt ?? ''),
235
+ confidence,
236
+ },
237
+ now,
238
+ });
239
+ }
240
+
241
+ async function emitInjected({ installRoot, session, actor, surface, result, now }) {
242
+ return appendEvent({
243
+ installRoot,
244
+ type: 'recall.injected',
245
+ session_id: session,
246
+ actor,
247
+ surface,
248
+ payload: {
249
+ prompt_hash: result.prompt_hash,
250
+ result_paths: result.result_paths,
251
+ result_count: result.result_count,
252
+ empty_result: result.empty_result,
253
+ latency_ms: result.latency_ms,
254
+ },
255
+ now,
256
+ });
257
+ }
258
+
259
+ function resolveRoot({ installRoot, dir, cwd }) {
260
+ if (installRoot) return installRoot;
261
+ const resolved = resolveInstall({ dir, cwd });
262
+ if (resolved.error) throw new Error(resolved.error);
263
+ return resolved.root;
264
+ }
265
+
266
+ async function readRegisteredDomains(root) {
267
+ const registry = JSON.parse(await readFile(join(root, '.phronesis', 'registry.json'), 'utf8'));
268
+ return Array.isArray(registry.domains) ? registry.domains : [];
269
+ }
270
+
271
+ function domainFromCwd(root, cwd, registered) {
272
+ if (!cwd) return undefined;
273
+ const parts = relative(root, resolve(cwd)).split(sep);
274
+ return parts[0] === 'domains' && registered.includes(parts[1]) ? parts[1] : undefined;
275
+ }
276
+
277
+ async function guardSearchScope(root, requestedDomain, cwd) {
278
+ if (requestedDomain !== undefined) return { domain: requestedDomain, allDomains: false };
279
+ const registered = await readRegisteredDomains(root);
280
+ const cwdDomain = domainFromCwd(root, cwd, registered);
281
+ if (cwdDomain) return { domain: cwdDomain, allDomains: false };
282
+ if (registered.length === 1) return { domain: registered[0], allDomains: false };
283
+ if (registered.length > 1) return { domain: undefined, allDomains: true };
284
+ return { domain: undefined, allDomains: false };
285
+ }
286
+
287
+ function needsIndexForDetection(prompt) {
288
+ const cheap = ruleMatches(prompt).rules;
289
+ return (
290
+ (cheap.includes('possessive') && cheap.includes('advice-verb')) ||
291
+ shouldCheckNameMatch(prompt)
292
+ );
293
+ }
294
+
295
+ function auditErrorFields(err) {
296
+ return {
297
+ error: 'guard execution failed',
298
+ error_name: err?.name || 'Error',
299
+ };
300
+ }
301
+
302
+ export async function runContextRelevanceGuard({
303
+ installRoot,
304
+ dir,
305
+ cwd = process.cwd(),
306
+ prompt,
307
+ session,
308
+ surface,
309
+ actor,
310
+ domain,
311
+ env = process.env,
312
+ now = new Date(),
313
+ loadIndex = loadSearchIndex,
314
+ } = {}) {
315
+ const startedAt = performance.now();
316
+ if (typeof prompt !== 'string' || prompt.trim() === '') {
317
+ throw new Error('guard prompt is required on stdin');
318
+ }
319
+ if (typeof session !== 'string' || session === '') {
320
+ throw new Error('guard requires --session <id>');
321
+ }
322
+ if (typeof surface !== 'string' || surface === '') {
323
+ throw new Error('guard requires --surface <name>');
324
+ }
325
+ if (typeof actor !== 'string' || actor === '') {
326
+ throw new Error('guard requires --actor <name> or PHRONESIS_ACTOR');
327
+ }
328
+
329
+ const root = resolveRoot({ installRoot, dir, cwd });
330
+ const promptHash = sha256(prompt);
331
+
332
+ if (isSkipped(env)) {
333
+ await auditGuard(root, {
334
+ session_id: session,
335
+ event_type: 'recall.candidate',
336
+ prompt_hash: promptHash,
337
+ outcome: 'skipped-kill-switch',
338
+ });
339
+ return { fired: false, bypassed: true };
340
+ }
341
+
342
+ if (!needsIndexForDetection(prompt)) return { fired: false };
343
+
344
+ const scope = await guardSearchScope(root, domain, cwd);
345
+ const index = await loadIndex({
346
+ installRoot: root,
347
+ cwd,
348
+ domain: scope.domain,
349
+ allDomains: scope.allDomains,
350
+ persist: false,
351
+ });
352
+ const detection = detectContextRelevance(prompt, index.scopedDocs);
353
+ if (!detection.fired) return { fired: false };
354
+
355
+ let candidate = null;
356
+ try {
357
+ candidate = await emitCandidate({
358
+ installRoot: root,
359
+ session,
360
+ actor,
361
+ surface,
362
+ prompt,
363
+ promptHash,
364
+ trigger: detection.trigger,
365
+ confidence: detection.confidence,
366
+ now,
367
+ });
368
+
369
+ const retrieval = index.query(prompt, { limit: DEFAULT_LIMIT, hops: DEFAULT_HOPS });
370
+ const result = normalizeResult({
371
+ promptHash,
372
+ trigger: detection.trigger,
373
+ retrieval,
374
+ startedAt,
375
+ });
376
+
377
+ await emitInjected({
378
+ installRoot: root,
379
+ session,
380
+ actor,
381
+ surface,
382
+ result,
383
+ now,
384
+ });
385
+
386
+ return result;
387
+ } catch (err) {
388
+ await auditGuard(root, {
389
+ session_id: session,
390
+ event_type: 'recall.candidate',
391
+ ...(candidate?.id ? { event_id: candidate.id } : {}),
392
+ prompt_hash: promptHash,
393
+ outcome: 'failed',
394
+ ...auditErrorFields(err),
395
+ });
396
+ throw err;
397
+ }
398
+ }