gsd-pi 2.24.0 → 2.25.0

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.
Files changed (113) hide show
  1. package/README.md +2 -1
  2. package/dist/models-resolver.d.ts +0 -11
  3. package/dist/models-resolver.js +0 -15
  4. package/dist/resource-loader.d.ts +0 -1
  5. package/dist/resource-loader.js +0 -9
  6. package/dist/resources/GSD-WORKFLOW.md +12 -9
  7. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  8. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  9. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  10. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  11. package/dist/resources/extensions/gsd/auto-worktree.ts +119 -1
  12. package/dist/resources/extensions/gsd/auto.ts +184 -36
  13. package/dist/resources/extensions/gsd/cache.ts +3 -1
  14. package/dist/resources/extensions/gsd/doctor.ts +2 -0
  15. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  16. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  17. package/dist/resources/extensions/gsd/guided-flow.ts +34 -12
  18. package/dist/resources/extensions/gsd/index.ts +14 -1
  19. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  20. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  21. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  22. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  23. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  24. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
  25. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  29. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  31. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  32. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  33. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  34. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  35. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  36. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  37. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  38. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  39. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  40. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  41. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  42. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  43. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  44. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  45. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  46. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  47. package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
  48. package/package.json +1 -1
  49. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  50. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  51. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  52. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  53. package/packages/pi-ai/dist/providers/anthropic.js +39 -0
  54. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  55. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  56. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  57. package/packages/pi-ai/dist/types.d.ts +23 -1
  58. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/types.js.map +1 -1
  60. package/packages/pi-ai/src/providers/anthropic.ts +38 -1
  61. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  62. package/packages/pi-ai/src/types.ts +19 -1
  63. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  64. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  65. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  71. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
  72. package/src/resources/GSD-WORKFLOW.md +12 -9
  73. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  74. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  75. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  76. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  77. package/src/resources/extensions/gsd/auto-worktree.ts +119 -1
  78. package/src/resources/extensions/gsd/auto.ts +184 -36
  79. package/src/resources/extensions/gsd/cache.ts +3 -1
  80. package/src/resources/extensions/gsd/doctor.ts +2 -0
  81. package/src/resources/extensions/gsd/git-service.ts +74 -14
  82. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  83. package/src/resources/extensions/gsd/guided-flow.ts +34 -12
  84. package/src/resources/extensions/gsd/index.ts +14 -1
  85. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  86. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  87. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  88. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  89. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  90. package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
  91. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  92. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  93. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  94. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  95. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  96. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  97. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  98. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  99. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  100. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  101. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  102. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  103. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  104. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  105. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  106. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  107. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  108. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  109. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  110. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  111. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  112. package/src/resources/extensions/gsd/worktree.ts +9 -2
  113. package/src/resources/extensions/search-the-web/native-search.ts +15 -5
@@ -29,6 +29,17 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
29
29
  import { showConfirm } from "../shared/confirm-ui.js";
30
30
  import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
31
31
 
32
+ // ─── Commit Instruction Helpers ──────────────────────────────────────────────
33
+
34
+ /** Build conditional commit instruction for planning prompts based on commit_docs preference. */
35
+ function buildDocsCommitInstruction(message: string): string {
36
+ const prefs = loadEffectiveGSDPreferences();
37
+ const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
38
+ return commitDocsEnabled
39
+ ? `Commit: \`${message}\``
40
+ : "Do not commit — planning docs are not tracked in git for this project.";
41
+ }
42
+
32
43
  // ─── Auto-start after discuss ─────────────────────────────────────────────────
33
44
 
34
45
  /** Stashed context + flag for auto-starting after discuss phase completes */
@@ -198,6 +209,8 @@ function buildDiscussPrompt(nextId: string, preamble: string, _basePath: string)
198
209
  contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
199
210
  roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
200
211
  inlinedTemplates,
212
+ commitInstruction: buildDocsCommitInstruction(`docs(${nextId}): context, requirements, and roadmap`),
213
+ multiMilestoneCommitInstruction: buildDocsCommitInstruction("docs: project plan — N milestones"),
201
214
  });
202
215
  }
203
216
 
@@ -220,6 +233,8 @@ function buildHeadlessDiscussPrompt(nextId: string, seedContext: string, _basePa
220
233
  contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
221
234
  roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
222
235
  inlinedTemplates,
236
+ commitInstruction: buildDocsCommitInstruction(`docs(${nextId}): context, requirements, and roadmap`),
237
+ multiMilestoneCommitInstruction: buildDocsCommitInstruction("docs: project plan — N milestones"),
223
238
  });
224
239
  }
225
240
 
@@ -648,6 +663,7 @@ async function showQueueAdd(
648
663
  nextIdPlus1,
649
664
  existingMilestonesContext: existingContext,
650
665
  inlinedTemplates: queueInlinedTemplates,
666
+ commitInstruction: buildDocsCommitInstruction("docs: queue <milestone list>"),
651
667
  });
652
668
 
653
669
  pi.sendMessage(
@@ -834,6 +850,7 @@ async function buildDiscussSlicePrompt(
834
850
  contextPath: sliceContextPath,
835
851
  projectRoot: base,
836
852
  inlinedTemplates,
853
+ commitInstruction: buildDocsCommitInstruction(`docs(${mid}/${sid}): slice context from discuss`),
837
854
  });
838
855
  }
839
856
 
@@ -870,7 +887,7 @@ export async function showDiscuss(
870
887
  const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
871
888
  const draftContent = draftFile ? await loadFile(draftFile) : null;
872
889
 
873
- const choice = await showNextAction(ctx as any, {
890
+ const choice = await showNextAction(ctx, {
874
891
  title: `GSD — ${mid}: ${milestoneTitle}`,
875
892
  summary: ["This milestone has a draft context from a prior discussion.", "It needs a dedicated discussion before auto-planning can begin."],
876
893
  actions: [
@@ -899,6 +916,7 @@ export async function showDiscuss(
899
916
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
900
917
  const basePrompt = loadPrompt("guided-discuss-milestone", {
901
918
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
919
+ commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
902
920
  });
903
921
  const seed = draftContent
904
922
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
@@ -911,6 +929,7 @@ export async function showDiscuss(
911
929
  pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
912
930
  dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
913
931
  milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
932
+ commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
914
933
  }), "gsd-discuss");
915
934
  } else if (choice === "skip_milestone") {
916
935
  const milestoneIds = findMilestoneIds(basePath);
@@ -947,7 +966,7 @@ export async function showDiscuss(
947
966
  recommended: i === 0,
948
967
  }));
949
968
 
950
- const choice = await showNextAction(ctx as any, {
969
+ const choice = await showNextAction(ctx, {
951
970
  title: "GSD — Discuss a slice",
952
971
  summary: [
953
972
  `${mid}: ${milestoneTitle}`,
@@ -1056,7 +1075,7 @@ export async function showSmartEntry(
1056
1075
  const crashLock = readCrashLock(basePath);
1057
1076
  if (crashLock) {
1058
1077
  clearLock(basePath);
1059
- const resume = await showNextAction(ctx as any, {
1078
+ const resume = await showNextAction(ctx, {
1060
1079
  title: "GSD — Interrupted Session Detected",
1061
1080
  summary: [formatCrashInfo(crashLock)],
1062
1081
  actions: [
@@ -1116,7 +1135,7 @@ export async function showSmartEntry(
1116
1135
  basePath
1117
1136
  ));
1118
1137
  } else {
1119
- const choice = await showNextAction(ctx as any, {
1138
+ const choice = await showNextAction(ctx, {
1120
1139
  title: "GSD — Get Shit Done",
1121
1140
  summary: ["No active milestone."],
1122
1141
  actions: [
@@ -1146,7 +1165,7 @@ export async function showSmartEntry(
1146
1165
 
1147
1166
  // ── All milestones complete → New milestone ──────────────────────────
1148
1167
  if (state.phase === "complete") {
1149
- const choice = await showNextAction(ctx as any, {
1168
+ const choice = await showNextAction(ctx, {
1150
1169
  title: `GSD — ${milestoneId}: ${milestoneTitle}`,
1151
1170
  summary: ["All milestones complete."],
1152
1171
  actions: [
@@ -1187,7 +1206,7 @@ export async function showSmartEntry(
1187
1206
  const draftFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT-DRAFT");
1188
1207
  const draftContent = draftFile ? await loadFile(draftFile) : null;
1189
1208
 
1190
- const choice = await showNextAction(ctx as any, {
1209
+ const choice = await showNextAction(ctx, {
1191
1210
  title: `GSD — ${milestoneId}: ${milestoneTitle}`,
1192
1211
  summary: ["This milestone has a draft context from a prior discussion.", "It needs a dedicated discussion before auto-planning can begin."],
1193
1212
  actions: [
@@ -1216,6 +1235,7 @@ export async function showSmartEntry(
1216
1235
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
1217
1236
  const basePrompt = loadPrompt("guided-discuss-milestone", {
1218
1237
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1238
+ commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
1219
1239
  });
1220
1240
  const seed = draftContent
1221
1241
  ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
@@ -1228,6 +1248,7 @@ export async function showSmartEntry(
1228
1248
  pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
1229
1249
  dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1230
1250
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1251
+ commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
1231
1252
  }), "gsd-discuss");
1232
1253
  } else if (choice === "skip_milestone") {
1233
1254
  const milestoneIds = findMilestoneIds(basePath);
@@ -1278,7 +1299,7 @@ export async function showSmartEntry(
1278
1299
  },
1279
1300
  ];
1280
1301
 
1281
- const choice = await showNextAction(ctx as any, {
1302
+ const choice = await showNextAction(ctx, {
1282
1303
  title: `GSD — ${milestoneId}: ${milestoneTitle}`,
1283
1304
  summary: [hasContext ? "Context captured. Ready to create roadmap." : "New milestone — no roadmap yet."],
1284
1305
  actions,
@@ -1302,6 +1323,7 @@ export async function showSmartEntry(
1302
1323
  const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
1303
1324
  dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
1304
1325
  milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
1326
+ commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
1305
1327
  }));
1306
1328
  } else if (choice === "skip_milestone") {
1307
1329
  const milestoneIds = findMilestoneIds(basePath);
@@ -1315,7 +1337,7 @@ export async function showSmartEntry(
1315
1337
  } else if (choice === "discard_milestone") {
1316
1338
  const mDir = resolveMilestonePath(basePath, milestoneId);
1317
1339
  if (!mDir) return;
1318
- const confirmed = await showConfirm(ctx as any, {
1340
+ const confirmed = await showConfirm(ctx, {
1319
1341
  title: "Discard milestone?",
1320
1342
  message: `This will permanently delete ${milestoneId} and all its contents.`,
1321
1343
  confirmLabel: "Discard",
@@ -1342,7 +1364,7 @@ export async function showSmartEntry(
1342
1364
  },
1343
1365
  ];
1344
1366
 
1345
- const choice = await showNextAction(ctx as any, {
1367
+ const choice = await showNextAction(ctx, {
1346
1368
  title: `GSD — ${milestoneId}: ${milestoneTitle}`,
1347
1369
  summary: ["Roadmap exists. Ready to execute."],
1348
1370
  actions,
@@ -1400,7 +1422,7 @@ export async function showSmartEntry(
1400
1422
  ? `${sliceId}: ${sliceTitle} (${summaryParts.join(", ")})`
1401
1423
  : `${sliceId}: ${sliceTitle} — ready for planning.`;
1402
1424
 
1403
- const choice = await showNextAction(ctx as any, {
1425
+ const choice = await showNextAction(ctx, {
1404
1426
  title: `GSD — ${milestoneId} / ${sliceId}: ${sliceTitle}`,
1405
1427
  summary: [summaryLine],
1406
1428
  actions,
@@ -1431,7 +1453,7 @@ export async function showSmartEntry(
1431
1453
 
1432
1454
  // ── All tasks done → Complete slice ──────────────────────────────────
1433
1455
  if (state.phase === "summarizing") {
1434
- const choice = await showNextAction(ctx as any, {
1456
+ const choice = await showNextAction(ctx, {
1435
1457
  title: `GSD — ${milestoneId} / ${sliceId}: ${sliceTitle}`,
1436
1458
  summary: ["All tasks complete. Ready for slice summary."],
1437
1459
  actions: [
@@ -1475,7 +1497,7 @@ export async function showSmartEntry(
1475
1497
  const hasInterrupted = !!(continueFile && await loadFile(continueFile)) ||
1476
1498
  !!(sDir && await loadFile(join(sDir, "continue.md")));
1477
1499
 
1478
- const choice = await showNextAction(ctx as any, {
1500
+ const choice = await showNextAction(ctx, {
1479
1501
  title: `GSD — ${milestoneId} / ${sliceId}: ${sliceTitle}`,
1480
1502
  summary: [
1481
1503
  hasInterrupted
@@ -566,6 +566,19 @@ export default function (pi: ExtensionAPI) {
566
566
  }
567
567
  }
568
568
 
569
+ // Inject auto-learned project memories
570
+ let memoryBlock = "";
571
+ try {
572
+ const { getActiveMemoriesRanked, formatMemoriesForPrompt } = await import("./memory-store.js");
573
+ const memories = getActiveMemoriesRanked(30);
574
+ if (memories.length > 0) {
575
+ const formatted = formatMemoriesForPrompt(memories, 2000);
576
+ if (formatted) {
577
+ memoryBlock = `\n\n${formatted}`;
578
+ }
579
+ }
580
+ } catch { /* non-fatal */ }
581
+
569
582
  // Detect skills installed during this auto-mode session
570
583
  let newSkillsBlock = "";
571
584
  if (hasSkillSnapshot()) {
@@ -625,7 +638,7 @@ export default function (pi: ExtensionAPI) {
625
638
  ].join("\n");
626
639
  }
627
640
 
628
- const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${agentInstructionsBlock}${knowledgeBlock}${newSkillsBlock}${worktreeBlock}`;
641
+ const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${agentInstructionsBlock}${knowledgeBlock}${memoryBlock}${newSkillsBlock}${worktreeBlock}`;
629
642
  stopContextTimer({
630
643
  systemPromptSize: fullSystem.length,
631
644
  injectionSize: injection?.length ?? 0,
@@ -0,0 +1,352 @@
1
+ // GSD Memory Extractor — Background LLM extraction from activity logs
2
+ //
3
+ // After each unit completes, extracts durable knowledge from the session
4
+ // transcript and stores it as memory entries. One extraction at a time
5
+ // (mutex guard). Fire-and-forget — never blocks auto-mode.
6
+
7
+ import { readFileSync, statSync } from 'node:fs';
8
+ import type { ExtensionContext } from '@gsd/pi-coding-agent';
9
+ import type { Api, AssistantMessage, Model } from '@gsd/pi-ai';
10
+ import {
11
+ getActiveMemories,
12
+ isUnitProcessed,
13
+ markUnitProcessed,
14
+ applyMemoryActions,
15
+ decayStaleMemories,
16
+ } from './memory-store.js';
17
+ import type { MemoryAction } from './memory-store.js';
18
+
19
+ // ─── Types ──────────────────────────────────────────────────────────────────
20
+
21
+ export type LLMCallFn = (system: string, user: string) => Promise<string>;
22
+
23
+ // ─── Concurrency Guard ──────────────────────────────────────────────────────
24
+
25
+ let _extracting = false;
26
+ let _lastExtractionTime = 0;
27
+
28
+ const MIN_EXTRACTION_INTERVAL_MS = 30_000;
29
+
30
+ // ─── Skip Conditions ────────────────────────────────────────────────────────
31
+
32
+ const SKIP_TYPES = new Set([
33
+ 'complete-slice',
34
+ 'rewrite-docs',
35
+ 'triage-captures',
36
+ ]);
37
+
38
+ const MIN_ACTIVITY_SIZE = 1024; // 1KB
39
+
40
+ // ─── Secret Redaction ───────────────────────────────────────────────────────
41
+
42
+ const SECRET_PATTERNS = [
43
+ /(?:sk|pk|api[_-]?key|token|secret|password|credential|auth)[_-]?\w*[\s:=]+['"]?[\w\-./+=]{20,}['"]?/gi,
44
+ /AKIA[0-9A-Z]{16}/g,
45
+ /gh[pousr]_[A-Za-z0-9_]{36,}/g,
46
+ /[rsp]k_(?:live|test)_[A-Za-z0-9]{20,}/g,
47
+ /eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+/g,
48
+ /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
49
+ /(?:Bearer\s+)[A-Za-z0-9\-._~+/]+=*/gi,
50
+ /npm_[A-Za-z0-9]{36,}/g,
51
+ /sk-ant-[A-Za-z0-9\-_]{20,}/g,
52
+ /sk-[A-Za-z0-9]{40,}/g,
53
+ ];
54
+
55
+ function redactSecrets(text: string): string {
56
+ let result = text;
57
+ for (const pattern of SECRET_PATTERNS) {
58
+ // Reset lastIndex for global regexes
59
+ pattern.lastIndex = 0;
60
+ result = result.replace(pattern, '[REDACTED]');
61
+ }
62
+ return result;
63
+ }
64
+
65
+ // ─── Model Selection ────────────────────────────────────────────────────────
66
+
67
+ /**
68
+ * Build an LLM call function using the cheapest available model (preferring Haiku).
69
+ * Returns null if no models available.
70
+ */
71
+ export function buildMemoryLLMCall(ctx: ExtensionContext): LLMCallFn | null {
72
+ try {
73
+ const available = ctx.modelRegistry.getAvailable();
74
+ if (!available || available.length === 0) return null;
75
+
76
+ // Prefer Haiku by ID substring match
77
+ let model = available.find(m =>
78
+ m.id.toLowerCase().includes('haiku'),
79
+ );
80
+
81
+ // Fallback: cheapest by input cost
82
+ if (!model) {
83
+ model = [...available].sort((a, b) => a.cost.input - b.cost.input)[0];
84
+ }
85
+
86
+ if (!model) return null;
87
+
88
+ const selectedModel = model as Model<Api>;
89
+
90
+ return async (system: string, user: string): Promise<string> => {
91
+ const { completeSimple } = await import('@gsd/pi-ai');
92
+ const result: AssistantMessage = await completeSimple(selectedModel, {
93
+ systemPrompt: system,
94
+ messages: [{ role: 'user', content: [{ type: 'text', text: user }], timestamp: Date.now() }],
95
+ }, {
96
+ maxTokens: 2048,
97
+ temperature: 0,
98
+ });
99
+
100
+ // Extract text from response
101
+ const textParts = result.content
102
+ .filter((c): c is { type: 'text'; text: string } => c.type === 'text')
103
+ .map(c => c.text);
104
+ return textParts.join('');
105
+ };
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // ─── Extraction Prompts ─────────────────────────────────────────────────────
112
+
113
+ const EXTRACTION_SYSTEM = `You are a memory extraction agent for a software project. Analyze the session
114
+ transcript and identify durable knowledge worth remembering for future sessions.
115
+
116
+ Categories: architecture, convention, gotcha, preference, environment, pattern
117
+
118
+ Actions (return JSON array):
119
+ - CREATE: {"action": "CREATE", "category": "<cat>", "content": "<text>", "confidence": <0.6-0.95>}
120
+ - UPDATE: {"action": "UPDATE", "id": "<MEM###>", "content": "<revised text>"}
121
+ - REINFORCE: {"action": "REINFORCE", "id": "<MEM###>"}
122
+ - SUPERSEDE: {"action": "SUPERSEDE", "id": "<MEM###>", "superseded_by": "<MEM###>"}
123
+
124
+ Rules:
125
+ - Don't create memories for one-off bug fixes or temporary state
126
+ - Don't duplicate existing memories — use REINFORCE or UPDATE
127
+ - Keep content to 1-3 sentences
128
+ - Confidence: 0.6 tentative, 0.8 solid, 0.95 well-confirmed
129
+ - Prefer fewer high-quality memories over many low-quality ones
130
+ - Return empty array [] if nothing worth remembering
131
+ - NEVER include secrets, API keys, or passwords
132
+
133
+ Return ONLY a valid JSON array.`;
134
+
135
+ function buildExtractionUserPrompt(
136
+ unitType: string,
137
+ unitId: string,
138
+ existingMemories: { id: string; category: string; content: string }[],
139
+ transcript: string,
140
+ ): string {
141
+ let memoriesSection: string;
142
+ if (existingMemories.length === 0) {
143
+ memoriesSection = '(none yet)';
144
+ } else {
145
+ memoriesSection = existingMemories
146
+ .map((m, i) => `${i + 1}. [${m.id}] (${m.category}) ${m.content}`)
147
+ .join('\n');
148
+ }
149
+
150
+ return `## Current Active Memories\n${memoriesSection}\n\n## Session Transcript (${unitType}: ${unitId})\n${transcript}`;
151
+ }
152
+
153
+ // ─── Activity JSONL Parsing ─────────────────────────────────────────────────
154
+
155
+ /**
156
+ * Extract assistant message text from activity JSONL.
157
+ * Returns concatenated text content from assistant role entries.
158
+ */
159
+ function extractTranscriptFromActivity(raw: string, maxChars = 30_000): string {
160
+ const lines = raw.split('\n');
161
+ const parts: string[] = [];
162
+ let totalChars = 0;
163
+
164
+ for (const line of lines) {
165
+ if (!line.trim()) continue;
166
+ try {
167
+ const entry = JSON.parse(line);
168
+ if (entry.role !== 'assistant') continue;
169
+
170
+ // Handle content array or direct text
171
+ if (Array.isArray(entry.content)) {
172
+ for (const block of entry.content) {
173
+ if (block.type === 'text' && block.text) {
174
+ const text = block.text;
175
+ if (totalChars + text.length > maxChars) {
176
+ parts.push(text.substring(0, maxChars - totalChars));
177
+ return parts.join('\n\n');
178
+ }
179
+ parts.push(text);
180
+ totalChars += text.length;
181
+ }
182
+ }
183
+ } else if (typeof entry.content === 'string') {
184
+ const text = entry.content;
185
+ if (totalChars + text.length > maxChars) {
186
+ parts.push(text.substring(0, maxChars - totalChars));
187
+ return parts.join('\n\n');
188
+ }
189
+ parts.push(text);
190
+ totalChars += text.length;
191
+ }
192
+ } catch {
193
+ // Skip malformed lines
194
+ }
195
+ }
196
+
197
+ return parts.join('\n\n');
198
+ }
199
+
200
+ // ─── Response Parsing ───────────────────────────────────────────────────────
201
+
202
+ /**
203
+ * Parse the LLM response into memory actions.
204
+ * Strips markdown fences, validates required fields.
205
+ * Returns [] on any parse failure.
206
+ */
207
+ export function parseMemoryResponse(raw: string): MemoryAction[] {
208
+ try {
209
+ // Strip markdown code fences
210
+ let cleaned = raw.trim();
211
+ if (cleaned.startsWith('```')) {
212
+ cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '');
213
+ }
214
+
215
+ const parsed = JSON.parse(cleaned);
216
+ if (!Array.isArray(parsed)) return [];
217
+
218
+ const actions: MemoryAction[] = [];
219
+ for (const item of parsed) {
220
+ if (!item || typeof item !== 'object' || !item.action) continue;
221
+
222
+ switch (item.action) {
223
+ case 'CREATE':
224
+ if (typeof item.category === 'string' && typeof item.content === 'string') {
225
+ actions.push({
226
+ action: 'CREATE',
227
+ category: item.category,
228
+ content: item.content,
229
+ confidence: typeof item.confidence === 'number' ? item.confidence : undefined,
230
+ });
231
+ }
232
+ break;
233
+ case 'UPDATE':
234
+ if (typeof item.id === 'string' && typeof item.content === 'string') {
235
+ actions.push({
236
+ action: 'UPDATE',
237
+ id: item.id,
238
+ content: item.content,
239
+ confidence: typeof item.confidence === 'number' ? item.confidence : undefined,
240
+ });
241
+ }
242
+ break;
243
+ case 'REINFORCE':
244
+ if (typeof item.id === 'string') {
245
+ actions.push({ action: 'REINFORCE', id: item.id });
246
+ }
247
+ break;
248
+ case 'SUPERSEDE':
249
+ if (typeof item.id === 'string' && typeof item.superseded_by === 'string') {
250
+ actions.push({
251
+ action: 'SUPERSEDE',
252
+ id: item.id,
253
+ superseded_by: item.superseded_by,
254
+ });
255
+ }
256
+ break;
257
+ }
258
+ }
259
+
260
+ return actions;
261
+ } catch {
262
+ return [];
263
+ }
264
+ }
265
+
266
+ // ─── Main Extraction Function ───────────────────────────────────────────────
267
+
268
+ /**
269
+ * Extract memories from a completed unit's activity log.
270
+ * Fire-and-forget — never throws, mutex-guarded, respects rate limiting.
271
+ */
272
+ export async function extractMemoriesFromUnit(
273
+ activityFile: string,
274
+ unitType: string,
275
+ unitId: string,
276
+ llmCallFn: LLMCallFn,
277
+ ): Promise<void> {
278
+ // Mutex guard
279
+ if (_extracting) return;
280
+
281
+ // Rate limit
282
+ const now = Date.now();
283
+ if (now - _lastExtractionTime < MIN_EXTRACTION_INTERVAL_MS) return;
284
+
285
+ // Skip certain unit types
286
+ if (SKIP_TYPES.has(unitType)) return;
287
+
288
+ const unitKey = `${unitType}/${unitId}`;
289
+
290
+ // Already processed
291
+ if (isUnitProcessed(unitKey)) return;
292
+
293
+ // Check file size
294
+ try {
295
+ const stat = statSync(activityFile);
296
+ if (stat.size < MIN_ACTIVITY_SIZE) return;
297
+ } catch {
298
+ return;
299
+ }
300
+
301
+ _extracting = true;
302
+ _lastExtractionTime = now;
303
+
304
+ try {
305
+ // Read and parse activity file
306
+ const raw = readFileSync(activityFile, 'utf-8');
307
+ const transcript = extractTranscriptFromActivity(raw);
308
+ if (!transcript.trim()) return;
309
+
310
+ // Redact secrets
311
+ const safeTranscript = redactSecrets(transcript);
312
+
313
+ // Get current memories for context
314
+ const activeMemories = getActiveMemories().map(m => ({
315
+ id: m.id,
316
+ category: m.category,
317
+ content: m.content,
318
+ }));
319
+
320
+ // Build prompts
321
+ const userPrompt = buildExtractionUserPrompt(unitType, unitId, activeMemories, safeTranscript);
322
+
323
+ // Call LLM
324
+ const response = await llmCallFn(EXTRACTION_SYSTEM, userPrompt);
325
+
326
+ // Parse response
327
+ const actions = parseMemoryResponse(response);
328
+
329
+ // Apply actions
330
+ if (actions.length > 0) {
331
+ applyMemoryActions(actions, unitType, unitId);
332
+ }
333
+
334
+ // Decay stale memories periodically
335
+ decayStaleMemories(20);
336
+
337
+ // Mark unit as processed
338
+ markUnitProcessed(unitKey, activityFile);
339
+ } catch {
340
+ // Non-fatal — memory extraction failure should never affect auto-mode
341
+ } finally {
342
+ _extracting = false;
343
+ }
344
+ }
345
+
346
+ // ─── Testing Helpers ────────────────────────────────────────────────────────
347
+
348
+ /** Reset extraction state (testing only). */
349
+ export function _resetExtractionState(): void {
350
+ _extracting = false;
351
+ _lastExtractionTime = 0;
352
+ }