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 +128 -1
- package/dist/hooks/elicitation-result.js +82 -1
- package/dist/hooks/post-tool-use.js +82 -1
- package/dist/hooks/pre-compact.js +30 -5
- package/dist/hooks/session-start.js +39 -11
- package/dist/hooks/stop.js +291 -14
- package/dist/server.js +360 -18
- package/package.json +1 -1
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/
|
|
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
|
|
1446
|
-
const
|
|
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/
|
|
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
|
|
425
|
-
const
|
|
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"
|
|
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)
|