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.
@@ -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: directory
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
- lines.push("## Recent Requests");
595
- for (const prompt of context.recentPrompts.slice(0, 5)) {
596
- const label = prompt.prompt_number > 0 ? `#${prompt.prompt_number}` : new Date(prompt.created_at_epoch * 1000).toISOString().split("T")[0];
597
- lines.push(`- ${label}: ${truncateText(prompt.prompt.replace(/\s+/g, " "), 160)}`);
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
- lines.push("## Recent Sessions");
610
- for (const session of context.recentSessions.slice(0, 4)) {
611
- const summary = session.request ?? session.completed ?? "(no summary)";
612
- lines.push(`- ${session.session_id}: ${truncateText(summary.replace(/\s+/g, " "), 140)} ` + `(prompts ${session.prompt_count}, tools ${session.tool_event_count}, obs ${session.observation_count})`);
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 = dirname(fileURLToPath(import.meta.url));
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: change.metadata?.type ?? "discovery",
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 brief${c2.reset}`);
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 = pickBestSummary(context);
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 = buildSessionFallbacks(context);
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", chooseRequest(latest.request, promptFallback ?? observationFallbacks.request), 1],
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 (promptFallback) {
2830
- lines.push(`${c2.cyan}Request:${c2.reset}`);
2831
- lines.push(` - ${truncateInline(promptFallback, 140)}`);
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, 140)}`);
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, 140)}`);
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, 140)}`);
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) => !looksLikeFileOperationTitle(obs.title)).slice(0, 2);
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, 10);
3081
+ return lines.slice(0, 14);
2860
3082
  }
2861
3083
  function buildPromptFallback(context) {
2862
- const latest = context.recentPrompts?.[0];
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}: ${detail}`.trim();
3104
+ return `${tool.tool_name}${detail ? `: ${detail}` : ""}`.trim();
2871
3105
  }).filter((item) => item.length > 0);
2872
3106
  }
2873
- function buildSessionFallbacks(context) {
3107
+ function sessionFallbacksFromContext(context) {
2874
3108
  return (context.recentSessions ?? []).slice(0, 2).map((session) => {
2875
- const summary = session.request ?? session.completed ?? "";
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) => stripInlineSectionLabel(line)).map((line) => dedupeFragments(line)).filter(Boolean).sort((a, b) => scoreSplashLine(b) - scoreSplashLine(a)).slice(0, maxItems).map((line) => `- ${truncateInline(line, 140)}`);
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 pickBestSummary(context) {
3173
+ function pickPrimarySummary(context) {
2895
3174
  const summaries = context.summaries || [];
2896
3175
  if (!summaries.length)
2897
3176
  return null;
2898
- return [...summaries].sort((a, b) => scoreSummary(b) - scoreSummary(a))[0] ?? null;
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 scoreSummary(summary) {
2901
- let score = 0;
2902
- if (summary.request)
2903
- score += 3;
2904
- if (summary.investigated)
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 = stripInlineSectionLabel(line).toLowerCase().replace(/\s+/g, " ").trim();
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 && !looksLikeFileOperationTitle(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) => looksLikeFileOperationTitle(item)).length;
3255
+ const weakCount = items.filter((item) => looksLikeFileOperationTitle2(item)).length;
2966
3256
  return weakCount === items.length;
2967
3257
  }
2968
- function looksLikeFileOperationTitle(value) {
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 (!looksLikeFileOperationTitle(value))
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) => !looksLikeFileOperationTitle(obs.title))?.title ?? null;
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) && !looksLikeFileOperationTitle(obs.title), 2);
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 = stripInlineSectionLabel(obs.title).toLowerCase().replace(/\s+/g, " ").trim();
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(`- ${stripInlineSectionLabel(obs.title)}`);
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 stripInlineSectionLabel(value) {
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
+ };