engrm 0.4.5 → 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/README.md +160 -90
- 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 +127 -22
- package/dist/hooks/stop.js +389 -29
- package/dist/server.js +361 -19
- package/package.json +3 -2
package/dist/hooks/stop.js
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
|
+
// src/intelligence/observation-priority.ts
|
|
6
|
+
var RECENCY_WINDOW_SECONDS = 30 * 86400;
|
|
7
|
+
function computeBlendedScore(quality, createdAtEpoch, nowEpoch) {
|
|
8
|
+
const age = nowEpoch - createdAtEpoch;
|
|
9
|
+
const recencyNorm = Math.max(0, Math.min(1, 1 - age / RECENCY_WINDOW_SECONDS));
|
|
10
|
+
return quality * 0.6 + recencyNorm * 0.4;
|
|
11
|
+
}
|
|
12
|
+
function observationTypeBoost(type) {
|
|
13
|
+
switch (type) {
|
|
14
|
+
case "decision":
|
|
15
|
+
return 0.2;
|
|
16
|
+
case "pattern":
|
|
17
|
+
return 0.18;
|
|
18
|
+
case "bugfix":
|
|
19
|
+
return 0.14;
|
|
20
|
+
case "feature":
|
|
21
|
+
return 0.12;
|
|
22
|
+
case "discovery":
|
|
23
|
+
return 0.1;
|
|
24
|
+
case "refactor":
|
|
25
|
+
return 0.05;
|
|
26
|
+
case "digest":
|
|
27
|
+
return 0.03;
|
|
28
|
+
case "change":
|
|
29
|
+
return 0;
|
|
30
|
+
default:
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function computeObservationPriority(obs, nowEpoch) {
|
|
35
|
+
return computeBlendedScore(obs.quality, obs.created_at_epoch, nowEpoch) + observationTypeBoost(obs.type);
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
// src/capture/retrospective.ts
|
|
6
39
|
function extractRetrospective(observations, sessionId, projectId, userId) {
|
|
7
40
|
if (observations.length === 0)
|
|
@@ -26,62 +59,140 @@ function extractRetrospective(observations, sessionId, projectId, userId) {
|
|
|
26
59
|
};
|
|
27
60
|
}
|
|
28
61
|
function extractRequest(observations) {
|
|
62
|
+
const requestCandidate = observations.find((obs) => ["decision", "feature", "change", "bugfix", "discovery"].includes(obs.type) && obs.title.trim().length > 0 && !looksLikeFileOperation(obs.title));
|
|
63
|
+
if (requestCandidate)
|
|
64
|
+
return requestCandidate.title;
|
|
29
65
|
const first = observations[0];
|
|
30
|
-
if (!first)
|
|
66
|
+
if (!first || !first.title.trim())
|
|
31
67
|
return null;
|
|
32
68
|
return first.title;
|
|
33
69
|
}
|
|
34
70
|
function extractInvestigated(observations) {
|
|
35
|
-
const discoveries = observations.filter((o) => o.type === "discovery");
|
|
71
|
+
const discoveries = observations.filter((o) => o.type === "discovery").sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a));
|
|
36
72
|
if (discoveries.length === 0)
|
|
37
73
|
return null;
|
|
38
|
-
return discoveries
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}).join(`
|
|
43
|
-
`);
|
|
74
|
+
return formatObservationGroup(discoveries, {
|
|
75
|
+
limit: 4,
|
|
76
|
+
factsPerItem: 2
|
|
77
|
+
});
|
|
44
78
|
}
|
|
45
79
|
function extractLearned(observations) {
|
|
46
80
|
const learnTypes = new Set(["bugfix", "decision", "pattern"]);
|
|
47
|
-
const learned = observations.filter((o) => learnTypes.has(o.type));
|
|
81
|
+
const learned = observations.filter((o) => learnTypes.has(o.type)).sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a));
|
|
48
82
|
if (learned.length === 0)
|
|
49
83
|
return null;
|
|
50
|
-
return learned
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}).join(`
|
|
55
|
-
`);
|
|
84
|
+
return formatObservationGroup(learned, {
|
|
85
|
+
limit: 4,
|
|
86
|
+
factsPerItem: 2
|
|
87
|
+
});
|
|
56
88
|
}
|
|
57
89
|
function extractCompleted(observations) {
|
|
58
90
|
const completeTypes = new Set(["change", "feature", "refactor"]);
|
|
59
91
|
const completed = observations.filter((o) => completeTypes.has(o.type));
|
|
60
92
|
if (completed.length === 0)
|
|
61
93
|
return null;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
const prioritized = dedupeObservationsByTitle(completed).sort((a, b) => scoreCompletedObservation(b) - scoreCompletedObservation(a)).slice(0, 4);
|
|
95
|
+
const lines = prioritized.map((o) => {
|
|
96
|
+
const title = normalizeCompletedTitle(o.title, o.files_modified);
|
|
97
|
+
const facts = extractTopFacts(o, 1);
|
|
98
|
+
return facts ? `- ${title}
|
|
99
|
+
${facts}` : `- ${title}`;
|
|
100
|
+
});
|
|
101
|
+
return dedupeBulletLines(lines).join(`
|
|
67
102
|
`);
|
|
68
103
|
}
|
|
69
104
|
function extractNextSteps(observations) {
|
|
70
105
|
if (observations.length < 2)
|
|
71
106
|
return null;
|
|
72
|
-
const lastQuarterStart = Math.floor(observations.length * 0.75);
|
|
107
|
+
const lastQuarterStart = Math.max(0, Math.min(observations.length - 1, observations.length - 3, Math.floor(observations.length * 0.75)));
|
|
73
108
|
const lastQuarter = observations.slice(lastQuarterStart);
|
|
74
109
|
const unresolved = lastQuarter.filter((o) => o.type === "bugfix" && o.narrative && /error|fail|exception/i.test(o.narrative));
|
|
75
|
-
|
|
110
|
+
const explicitDecisions = lastQuarter.filter((o) => o.type === "decision").sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a)).slice(0, 2).map((o) => `- Follow through: ${o.title}`);
|
|
111
|
+
if (unresolved.length === 0 && explicitDecisions.length === 0)
|
|
76
112
|
return null;
|
|
77
|
-
|
|
113
|
+
const lines = unresolved.map((o) => `- Investigate: ${o.title}`).slice(0, 3).concat(explicitDecisions);
|
|
114
|
+
return dedupeBulletLines(lines).join(`
|
|
78
115
|
`);
|
|
79
116
|
}
|
|
117
|
+
function formatObservationGroup(observations, options) {
|
|
118
|
+
const lines = dedupeObservationsByTitle(observations).sort((a, b) => scoreNarrativeObservation(b) - scoreNarrativeObservation(a)).slice(0, options.limit).map((o) => {
|
|
119
|
+
const facts = extractTopFacts(o, options.factsPerItem);
|
|
120
|
+
return facts ? `- ${o.title}
|
|
121
|
+
${facts}` : `- ${o.title}`;
|
|
122
|
+
});
|
|
123
|
+
const deduped = dedupeBulletLines(lines);
|
|
124
|
+
return deduped.length ? deduped.join(`
|
|
125
|
+
`) : null;
|
|
126
|
+
}
|
|
127
|
+
function dedupeBulletLines(lines) {
|
|
128
|
+
const seen = new Set;
|
|
129
|
+
const deduped = [];
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
const normalized = line.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
132
|
+
if (!normalized || seen.has(normalized))
|
|
133
|
+
continue;
|
|
134
|
+
seen.add(normalized);
|
|
135
|
+
deduped.push(line);
|
|
136
|
+
}
|
|
137
|
+
return deduped;
|
|
138
|
+
}
|
|
139
|
+
function dedupeObservationsByTitle(observations) {
|
|
140
|
+
const seen = new Set;
|
|
141
|
+
const deduped = [];
|
|
142
|
+
for (const obs of observations) {
|
|
143
|
+
const normalized = normalizeObservationKey(obs.title);
|
|
144
|
+
if (!normalized || seen.has(normalized))
|
|
145
|
+
continue;
|
|
146
|
+
seen.add(normalized);
|
|
147
|
+
deduped.push(obs);
|
|
148
|
+
}
|
|
149
|
+
return deduped;
|
|
150
|
+
}
|
|
151
|
+
function scoreCompletedObservation(obs) {
|
|
152
|
+
let score = scoreNarrativeObservation(obs);
|
|
153
|
+
if (obs.type === "feature")
|
|
154
|
+
score += 0.5;
|
|
155
|
+
if (obs.type === "refactor")
|
|
156
|
+
score += 0.2;
|
|
157
|
+
if (looksLikeFileOperation(obs.title))
|
|
158
|
+
score -= 0.6;
|
|
159
|
+
return score;
|
|
160
|
+
}
|
|
161
|
+
function scoreNarrativeObservation(obs) {
|
|
162
|
+
const nowEpoch = Math.floor(Date.now() / 1000);
|
|
163
|
+
let score = computeObservationPriority(obs, nowEpoch);
|
|
164
|
+
if (hasMeaningfulFacts(obs))
|
|
165
|
+
score += 0.4;
|
|
166
|
+
if (obs.narrative && obs.narrative.length > 80)
|
|
167
|
+
score += 0.2;
|
|
168
|
+
if (looksLikeFileOperation(obs.title))
|
|
169
|
+
score -= 0.25;
|
|
170
|
+
return score;
|
|
171
|
+
}
|
|
172
|
+
function hasMeaningfulFacts(obs) {
|
|
173
|
+
return parseJsonArray(obs.facts).some((fact) => fact.trim().length > 20);
|
|
174
|
+
}
|
|
175
|
+
function looksLikeFileOperation(title) {
|
|
176
|
+
return /^(modified|updated|edited|touched|changed)\s+[A-Za-z0-9_.\-\/]+$/i.test(title.trim());
|
|
177
|
+
}
|
|
178
|
+
function normalizeCompletedTitle(title, filesModified) {
|
|
179
|
+
const trimmed = title.trim();
|
|
180
|
+
if (!trimmed)
|
|
181
|
+
return "Completed work";
|
|
182
|
+
if (!looksLikeFileOperation(trimmed))
|
|
183
|
+
return trimmed;
|
|
184
|
+
const files = parseJsonArray(filesModified);
|
|
185
|
+
const filename = files[0]?.split("/").pop();
|
|
186
|
+
if (filename) {
|
|
187
|
+
return `Updated implementation in ${filename}`;
|
|
188
|
+
}
|
|
189
|
+
return trimmed;
|
|
190
|
+
}
|
|
80
191
|
function extractTopFacts(obs, n) {
|
|
81
|
-
const facts = parseJsonArray(obs.facts);
|
|
192
|
+
const facts = parseJsonArray(obs.facts).filter((fact) => isUsefulFact(fact, obs.title)).slice(0, n);
|
|
82
193
|
if (facts.length === 0)
|
|
83
194
|
return null;
|
|
84
|
-
return facts.
|
|
195
|
+
return facts.map((f) => ` ${f}`).join(`
|
|
85
196
|
`);
|
|
86
197
|
}
|
|
87
198
|
function parseJsonArray(json) {
|
|
@@ -95,6 +206,23 @@ function parseJsonArray(json) {
|
|
|
95
206
|
} catch {}
|
|
96
207
|
return [];
|
|
97
208
|
}
|
|
209
|
+
function isUsefulFact(fact, title) {
|
|
210
|
+
const cleaned = fact.trim();
|
|
211
|
+
if (!cleaned)
|
|
212
|
+
return false;
|
|
213
|
+
const normalizedFact = normalizeObservationKey(cleaned);
|
|
214
|
+
const normalizedTitle = normalizeObservationKey(title);
|
|
215
|
+
if (normalizedFact && normalizedFact === normalizedTitle)
|
|
216
|
+
return false;
|
|
217
|
+
if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
|
|
218
|
+
return false;
|
|
219
|
+
if (/^\(?[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+\)?$/.test(cleaned))
|
|
220
|
+
return false;
|
|
221
|
+
return cleaned.length > 16 || /[:;]/.test(cleaned);
|
|
222
|
+
}
|
|
223
|
+
function normalizeObservationKey(value) {
|
|
224
|
+
return value.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\b(modified|updated|edited|touched|changed)\b/g, "").replace(/\s+/g, " ").trim();
|
|
225
|
+
}
|
|
98
226
|
|
|
99
227
|
// src/config.ts
|
|
100
228
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -1423,6 +1551,31 @@ function markFailed(db, entryId, error) {
|
|
|
1423
1551
|
WHERE id = ?`).run(error, now, entryId);
|
|
1424
1552
|
}
|
|
1425
1553
|
|
|
1554
|
+
// src/intelligence/value-signals.ts
|
|
1555
|
+
var LESSON_TYPES = new Set(["bugfix", "decision", "pattern"]);
|
|
1556
|
+
function computeSessionValueSignals(observations, securityFindings = []) {
|
|
1557
|
+
const decisionsCount = observations.filter((o) => o.type === "decision").length;
|
|
1558
|
+
const lessonsCount = observations.filter((o) => LESSON_TYPES.has(o.type)).length;
|
|
1559
|
+
const discoveriesCount = observations.filter((o) => o.type === "discovery").length;
|
|
1560
|
+
const featuresCount = observations.filter((o) => o.type === "feature").length;
|
|
1561
|
+
const refactorsCount = observations.filter((o) => o.type === "refactor").length;
|
|
1562
|
+
const repeatedPatternsCount = observations.filter((o) => o.type === "pattern").length;
|
|
1563
|
+
const hasRequestSignal = observations.some((o) => ["feature", "decision", "change", "bugfix", "discovery"].includes(o.type));
|
|
1564
|
+
const hasCompletionSignal = observations.some((o) => ["feature", "change", "refactor", "bugfix"].includes(o.type));
|
|
1565
|
+
return {
|
|
1566
|
+
decisions_count: decisionsCount,
|
|
1567
|
+
lessons_count: lessonsCount,
|
|
1568
|
+
discoveries_count: discoveriesCount,
|
|
1569
|
+
features_count: featuresCount,
|
|
1570
|
+
refactors_count: refactorsCount,
|
|
1571
|
+
repeated_patterns_count: repeatedPatternsCount,
|
|
1572
|
+
security_findings_count: securityFindings.length,
|
|
1573
|
+
critical_security_findings_count: securityFindings.filter((f) => f.severity === "critical").length,
|
|
1574
|
+
delivery_review_ready: hasRequestSignal && hasCompletionSignal,
|
|
1575
|
+
vibe_guardian_active: securityFindings.length > 0
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1426
1579
|
// src/sync/push.ts
|
|
1427
1580
|
function buildVectorDocument(obs, config, project) {
|
|
1428
1581
|
const parts = [obs.title];
|
|
@@ -1471,7 +1624,7 @@ function buildVectorDocument(obs, config, project) {
|
|
|
1471
1624
|
}
|
|
1472
1625
|
};
|
|
1473
1626
|
}
|
|
1474
|
-
function buildSummaryVectorDocument(summary, config, project) {
|
|
1627
|
+
function buildSummaryVectorDocument(summary, config, project, observations = []) {
|
|
1475
1628
|
const parts = [];
|
|
1476
1629
|
if (summary.request)
|
|
1477
1630
|
parts.push(`Request: ${summary.request}`);
|
|
@@ -1483,6 +1636,7 @@ function buildSummaryVectorDocument(summary, config, project) {
|
|
|
1483
1636
|
parts.push(`Completed: ${summary.completed}`);
|
|
1484
1637
|
if (summary.next_steps)
|
|
1485
1638
|
parts.push(`Next Steps: ${summary.next_steps}`);
|
|
1639
|
+
const valueSignals = computeSessionValueSignals(observations, []);
|
|
1486
1640
|
return {
|
|
1487
1641
|
site_id: config.site_id,
|
|
1488
1642
|
namespace: config.namespace,
|
|
@@ -1501,6 +1655,19 @@ function buildSummaryVectorDocument(summary, config, project) {
|
|
|
1501
1655
|
learned: summary.learned,
|
|
1502
1656
|
completed: summary.completed,
|
|
1503
1657
|
next_steps: summary.next_steps,
|
|
1658
|
+
summary_sections_present: countPresentSections(summary),
|
|
1659
|
+
investigated_items: extractSectionItems(summary.investigated),
|
|
1660
|
+
learned_items: extractSectionItems(summary.learned),
|
|
1661
|
+
completed_items: extractSectionItems(summary.completed),
|
|
1662
|
+
next_step_items: extractSectionItems(summary.next_steps),
|
|
1663
|
+
decisions_count: valueSignals.decisions_count,
|
|
1664
|
+
lessons_count: valueSignals.lessons_count,
|
|
1665
|
+
discoveries_count: valueSignals.discoveries_count,
|
|
1666
|
+
features_count: valueSignals.features_count,
|
|
1667
|
+
refactors_count: valueSignals.refactors_count,
|
|
1668
|
+
repeated_patterns_count: valueSignals.repeated_patterns_count,
|
|
1669
|
+
delivery_review_ready: valueSignals.delivery_review_ready,
|
|
1670
|
+
vibe_guardian_active: valueSignals.vibe_guardian_active,
|
|
1504
1671
|
created_at_epoch: summary.created_at_epoch,
|
|
1505
1672
|
local_id: summary.id
|
|
1506
1673
|
}
|
|
@@ -1530,10 +1697,11 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
1530
1697
|
continue;
|
|
1531
1698
|
}
|
|
1532
1699
|
markSyncing(db, entry.id);
|
|
1700
|
+
const summaryObservations = db.getObservationsBySession(summary.session_id);
|
|
1533
1701
|
const doc2 = buildSummaryVectorDocument(summary, config, {
|
|
1534
1702
|
canonical_id: project2.canonical_id,
|
|
1535
1703
|
name: project2.name
|
|
1536
|
-
});
|
|
1704
|
+
}, summaryObservations);
|
|
1537
1705
|
batch.push({ entryId: entry.id, doc: doc2 });
|
|
1538
1706
|
continue;
|
|
1539
1707
|
}
|
|
@@ -1592,6 +1760,21 @@ async function pushOutbox(db, client, config, batchSize = 50) {
|
|
|
1592
1760
|
}
|
|
1593
1761
|
return { pushed, failed, skipped };
|
|
1594
1762
|
}
|
|
1763
|
+
function countPresentSections(summary) {
|
|
1764
|
+
return [
|
|
1765
|
+
summary.request,
|
|
1766
|
+
summary.investigated,
|
|
1767
|
+
summary.learned,
|
|
1768
|
+
summary.completed,
|
|
1769
|
+
summary.next_steps
|
|
1770
|
+
].filter((value) => Boolean(value && value.trim())).length;
|
|
1771
|
+
}
|
|
1772
|
+
function extractSectionItems(section) {
|
|
1773
|
+
if (!section)
|
|
1774
|
+
return [];
|
|
1775
|
+
return section.split(`
|
|
1776
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean).slice(0, 4);
|
|
1777
|
+
}
|
|
1595
1778
|
|
|
1596
1779
|
// src/embeddings/embedder.ts
|
|
1597
1780
|
var _available = null;
|
|
@@ -1764,6 +1947,77 @@ function detectStacks(filePaths) {
|
|
|
1764
1947
|
return Array.from(stacks).sort();
|
|
1765
1948
|
}
|
|
1766
1949
|
|
|
1950
|
+
// src/intelligence/session-insights.ts
|
|
1951
|
+
function computeSessionInsights(summaries, observations) {
|
|
1952
|
+
const orderedSummaries = [...summaries].sort((a, b) => (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0));
|
|
1953
|
+
const summaryCount = orderedSummaries.length;
|
|
1954
|
+
const summariesWithLearned = orderedSummaries.filter((s) => hasContent(s.learned)).length;
|
|
1955
|
+
const summariesWithCompleted = orderedSummaries.filter((s) => hasContent(s.completed)).length;
|
|
1956
|
+
const summariesWithNextSteps = orderedSummaries.filter((s) => hasContent(s.next_steps)).length;
|
|
1957
|
+
const totalSummarySectionsPresent = orderedSummaries.reduce((total, summary) => total + countPresentSections2(summary), 0);
|
|
1958
|
+
const recentRequests = dedupeLines(orderedSummaries.map((summary) => summary.request?.trim() ?? "").filter(Boolean)).slice(0, 3);
|
|
1959
|
+
const recentLessons = dedupeLines([
|
|
1960
|
+
...orderedSummaries.flatMap((summary) => extractSectionItems2(summary.learned)),
|
|
1961
|
+
...extractObservationTitles(observations, ["decision", "pattern", "bugfix"])
|
|
1962
|
+
]).slice(0, 4);
|
|
1963
|
+
const recentCompleted = dedupeLines([
|
|
1964
|
+
...orderedSummaries.flatMap((summary) => extractSectionItems2(summary.completed)),
|
|
1965
|
+
...extractObservationTitles(observations, ["feature", "refactor", "change"])
|
|
1966
|
+
]).slice(0, 4);
|
|
1967
|
+
const nextSteps = dedupeLines([
|
|
1968
|
+
...orderedSummaries.flatMap((summary) => extractSectionItems2(summary.next_steps)),
|
|
1969
|
+
...extractObservationTitles(observations, ["decision"]).map((title) => `Follow through: ${title}`)
|
|
1970
|
+
]).slice(0, 4);
|
|
1971
|
+
return {
|
|
1972
|
+
summary_count: summaryCount,
|
|
1973
|
+
summaries_with_learned: summariesWithLearned,
|
|
1974
|
+
summaries_with_completed: summariesWithCompleted,
|
|
1975
|
+
summaries_with_next_steps: summariesWithNextSteps,
|
|
1976
|
+
total_summary_sections_present: totalSummarySectionsPresent,
|
|
1977
|
+
recent_requests: recentRequests,
|
|
1978
|
+
recent_lessons: recentLessons,
|
|
1979
|
+
recent_completed: recentCompleted,
|
|
1980
|
+
next_steps: nextSteps
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function countPresentSections2(summary) {
|
|
1984
|
+
return [
|
|
1985
|
+
summary.request,
|
|
1986
|
+
summary.investigated,
|
|
1987
|
+
summary.learned,
|
|
1988
|
+
summary.completed,
|
|
1989
|
+
summary.next_steps
|
|
1990
|
+
].filter(hasContent).length;
|
|
1991
|
+
}
|
|
1992
|
+
function extractSectionItems2(section) {
|
|
1993
|
+
if (!hasContent(section))
|
|
1994
|
+
return [];
|
|
1995
|
+
return section.split(`
|
|
1996
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
1997
|
+
}
|
|
1998
|
+
function extractObservationTitles(observations, types) {
|
|
1999
|
+
const typeSet = new Set(types);
|
|
2000
|
+
return observations.filter((obs) => typeSet.has(obs.type)).sort((a, b) => b.created_at_epoch - a.created_at_epoch).map((obs) => obs.title.trim()).filter(Boolean);
|
|
2001
|
+
}
|
|
2002
|
+
function dedupeLines(lines) {
|
|
2003
|
+
const seen = new Set;
|
|
2004
|
+
const result = [];
|
|
2005
|
+
for (const line of lines) {
|
|
2006
|
+
const cleaned = line.replace(/\s+/g, " ").trim();
|
|
2007
|
+
if (!cleaned)
|
|
2008
|
+
continue;
|
|
2009
|
+
const key = cleaned.toLowerCase();
|
|
2010
|
+
if (seen.has(key))
|
|
2011
|
+
continue;
|
|
2012
|
+
seen.add(key);
|
|
2013
|
+
result.push(cleaned);
|
|
2014
|
+
}
|
|
2015
|
+
return result;
|
|
2016
|
+
}
|
|
2017
|
+
function hasContent(value) {
|
|
2018
|
+
return Boolean(value && value.trim());
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1767
2021
|
// src/telemetry/beacon.ts
|
|
1768
2022
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
1769
2023
|
import { join as join3 } from "node:path";
|
|
@@ -1794,6 +2048,17 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
1794
2048
|
for (const obs of observations) {
|
|
1795
2049
|
byType[obs.type] = (byType[obs.type] ?? 0) + 1;
|
|
1796
2050
|
}
|
|
2051
|
+
let securityFindings = [];
|
|
2052
|
+
try {
|
|
2053
|
+
if (session.project_id) {
|
|
2054
|
+
securityFindings = db.getSecurityFindings(session.project_id, { limit: 200 }).filter((f) => f.session_id === sessionId);
|
|
2055
|
+
}
|
|
2056
|
+
} catch {
|
|
2057
|
+
securityFindings = [];
|
|
2058
|
+
}
|
|
2059
|
+
const valueSignals = computeSessionValueSignals(observations, securityFindings);
|
|
2060
|
+
const summaries = session.project_id ? db.getRecentSummaries(session.project_id, 20).filter((summary) => summary.session_id === sessionId) : [];
|
|
2061
|
+
const sessionInsights = computeSessionInsights(summaries, observations);
|
|
1797
2062
|
const filePaths = [];
|
|
1798
2063
|
for (const obs of observations) {
|
|
1799
2064
|
if (obs.files_modified) {
|
|
@@ -1843,7 +2108,7 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
1843
2108
|
observer_events: observerEvents,
|
|
1844
2109
|
observer_observations: observerObservations,
|
|
1845
2110
|
observer_skips: observerSkips,
|
|
1846
|
-
sentinel_used:
|
|
2111
|
+
sentinel_used: valueSignals.security_findings_count > 0,
|
|
1847
2112
|
risk_score: riskScore,
|
|
1848
2113
|
stacks_detected: stacks,
|
|
1849
2114
|
client_version: "0.4.0",
|
|
@@ -1853,6 +2118,20 @@ function buildBeacon(db, config, sessionId, metrics) {
|
|
|
1853
2118
|
recall_hits: metrics?.recallHits ?? 0,
|
|
1854
2119
|
search_count: metrics?.searchCount ?? 0,
|
|
1855
2120
|
search_results_total: metrics?.searchResultsTotal ?? 0,
|
|
2121
|
+
decisions_count: valueSignals.decisions_count,
|
|
2122
|
+
lessons_count: valueSignals.lessons_count,
|
|
2123
|
+
discoveries_count: valueSignals.discoveries_count,
|
|
2124
|
+
features_count: valueSignals.features_count,
|
|
2125
|
+
refactors_count: valueSignals.refactors_count,
|
|
2126
|
+
repeated_patterns_count: valueSignals.repeated_patterns_count,
|
|
2127
|
+
security_findings_count: valueSignals.security_findings_count,
|
|
2128
|
+
critical_security_findings_count: valueSignals.critical_security_findings_count,
|
|
2129
|
+
delivery_review_ready: valueSignals.delivery_review_ready,
|
|
2130
|
+
vibe_guardian_active: valueSignals.vibe_guardian_active,
|
|
2131
|
+
summaries_with_learned: sessionInsights.summaries_with_learned,
|
|
2132
|
+
summaries_with_completed: sessionInsights.summaries_with_completed,
|
|
2133
|
+
summaries_with_next_steps: sessionInsights.summaries_with_next_steps,
|
|
2134
|
+
total_summary_sections_present: sessionInsights.total_summary_sections_present,
|
|
1856
2135
|
config_hash: configHash,
|
|
1857
2136
|
config_changed: configChanged,
|
|
1858
2137
|
config_fingerprint_detail: configFingerprintDetail
|
|
@@ -2232,6 +2511,80 @@ function findDuplicate(newTitle, candidates) {
|
|
|
2232
2511
|
return bestMatch;
|
|
2233
2512
|
}
|
|
2234
2513
|
|
|
2514
|
+
// src/capture/facts.ts
|
|
2515
|
+
var FACT_ELIGIBLE_TYPES = new Set([
|
|
2516
|
+
"bugfix",
|
|
2517
|
+
"decision",
|
|
2518
|
+
"discovery",
|
|
2519
|
+
"pattern",
|
|
2520
|
+
"feature",
|
|
2521
|
+
"refactor",
|
|
2522
|
+
"change"
|
|
2523
|
+
]);
|
|
2524
|
+
function buildStructuredFacts(input) {
|
|
2525
|
+
const seedFacts = dedupeFacts(input.facts ?? []);
|
|
2526
|
+
if (!FACT_ELIGIBLE_TYPES.has(input.type)) {
|
|
2527
|
+
return seedFacts;
|
|
2528
|
+
}
|
|
2529
|
+
const derived = [...seedFacts];
|
|
2530
|
+
if (seedFacts.length === 0 && looksMeaningful(input.title)) {
|
|
2531
|
+
derived.push(input.title.trim());
|
|
2532
|
+
}
|
|
2533
|
+
for (const sentence of extractNarrativeFacts(input.narrative)) {
|
|
2534
|
+
derived.push(sentence);
|
|
2535
|
+
}
|
|
2536
|
+
const fileFact = buildFilesFact(input.filesModified);
|
|
2537
|
+
if (fileFact) {
|
|
2538
|
+
derived.push(fileFact);
|
|
2539
|
+
}
|
|
2540
|
+
return dedupeFacts(derived).slice(0, 4);
|
|
2541
|
+
}
|
|
2542
|
+
function extractNarrativeFacts(narrative) {
|
|
2543
|
+
if (!narrative)
|
|
2544
|
+
return [];
|
|
2545
|
+
const cleaned = narrative.replace(/\s+/g, " ").trim();
|
|
2546
|
+
if (cleaned.length < 24)
|
|
2547
|
+
return [];
|
|
2548
|
+
const parts = cleaned.split(/(?<=[.!?;])\s+/).map((part) => part.trim().replace(/[.!?;]+$/, "")).filter(Boolean).filter(looksMeaningful);
|
|
2549
|
+
return parts.slice(0, 2);
|
|
2550
|
+
}
|
|
2551
|
+
function buildFilesFact(filesModified) {
|
|
2552
|
+
if (!filesModified || filesModified.length === 0)
|
|
2553
|
+
return null;
|
|
2554
|
+
const cleaned = filesModified.map((file) => file.trim()).filter(Boolean).slice(0, 3);
|
|
2555
|
+
if (cleaned.length === 0)
|
|
2556
|
+
return null;
|
|
2557
|
+
if (cleaned.length === 1) {
|
|
2558
|
+
return `Touched ${cleaned[0]}`;
|
|
2559
|
+
}
|
|
2560
|
+
return `Touched ${cleaned.join(", ")}`;
|
|
2561
|
+
}
|
|
2562
|
+
function dedupeFacts(facts) {
|
|
2563
|
+
const seen = new Set;
|
|
2564
|
+
const result = [];
|
|
2565
|
+
for (const fact of facts) {
|
|
2566
|
+
const cleaned = fact.trim().replace(/\s+/g, " ");
|
|
2567
|
+
if (!looksMeaningful(cleaned))
|
|
2568
|
+
continue;
|
|
2569
|
+
const key = cleaned.toLowerCase().replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim();
|
|
2570
|
+
if (!key || seen.has(key))
|
|
2571
|
+
continue;
|
|
2572
|
+
seen.add(key);
|
|
2573
|
+
result.push(cleaned);
|
|
2574
|
+
}
|
|
2575
|
+
return result;
|
|
2576
|
+
}
|
|
2577
|
+
function looksMeaningful(value) {
|
|
2578
|
+
const cleaned = value.trim();
|
|
2579
|
+
if (cleaned.length < 12)
|
|
2580
|
+
return false;
|
|
2581
|
+
if (/^[A-Za-z0-9_.\-\/]+\.[A-Za-z0-9]+$/.test(cleaned))
|
|
2582
|
+
return false;
|
|
2583
|
+
if (/^(updated|modified|edited|changed|touched)\s+[A-Za-z0-9_.\-\/]+$/i.test(cleaned))
|
|
2584
|
+
return false;
|
|
2585
|
+
return true;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2235
2588
|
// src/capture/recurrence.ts
|
|
2236
2589
|
var DISTANCE_THRESHOLD = 0.15;
|
|
2237
2590
|
async function detectRecurrence(db, config, observation) {
|
|
@@ -2449,10 +2802,17 @@ async function saveObservation(db, config, input) {
|
|
|
2449
2802
|
const customPatterns = config.scrubbing.enabled ? config.scrubbing.custom_patterns : [];
|
|
2450
2803
|
const title = config.scrubbing.enabled ? scrubSecrets(input.title, customPatterns) : input.title;
|
|
2451
2804
|
const narrative = input.narrative ? config.scrubbing.enabled ? scrubSecrets(input.narrative, customPatterns) : input.narrative : null;
|
|
2452
|
-
const factsJson = input.facts ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(input.facts), customPatterns) : JSON.stringify(input.facts) : null;
|
|
2453
2805
|
const conceptsJson = input.concepts ? JSON.stringify(input.concepts) : null;
|
|
2454
2806
|
const filesRead = input.files_read ? input.files_read.map((f) => toRelativePath(f, cwd)) : null;
|
|
2455
2807
|
const filesModified = input.files_modified ? input.files_modified.map((f) => toRelativePath(f, cwd)) : null;
|
|
2808
|
+
const structuredFacts = buildStructuredFacts({
|
|
2809
|
+
type: input.type,
|
|
2810
|
+
title: input.title,
|
|
2811
|
+
narrative: input.narrative,
|
|
2812
|
+
facts: input.facts,
|
|
2813
|
+
filesModified
|
|
2814
|
+
});
|
|
2815
|
+
const factsJson = structuredFacts.length > 0 ? config.scrubbing.enabled ? scrubSecrets(JSON.stringify(structuredFacts), customPatterns) : JSON.stringify(structuredFacts) : null;
|
|
2456
2816
|
const filesReadJson = filesRead ? JSON.stringify(filesRead) : null;
|
|
2457
2817
|
const filesModifiedJson = filesModified ? JSON.stringify(filesModified) : null;
|
|
2458
2818
|
let sensitivity = input.sensitivity ?? config.scrubbing.default_sensitivity;
|