From 8b07d69feab45cf007b88658a598f9208574ac31 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 11:54:09 -0500 Subject: [PATCH 1/2] feat: add reflect_scars tool and default execution_successful at session close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the scar enforcement gap where agents confirm scars (APPLYING) but never report outcomes, causing 80% null execution_successful in scar_usage. Option A: session_close defaults execution_successful based on confirm_scars decision (APPLYING→true, unmentioned→false). Option B: new reflect_scars tool (gitmem-rf/gm-reflect) for end-of-session OBEYED/REFUTED evidence, bridged into scar_usage with priority over defaults. Refs: OD-772 Co-Authored-By: Claude Opus 4.6 --- src/server.ts | 8 + src/services/metrics.ts | 2 + src/services/session-state.ts | 36 ++++- src/tools/definitions.ts | 83 ++++++++++- src/tools/reflect-scars.ts | 265 ++++++++++++++++++++++++++++++++++ src/tools/session-close.ts | 34 ++++- src/types/index.ts | 29 ++++ 7 files changed, 450 insertions(+), 7 deletions(-) create mode 100644 src/tools/reflect-scars.ts diff --git a/src/server.ts b/src/server.ts index 2126432..3c0e36c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -25,6 +25,7 @@ import { recordScarUsage } from "./tools/record-scar-usage.js"; import { recordScarUsageBatch } from "./tools/record-scar-usage-batch.js"; import { recall } from "./tools/recall.js"; import { confirmScars } from "./tools/confirm-scars.js"; +import { reflectScars } from "./tools/reflect-scars.js"; import { saveTranscript } from "./tools/save-transcript.js"; import { getTranscript } from "./tools/get-transcript.js"; import { searchTranscripts } from "./tools/search-transcripts.js"; @@ -82,6 +83,7 @@ import type { SaveTranscriptParams, GetTranscriptParams, ConfirmScarsParams, + ReflectScarsParams, } from "./types/index.js"; import type { RecallParams } from "./tools/recall.js"; import type { SearchParams } from "./tools/search.js"; @@ -159,6 +161,11 @@ export function createServer(): Server { case "gm-confirm": result = await confirmScars(toolArgs as unknown as ConfirmScarsParams); break; + case "reflect_scars": + case "gitmem-rf": + case "gm-reflect": + result = await reflectScars(toolArgs as unknown as ReflectScarsParams); + break; case "session_start": case "gitmem-ss": case "gm-open": @@ -269,6 +276,7 @@ export function createServer(): Server { const commands = [ { alias: "gitmem-r", full: "recall", description: "Check scars before taking action" }, { alias: "gitmem-cs", full: "confirm_scars", description: "Confirm recalled scars (APPLYING/N_A/REFUTED)" }, + { alias: "gitmem-rf", full: "reflect_scars", description: "End-of-session scar reflection (OBEYED/REFUTED)" }, { alias: "gitmem-ss", full: "session_start", description: "Initialize session with context" }, { alias: "gitmem-sr", full: "session_refresh", description: "Refresh context for active session" }, { alias: "gitmem-sc", full: "session_close", description: "Close session with compliance validation" }, diff --git a/src/services/metrics.ts b/src/services/metrics.ts index 63d8c40..a765f03 100644 --- a/src/services/metrics.ts +++ b/src/services/metrics.ts @@ -35,6 +35,7 @@ export type ToolName = | "resolve_thread" | "create_thread" | "confirm_scars" + | "reflect_scars" | "cleanup_threads" | "health"; @@ -106,6 +107,7 @@ export const PERFORMANCE_TARGETS: Record = { resolve_thread: 100, // In-memory mutation + file write create_thread: 100, // In-memory mutation + file write confirm_scars: 500, // In-memory validation + file write + reflect_scars: 500, // In-memory validation + file write cleanup_threads: 2000, // Fetch all threads + lifecycle computation health: 100, // In-memory read from EffectTracker }; diff --git a/src/services/session-state.ts b/src/services/session-state.ts index 89a6557..60b9021 100644 --- a/src/services/session-state.ts +++ b/src/services/session-state.ts @@ -12,7 +12,7 @@ * This allows recall() to always assign variants even without explicit parameters. */ -import type { SurfacedScar, ScarConfirmation, Observation, SessionChild, ThreadObject } from "../types/index.js"; +import type { SurfacedScar, ScarConfirmation, ScarReflection, Observation, SessionChild, ThreadObject } from "../types/index.js"; interface SessionContext { sessionId: string; @@ -22,6 +22,7 @@ interface SessionContext { startedAt: Date; surfacedScars: SurfacedScar[]; // Track all scars surfaced during session confirmations: ScarConfirmation[]; // Refute-or-obey confirmations for recall-surfaced scars + reflections: ScarReflection[]; // End-of-session scar reflections (OBEYED/REFUTED) observations: Observation[]; // v2 Phase 2: Sub-agent/teammate observations children: SessionChild[]; // v2 Phase 2: Child agent records threads: ThreadObject[]; // : Working thread state @@ -34,11 +35,12 @@ let currentSession: SessionContext | null = null; * Set the current active session * Called by session_start */ -export function setCurrentSession(context: Omit & { surfacedScars?: SurfacedScar[]; observations?: Observation[]; children?: SessionChild[]; threads?: ThreadObject[] }): void { +export function setCurrentSession(context: Omit & { surfacedScars?: SurfacedScar[]; observations?: Observation[]; children?: SessionChild[]; threads?: ThreadObject[] }): void { currentSession = { ...context, surfacedScars: context.surfacedScars || [], confirmations: [], + reflections: [], observations: context.observations || [], children: context.children || [], threads: context.threads || [], @@ -137,6 +139,36 @@ export function getConfirmations(): ScarConfirmation[] { return currentSession?.confirmations || []; } +/** + * Add end-of-session scar reflections (OBEYED/REFUTED) to the current session. + * Called by reflect_scars tool after validation. + */ +export function addReflections(reflections: ScarReflection[]): void { + if (!currentSession) { + console.warn("[session-state] Cannot add reflections: no active session"); + return; + } + + for (const ref of reflections) { + // Replace existing reflection for same scar_id (allow re-reflection) + const idx = currentSession.reflections.findIndex(r => r.scar_id === ref.scar_id); + if (idx >= 0) { + currentSession.reflections[idx] = ref; + } else { + currentSession.reflections.push(ref); + } + } + + console.error(`[session-state] Reflections tracked: ${currentSession.reflections.length} total`); +} + +/** + * Get all end-of-session scar reflections for the current session. + */ +export function getReflections(): ScarReflection[] { + return currentSession?.reflections || []; +} + /** * Check if there are recall-surfaced scars that haven't been confirmed. * Only checks scars with source "recall" — session_start scars don't require confirmation. diff --git a/src/tools/definitions.ts b/src/tools/definitions.ts index b48ae7e..3314712 100644 --- a/src/tools/definitions.ts +++ b/src/tools/definitions.ts @@ -87,6 +87,39 @@ export const TOOLS = [ required: ["confirmations"], }, }, + { + name: "reflect_scars", + description: "End-of-session scar reflection — the closing counterpart to confirm_scars. Mirrors CODA-1's [Scar Reflection] protocol. Call BEFORE session_close to provide evidence of how each surfaced scar was handled. OBEYED: concrete evidence of compliance (min 15 chars). REFUTED: why it didn't apply + what was done instead (min 30 chars). Session close uses reflections to set execution_successful accurately.", + inputSchema: { + type: "object" as const, + properties: { + reflections: { + type: "array", + items: { + type: "object", + properties: { + scar_id: { + type: "string", + description: "UUID of the surfaced scar (from recall or session_start)", + }, + outcome: { + type: "string", + enum: ["OBEYED", "REFUTED"], + description: "OBEYED: followed the scar with evidence. REFUTED: scar didn't apply, explain why.", + }, + evidence: { + type: "string", + description: "Concrete evidence of compliance (OBEYED, min 15 chars) or explanation of why scar didn't apply (REFUTED, min 30 chars).", + }, + }, + required: ["scar_id", "outcome", "evidence"], + }, + description: "One reflection per surfaced scar.", + }, + }, + required: ["reflections"], + }, + }, { name: "session_start", description: "Initialize session, detect agent, load institutional context (last session, recent decisions, open threads). Scars surface on-demand via recall(). DISPLAY: The result includes a pre-formatted 'display' field visible in the tool result. Output the display field verbatim as your response — tool results are collapsed in the CLI.", @@ -812,6 +845,29 @@ export const TOOLS = [ required: ["confirmations"], }, }, + { + name: "gitmem-rf", + description: "gitmem-rf (reflect_scars) - End-of-session scar reflection (OBEYED/REFUTED with evidence)", + inputSchema: { + type: "object" as const, + properties: { + reflections: { + type: "array", + items: { + type: "object", + properties: { + scar_id: { type: "string", description: "UUID of the surfaced scar" }, + outcome: { type: "string", enum: ["OBEYED", "REFUTED"], description: "Reflection outcome" }, + evidence: { type: "string", description: "Evidence (OBEYED min 15 chars, REFUTED min 30 chars)" }, + }, + required: ["scar_id", "outcome", "evidence"], + }, + description: "One reflection per surfaced scar", + }, + }, + required: ["reflections"], + }, + }, { name: "gitmem-ss", description: "gitmem-ss (session_start) - Initialize session with institutional context. DISPLAY: The result includes a pre-formatted 'display' field. Output the display field verbatim as your response — tool results are collapsed in the CLI.", @@ -1510,6 +1566,29 @@ export const TOOLS = [ required: ["confirmations"], }, }, + { + name: "gm-reflect", + description: "gm-reflect (reflect_scars) - End-of-session scar reflection", + inputSchema: { + type: "object" as const, + properties: { + reflections: { + type: "array", + items: { + type: "object", + properties: { + scar_id: { type: "string", description: "UUID of the surfaced scar" }, + outcome: { type: "string", enum: ["OBEYED", "REFUTED"] }, + evidence: { type: "string", description: "Evidence of compliance or refutation" }, + }, + required: ["scar_id", "outcome", "evidence"], + }, + description: "One reflection per surfaced scar", + }, + }, + required: ["reflections"], + }, + }, { name: "gm-refresh", description: "gm-refresh (session_refresh) - Refresh context for the active session without creating a new one. DISPLAY: The result includes a pre-formatted 'display' field. Output the display field verbatim as your response — tool results are collapsed in the CLI.", @@ -2196,7 +2275,7 @@ export const TOOLS = [ */ export const ALIAS_TOOL_NAMES = new Set([ // gitmem-* aliases - "gitmem-r", "gitmem-cs", "gitmem-ss", "gitmem-sr", "gitmem-sc", + "gitmem-r", "gitmem-cs", "gitmem-rf", "gitmem-ss", "gitmem-sr", "gitmem-sc", "gitmem-cl", "gitmem-cd", "gitmem-rs", "gitmem-rsb", "gitmem-st", "gitmem-gt", "gitmem-stx", "gitmem-search", "gitmem-log", "gitmem-analyze", @@ -2204,7 +2283,7 @@ export const ALIAS_TOOL_NAMES = new Set([ "gitmem-lt", "gitmem-rt", "gitmem-ct", "gitmem-ps", "gitmem-ds", "gitmem-cleanup", "gitmem-health", "gitmem-al", "gitmem-graph", // gm-* aliases - "gm-open", "gm-confirm", "gm-refresh", "gm-close", + "gm-open", "gm-confirm", "gm-reflect", "gm-refresh", "gm-close", "gm-scar", "gm-search", "gm-log", "gm-analyze", "gm-pc", "gm-absorb", "gm-threads", "gm-resolve", "gm-thread-new", "gm-promote", "gm-dismiss", diff --git a/src/tools/reflect-scars.ts b/src/tools/reflect-scars.ts new file mode 100644 index 0000000..11c7634 --- /dev/null +++ b/src/tools/reflect-scars.ts @@ -0,0 +1,265 @@ +/** + * reflect_scars Tool + * + * End-of-session scar reflection — the closing counterpart to confirm_scars. + * Mirrors CODA-1's [Scar Reflection] protocol for CLI/DAC/Brain agents. + * + * Flow: + * recall → confirm_scars (START: "I will...") → work + * → reflect_scars (END: "I did...") → session_close uses reflections + * + * Each scar surfaced during the session should be reflected upon with: + * OBEYED — Followed the scar, with concrete evidence (min 15 chars) + * REFUTED — Scar didn't apply or was overridden (min 30 chars) + */ + +import { + getCurrentSession, + getSurfacedScars, + addReflections, + getReflections, +} from "../services/session-state.js"; +import { Timer, buildPerformanceData } from "../services/metrics.js"; +import { getSessionPath } from "../services/gitmem-dir.js"; +import { wrapDisplay, STATUS, ANSI } from "../services/display-protocol.js"; +import type { + ReflectScarsParams, + ReflectScarsResult, + ScarReflection, + ReflectionOutcome, + SurfacedScar, + PerformanceData, +} from "../types/index.js"; +import * as fs from "fs"; + +// Minimum evidence lengths (matching CODA-1's refute-or-obey.test.js) +const MIN_OBEYED_LENGTH = 15; +const MIN_REFUTED_LENGTH = 30; + +/** + * Validate a single reflection. + * Returns null if valid, or an error string if invalid. + */ +function validateReflection( + reflection: { scar_id: string; outcome: ReflectionOutcome; evidence: string }, + scar: SurfacedScar, +): string | null { + const { outcome, evidence } = reflection; + + if (!evidence || evidence.trim().length === 0) { + return `${scar.scar_title}: Evidence is required for ${outcome}.`; + } + + switch (outcome) { + case "OBEYED": + if (evidence.trim().length < MIN_OBEYED_LENGTH) { + return `${scar.scar_title}: OBEYED evidence too short (${evidence.trim().length} chars, minimum ${MIN_OBEYED_LENGTH}). Provide concrete evidence of compliance.`; + } + break; + + case "REFUTED": + if (evidence.trim().length < MIN_REFUTED_LENGTH) { + return `${scar.scar_title}: REFUTED evidence too short (${evidence.trim().length} chars, minimum ${MIN_REFUTED_LENGTH}). Explain why the scar didn't apply and what was done instead.`; + } + break; + + default: + return `${scar.scar_title}: Invalid outcome "${outcome}". Must be OBEYED or REFUTED.`; + } + + return null; +} + +/** + * Format the reflection result as markdown. + */ +function formatResponse( + valid: boolean, + reflections: ScarReflection[], + errors: string[], + missingScars: string[], +): string { + const lines: string[] = []; + + if (valid) { + lines.push(`${STATUS.ok} SCAR REFLECTIONS ACCEPTED`); + lines.push(""); + for (const ref of reflections) { + const indicator = ref.outcome === "OBEYED" ? STATUS.pass : `${ANSI.yellow}!${ANSI.reset}`; + lines.push(`${indicator} **${ref.scar_title}** → ${ref.outcome}`); + } + lines.push(""); + lines.push("All surfaced scars reflected upon. Session close will use these for execution_successful."); + } else { + lines.push(`${STATUS.rejected} SCAR REFLECTIONS REJECTED`); + lines.push(""); + + if (errors.length > 0) { + lines.push("**Validation errors:**"); + for (const err of errors) { + lines.push(`- ${err}`); + } + lines.push(""); + } + + if (missingScars.length > 0) { + lines.push("**Unreflected scars (must reflect on all surfaced scars):**"); + for (const title of missingScars) { + lines.push(`- ${title}`); + } + lines.push(""); + } + + lines.push("Fix the errors above and call reflect_scars again."); + } + + return lines.join("\n"); +} + +/** + * Persist reflections to the per-session file. + */ +function persistReflectionsToFile(reflections: ScarReflection[]): void { + try { + const session = getCurrentSession(); + if (!session) return; + + const sessionFilePath = getSessionPath(session.sessionId, "session.json"); + if (!fs.existsSync(sessionFilePath)) return; + + const data = JSON.parse(fs.readFileSync(sessionFilePath, "utf8")); + data.reflections = reflections; + fs.writeFileSync(sessionFilePath, JSON.stringify(data, null, 2)); + console.error(`[reflect_scars] Reflections persisted to ${sessionFilePath}`); + } catch (error) { + console.warn("[reflect_scars] Failed to persist reflections to file:", error); + } +} + +/** + * Main tool implementation: reflect_scars + */ +export async function reflectScars(params: ReflectScarsParams): Promise { + const timer = new Timer(); + + // Validate session exists + const session = getCurrentSession(); + if (!session) { + const performance = buildPerformanceData("reflect_scars", timer.elapsed(), 0); + const noSessionMsg = `${STATUS.rejected} No active session. Call session_start before reflect_scars.`; + return { + valid: false, + errors: ["No active session. Call session_start first."], + reflections: [], + missing_scars: [], + formatted_response: noSessionMsg, + display: wrapDisplay(noSessionMsg), + performance, + }; + } + + // Get ALL surfaced scars (both session_start and recall — reflections cover the whole session) + const allSurfacedScars = getSurfacedScars(); + + if (allSurfacedScars.length === 0) { + const performance = buildPerformanceData("reflect_scars", timer.elapsed(), 0); + const noScarsMsg = `${STATUS.ok} No surfaced scars to reflect upon. Proceed to session close.`; + return { + valid: true, + errors: [], + reflections: [], + missing_scars: [], + formatted_response: noScarsMsg, + display: wrapDisplay(noScarsMsg), + performance, + }; + } + + // Build scar lookup by ID + const scarById = new Map(); + for (const scar of allSurfacedScars) { + scarById.set(scar.scar_id, scar); + } + + // Validate each reflection + const errors: string[] = []; + const validReflections: ScarReflection[] = []; + const reflectedIds = new Set(); + + if (!params.reflections || params.reflections.length === 0) { + errors.push("No reflections provided. Each surfaced scar should be reflected upon."); + } else { + for (const ref of params.reflections) { + // Check scar exists (try exact match first) + let scar = scarById.get(ref.scar_id); + + // 8-char prefix match (same as confirm_scars) + if (!scar && /^[0-9a-f]{8}$/i.test(ref.scar_id)) { + let matchedId: string | null = null; + for (const [fullId, scarData] of scarById.entries()) { + if (fullId.startsWith(ref.scar_id)) { + if (matchedId) { + errors.push(`Ambiguous scar_id prefix "${ref.scar_id}" matches multiple scars. Use full UUID.`); + scar = undefined; + break; + } + matchedId = fullId; + scar = scarData; + } + } + } + + if (!scar) { + errors.push(`Unknown scar_id "${ref.scar_id}". Only reflect on scars surfaced during this session.`); + continue; + } + + // Validate the reflection + const error = validateReflection(ref, scar); + if (error) { + errors.push(error); + } else { + validReflections.push({ + scar_id: scar.scar_id, + scar_title: scar.scar_title, + outcome: ref.outcome, + evidence: ref.evidence.trim(), + reflected_at: new Date().toISOString(), + }); + reflectedIds.add(scar.scar_id); + } + } + } + + // Check for unreflected scars — advisory, not blocking + // (Unlike confirm_scars which requires all recall scars, reflect_scars + // is softer — missing scars are noted but don't invalidate the call) + const previouslyReflectedIds = new Set(getReflections().map(r => r.scar_id)); + const missingScars: string[] = []; + for (const scar of allSurfacedScars) { + if (!reflectedIds.has(scar.scar_id) && !previouslyReflectedIds.has(scar.scar_id)) { + missingScars.push(scar.scar_title); + } + } + + const valid = errors.length === 0; + + // If valid, persist to session state and file + if (valid && validReflections.length > 0) { + addReflections(validReflections); + persistReflectionsToFile([...getReflections()]); + } + + const performance = buildPerformanceData("reflect_scars", timer.elapsed(), validReflections.length); + const formatted_response = formatResponse(valid, validReflections, errors, missingScars); + + return { + valid, + errors, + reflections: validReflections, + missing_scars: missingScars, + formatted_response, + display: wrapDisplay(formatted_response), + performance, + }; +} diff --git a/src/tools/session-close.ts b/src/tools/session-close.ts index ea197f5..256f506 100644 --- a/src/tools/session-close.ts +++ b/src/tools/session-close.ts @@ -13,7 +13,7 @@ import * as supabase from "../services/supabase-client.js"; import { embed, isEmbeddingAvailable } from "../services/embedding.js"; import { hasSupabase, getTableName } from "../services/tier.js"; import { getStorage } from "../services/storage.js"; -import { clearCurrentSession, getSurfacedScars, getConfirmations, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js"; +import { clearCurrentSession, getSurfacedScars, getConfirmations, getReflections, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js"; import { normalizeThreads, mergeThreadStates, migrateStringThread, saveThreadsFile } from "../services/thread-manager.js"; // import { deduplicateThreadList } from "../services/thread-dedup.js"; import { syncThreadsToSupabase, loadOpenThreadEmbeddings } from "../services/thread-supabase.js"; @@ -681,25 +681,51 @@ function bridgeScarsToUsageRecords( const autoBridgedScars: ScarUsageEntry[] = []; const matchedScarIds = new Set(); - // Load structured confirmations from confirm_scars (preferred source) + // Load structured confirmations from confirm_scars (start-of-task) const confirmations: ScarConfirmation[] = getConfirmations(); const confirmationMap = new Map(); for (const conf of confirmations) { confirmationMap.set(conf.scar_id, conf); } + // Load end-of-session reflections from reflect_scars (end-of-task) + // Reflections provide the most accurate execution_successful signal + const reflections = getReflections(); + const reflectionMap = new Map(); + for (const ref of reflections) { + reflectionMap.set(ref.scar_id, { outcome: ref.outcome, evidence: ref.evidence }); + } + // First pass: match surfaced scars against structured confirmations for (const scar of surfacedScars) { const confirmation = confirmationMap.get(scar.scar_id); if (confirmation) { matchedScarIds.add(scar.scar_id); + + // Prefer reflect_scars outcome over confirmation default + // reflect_scars gives actual end-of-session evidence; confirm_scars is intent + const reflection = reflectionMap.get(scar.scar_id); + let executionSuccessful: boolean | undefined; + let context: string; + + if (reflection) { + // Reflection provides definitive signal + executionSuccessful = reflection.outcome === "OBEYED" ? true : false; + context = `Confirmed: ${confirmation.decision} → Reflected: ${reflection.outcome} — ${reflection.evidence.slice(0, 80)}`; + } else { + // Fall back to confirmation-based default (Option A) + executionSuccessful = confirmation.decision === "APPLYING" ? true : undefined; + context = `Confirmed via confirm_scars: ${confirmation.decision} — ${confirmation.evidence.slice(0, 100)}`; + } + autoBridgedScars.push({ scar_identifier: scar.scar_id, session_id: sessionId, agent: agentIdentity, surfaced_at: scar.surfaced_at, reference_type: decisionToRefType(confirmation.decision), - reference_context: `Confirmed via confirm_scars: ${confirmation.decision} — ${confirmation.evidence.slice(0, 100)}`, + reference_context: context, + execution_successful: executionSuccessful, variant_id: scar.variant_id, }); } @@ -733,6 +759,7 @@ function bridgeScarsToUsageRecords( } // For surfaced scars NOT matched by either method, record as "none" + // execution_successful = false — scar was surfaced but ignored entirely for (const scar of surfacedScars) { if (!matchedScarIds.has(scar.scar_id)) { autoBridgedScars.push({ @@ -742,6 +769,7 @@ function bridgeScarsToUsageRecords( surfaced_at: scar.surfaced_at, reference_type: "none", reference_context: `Surfaced during ${scar.source} but not mentioned in closing reflection`, + execution_successful: false, variant_id: scar.variant_id, }); } diff --git a/src/types/index.ts b/src/types/index.ts index 506be2e..6bd49ec 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -399,6 +399,35 @@ export interface ConfirmScarsResult { performance: PerformanceData; } +// End-of-session scar reflection (mirrors CODA-1's [Scar Reflection]) +export type ReflectionOutcome = "OBEYED" | "REFUTED"; + +export interface ScarReflection { + scar_id: string; + scar_title: string; + outcome: ReflectionOutcome; + evidence: string; + reflected_at: string; // ISO timestamp +} + +export interface ReflectScarsParams { + reflections: Array<{ + scar_id: string; + outcome: ReflectionOutcome; + evidence: string; + }>; +} + +export interface ReflectScarsResult { + valid: boolean; + errors: string[]; + reflections: ScarReflection[]; + missing_scars: string[]; // scar titles not reflected + formatted_response: string; + display?: string; + performance: PerformanceData; +} + // Scar usage tracking export type ReferenceType = "explicit" | "implicit" | "acknowledged" | "refuted" | "none"; From 4dc4f396afb4e00cd1aff0f581a622aa4218d64e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Feb 2026 15:13:10 -0500 Subject: [PATCH 2/2] feat: add knowledge-retrieval detection to auto-retrieve hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes institutional-memory queries ("what's our process for X", "recall how we handle Y") through GitMem search instead of passive scar injection. Fixes UserPromptSubmit missing from init wizard's buildClaudeHooks(). Updates stale EXPECTED_TOOL_COUNTS (free 21→22, pro 26→27, dev 30→31). Co-Authored-By: Claude Opus 4.6 --- bin/init-wizard.js | 11 ++++++++++ hooks/scripts/auto-retrieve-hook.sh | 31 +++++++++++++++++++++++++++++ tests/e2e/mcp-client.ts | 8 ++++---- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/bin/init-wizard.js b/bin/init-wizard.js index f75971c..81a5e44 100644 --- a/bin/init-wizard.js +++ b/bin/init-wizard.js @@ -253,6 +253,17 @@ function buildClaudeHooks() { ], }, ], + UserPromptSubmit: [ + { + hooks: [ + { + type: "command", + command: `bash ${relScripts}/auto-retrieve-hook.sh`, + timeout: 3000, + }, + ], + }, + ], PreToolUse: [ { matcher: "Bash", diff --git a/hooks/scripts/auto-retrieve-hook.sh b/hooks/scripts/auto-retrieve-hook.sh index ff57b9c..7bdfabb 100755 --- a/hooks/scripts/auto-retrieve-hook.sh +++ b/hooks/scripts/auto-retrieve-hook.sh @@ -65,6 +65,37 @@ PROMPT_LEN=${#PROMPT} RETRIEVAL_LEVEL="" +# Priority 0: Knowledge-retrieval queries — route through gitmem, don't inject scars +# These are queries where the user wants to ACCESS institutional memory, not just +# have scars passively injected. Output a routing instruction instead of scars. +IS_KNOWLEDGE_QUERY=false + +# Explicit recall/remember commands +if echo "$PROMPT_LOWER" | grep -qE '\b(recall|remember)\b.*(doc|process|tree|structure|how|what|where|our)'; then + IS_KNOWLEDGE_QUERY=true +# Process/documentation queries ("what's our process for X", "how do we usually Y") +elif echo "$PROMPT_LOWER" | grep -qE "(what('s| is) our (process|approach|pattern|method)|how do we (usually|typically|normally)|remind me (how|what|about)|what('s| is) the (process|protocol|procedure) for)"; then + IS_KNOWLEDGE_QUERY=true +# Documentation discovery ("show me the docs", "where are the scars") +elif echo "$PROMPT_LOWER" | grep -qE '\b(show me|find|where (is|are|do)).*(doc(s|umentation)?|process(es)?|pattern(s)?|decision(s)?|scar(s)?|learning(s)?)\b'; then + IS_KNOWLEDGE_QUERY=true +# Institutional knowledge queries ("what did we decide about", "what scars exist") +elif echo "$PROMPT_LOWER" | grep -qE '\b(what did we (decide|learn|document)|what scars|what learnings|past decisions about|institutional (memory|knowledge))\b'; then + IS_KNOWLEDGE_QUERY=true +# Direct "recall" as a verb/command at start of prompt +elif echo "$PROMPT_LOWER" | grep -qE '^\s*recall\b'; then + IS_KNOWLEDGE_QUERY=true +fi + +if [ "$IS_KNOWLEDGE_QUERY" = "true" ]; then + cat <