engrm 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1072,6 +1072,31 @@ function getOutboxStats(db) {
1072
1072
  return stats;
1073
1073
  }
1074
1074
 
1075
+ // src/intelligence/value-signals.ts
1076
+ var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
1077
+ function computeSessionValueSignals(observations, securityFindings = []) {
1078
+ const decisionsCount = observations.filter((o) => o.type === "decision").length;
1079
+ const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
1080
+ const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
1081
+ const featuresCount = observations.filter((o) => o.type === "feature").length;
1082
+ const refactorsCount = observations.filter((o) => o.type === "refactor").length;
1083
+ const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
1084
+ const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
1085
+ const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
1086
+ return {
1087
+ decisions_count: decisionsCount,
1088
+ lessons_count: lessonsCount,
1089
+ discoveries_count: discoveriesCount,
1090
+ features_count: featuresCount,
1091
+ refactors_count: refactorsCount,
1092
+ repeated_patterns_count: repeatedPatternsCount,
1093
+ security_findings_count: securityFindings.length,
1094
+ critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
1095
+ delivery_review_ready: hasRequestSignal && hasCompletionSignal,
1096
+ vibe_guardian_active: securityFindings.length > 0
1097
+ };
1098
+ }
1099
+
1075
1100
  // src/storage/migrations.ts
1076
1101
  var MIGRATIONS2 = [
1077
1102
  {
@@ -2104,6 +2129,80 @@ function findDuplicate(newTitle, candidates) {
2104
2129
  return bestMatch;
2105
2130
  }
2106
2131
 
2132
+ // src/capture/facts.ts
2133
+ var FACT_ELIGIBLE_TYPES = new Set([
2134
+ "bugfix",
2135
+ "decision",
2136
+ "discovery",
2137
+ "pattern",
2138
+ "feature",
2139
+ "refactor",
2140
+ "change"
2141
+ ]);
2142
+ function buildStructuredFacts(input) {
2143
+ const seedFacts = dedupeFacts(input.facts ?? []);
2144
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
2145
+ return seedFacts;
2146
+ }
2147
+ const derived = [...seedFacts];
2148
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
2149
+ derived.push(input.title.trim());
2150
+ }
2151
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
2152
+ derived.push(sentence);
2153
+ }
2154
+ const fileFact = buildFilesFact(input.filesModified);
2155
+ if (fileFact) {
2156
+ derived.push(fileFact);
2157
+ }
2158
+ return dedupeFacts(derived).slice(0, 4);
2159
+ }
2160
+ function extractNarrativeFacts(narrative) {
2161
+ if (!narrative)
2162
+ return [];
2163
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
2164
+ if (cleaned.length < 24)
2165
+ return [];
2166
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
2167
+ return parts.slice(0, 2);
2168
+ }
2169
+ function buildFilesFact(filesModified) {
2170
+ if (!filesModified || filesModified.length === 0)
2171
+ return null;
2172
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
2173
+ if (cleaned.length === 0)
2174
+ return null;
2175
+ if (cleaned.length === 1) {
2176
+ return `Touched ${cleaned[0]}`;
2177
+ }
2178
+ return `Touched ${cleaned.join(", ")}`;
2179
+ }
2180
+ function dedupeFacts(facts) {
2181
+ const seen = new Set;
2182
+ const result = [];
2183
+ for (const fact of facts) {
2184
+ const cleaned = fact.trim().replace(/\s+/g, " ");
2185
+ if (!looksMeaningful(cleaned))
2186
+ continue;
2187
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
2188
+ if (!key || seen.has(key))
2189
+ continue;
2190
+ seen.add(key);
2191
+ result.push(cleaned);
2192
+ }
2193
+ return result;
2194
+ }
2195
+ function looksMeaningful(value) {
2196
+ const cleaned = value.trim();
2197
+ if (cleaned.length < 12)
2198
+ return false;
2199
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
2200
+ return false;
2201
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
2202
+ return false;
2203
+ return true;
2204
+ }
2205
+
2107
2206
  // src/storage/projects.ts
2108
2207
  import { execSync } from "node:child_process";
2109
2208
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
@@ -2495,10 +2594,17 @@ async function saveObservation(db, config, input) {
2495
2594
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
2496
2595
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
2497
2596
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
2498
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
2499
2597
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
2500
2598
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
2501
2599
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
2600
+ const structuredFacts = buildStructuredFacts({
2601
+ type: input.type,
2602
+ title: input.title,
2603
+ narrative: input.narrative,
2604
+ facts: input.facts,
2605
+ filesModified
2606
+ });
2607
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
2502
2608
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
2503
2609
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
2504
2610
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
@@ -3195,6 +3301,27 @@ function handleStatus() {
3195
3301
  } catch {}
3196
3302
  const summaryCount = db.db.query("SELECT COUNT(*) as count FROM session_summaries").get()?.count ?? 0;
3197
3303
  console.log(` Sessions: ${summaryCount} summarised`);
3304
+ try {
3305
+ const activeObservations = db.db.query(`SELECT * FROM observations
3306
+ WHERE lifecycle IN ('active', 'aging', 'pinned') AND superseded_by IS NULL`).all();
3307
+ const securityFindings = db.db.query(`SELECT * FROM security_findings
3308
+ ORDER BY created_at_epoch DESC
3309
+ LIMIT 500`).all();
3310
+ const signals = computeSessionValueSignals(activeObservations, securityFindings);
3311
+ const signalParts = [
3312
+ `lessons: ${signals.lessons_count}`,
3313
+ `decisions: ${signals.decisions_count}`,
3314
+ `discoveries: ${signals.discoveries_count}`,
3315
+ `features: ${signals.features_count}`
3316
+ ];
3317
+ if (signals.repeated_patterns_count > 0) {
3318
+ signalParts.push(`patterns: ${signals.repeated_patterns_count}`);
3319
+ }
3320
+ console.log(` Value: ${signalParts.join(", ")}`);
3321
+ if (signals.security_findings_count > 0 || signals.delivery_review_ready) {
3322
+ console.log(` Review/Safety: ${signals.delivery_review_ready ? "delivery-ready" : "not ready"}, ` + `${signals.security_findings_count} finding${signals.security_findings_count === 1 ? "" : "s"}`);
3323
+ }
3324
+ } catch {}
3198
3325
  try {
3199
3326
  const lastSummary = db.db.query(`SELECT request, created_at_epoch FROM session_summaries
3200
3327
  ORDER BY created_at_epoch DESC LIMIT 1`).get();
@@ -250,6 +250,80 @@ function findDuplicate(newTitle, candidates) {
250
250
  return bestMatch;
251
251
  }
252
252
 
253
+ // src/capture/facts.ts
254
+ var FACT_ELIGIBLE_TYPES = new Set([
255
+ "bugfix",
256
+ "decision",
257
+ "discovery",
258
+ "pattern",
259
+ "feature",
260
+ "refactor",
261
+ "change"
262
+ ]);
263
+ function buildStructuredFacts(input) {
264
+ const seedFacts = dedupeFacts(input.facts ?? []);
265
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
266
+ return seedFacts;
267
+ }
268
+ const derived = [...seedFacts];
269
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
270
+ derived.push(input.title.trim());
271
+ }
272
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
273
+ derived.push(sentence);
274
+ }
275
+ const fileFact = buildFilesFact(input.filesModified);
276
+ if (fileFact) {
277
+ derived.push(fileFact);
278
+ }
279
+ return dedupeFacts(derived).slice(0, 4);
280
+ }
281
+ function extractNarrativeFacts(narrative) {
282
+ if (!narrative)
283
+ return [];
284
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
285
+ if (cleaned.length < 24)
286
+ return [];
287
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
288
+ return parts.slice(0, 2);
289
+ }
290
+ function buildFilesFact(filesModified) {
291
+ if (!filesModified || filesModified.length === 0)
292
+ return null;
293
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
294
+ if (cleaned.length === 0)
295
+ return null;
296
+ if (cleaned.length === 1) {
297
+ return `Touched ${cleaned[0]}`;
298
+ }
299
+ return `Touched ${cleaned.join(", ")}`;
300
+ }
301
+ function dedupeFacts(facts) {
302
+ const seen = new Set;
303
+ const result = [];
304
+ for (const fact of facts) {
305
+ const cleaned = fact.trim().replace(/\s+/g, " ");
306
+ if (!looksMeaningful(cleaned))
307
+ continue;
308
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
309
+ if (!key || seen.has(key))
310
+ continue;
311
+ seen.add(key);
312
+ result.push(cleaned);
313
+ }
314
+ return result;
315
+ }
316
+ function looksMeaningful(value) {
317
+ const cleaned = value.trim();
318
+ if (cleaned.length < 12)
319
+ return false;
320
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
321
+ return false;
322
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
323
+ return false;
324
+ return true;
325
+ }
326
+
253
327
  // src/storage/projects.ts
254
328
  import { execSync } from "node:child_process";
255
329
  import { existsSync, readFileSync } from "node:fs";
@@ -630,10 +704,17 @@ async function saveObservation(db, config, input) {
630
704
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
631
705
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
632
706
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
633
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
634
707
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
635
708
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
636
709
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
710
+ const structuredFacts = buildStructuredFacts({
711
+ type: input.type,
712
+ title: input.title,
713
+ narrative: input.narrative,
714
+ facts: input.facts,
715
+ filesModified
716
+ });
717
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
637
718
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
638
719
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
639
720
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
@@ -1537,6 +1537,80 @@ function findDuplicate(newTitle, candidates) {
1537
1537
  return bestMatch;
1538
1538
  }
1539
1539
 
1540
+ // src/capture/facts.ts
1541
+ var FACT_ELIGIBLE_TYPES = new Set([
1542
+ "bugfix",
1543
+ "decision",
1544
+ "discovery",
1545
+ "pattern",
1546
+ "feature",
1547
+ "refactor",
1548
+ "change"
1549
+ ]);
1550
+ function buildStructuredFacts(input) {
1551
+ const seedFacts = dedupeFacts(input.facts ?? []);
1552
+ if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
1553
+ return seedFacts;
1554
+ }
1555
+ const derived = [...seedFacts];
1556
+ if (seedFacts.length === 0 && looksMeaningful(input.title)) {
1557
+ derived.push(input.title.trim());
1558
+ }
1559
+ for (const sentence of extractNarrativeFacts(input.narrative)) {
1560
+ derived.push(sentence);
1561
+ }
1562
+ const fileFact = buildFilesFact(input.filesModified);
1563
+ if (fileFact) {
1564
+ derived.push(fileFact);
1565
+ }
1566
+ return dedupeFacts(derived).slice(0, 4);
1567
+ }
1568
+ function extractNarrativeFacts(narrative) {
1569
+ if (!narrative)
1570
+ return [];
1571
+ const cleaned = narrative.replace(/\s+/g, " ").trim();
1572
+ if (cleaned.length < 24)
1573
+ return [];
1574
+ const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
1575
+ return parts.slice(0, 2);
1576
+ }
1577
+ function buildFilesFact(filesModified) {
1578
+ if (!filesModified || filesModified.length === 0)
1579
+ return null;
1580
+ const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
1581
+ if (cleaned.length === 0)
1582
+ return null;
1583
+ if (cleaned.length === 1) {
1584
+ return `Touched ${cleaned[0]}`;
1585
+ }
1586
+ return `Touched ${cleaned.join(", ")}`;
1587
+ }
1588
+ function dedupeFacts(facts) {
1589
+ const seen = new Set;
1590
+ const result = [];
1591
+ for (const fact of facts) {
1592
+ const cleaned = fact.trim().replace(/\s+/g, " ");
1593
+ if (!looksMeaningful(cleaned))
1594
+ continue;
1595
+ const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
1596
+ if (!key || seen.has(key))
1597
+ continue;
1598
+ seen.add(key);
1599
+ result.push(cleaned);
1600
+ }
1601
+ return result;
1602
+ }
1603
+ function looksMeaningful(value) {
1604
+ const cleaned = value.trim();
1605
+ if (cleaned.length < 12)
1606
+ return false;
1607
+ if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
1608
+ return false;
1609
+ if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
1610
+ return false;
1611
+ return true;
1612
+ }
1613
+
1540
1614
  // src/storage/projects.ts
1541
1615
  import { execSync } from "node:child_process";
1542
1616
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -1917,10 +1991,17 @@ async function saveObservation(db, config, input) {
1917
1991
  const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
1918
1992
  const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
1919
1993
  const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
1920
- const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
1921
1994
  const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
1922
1995
  const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
1923
1996
  const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
1997
+ const structuredFacts = buildStructuredFacts({
1998
+ type: input.type,
1999
+ title: input.title,
2000
+ narrative: input.narrative,
2001
+ facts: input.facts,
2002
+ filesModified
2003
+ });
2004
+ const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
1924
2005
  const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
1925
2006
  const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
1926
2007
  let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
@@ -1343,13 +1343,40 @@ function findStaleDecisionsGlobal(db, options) {
1343
1343
  return stale.slice(0, 5);
1344
1344
  }
1345
1345
 
1346
- // src/context/inject.ts
1346
+ // src/intelligence/observation-priority.ts
1347
1347
  var RECENCY_WINDOW_SECONDS = 30 * 86400;
1348
1348
  function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
1349
1349
  const age = nowEpoch - createdAtEpoch;
1350
1350
  const recencyNorm = Math.max(0, Math.min(1, 1 - age / RECENCY_WINDOW_SECONDS));
1351
1351
  return quality * 0.6 + recencyNorm * 0.4;
1352
1352
  }
1353
+ function observationTypeBoost(type) {
1354
+ switch (type) {
1355
+ case "decision":
1356
+ return 0.2;
1357
+ case "pattern":
1358
+ return 0.18;
1359
+ case "bugfix":
1360
+ return 0.14;
1361
+ case "feature":
1362
+ return 0.12;
1363
+ case "discovery":
1364
+ return 0.1;
1365
+ case "refactor":
1366
+ return 0.05;
1367
+ case "digest":
1368
+ return 0.03;
1369
+ case "change":
1370
+ return 0;
1371
+ default:
1372
+ return 0;
1373
+ }
1374
+ }
1375
+ function computeObservationPriority(obs, nowEpoch) {
1376
+ return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
1377
+ }
1378
+
1379
+ // src/context/inject.ts
1353
1380
  function estimateTokens(text) {
1354
1381
  if (!text)
1355
1382
  return 0;
@@ -1442,10 +1469,8 @@ function buildSessionContext(db, cwd, options = {}) {
1442
1469
  }
1443
1470
  const nowEpoch = Math.floor(Date.now() / 1000);
1444
1471
  const sorted = [...deduped].sort((a, b) => {
1445
- const boostA = a.type === "digest" ? 0.15 : 0;
1446
- const boostB = b.type === "digest" ? 0.15 : 0;
1447
- const scoreA = computeBlendedScore(a.quality, a.created_at_epoch, nowEpoch) + boostA;
1448
- const scoreB = computeBlendedScore(b.quality, b.created_at_epoch, nowEpoch) + boostB;
1472
+ const scoreA = computeObservationPriority(a, nowEpoch);
1473
+ const scoreB = computeObservationPriority(b, nowEpoch);
1449
1474
  return scoreB - scoreA;
1450
1475
  });
1451
1476
  const projectName = project?.name ?? detected.name;
@@ -322,13 +322,40 @@ function findStaleDecisionsGlobal(db, options) {
322
322
  return stale.slice(0, 5);
323
323
  }
324
324
 
325
- // src/context/inject.ts
325
+ // src/intelligence/observation-priority.ts
326
326
  var RECENCY_WINDOW_SECONDS = 30 * 86400;
327
327
  function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
328
328
  const age = nowEpoch - createdAtEpoch;
329
329
  const recencyNorm = Math.max(0, Math.min(1, 1 - age / RECENCY_WINDOW_SECONDS));
330
330
  return quality * 0.6 + recencyNorm * 0.4;
331
331
  }
332
+ function observationTypeBoost(type) {
333
+ switch (type) {
334
+ case "decision":
335
+ return 0.2;
336
+ case "pattern":
337
+ return 0.18;
338
+ case "bugfix":
339
+ return 0.14;
340
+ case "feature":
341
+ return 0.12;
342
+ case "discovery":
343
+ return 0.1;
344
+ case "refactor":
345
+ return 0.05;
346
+ case "digest":
347
+ return 0.03;
348
+ case "change":
349
+ return 0;
350
+ default:
351
+ return 0;
352
+ }
353
+ }
354
+ function computeObservationPriority(obs, nowEpoch) {
355
+ return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
356
+ }
357
+
358
+ // src/context/inject.ts
332
359
  function estimateTokens(text) {
333
360
  if (!text)
334
361
  return 0;
@@ -421,10 +448,8 @@ function buildSessionContext(db, cwd, options = {}) {
421
448
  }
422
449
  const nowEpoch = Math.floor(Date.now() / 1000);
423
450
  const sorted = [...deduped].sort((a, b) => {
424
- const boostA = a.type === "digest" ? 0.15 : 0;
425
- const boostB = b.type === "digest" ? 0.15 : 0;
426
- const scoreA = computeBlendedScore(a.quality, a.created_at_epoch, nowEpoch) + boostA;
427
- const scoreB = computeBlendedScore(b.quality, b.created_at_epoch, nowEpoch) + boostB;
451
+ const scoreA = computeObservationPriority(a, nowEpoch);
452
+ const scoreB = computeObservationPriority(b, nowEpoch);
428
453
  return scoreB - scoreA;
429
454
  });
430
455
  const projectName = project?.name ?? detected.name;
@@ -2521,7 +2546,7 @@ function formatVisibleStartupBrief(context) {
2521
2546
  lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
2522
2547
  }
2523
2548
  if (lines.length === 0 && context.observations.length > 0) {
2524
- const top = context.observations.filter((obs) => !looksLikeFileOperationTitle(obs.title)).slice(0, 2);
2549
+ const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => !looksLikeFileOperationTitle(obs.title)).slice(0, 2);
2525
2550
  for (const obs of top) {
2526
2551
  lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
2527
2552
  }
@@ -2532,7 +2557,7 @@ function toSplashLines(value, maxItems) {
2532
2557
  if (!value)
2533
2558
  return [];
2534
2559
  const lines = value.split(`
2535
- `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
2560
+ `).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
2536
2561
  return dedupeFragmentsInLines(lines);
2537
2562
  }
2538
2563
  function pickBestSummary(context) {
@@ -2579,7 +2604,7 @@ function dedupeFragmentsInLines(lines) {
2579
2604
  const seen = new Set;
2580
2605
  const deduped = [];
2581
2606
  for (const line of lines) {
2582
- const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
2607
+ const normalized = stripInlineSectionLabel(line).toLowerCase().replace(/\s+/g, " ").trim();
2583
2608
  if (!normalized || seen.has(normalized))
2584
2609
  continue;
2585
2610
  seen.add(normalized);
@@ -2625,7 +2650,7 @@ function scoreSplashLine(value) {
2625
2650
  function buildObservationFallbacks(context) {
2626
2651
  const request = context.observations.find((obs) => !looksLikeFileOperationTitle(obs.title))?.title ?? null;
2627
2652
  const investigated = collectObservationTitles(context, (obs) => obs.type === "discovery", 2);
2628
- const completed = collectObservationTitles(context, (obs) => ["feature", "refactor", "change", "digest"].includes(obs.type) && !looksLikeFileOperationTitle(obs.title), 2);
2653
+ const completed = collectObservationTitles(context, (obs) => ["feature", "refactor", "change"].includes(obs.type) && !looksLikeFileOperationTitle(obs.title), 2);
2629
2654
  return {
2630
2655
  request,
2631
2656
  investigated,
@@ -2638,17 +2663,20 @@ function collectObservationTitles(context, predicate, limit) {
2638
2663
  for (const obs of context.observations) {
2639
2664
  if (!predicate(obs))
2640
2665
  continue;
2641
- const normalized = obs.title.toLowerCase().replace(/\s+/g, " ").trim();
2666
+ const normalized = stripInlineSectionLabel(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
2642
2667
  if (!normalized || seen.has(normalized))
2643
2668
  continue;
2644
2669
  seen.add(normalized);
2645
- picked.push(`- ${obs.title}`);
2670
+ picked.push(`- ${stripInlineSectionLabel(obs.title)}`);
2646
2671
  if (picked.length >= limit)
2647
2672
  break;
2648
2673
  }
2649
2674
  return picked.length ? picked.join(`
2650
2675
  `) : null;
2651
2676
  }
2677
+ function stripInlineSectionLabel(value) {
2678
+ return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
2679
+ }
2652
2680
  function pickRelevantStaleDecision(context, summary) {
2653
2681
  const stale = context.staleDecisions || [];
2654
2682
  if (!stale.length)