engrm 0.4.8 → 0.4.9
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 +149 -14
- package/dist/hooks/elicitation-result.js +59 -4
- package/dist/hooks/post-tool-use.js +154 -33
- package/dist/hooks/pre-compact.js +188 -14
- package/dist/hooks/sentinel.js +57 -3
- package/dist/hooks/session-start.js +357 -61
- package/dist/hooks/stop.js +59 -4
- package/dist/hooks/user-prompt-submit.js +57 -3
- package/dist/server.js +878 -311
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { homedir as homedir3 } from "os";
|
|
|
11
11
|
// src/storage/projects.ts
|
|
12
12
|
import { execSync } from "node:child_process";
|
|
13
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
-
import { basename, join } from "node:path";
|
|
14
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
15
15
|
function normaliseGitRemoteUrl(remoteUrl) {
|
|
16
16
|
let url = remoteUrl.trim();
|
|
17
17
|
url = url.replace(/^(?:https?|ssh|git):\/\//, "");
|
|
@@ -65,6 +65,19 @@ function getGitRemoteUrl(directory) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
function getGitTopLevel(directory) {
|
|
69
|
+
try {
|
|
70
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
71
|
+
cwd: directory,
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
75
|
+
}).trim();
|
|
76
|
+
return root || null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
68
81
|
function readProjectConfigFile(directory) {
|
|
69
82
|
const configPath = join(directory, ".engrm.json");
|
|
70
83
|
if (!existsSync(configPath))
|
|
@@ -87,11 +100,12 @@ function detectProject(directory) {
|
|
|
87
100
|
const remoteUrl = getGitRemoteUrl(directory);
|
|
88
101
|
if (remoteUrl) {
|
|
89
102
|
const canonicalId = normaliseGitRemoteUrl(remoteUrl);
|
|
103
|
+
const repoRoot = getGitTopLevel(directory) ?? directory;
|
|
90
104
|
return {
|
|
91
105
|
canonical_id: canonicalId,
|
|
92
106
|
name: projectNameFromCanonicalId(canonicalId),
|
|
93
107
|
remote_url: remoteUrl,
|
|
94
|
-
local_path:
|
|
108
|
+
local_path: repoRoot
|
|
95
109
|
};
|
|
96
110
|
}
|
|
97
111
|
const configFile = readProjectConfigFile(directory);
|
|
@@ -111,6 +125,32 @@ function detectProject(directory) {
|
|
|
111
125
|
local_path: directory
|
|
112
126
|
};
|
|
113
127
|
}
|
|
128
|
+
function detectProjectForPath(filePath, fallbackCwd) {
|
|
129
|
+
const absolutePath = resolve(fallbackCwd ?? process.cwd(), filePath);
|
|
130
|
+
const candidateDir = existsSync(absolutePath) && !absolutePath.endsWith("/") ? dirname(absolutePath) : dirname(absolutePath);
|
|
131
|
+
const detected = detectProject(candidateDir);
|
|
132
|
+
if (detected.canonical_id.startsWith("local/"))
|
|
133
|
+
return null;
|
|
134
|
+
return detected;
|
|
135
|
+
}
|
|
136
|
+
function detectProjectFromTouchedPaths(paths, fallbackCwd) {
|
|
137
|
+
const counts = new Map;
|
|
138
|
+
for (const rawPath of paths) {
|
|
139
|
+
if (!rawPath || !rawPath.trim())
|
|
140
|
+
continue;
|
|
141
|
+
const detected = detectProjectForPath(rawPath, fallbackCwd);
|
|
142
|
+
if (!detected)
|
|
143
|
+
continue;
|
|
144
|
+
const existing = counts.get(detected.canonical_id);
|
|
145
|
+
if (existing) {
|
|
146
|
+
existing.count += 1;
|
|
147
|
+
} else {
|
|
148
|
+
counts.set(detected.canonical_id, { project: detected, count: 1 });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const ranked = [...counts.values()].sort((a, b) => b.count - a.count || a.project.name.localeCompare(b.project.name));
|
|
152
|
+
return ranked[0]?.project ?? detectProject(fallbackCwd);
|
|
153
|
+
}
|
|
114
154
|
|
|
115
155
|
// src/capture/dedup.ts
|
|
116
156
|
function tokenise(text) {
|
|
@@ -356,6 +396,32 @@ function computeObservationPriority(obs, nowEpoch) {
|
|
|
356
396
|
}
|
|
357
397
|
|
|
358
398
|
// src/context/inject.ts
|
|
399
|
+
function tokenizeProjectHint(text) {
|
|
400
|
+
return Array.from(new Set((text.toLowerCase().match(/[a-z0-9_+-]{4,}/g) ?? []).filter(Boolean)));
|
|
401
|
+
}
|
|
402
|
+
function isObservationRelatedToProject(obs, detected) {
|
|
403
|
+
const hints = new Set([
|
|
404
|
+
...tokenizeProjectHint(detected.name),
|
|
405
|
+
...tokenizeProjectHint(detected.canonical_id)
|
|
406
|
+
]);
|
|
407
|
+
if (hints.size === 0)
|
|
408
|
+
return false;
|
|
409
|
+
const haystack = [
|
|
410
|
+
obs.title,
|
|
411
|
+
obs.narrative ?? "",
|
|
412
|
+
obs.facts ?? "",
|
|
413
|
+
obs.concepts ?? "",
|
|
414
|
+
obs.files_read ?? "",
|
|
415
|
+
obs.files_modified ?? "",
|
|
416
|
+
obs._source_project ?? ""
|
|
417
|
+
].join(`
|
|
418
|
+
`).toLowerCase();
|
|
419
|
+
for (const hint of hints) {
|
|
420
|
+
if (haystack.includes(hint))
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
359
425
|
function estimateTokens(text) {
|
|
360
426
|
if (!text)
|
|
361
427
|
return 0;
|
|
@@ -431,6 +497,9 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
431
497
|
}
|
|
432
498
|
return { ...obs, _source_project: projectNameCache.get(obs.project_id) };
|
|
433
499
|
});
|
|
500
|
+
if (isNewProject) {
|
|
501
|
+
crossProjectCandidates = crossProjectCandidates.filter((obs) => isObservationRelatedToProject(obs, detected));
|
|
502
|
+
}
|
|
434
503
|
}
|
|
435
504
|
const seenIds = new Set(pinned.map((o) => o.id));
|
|
436
505
|
const dedupedRecent = recent.filter((o) => {
|
|
@@ -461,6 +530,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
461
530
|
const recentToolEvents2 = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
462
531
|
const recentSessions2 = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
463
532
|
const projectTypeCounts2 = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
533
|
+
const recentOutcomes2 = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
464
534
|
return {
|
|
465
535
|
project_name: projectName,
|
|
466
536
|
canonical_id: canonicalId,
|
|
@@ -470,7 +540,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
470
540
|
recentPrompts: recentPrompts2.length > 0 ? recentPrompts2 : undefined,
|
|
471
541
|
recentToolEvents: recentToolEvents2.length > 0 ? recentToolEvents2 : undefined,
|
|
472
542
|
recentSessions: recentSessions2.length > 0 ? recentSessions2 : undefined,
|
|
473
|
-
projectTypeCounts: projectTypeCounts2
|
|
543
|
+
projectTypeCounts: projectTypeCounts2,
|
|
544
|
+
recentOutcomes: recentOutcomes2
|
|
474
545
|
};
|
|
475
546
|
}
|
|
476
547
|
let remainingBudget = tokenBudget - 30;
|
|
@@ -497,6 +568,7 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
497
568
|
const recentToolEvents = db.getRecentToolEvents(isNewProject ? null : projectId, isNewProject ? 8 : 6, opts.userId);
|
|
498
569
|
const recentSessions = isNewProject ? [] : db.getRecentSessions(projectId, 5, opts.userId);
|
|
499
570
|
const projectTypeCounts = isNewProject ? undefined : getProjectTypeCounts(db, projectId, opts.userId);
|
|
571
|
+
const recentOutcomes = isNewProject ? undefined : getRecentOutcomes(db, projectId, opts.userId);
|
|
500
572
|
let securityFindings = [];
|
|
501
573
|
if (!isNewProject) {
|
|
502
574
|
try {
|
|
@@ -554,7 +626,8 @@ function buildSessionContext(db, cwd, options = {}) {
|
|
|
554
626
|
recentPrompts: recentPrompts.length > 0 ? recentPrompts : undefined,
|
|
555
627
|
recentToolEvents: recentToolEvents.length > 0 ? recentToolEvents : undefined,
|
|
556
628
|
recentSessions: recentSessions.length > 0 ? recentSessions : undefined,
|
|
557
|
-
projectTypeCounts
|
|
629
|
+
projectTypeCounts,
|
|
630
|
+
recentOutcomes
|
|
558
631
|
};
|
|
559
632
|
}
|
|
560
633
|
function estimateObservationTokens(obs, index) {
|
|
@@ -591,12 +664,15 @@ function formatContextForInjection(context) {
|
|
|
591
664
|
lines.push("");
|
|
592
665
|
}
|
|
593
666
|
if (context.recentPrompts && context.recentPrompts.length > 0) {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
667
|
+
const promptLines = context.recentPrompts.filter((prompt) => isMeaningfulPrompt(prompt.prompt)).slice(0, 5);
|
|
668
|
+
if (promptLines.length > 0) {
|
|
669
|
+
lines.push("## Recent Requests");
|
|
670
|
+
for (const prompt of promptLines) {
|
|
671
|
+
const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
|
|
672
|
+
lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
|
|
673
|
+
}
|
|
674
|
+
lines.push("");
|
|
598
675
|
}
|
|
599
|
-
lines.push("");
|
|
600
676
|
}
|
|
601
677
|
if (context.recentToolEvents && context.recentToolEvents.length > 0) {
|
|
602
678
|
lines.push("## Recent Tools");
|
|
@@ -606,10 +682,22 @@ function formatContextForInjection(context) {
|
|
|
606
682
|
lines.push("");
|
|
607
683
|
}
|
|
608
684
|
if (context.recentSessions && context.recentSessions.length > 0) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
685
|
+
const recentSessionLines = context.recentSessions.slice(0, 4).map((session) => {
|
|
686
|
+
const summary = chooseMeaningfulSessionHeadline(session.request, session.completed);
|
|
687
|
+
if (summary === "(no summary)")
|
|
688
|
+
return null;
|
|
689
|
+
return `- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
690
|
+
}).filter((line) => Boolean(line));
|
|
691
|
+
if (recentSessionLines.length > 0) {
|
|
692
|
+
lines.push("## Recent Sessions");
|
|
693
|
+
lines.push(...recentSessionLines);
|
|
694
|
+
lines.push("");
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (context.recentOutcomes && context.recentOutcomes.length > 0) {
|
|
698
|
+
lines.push("## Recent Outcomes");
|
|
699
|
+
for (const outcome of context.recentOutcomes.slice(0, 5)) {
|
|
700
|
+
lines.push(`- ${truncateText(outcome, 160)}`);
|
|
613
701
|
}
|
|
614
702
|
lines.push("");
|
|
615
703
|
}
|
|
@@ -689,6 +777,14 @@ function formatSessionBrief(summary) {
|
|
|
689
777
|
}
|
|
690
778
|
return lines;
|
|
691
779
|
}
|
|
780
|
+
function chooseMeaningfulSessionHeadline(request, completed) {
|
|
781
|
+
if (request && !looksLikeFileOperationTitle(request))
|
|
782
|
+
return request;
|
|
783
|
+
const completedItems = extractMeaningfulLines(completed, 1);
|
|
784
|
+
if (completedItems.length > 0)
|
|
785
|
+
return completedItems[0];
|
|
786
|
+
return request ?? completed ?? "(no summary)";
|
|
787
|
+
}
|
|
692
788
|
function formatSummarySection(value, maxLen) {
|
|
693
789
|
if (!value)
|
|
694
790
|
return null;
|
|
@@ -713,6 +809,26 @@ function truncateText(text, maxLen) {
|
|
|
713
809
|
return text;
|
|
714
810
|
return text.slice(0, maxLen - 3) + "...";
|
|
715
811
|
}
|
|
812
|
+
function isMeaningfulPrompt(value) {
|
|
813
|
+
if (!value)
|
|
814
|
+
return false;
|
|
815
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
816
|
+
if (compact.length < 8)
|
|
817
|
+
return false;
|
|
818
|
+
return /[a-z]{3,}/i.test(compact);
|
|
819
|
+
}
|
|
820
|
+
function looksLikeFileOperationTitle(value) {
|
|
821
|
+
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
822
|
+
}
|
|
823
|
+
function stripInlineSectionLabel(value) {
|
|
824
|
+
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
825
|
+
}
|
|
826
|
+
function extractMeaningfulLines(value, limit) {
|
|
827
|
+
if (!value)
|
|
828
|
+
return [];
|
|
829
|
+
return value.split(`
|
|
830
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel(line)).filter((line) => line.length > 0 && !looksLikeFileOperationTitle(line)).slice(0, limit);
|
|
831
|
+
}
|
|
716
832
|
function formatObservationDetailFromContext(obs) {
|
|
717
833
|
if (obs.facts) {
|
|
718
834
|
const bullets = parseFacts(obs.facts);
|
|
@@ -812,6 +928,50 @@ function getProjectTypeCounts(db, projectId, userId) {
|
|
|
812
928
|
}
|
|
813
929
|
return counts;
|
|
814
930
|
}
|
|
931
|
+
function getRecentOutcomes(db, projectId, userId) {
|
|
932
|
+
const visibilityClause = userId ? " AND (sensitivity != 'personal' OR user_id = ?)" : "";
|
|
933
|
+
const visibilityParams = userId ? [userId] : [];
|
|
934
|
+
const summaries = db.db.query(`SELECT * FROM session_summaries
|
|
935
|
+
WHERE project_id = ?
|
|
936
|
+
ORDER BY created_at_epoch DESC
|
|
937
|
+
LIMIT 6`).all(projectId);
|
|
938
|
+
const picked = [];
|
|
939
|
+
const seen = new Set;
|
|
940
|
+
for (const summary of summaries) {
|
|
941
|
+
for (const line of [
|
|
942
|
+
...extractMeaningfulLines(summary.completed, 2),
|
|
943
|
+
...extractMeaningfulLines(summary.learned, 1)
|
|
944
|
+
]) {
|
|
945
|
+
const normalized = line.toLowerCase().replace(/\s+/g, " ").trim();
|
|
946
|
+
if (!normalized || seen.has(normalized))
|
|
947
|
+
continue;
|
|
948
|
+
seen.add(normalized);
|
|
949
|
+
picked.push(line);
|
|
950
|
+
if (picked.length >= 5)
|
|
951
|
+
return picked;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const rows = db.db.query(`SELECT * FROM observations
|
|
955
|
+
WHERE project_id = ?
|
|
956
|
+
AND lifecycle IN ('active', 'aging', 'pinned')
|
|
957
|
+
AND superseded_by IS NULL
|
|
958
|
+
${visibilityClause}
|
|
959
|
+
ORDER BY created_at_epoch DESC
|
|
960
|
+
LIMIT 20`).all(projectId, ...visibilityParams);
|
|
961
|
+
for (const obs of rows) {
|
|
962
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
963
|
+
continue;
|
|
964
|
+
const title = stripInlineSectionLabel(obs.title);
|
|
965
|
+
const normalized = title.toLowerCase().replace(/\s+/g, " ").trim();
|
|
966
|
+
if (!normalized || seen.has(normalized) || looksLikeFileOperationTitle(title))
|
|
967
|
+
continue;
|
|
968
|
+
seen.add(normalized);
|
|
969
|
+
picked.push(title);
|
|
970
|
+
if (picked.length >= 5)
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
return picked;
|
|
974
|
+
}
|
|
815
975
|
|
|
816
976
|
// src/telemetry/stack-detect.ts
|
|
817
977
|
import { existsSync as existsSync2 } from "node:fs";
|
|
@@ -1003,7 +1163,7 @@ function computeAndSaveFingerprint(cwd) {
|
|
|
1003
1163
|
|
|
1004
1164
|
// src/packs/recommender.ts
|
|
1005
1165
|
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1006
|
-
import { join as join4, basename as basename3, dirname } from "node:path";
|
|
1166
|
+
import { join as join4, basename as basename3, dirname as dirname2 } from "node:path";
|
|
1007
1167
|
import { fileURLToPath } from "node:url";
|
|
1008
1168
|
var STACK_PACK_MAP = {
|
|
1009
1169
|
typescript: ["typescript-patterns"],
|
|
@@ -1015,7 +1175,7 @@ var STACK_PACK_MAP = {
|
|
|
1015
1175
|
bun: ["node-security"]
|
|
1016
1176
|
};
|
|
1017
1177
|
function getPacksDir() {
|
|
1018
|
-
const thisDir =
|
|
1178
|
+
const thisDir = dirname2(fileURLToPath(import.meta.url));
|
|
1019
1179
|
return join4(thisDir, "../../packs");
|
|
1020
1180
|
}
|
|
1021
1181
|
function listAvailablePacks() {
|
|
@@ -1420,10 +1580,15 @@ function mergeChanges(db, config, changes) {
|
|
|
1420
1580
|
name: change.metadata?.project_name ?? projectCanonical.split("/").pop() ?? "unknown"
|
|
1421
1581
|
});
|
|
1422
1582
|
}
|
|
1583
|
+
const normalizedType = normalizeRemoteObservationType(change.metadata?.type, change.source_id);
|
|
1584
|
+
if (!normalizedType) {
|
|
1585
|
+
skipped++;
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1423
1588
|
const obs = db.insertObservation({
|
|
1424
1589
|
session_id: change.metadata?.session_id ?? null,
|
|
1425
1590
|
project_id: project.id,
|
|
1426
|
-
type:
|
|
1591
|
+
type: normalizedType,
|
|
1427
1592
|
title: change.metadata?.title ?? change.content.split(`
|
|
1428
1593
|
`)[0] ?? "Untitled",
|
|
1429
1594
|
narrative: extractNarrative(change.content),
|
|
@@ -1446,6 +1611,23 @@ function mergeChanges(db, config, changes) {
|
|
|
1446
1611
|
}
|
|
1447
1612
|
return { merged, skipped };
|
|
1448
1613
|
}
|
|
1614
|
+
function normalizeRemoteObservationType(rawType, sourceId) {
|
|
1615
|
+
const type = typeof rawType === "string" ? rawType.trim().toLowerCase() : "";
|
|
1616
|
+
if (type === "bugfix" || type === "discovery" || type === "decision" || type === "pattern" || type === "change" || type === "feature" || type === "refactor" || type === "digest" || type === "standard" || type === "message") {
|
|
1617
|
+
return type;
|
|
1618
|
+
}
|
|
1619
|
+
if (type === "summary") {
|
|
1620
|
+
return "digest";
|
|
1621
|
+
}
|
|
1622
|
+
if (!type) {
|
|
1623
|
+
if (sourceId.includes("-summary-"))
|
|
1624
|
+
return "digest";
|
|
1625
|
+
if (sourceId.includes("-message-"))
|
|
1626
|
+
return "message";
|
|
1627
|
+
return "standard";
|
|
1628
|
+
}
|
|
1629
|
+
return "standard";
|
|
1630
|
+
}
|
|
1449
1631
|
async function embedAndInsert(db, obs) {
|
|
1450
1632
|
const text = composeEmbeddingText(obs);
|
|
1451
1633
|
const embedding = await embedText(text);
|
|
@@ -2178,6 +2360,15 @@ class MemDatabase {
|
|
|
2178
2360
|
}
|
|
2179
2361
|
return row;
|
|
2180
2362
|
}
|
|
2363
|
+
reassignObservationProject(observationId, projectId) {
|
|
2364
|
+
const existing = this.getObservationById(observationId);
|
|
2365
|
+
if (!existing)
|
|
2366
|
+
return false;
|
|
2367
|
+
if (existing.project_id === projectId)
|
|
2368
|
+
return true;
|
|
2369
|
+
this.db.query("UPDATE observations SET project_id = ? WHERE id = ?").run(projectId, observationId);
|
|
2370
|
+
return true;
|
|
2371
|
+
}
|
|
2181
2372
|
getObservationById(id) {
|
|
2182
2373
|
return this.db.query("SELECT * FROM observations WHERE id = ?").get(id) ?? null;
|
|
2183
2374
|
}
|
|
@@ -2311,8 +2502,13 @@ class MemDatabase {
|
|
|
2311
2502
|
}
|
|
2312
2503
|
upsertSession(sessionId, projectId, userId, deviceId, agent = "claude-code") {
|
|
2313
2504
|
const existing = this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
2314
|
-
if (existing)
|
|
2505
|
+
if (existing) {
|
|
2506
|
+
if (existing.project_id === null && projectId !== null) {
|
|
2507
|
+
this.db.query("UPDATE sessions SET project_id = ? WHERE session_id = ?").run(projectId, sessionId);
|
|
2508
|
+
return this.db.query("SELECT * FROM sessions WHERE session_id = ?").get(sessionId);
|
|
2509
|
+
}
|
|
2315
2510
|
return existing;
|
|
2511
|
+
}
|
|
2316
2512
|
const now = Math.floor(Date.now() / 1000);
|
|
2317
2513
|
this.db.query(`INSERT INTO sessions (session_id, project_id, user_id, device_id, agent, started_at_epoch)
|
|
2318
2514
|
VALUES (?, ?, ?, ?, ?, ?)`).run(sessionId, projectId, userId, deviceId, agent, now);
|
|
@@ -2792,7 +2988,7 @@ function formatSplashScreen(data) {
|
|
|
2792
2988
|
const brief = formatVisibleStartupBrief(data.context);
|
|
2793
2989
|
if (brief.length > 0) {
|
|
2794
2990
|
lines.push("");
|
|
2795
|
-
lines.push(` ${c2.bold}Startup
|
|
2991
|
+
lines.push(` ${c2.bold}Startup context${c2.reset}`);
|
|
2796
2992
|
for (const line of brief) {
|
|
2797
2993
|
lines.push(` ${line}`);
|
|
2798
2994
|
}
|
|
@@ -2803,15 +2999,25 @@ function formatSplashScreen(data) {
|
|
|
2803
2999
|
}
|
|
2804
3000
|
function formatVisibleStartupBrief(context) {
|
|
2805
3001
|
const lines = [];
|
|
2806
|
-
const latest =
|
|
3002
|
+
const latest = pickPrimarySummary(context);
|
|
2807
3003
|
const observationFallbacks = buildObservationFallbacks(context);
|
|
2808
3004
|
const promptFallback = buildPromptFallback(context);
|
|
3005
|
+
const promptLines = buildPromptLines(context);
|
|
3006
|
+
const latestPromptLine = promptLines[0] ?? null;
|
|
3007
|
+
const currentRequest = latest ? chooseRequest(latest.request, promptFallback ?? sessionFallbacksFromContext(context)[0] ?? observationFallbacks.request) : promptFallback;
|
|
2809
3008
|
const toolFallbacks = buildToolFallbacks(context);
|
|
2810
|
-
const sessionFallbacks =
|
|
3009
|
+
const sessionFallbacks = sessionFallbacksFromContext(context);
|
|
3010
|
+
const recentOutcomeLines = buildRecentOutcomeLines(context, latest);
|
|
2811
3011
|
const projectSignals = buildProjectSignalLine(context);
|
|
3012
|
+
if (promptLines.length > 0) {
|
|
3013
|
+
lines.push(`${c2.cyan}Recent Requests:${c2.reset}`);
|
|
3014
|
+
for (const item of promptLines) {
|
|
3015
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
2812
3018
|
if (latest) {
|
|
2813
3019
|
const sections = [
|
|
2814
|
-
["Request",
|
|
3020
|
+
["Request", currentRequest, 1],
|
|
2815
3021
|
["Investigated", chooseSection(latest.investigated, observationFallbacks.investigated, "Investigated"), 2],
|
|
2816
3022
|
["Learned", latest.learned, 2],
|
|
2817
3023
|
["Completed", chooseSection(latest.completed, observationFallbacks.completed, "Completed"), 2],
|
|
@@ -2826,58 +3032,131 @@ function formatVisibleStartupBrief(context) {
|
|
|
2826
3032
|
}
|
|
2827
3033
|
}
|
|
2828
3034
|
}
|
|
2829
|
-
} else if (
|
|
2830
|
-
lines.push(`${c2.cyan}Request:${c2.reset}`);
|
|
2831
|
-
lines.push(` - ${truncateInline(
|
|
3035
|
+
} else if (currentRequest && !duplicatesPromptLine(currentRequest, latestPromptLine)) {
|
|
3036
|
+
lines.push(`${c2.cyan}Current Request:${c2.reset}`);
|
|
3037
|
+
lines.push(` - ${truncateInline(currentRequest, 160)}`);
|
|
2832
3038
|
if (toolFallbacks.length > 0) {
|
|
2833
3039
|
lines.push(`${c2.cyan}Recent Tools:${c2.reset}`);
|
|
2834
3040
|
for (const item of toolFallbacks) {
|
|
2835
|
-
lines.push(` - ${truncateInline(item,
|
|
3041
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
2836
3042
|
}
|
|
2837
3043
|
}
|
|
2838
3044
|
}
|
|
3045
|
+
if (latest && currentRequest && !hasRequestSection(lines) && !duplicatesPromptLine(currentRequest, latestPromptLine)) {
|
|
3046
|
+
lines.push(`${c2.cyan}Current Request:${c2.reset}`);
|
|
3047
|
+
lines.push(` - ${truncateInline(currentRequest, 160)}`);
|
|
3048
|
+
}
|
|
3049
|
+
if (recentOutcomeLines.length > 0) {
|
|
3050
|
+
lines.push(`${c2.cyan}Recent Work:${c2.reset}`);
|
|
3051
|
+
for (const item of recentOutcomeLines) {
|
|
3052
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
if (toolFallbacks.length > 0 && latest) {
|
|
3056
|
+
lines.push(`${c2.cyan}Recent Tools:${c2.reset}`);
|
|
3057
|
+
for (const item of toolFallbacks) {
|
|
3058
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
2839
3061
|
if (sessionFallbacks.length > 0) {
|
|
2840
3062
|
lines.push(`${c2.cyan}Recent Sessions:${c2.reset}`);
|
|
2841
3063
|
for (const item of sessionFallbacks) {
|
|
2842
|
-
lines.push(` - ${truncateInline(item,
|
|
3064
|
+
lines.push(` - ${truncateInline(item, 160)}`);
|
|
2843
3065
|
}
|
|
2844
3066
|
}
|
|
2845
3067
|
if (projectSignals) {
|
|
2846
3068
|
lines.push(`${c2.cyan}Project Signals:${c2.reset}`);
|
|
2847
|
-
lines.push(` - ${truncateInline(projectSignals,
|
|
3069
|
+
lines.push(` - ${truncateInline(projectSignals, 160)}`);
|
|
2848
3070
|
}
|
|
2849
3071
|
const stale = pickRelevantStaleDecision(context, latest);
|
|
2850
3072
|
if (stale) {
|
|
2851
3073
|
lines.push(`${c2.yellow}Watch:${c2.reset} ${truncateInline(`Decision still looks unfinished: ${stale.title}`, 170)}`);
|
|
2852
3074
|
}
|
|
2853
3075
|
if (lines.length === 0 && context.observations.length > 0) {
|
|
2854
|
-
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => !
|
|
3076
|
+
const top = context.observations.filter((obs) => obs.type !== "digest").filter((obs) => obs.type !== "decision").filter((obs) => !looksLikeFileOperationTitle2(obs.title)).slice(0, 3);
|
|
2855
3077
|
for (const obs of top) {
|
|
2856
3078
|
lines.push(`${c2.cyan}${capitalize(obs.type)}:${c2.reset} ${truncateInline(obs.title, 170)}`);
|
|
2857
3079
|
}
|
|
2858
3080
|
}
|
|
2859
|
-
return lines.slice(0,
|
|
3081
|
+
return lines.slice(0, 14);
|
|
2860
3082
|
}
|
|
2861
3083
|
function buildPromptFallback(context) {
|
|
2862
|
-
const latest = context.recentPrompts
|
|
3084
|
+
const latest = (context.recentPrompts ?? []).find((prompt) => isMeaningfulPrompt2(prompt.prompt));
|
|
2863
3085
|
if (!latest?.prompt)
|
|
2864
3086
|
return null;
|
|
2865
3087
|
return latest.prompt.replace(/\s+/g, " ").trim();
|
|
2866
3088
|
}
|
|
3089
|
+
function buildPromptLines(context) {
|
|
3090
|
+
return (context.recentPrompts ?? []).filter((prompt) => isMeaningfulPrompt2(prompt.prompt)).slice(0, 2).map((prompt) => {
|
|
3091
|
+
const prefix = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : "request";
|
|
3092
|
+
return `${prefix}: ${prompt.prompt.replace(/\s+/g, " ").trim()}`;
|
|
3093
|
+
}).filter((item) => item.length > 0);
|
|
3094
|
+
}
|
|
3095
|
+
function duplicatesPromptLine(request, promptLine) {
|
|
3096
|
+
if (!request || !promptLine)
|
|
3097
|
+
return false;
|
|
3098
|
+
const promptBody = promptLine.replace(/^#?\d+:\s*/, "").trim();
|
|
3099
|
+
return normalizeStartupItem(request) === normalizeStartupItem(promptBody);
|
|
3100
|
+
}
|
|
2867
3101
|
function buildToolFallbacks(context) {
|
|
2868
3102
|
return (context.recentToolEvents ?? []).slice(0, 3).map((tool) => {
|
|
2869
3103
|
const detail = tool.file_path ?? tool.command ?? tool.tool_response_preview ?? "";
|
|
2870
|
-
return `${tool.tool_name}
|
|
3104
|
+
return `${tool.tool_name}${detail ? `: ${detail}` : ""}`.trim();
|
|
2871
3105
|
}).filter((item) => item.length > 0);
|
|
2872
3106
|
}
|
|
2873
|
-
function
|
|
3107
|
+
function sessionFallbacksFromContext(context) {
|
|
2874
3108
|
return (context.recentSessions ?? []).slice(0, 2).map((session) => {
|
|
2875
|
-
const summary = session.request
|
|
3109
|
+
const summary = chooseMeaningfulSessionSummary(session.request, session.completed);
|
|
2876
3110
|
if (!summary)
|
|
2877
3111
|
return "";
|
|
2878
|
-
return `${session.session_id}: ${summary}`;
|
|
3112
|
+
return `${session.session_id}: ${summary} (prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`;
|
|
2879
3113
|
}).filter((item) => item.length > 0);
|
|
2880
3114
|
}
|
|
3115
|
+
function buildRecentOutcomeLines(context, summary) {
|
|
3116
|
+
const picked = [];
|
|
3117
|
+
const seen = new Set;
|
|
3118
|
+
const push = (value) => {
|
|
3119
|
+
for (const line of toSplashLines(value ?? null, 2)) {
|
|
3120
|
+
const normalized = normalizeStartupItem(line);
|
|
3121
|
+
if (!normalized || seen.has(normalized))
|
|
3122
|
+
continue;
|
|
3123
|
+
seen.add(normalized);
|
|
3124
|
+
picked.push(line.replace(/^-\s*/, ""));
|
|
3125
|
+
if (picked.length >= 2)
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
};
|
|
3129
|
+
push(summary?.completed);
|
|
3130
|
+
push(summary?.learned);
|
|
3131
|
+
if (picked.length < 2) {
|
|
3132
|
+
for (const obs of context.observations) {
|
|
3133
|
+
if (!["bugfix", "feature", "refactor", "change", "decision"].includes(obs.type))
|
|
3134
|
+
continue;
|
|
3135
|
+
const title = stripInlineSectionLabel2(obs.title);
|
|
3136
|
+
if (!title || looksLikeFileOperationTitle2(title))
|
|
3137
|
+
continue;
|
|
3138
|
+
const normalized = normalizeStartupItem(title);
|
|
3139
|
+
if (!normalized || seen.has(normalized))
|
|
3140
|
+
continue;
|
|
3141
|
+
seen.add(normalized);
|
|
3142
|
+
picked.push(title);
|
|
3143
|
+
if (picked.length >= 2)
|
|
3144
|
+
break;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
return picked;
|
|
3148
|
+
}
|
|
3149
|
+
function chooseMeaningfulSessionSummary(request, completed) {
|
|
3150
|
+
if (request && !looksLikeFileOperationTitle2(request))
|
|
3151
|
+
return request;
|
|
3152
|
+
if (completed) {
|
|
3153
|
+
const lines = completed.split(`
|
|
3154
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).map((line) => stripInlineSectionLabel2(line)).filter((line) => !looksLikeFileOperationTitle2(line));
|
|
3155
|
+
if (lines.length > 0)
|
|
3156
|
+
return lines[0] ?? null;
|
|
3157
|
+
}
|
|
3158
|
+
return request ?? completed ?? null;
|
|
3159
|
+
}
|
|
2881
3160
|
function buildProjectSignalLine(context) {
|
|
2882
3161
|
if (!context.projectTypeCounts)
|
|
2883
3162
|
return null;
|
|
@@ -2888,29 +3167,26 @@ function toSplashLines(value, maxItems) {
|
|
|
2888
3167
|
if (!value)
|
|
2889
3168
|
return [];
|
|
2890
3169
|
const lines = value.split(`
|
|
2891
|
-
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) =>
|
|
3170
|
+
`).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^[-*]\s*/, "")).map((line) => stripInlineSectionLabel2(line)).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
|
|
2892
3171
|
return dedupeFragmentsInLines(lines);
|
|
2893
3172
|
}
|
|
2894
|
-
function
|
|
3173
|
+
function pickPrimarySummary(context) {
|
|
2895
3174
|
const summaries = context.summaries || [];
|
|
2896
3175
|
if (!summaries.length)
|
|
2897
3176
|
return null;
|
|
2898
|
-
|
|
3177
|
+
const meaningfulRecent = summaries.find((summary) => {
|
|
3178
|
+
const request = summary.request?.trim();
|
|
3179
|
+
const learned = summary.learned?.trim();
|
|
3180
|
+
const completed = summary.completed?.trim();
|
|
3181
|
+
return Boolean(request && !looksLikeFileOperationTitle2(request) || learned || hasMeaningfulCompleted(completed));
|
|
3182
|
+
});
|
|
3183
|
+
return meaningfulRecent ?? summaries[0] ?? null;
|
|
2899
3184
|
}
|
|
2900
|
-
function
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
score += 4;
|
|
2906
|
-
if (summary.learned)
|
|
2907
|
-
score += 5;
|
|
2908
|
-
if (summary.completed)
|
|
2909
|
-
score += 5;
|
|
2910
|
-
if (summary.next_steps)
|
|
2911
|
-
score += 4;
|
|
2912
|
-
score += Math.min(4, sectionItemCount(summary.completed) + sectionItemCount(summary.learned));
|
|
2913
|
-
return score;
|
|
3185
|
+
function hasMeaningfulCompleted(value) {
|
|
3186
|
+
if (!value)
|
|
3187
|
+
return false;
|
|
3188
|
+
return value.split(`
|
|
3189
|
+
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean).some((line) => !looksLikeFileOperationTitle2(stripInlineSectionLabel2(line)));
|
|
2914
3190
|
}
|
|
2915
3191
|
function sectionItemCount(value) {
|
|
2916
3192
|
if (!value)
|
|
@@ -2935,7 +3211,7 @@ function dedupeFragmentsInLines(lines) {
|
|
|
2935
3211
|
const seen = new Set;
|
|
2936
3212
|
const deduped = [];
|
|
2937
3213
|
for (const line of lines) {
|
|
2938
|
-
const normalized =
|
|
3214
|
+
const normalized = stripInlineSectionLabel2(line).toLowerCase().replace(/\s+/g, " ").trim();
|
|
2939
3215
|
if (!normalized || seen.has(normalized))
|
|
2940
3216
|
continue;
|
|
2941
3217
|
seen.add(normalized);
|
|
@@ -2943,8 +3219,22 @@ function dedupeFragmentsInLines(lines) {
|
|
|
2943
3219
|
}
|
|
2944
3220
|
return deduped;
|
|
2945
3221
|
}
|
|
3222
|
+
function hasRequestSection(lines) {
|
|
3223
|
+
return lines.some((line) => line.includes("Request:"));
|
|
3224
|
+
}
|
|
3225
|
+
function normalizeStartupItem(value) {
|
|
3226
|
+
return stripInlineSectionLabel2(value).replace(/^#?\d+:\s*/, "").replace(/^-\s*/, "").toLowerCase().replace(/\s+/g, " ").trim();
|
|
3227
|
+
}
|
|
3228
|
+
function isMeaningfulPrompt2(value) {
|
|
3229
|
+
if (!value)
|
|
3230
|
+
return false;
|
|
3231
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
3232
|
+
if (compact.length < 8)
|
|
3233
|
+
return false;
|
|
3234
|
+
return /[a-z]{3,}/i.test(compact);
|
|
3235
|
+
}
|
|
2946
3236
|
function chooseRequest(primary, fallback) {
|
|
2947
|
-
if (primary && !
|
|
3237
|
+
if (primary && !looksLikeFileOperationTitle2(primary))
|
|
2948
3238
|
return primary;
|
|
2949
3239
|
return fallback;
|
|
2950
3240
|
}
|
|
@@ -2962,15 +3252,15 @@ function isWeakCompletedSection(value) {
|
|
|
2962
3252
|
`).map((line) => line.trim().replace(/^[-*]\s*/, "")).filter(Boolean);
|
|
2963
3253
|
if (!items.length)
|
|
2964
3254
|
return true;
|
|
2965
|
-
const weakCount = items.filter((item) =>
|
|
3255
|
+
const weakCount = items.filter((item) => looksLikeFileOperationTitle2(item)).length;
|
|
2966
3256
|
return weakCount === items.length;
|
|
2967
3257
|
}
|
|
2968
|
-
function
|
|
3258
|
+
function looksLikeFileOperationTitle2(value) {
|
|
2969
3259
|
return /^(modified|updated|edited|touched|changed|extended|refactored|redesigned)\s+[A-Za-z0-9_.\-\/]+(?:\s*\([^)]*\))?$/i.test(value.trim());
|
|
2970
3260
|
}
|
|
2971
3261
|
function scoreSplashLine(value) {
|
|
2972
3262
|
let score = 0;
|
|
2973
|
-
if (!
|
|
3263
|
+
if (!looksLikeFileOperationTitle2(value))
|
|
2974
3264
|
score += 2;
|
|
2975
3265
|
if (/[:;]/.test(value))
|
|
2976
3266
|
score += 1;
|
|
@@ -2979,9 +3269,9 @@ function scoreSplashLine(value) {
|
|
|
2979
3269
|
return score;
|
|
2980
3270
|
}
|
|
2981
3271
|
function buildObservationFallbacks(context) {
|
|
2982
|
-
const request = context.observations.find((obs) => !
|
|
3272
|
+
const request = context.observations.find((obs) => obs.type !== "decision" && !looksLikeFileOperationTitle2(obs.title))?.title ?? null;
|
|
2983
3273
|
const investigated = collectObservationTitles(context, (obs) => obs.type === "discovery", 2);
|
|
2984
|
-
const completed = collectObservationTitles(context, (obs) => ["feature", "refactor", "change"].includes(obs.type) && !
|
|
3274
|
+
const completed = collectObservationTitles(context, (obs) => ["bugfix", "feature", "refactor", "change"].includes(obs.type) && !looksLikeFileOperationTitle2(obs.title), 2);
|
|
2985
3275
|
return {
|
|
2986
3276
|
request,
|
|
2987
3277
|
investigated,
|
|
@@ -2994,18 +3284,18 @@ function collectObservationTitles(context, predicate, limit) {
|
|
|
2994
3284
|
for (const obs of context.observations) {
|
|
2995
3285
|
if (!predicate(obs))
|
|
2996
3286
|
continue;
|
|
2997
|
-
const normalized =
|
|
3287
|
+
const normalized = stripInlineSectionLabel2(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
|
|
2998
3288
|
if (!normalized || seen.has(normalized))
|
|
2999
3289
|
continue;
|
|
3000
3290
|
seen.add(normalized);
|
|
3001
|
-
picked.push(`- ${
|
|
3291
|
+
picked.push(`- ${stripInlineSectionLabel2(obs.title)}`);
|
|
3002
3292
|
if (picked.length >= limit)
|
|
3003
3293
|
break;
|
|
3004
3294
|
}
|
|
3005
3295
|
return picked.length ? picked.join(`
|
|
3006
3296
|
`) : null;
|
|
3007
3297
|
}
|
|
3008
|
-
function
|
|
3298
|
+
function stripInlineSectionLabel2(value) {
|
|
3009
3299
|
return value.replace(/^(request|investigated|learned|completed|next steps|digest|summary):\s*/i, "").trim();
|
|
3010
3300
|
}
|
|
3011
3301
|
function pickRelevantStaleDecision(context, summary) {
|
|
@@ -3094,4 +3384,10 @@ function capitalize(value) {
|
|
|
3094
3384
|
return value;
|
|
3095
3385
|
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3096
3386
|
}
|
|
3387
|
+
var __testables = {
|
|
3388
|
+
formatVisibleStartupBrief
|
|
3389
|
+
};
|
|
3097
3390
|
runHook("session-start", main);
|
|
3391
|
+
export {
|
|
3392
|
+
__testables
|
|
3393
|
+
};
|