gitmem-mcp 1.1.3 โ†’ 1.2.1

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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Nudge Variant Support
3
+ *
4
+ * Reads GITMEM_NUDGE_VARIANT env var to select alternative header
5
+ * framings for recall/prepare-context output. Used by nudge-bench
6
+ * to A/B test how different wording affects agent compliance.
7
+ *
8
+ * When no env var is set, returns the production default.
9
+ *
10
+ * Variant IDs match nudge-bench/variants/n001-header.ts
11
+ */
12
+ const VARIANTS = {
13
+ // Production default
14
+ "n001-a-institutional": {
15
+ icon: "๐Ÿง ",
16
+ text: (n) => `INSTITUTIONAL MEMORY ACTIVATED\n\nFound ${n} relevant scar${n === 1 ? "" : "s"} for your plan:`,
17
+ },
18
+ // Informational/passive
19
+ "n001-b-recalled": {
20
+ icon: "\x1b[38;5;37mโฌข\x1b[0m",
21
+ text: (n) => `gitmem โ”€โ”€ ${n} learnings recalled`,
22
+ },
23
+ // Obligation
24
+ "n001-c-review": {
25
+ icon: "\x1b[38;5;37mโฌข\x1b[0m",
26
+ text: (n) => `gitmem โ”€โ”€ ${n} scar${n === 1 ? "" : "s"} to review`,
27
+ },
28
+ // Directive
29
+ "n001-d-directive": {
30
+ icon: "\x1b[38;5;37mโฌข\x1b[0m",
31
+ text: (n) => `gitmem โ”€โ”€ review ${n} learning${n === 1 ? "" : "s"} before proceeding`,
32
+ },
33
+ // Procedural โ€” ties to confirm_scars
34
+ "n001-e-confirm": {
35
+ icon: "\x1b[38;5;37mโฌข\x1b[0m",
36
+ text: (n) => `gitmem โ”€โ”€ ${n} scar${n === 1 ? "" : "s"} found โ€” confirm before acting`,
37
+ },
38
+ // Reasoning โ€” explains WHY (Karpathy: reasoning > command)
39
+ "n001-f-reasoning": {
40
+ icon: "\x1b[38;5;37mโฌข\x1b[0m",
41
+ text: (n) => `gitmem โ”€โ”€ ${n} past mistake${n === 1 ? "" : "s"} detected that may repeat here โ€” review to avoid the same outcome`,
42
+ },
43
+ };
44
+ const DEFAULT_VARIANT = "n001-c-review";
45
+ /**
46
+ * Get the active nudge header based on GITMEM_NUDGE_VARIANT env var.
47
+ * Falls back to production default if unset or invalid.
48
+ */
49
+ export function getNudgeHeader() {
50
+ const variantId = process.env.GITMEM_NUDGE_VARIANT;
51
+ if (!variantId)
52
+ return VARIANTS[DEFAULT_VARIANT];
53
+ return VARIANTS[variantId] || VARIANTS[DEFAULT_VARIANT];
54
+ }
55
+ /**
56
+ * Format the recall header line using the active nudge variant.
57
+ */
58
+ export function formatNudgeHeader(scarCount) {
59
+ const header = getNudgeHeader();
60
+ return `${header.icon} ${header.text(scarCount)}`;
61
+ }
62
+ /**
63
+ * Get the active variant ID (for logging/metrics).
64
+ */
65
+ export function getActiveVariantId() {
66
+ const variantId = process.env.GITMEM_NUDGE_VARIANT;
67
+ if (variantId && VARIANTS[variantId])
68
+ return variantId;
69
+ return DEFAULT_VARIANT;
70
+ }
71
+ //# sourceMappingURL=nudge-variants.js.map
@@ -14,7 +14,7 @@ import { computeLifecycleStatus } from "../services/thread-vitality.js";
14
14
  import { archiveDormantThreads } from "../services/thread-supabase.js";
15
15
  import { loadThreadsFile } from "../services/thread-manager.js";
16
16
  import { Timer, recordMetrics, buildPerformanceData, } from "../services/metrics.js";
17
- import { wrapDisplay, truncate } from "../services/display-protocol.js";
17
+ import { wrapDisplay, truncate, productLine, boldText } from "../services/display-protocol.js";
18
18
  // --- Display Formatting ---
19
19
  function formatDaysAgo(days) {
20
20
  if (days < 1)
@@ -28,7 +28,7 @@ function formatDaysAgo(days) {
28
28
  }
29
29
  function buildCleanupDisplay(summary, groups, archivedCount) {
30
30
  const lines = [];
31
- lines.push(`gitmem cleanup ยท ${summary.total_open} open ยท ${summary.active} active ยท ${summary.cooling} cooling ยท ${summary.dormant} dormant`);
31
+ lines.push(productLine("cleanup", `${summary.total_open} open ยท ${summary.active} active ยท ${summary.cooling} cooling ยท ${summary.dormant} dormant`));
32
32
  lines.push("");
33
33
  const totalItems = summary.total_open;
34
34
  if (totalItems === 0 && archivedCount === 0) {
@@ -44,7 +44,7 @@ function buildCleanupDisplay(summary, groups, archivedCount) {
44
44
  for (const [label, items] of sections) {
45
45
  if (items.length === 0)
46
46
  continue;
47
- lines.push(`**${label}** (${items.length}):`);
47
+ lines.push(`${boldText(label)} (${items.length}):`);
48
48
  lines.push("");
49
49
  lines.push("| ID | Thread | Last Touch |");
50
50
  lines.push("|----|--------|------------|");
@@ -18,7 +18,7 @@
18
18
  import { getCurrentSession, getSurfacedScars, addConfirmations, getConfirmations, } from "../services/session-state.js";
19
19
  import { Timer, buildPerformanceData } from "../services/metrics.js";
20
20
  import { getSessionPath } from "../services/gitmem-dir.js";
21
- import { wrapDisplay } from "../services/display-protocol.js";
21
+ import { wrapDisplay, STATUS, ANSI } from "../services/display-protocol.js";
22
22
  import * as fs from "fs";
23
23
  // Minimum evidence length per decision type
24
24
  const MIN_EVIDENCE_LENGTH = 50;
@@ -65,17 +65,17 @@ function validateConfirmation(confirmation, scar) {
65
65
  function formatResponse(valid, confirmations, errors, missingScars) {
66
66
  const lines = [];
67
67
  if (valid) {
68
- lines.push("โœ… SCAR CONFIRMATIONS ACCEPTED");
68
+ lines.push(`${STATUS.ok} SCAR CONFIRMATIONS ACCEPTED`);
69
69
  lines.push("");
70
70
  for (const conf of confirmations) {
71
- const emoji = conf.decision === "APPLYING" ? "๐ŸŸข" : conf.decision === "N_A" ? "โšช" : "๐ŸŸ ";
72
- lines.push(`${emoji} **${conf.scar_title}** โ†’ ${conf.decision}`);
71
+ const indicator = conf.decision === "APPLYING" ? STATUS.pass : conf.decision === "N_A" ? `${ANSI.dim}-${ANSI.reset}` : `${ANSI.yellow}!${ANSI.reset}`;
72
+ lines.push(`${indicator} **${conf.scar_title}** โ†’ ${conf.decision}`);
73
73
  }
74
74
  lines.push("");
75
75
  lines.push("All recalled scars addressed. Consequential actions are now unblocked.");
76
76
  }
77
77
  else {
78
- lines.push("โ›” SCAR CONFIRMATIONS REJECTED");
78
+ lines.push(`${STATUS.rejected} SCAR CONFIRMATIONS REJECTED`);
79
79
  lines.push("");
80
80
  if (errors.length > 0) {
81
81
  lines.push("**Validation errors:**");
@@ -124,7 +124,7 @@ export async function confirmScars(params) {
124
124
  const session = getCurrentSession();
125
125
  if (!session) {
126
126
  const performance = buildPerformanceData("confirm_scars", timer.elapsed(), 0);
127
- const noSessionMsg = "โ›” No active session. Call session_start before confirm_scars.";
127
+ const noSessionMsg = `${STATUS.rejected} No active session. Call session_start before confirm_scars.`;
128
128
  return {
129
129
  valid: false,
130
130
  errors: ["No active session. Call session_start first."],
@@ -140,7 +140,7 @@ export async function confirmScars(params) {
140
140
  const recallScars = allSurfacedScars.filter(s => s.source === "recall");
141
141
  if (recallScars.length === 0) {
142
142
  const performance = buildPerformanceData("confirm_scars", timer.elapsed(), 0);
143
- const noScarsMsg = "โœ… No recall-surfaced scars to confirm. Proceed freely.";
143
+ const noScarsMsg = `${STATUS.ok} No recall-surfaced scars to confirm. Proceed freely.`;
144
144
  return {
145
145
  valid: true,
146
146
  errors: [],
@@ -165,8 +165,25 @@ export async function confirmScars(params) {
165
165
  }
166
166
  else {
167
167
  for (const conf of params.confirmations) {
168
- // Check scar exists in recalled set
169
- const scar = scarById.get(conf.scar_id);
168
+ // Check scar exists in recalled set (try exact match first)
169
+ let scar = scarById.get(conf.scar_id);
170
+ // If not found and looks like 8-char prefix, try prefix match
171
+ // This allows agents to copy IDs from recall display (which shows truncated IDs)
172
+ if (!scar && /^[0-9a-f]{8}$/i.test(conf.scar_id)) {
173
+ let matchedId = null;
174
+ for (const [fullId, scarData] of scarById.entries()) {
175
+ if (fullId.startsWith(conf.scar_id)) {
176
+ if (matchedId) {
177
+ // Ambiguous prefix - multiple matches
178
+ errors.push(`Ambiguous scar_id prefix "${conf.scar_id}" matches multiple scars. Use full UUID.`);
179
+ scar = undefined;
180
+ break;
181
+ }
182
+ matchedId = fullId;
183
+ scar = scarData;
184
+ }
185
+ }
186
+ }
170
187
  if (!scar) {
171
188
  errors.push(`Unknown scar_id "${conf.scar_id}". Only confirm scars returned by recall().`);
172
189
  continue;
@@ -181,14 +198,14 @@ export async function confirmScars(params) {
181
198
  const relevance = conf.relevance ??
182
199
  (conf.decision === "APPLYING" ? "high" : conf.decision === "N_A" ? "low" : "low");
183
200
  validConfirmations.push({
184
- scar_id: conf.scar_id,
201
+ scar_id: scar.scar_id, // Use full UUID from matched scar, not potentially truncated input
185
202
  scar_title: scar.scar_title,
186
203
  decision: conf.decision,
187
204
  evidence: conf.evidence.trim(),
188
205
  confirmed_at: new Date().toISOString(),
189
206
  relevance,
190
207
  });
191
- confirmedIds.add(conf.scar_id);
208
+ confirmedIds.add(scar.scar_id); // Track by full UUID
192
209
  }
193
210
  }
194
211
  }
@@ -131,7 +131,7 @@ export const TOOLS = [
131
131
  },
132
132
  {
133
133
  name: "session_close",
134
- description: "Persist session with compliance validation. IMPORTANT: Before calling this tool, write all heavy payload data (closing_reflection, task_completion, human_corrections, scars_to_record, open_threads, decisions, learnings_created) to {gitmem_dir}/closing-payload.json using your file write tool โ€” the gitmem_dir path is returned by session_start (also shown in session start display as 'Payload path'). Then call this tool with ONLY session_id and close_type. The tool reads the payload file automatically and deletes it after processing. DISPLAY: The result includes a pre-formatted 'display' field. Output the display field verbatim as your response โ€” tool results are collapsed in the CLI.",
134
+ description: "Persist session with compliance validation. IMPORTANT: Before calling this tool, write all heavy payload data (closing_reflection, human_corrections, scars_to_record, open_threads, decisions, learnings_created) to {gitmem_dir}/closing-payload.json using your file write tool โ€” the gitmem_dir path is returned by session_start (also shown in session start display as 'Payload path'). Then call this tool with ONLY session_id and close_type. The tool reads the payload file automatically and deletes it after processing. task_completion is auto-generated from closing_reflection timestamps and human_corrections โ€” do NOT write it to the payload. DISPLAY: The result includes a pre-formatted 'display' field. Output the display field verbatim as your response โ€” tool results are collapsed in the CLI.",
135
135
  inputSchema: {
136
136
  type: "object",
137
137
  properties: {
@@ -595,7 +595,7 @@ export const TOOLS = [
595
595
  // --- Thread Lifecycle Tools () ---
596
596
  {
597
597
  name: "list_threads",
598
- description: "List open threads across recent sessions. Shows unresolved work items that carry over between sessions, with IDs for resolution. Use resolve_thread to mark threads as done.",
598
+ description: "List open threads across recent sessions. Shows unresolved work items that carry over between sessions. Use resolve_thread to mark threads as done.",
599
599
  inputSchema: {
600
600
  type: "object",
601
601
  properties: {
@@ -2,7 +2,7 @@
2
2
  * list_threads Tool
3
3
  *
4
4
  * List open threads across recent sessions. Shows unresolved work items
5
- * that carry over between sessions, with IDs for resolution.
5
+ * that carry over between sessions.
6
6
  *
7
7
  * Primary read from Supabase (source of truth).
8
8
  * Falls back to session-based aggregation (same as session_start),
@@ -2,7 +2,7 @@
2
2
  * list_threads Tool
3
3
  *
4
4
  * List open threads across recent sessions. Shows unresolved work items
5
- * that carry over between sessions, with IDs for resolution.
5
+ * that carry over between sessions.
6
6
  *
7
7
  * Primary read from Supabase (source of truth).
8
8
  * Falls back to session-based aggregation (same as session_start),
@@ -19,7 +19,7 @@ import { listThreadsFromSupabase } from "../services/thread-supabase.js";
19
19
  import * as supabase from "../services/supabase-client.js";
20
20
  import { Timer, recordMetrics, buildPerformanceData, } from "../services/metrics.js";
21
21
  import { formatThreadForDisplay } from "../services/timezone.js";
22
- import { wrapDisplay, truncate } from "../services/display-protocol.js";
22
+ import { wrapDisplay, truncate, productLine } from "../services/display-protocol.js";
23
23
  // --- Display Formatting ---
24
24
  /** Format date as short absolute string: "Feb 13" or "Jan 5" */
25
25
  function shortDate(date) {
@@ -31,7 +31,7 @@ function shortDate(date) {
31
31
  }
32
32
  function buildThreadsDisplay(threads, totalOpen, totalResolved) {
33
33
  const lines = [];
34
- lines.push(`gitmem threads ยท ${totalOpen} open ยท ${totalResolved} resolved`);
34
+ lines.push(productLine("threads", `${totalOpen} open ยท ${totalResolved} resolved`));
35
35
  lines.push("");
36
36
  if (threads.length === 0) {
37
37
  lines.push("No threads found.");
@@ -40,14 +40,13 @@ function buildThreadsDisplay(threads, totalOpen, totalResolved) {
40
40
  // Deterministic sort: oldest first by created_at
41
41
  const sorted = [...threads].sort((a, b) => a.created_at.localeCompare(b.created_at));
42
42
  // Markdown table โ€” renders cleanly in all MCP clients
43
- lines.push("| # | ID | Thread | Active |");
44
- lines.push("|---|-----|--------|--------|");
43
+ lines.push("| # | Thread | Active |");
44
+ lines.push("|---|--------|--------|");
45
45
  for (let i = 0; i < sorted.length; i++) {
46
46
  const t = sorted[i];
47
- const shortId = t.id;
48
47
  const text = truncate(t.text, 60);
49
48
  const date = shortDate(t.last_touched_at || t.created_at);
50
- lines.push(`| ${i + 1} | ${shortId} | ${text} | ${date} |`);
49
+ lines.push(`| ${i + 1} | ${text} | ${date} |`);
51
50
  }
52
51
  return wrapDisplay(lines.join("\n"));
53
52
  }
package/dist/tools/log.js CHANGED
@@ -16,11 +16,11 @@ import { getStorage } from "../services/storage.js";
16
16
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
17
17
  import { v4 as uuidv4 } from "uuid";
18
18
  import { formatTimestamp } from "../services/timezone.js";
19
- import { wrapDisplay, relativeTime, truncate, SEV, TYPE } from "../services/display-protocol.js";
19
+ import { wrapDisplay, relativeTime, truncate, SEV, TYPE, productLine } from "../services/display-protocol.js";
20
20
  // --- Display Formatting ---
21
21
  function buildLogDisplay(entries, total, filters) {
22
22
  const lines = [];
23
- lines.push(`gitmem log ยท ${total} most recent learnings ยท ${filters.project}`);
23
+ lines.push(productLine("log", `${total} most recent ยท ${filters.project}`));
24
24
  const fp = [];
25
25
  if (filters.learning_type)
26
26
  fp.push(`type=${filters.learning_type}`);
@@ -37,7 +37,7 @@ function buildLogDisplay(entries, total, filters) {
37
37
  }
38
38
  for (const e of entries) {
39
39
  const te = TYPE[e.learning_type] || "ยท";
40
- const se = e.learning_type === "decision" ? "\u{1F4CB}" : (SEV[e.severity] || "\u{26AA}");
40
+ const se = e.learning_type === "decision" ? "[d]" : (SEV[e.severity] || "[?]");
41
41
  const t = truncate(e.title, 50);
42
42
  const time = relativeTime(e.created_at);
43
43
  const issue = e.source_linear_issue ? ` ${e.source_linear_issue}` : "";
@@ -22,7 +22,8 @@ import { hasSupabase, getTableName } from "../services/tier.js";
22
22
  import { getStorage } from "../services/storage.js";
23
23
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
24
24
  import { v4 as uuidv4 } from "uuid";
25
- import { wrapDisplay } from "../services/display-protocol.js";
25
+ import { wrapDisplay, productLine } from "../services/display-protocol.js";
26
+ import { formatNudgeHeader } from "../services/nudge-variants.js";
26
27
  import { estimateTokens, formatCompact, formatGate, SEVERITY_EMOJI, } from "../hooks/format-utils.js";
27
28
  /**
28
29
  * Format scars in full mode.
@@ -36,17 +37,13 @@ function formatFull(scars, plan) {
36
37
  Proceed with caution โ€” this may be new territory without documented lessons.`;
37
38
  }
38
39
  const lines = [
39
- "๐Ÿง  INSTITUTIONAL MEMORY ACTIVATED",
40
- "",
41
- `Found ${scars.length} relevant scar${scars.length === 1 ? "" : "s"} for your plan:`,
40
+ formatNudgeHeader(scars.length),
42
41
  "",
43
42
  ];
44
43
  // Blocking verification requirements first
45
44
  const blockingScars = scars.filter((s) => s.required_verification?.blocking);
46
45
  if (blockingScars.length > 0) {
47
- lines.push("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
48
- lines.push("๐Ÿšจ **VERIFICATION REQUIRED BEFORE PROCEEDING**");
49
- lines.push("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
46
+ lines.push("[!!] VERIFICATION REQUIRED BEFORE PROCEEDING");
50
47
  lines.push("");
51
48
  for (const scar of blockingScars) {
52
49
  const rv = scar.required_verification;
@@ -63,11 +60,11 @@ Proceed with caution โ€” this may be new territory without documented lessons.`;
63
60
  lines.push(`**MUST SHOW:** ${rv.must_show}`);
64
61
  lines.push("");
65
62
  }
66
- lines.push("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
63
+ lines.push("---");
67
64
  lines.push("");
68
65
  }
69
66
  for (const scar of scars) {
70
- const emoji = SEVERITY_EMOJI[scar.severity] || "โšช";
67
+ const emoji = SEVERITY_EMOJI[scar.severity] || "[?]";
71
68
  lines.push(`${emoji} **${scar.title}** (${scar.severity}, score: ${(scar.similarity || 0).toFixed(2)})`);
72
69
  lines.push(scar.description);
73
70
  if (scar.counter_arguments && scar.counter_arguments.length > 0) {
@@ -167,7 +164,7 @@ function buildResult(scars, plan, format, maxTokens, timer, metricsId, project,
167
164
  blocking_scars,
168
165
  },
169
166
  }).catch(() => { });
170
- const display = wrapDisplay(`prepare_context \u00b7 ${format} \u00b7 ${scars_included} scars (${blocking_scars} blocking) \u00b7 ~${token_estimate} tokens\n\n${memory_payload}`);
167
+ const display = wrapDisplay(`${productLine("prepare_context", `${format} ยท ${scars_included} scars (${blocking_scars} blocking) ยท ~${token_estimate} tokens`)}\n\n${memory_payload}`);
171
168
  return {
172
169
  memory_payload,
173
170
  display,
@@ -24,7 +24,8 @@ import { getAgentIdentity } from "../services/agent-detection.js";
24
24
  import { v4 as uuidv4 } from "uuid";
25
25
  import * as fs from "fs";
26
26
  import { getSessionPath } from "../services/gitmem-dir.js";
27
- import { wrapDisplay } from "../services/display-protocol.js";
27
+ import { wrapDisplay, SEV, dimText, ANSI } from "../services/display-protocol.js";
28
+ import { formatNudgeHeader } from "../services/nudge-variants.js";
28
29
  import { fetchDismissalCounts } from "../services/behavioral-decay.js";
29
30
  /**
30
31
  * Format scars into a readable response for Claude
@@ -38,16 +39,12 @@ No past lessons match this plan closely enough. Scars accumulate as you work โ€”
38
39
  // Check if any scars have required_verification (blocking gates)
39
40
  const scarsWithVerification = scars.filter((s) => s.required_verification?.blocking);
40
41
  const lines = [
41
- "๐Ÿง  INSTITUTIONAL MEMORY ACTIVATED",
42
- "",
43
- `Found ${scars.length} relevant scar${scars.length === 1 ? "" : "s"} for your plan:`,
42
+ formatNudgeHeader(scars.length),
44
43
  "",
45
44
  ];
46
45
  // Display blocking verification requirements FIRST and prominently
47
46
  if (scarsWithVerification.length > 0) {
48
- lines.push("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
49
- lines.push("๐Ÿšจ **VERIFICATION REQUIRED BEFORE PROCEEDING**");
50
- lines.push("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
47
+ lines.push(`${ANSI.yellow}VERIFICATION REQUIRED${ANSI.reset}`);
51
48
  lines.push("");
52
49
  for (const scar of scarsWithVerification) {
53
50
  const rv = scar.required_verification;
@@ -63,20 +60,13 @@ No past lessons match this plan closely enough. Scars accumulate as you work โ€”
63
60
  lines.push("");
64
61
  lines.push(`**MUST SHOW:** ${rv.must_show}`);
65
62
  lines.push("");
66
- lines.push("โ›” DO NOT write SQL until verification output is shown above.");
63
+ lines.push(`${ANSI.red}Do not proceed until verification output is shown.${ANSI.reset}`);
67
64
  lines.push("");
68
65
  }
69
- lines.push("โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•");
70
- lines.push("");
71
66
  }
72
67
  for (const scar of scars) {
73
- const severityEmoji = {
74
- critical: "๐Ÿ”ด",
75
- high: "๐ŸŸ ",
76
- medium: "๐ŸŸก",
77
- low: "๐ŸŸข",
78
- }[scar.severity] || "โšช";
79
- lines.push(`${severityEmoji} **${scar.title}** (${scar.severity}, score: ${scar.similarity.toFixed(2)}) ยท id: ${scar.id}`);
68
+ const sev = SEV[scar.severity] || "[?]";
69
+ lines.push(`${sev} **${scar.title}** (${scar.severity}, ${scar.similarity.toFixed(2)}) ${dimText(`id:${scar.id.slice(0, 8)}`)}`);
80
70
  // Inline archival hint: scars with high dismiss rates get annotated
81
71
  if (dismissals) {
82
72
  const counts = dismissals.get(scar.id);
@@ -17,11 +17,11 @@ import { getProject } from "../services/session-state.js";
17
17
  import { getStorage } from "../services/storage.js";
18
18
  import { Timer, recordMetrics, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
19
19
  import { v4 as uuidv4 } from "uuid";
20
- import { wrapDisplay, truncate, SEV, TYPE } from "../services/display-protocol.js";
20
+ import { wrapDisplay, truncate, SEV, TYPE, productLine } from "../services/display-protocol.js";
21
21
  // --- Display Formatting ---
22
22
  function buildSearchDisplay(results, total_found, query, filters) {
23
23
  const lines = [];
24
- lines.push(`gitmem search ยท ${total_found} results ยท "${truncate(query, 60)}"`);
24
+ lines.push(productLine("search", `${total_found} results ยท "${truncate(query, 60)}"`));
25
25
  const fp = [];
26
26
  if (filters.severity)
27
27
  fp.push(`severity=${filters.severity}`);
@@ -36,7 +36,7 @@ function buildSearchDisplay(results, total_found, query, filters) {
36
36
  }
37
37
  for (const r of results) {
38
38
  const te = TYPE[r.learning_type] || "ยท";
39
- const se = SEV[r.severity] || "โšช";
39
+ const se = SEV[r.severity] || "[?]";
40
40
  const t = truncate(r.title, 50);
41
41
  const sim = `(${r.similarity.toFixed(2)})`;
42
42
  const issue = r.source_linear_issue ? ` ${r.source_linear_issue}` : "";