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.
- package/README.md +2 -1
- package/dist/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +0 -9
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/dist/resources/extensions/gsd/auto-worktree.ts +119 -1
- package/dist/resources/extensions/gsd/auto.ts +184 -36
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- package/dist/resources/extensions/gsd/doctor.ts +2 -0
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +34 -12
- package/dist/resources/extensions/gsd/index.ts +14 -1
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +39 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +38 -1
- package/packages/pi-ai/src/providers/mistral.ts +3 -0
- package/packages/pi-ai/src/types.ts +19 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
- package/src/resources/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +119 -1
- package/src/resources/extensions/gsd/auto.ts +184 -36
- package/src/resources/extensions/gsd/cache.ts +3 -1
- package/src/resources/extensions/gsd/doctor.ts +2 -0
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +34 -12
- package/src/resources/extensions/gsd/index.ts +14 -1
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|