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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// GSD Memory Store — CRUD, ranked queries, maintenance, and prompt formatting
|
|
2
|
+
//
|
|
3
|
+
// Storage layer for auto-learned project memories. Follows context-store.ts patterns.
|
|
4
|
+
// All functions degrade gracefully: return empty results when DB unavailable, never throw.
|
|
5
|
+
|
|
6
|
+
import { isDbAvailable, _getAdapter, transaction } from './gsd-db.js';
|
|
7
|
+
|
|
8
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface Memory {
|
|
11
|
+
seq: number;
|
|
12
|
+
id: string;
|
|
13
|
+
category: string;
|
|
14
|
+
content: string;
|
|
15
|
+
confidence: number;
|
|
16
|
+
source_unit_type: string | null;
|
|
17
|
+
source_unit_id: string | null;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
superseded_by: string | null;
|
|
21
|
+
hit_count: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type MemoryActionCreate = {
|
|
25
|
+
action: 'CREATE';
|
|
26
|
+
category: string;
|
|
27
|
+
content: string;
|
|
28
|
+
confidence?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type MemoryActionUpdate = {
|
|
32
|
+
action: 'UPDATE';
|
|
33
|
+
id: string;
|
|
34
|
+
content: string;
|
|
35
|
+
confidence?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MemoryActionReinforce = {
|
|
39
|
+
action: 'REINFORCE';
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type MemoryActionSupersede = {
|
|
44
|
+
action: 'SUPERSEDE';
|
|
45
|
+
id: string;
|
|
46
|
+
superseded_by: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type MemoryAction =
|
|
50
|
+
| MemoryActionCreate
|
|
51
|
+
| MemoryActionUpdate
|
|
52
|
+
| MemoryActionReinforce
|
|
53
|
+
| MemoryActionSupersede;
|
|
54
|
+
|
|
55
|
+
// ─── Category Display Order ─────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const CATEGORY_PRIORITY: Record<string, number> = {
|
|
58
|
+
gotcha: 0,
|
|
59
|
+
convention: 1,
|
|
60
|
+
architecture: 2,
|
|
61
|
+
pattern: 3,
|
|
62
|
+
environment: 4,
|
|
63
|
+
preference: 5,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ─── Row Mapping ────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function rowToMemory(row: Record<string, unknown>): Memory {
|
|
69
|
+
return {
|
|
70
|
+
seq: row['seq'] as number,
|
|
71
|
+
id: row['id'] as string,
|
|
72
|
+
category: row['category'] as string,
|
|
73
|
+
content: row['content'] as string,
|
|
74
|
+
confidence: row['confidence'] as number,
|
|
75
|
+
source_unit_type: (row['source_unit_type'] as string) ?? null,
|
|
76
|
+
source_unit_id: (row['source_unit_id'] as string) ?? null,
|
|
77
|
+
created_at: row['created_at'] as string,
|
|
78
|
+
updated_at: row['updated_at'] as string,
|
|
79
|
+
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
80
|
+
hit_count: row['hit_count'] as number,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Query Functions ────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get all memories where superseded_by IS NULL.
|
|
88
|
+
* Returns [] if DB is not available. Never throws.
|
|
89
|
+
*/
|
|
90
|
+
export function getActiveMemories(): Memory[] {
|
|
91
|
+
if (!isDbAvailable()) return [];
|
|
92
|
+
const adapter = _getAdapter();
|
|
93
|
+
if (!adapter) return [];
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const rows = adapter.prepare('SELECT * FROM memories WHERE superseded_by IS NULL').all();
|
|
97
|
+
return rows.map(rowToMemory);
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get active memories ordered by ranking score: confidence * (1 + hit_count * 0.1).
|
|
105
|
+
* Higher-scored memories are more relevant and frequently confirmed.
|
|
106
|
+
*/
|
|
107
|
+
export function getActiveMemoriesRanked(limit = 30): Memory[] {
|
|
108
|
+
if (!isDbAvailable()) return [];
|
|
109
|
+
const adapter = _getAdapter();
|
|
110
|
+
if (!adapter) return [];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const rows = adapter.prepare(
|
|
114
|
+
`SELECT * FROM memories
|
|
115
|
+
WHERE superseded_by IS NULL
|
|
116
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) DESC
|
|
117
|
+
LIMIT :limit`,
|
|
118
|
+
).all({ ':limit': limit });
|
|
119
|
+
return rows.map(rowToMemory);
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate the next memory ID: MEM + zero-padded 3-digit from MAX(seq).
|
|
127
|
+
* Returns MEM001 if no memories exist.
|
|
128
|
+
*/
|
|
129
|
+
export function nextMemoryId(): string {
|
|
130
|
+
if (!isDbAvailable()) return 'MEM001';
|
|
131
|
+
const adapter = _getAdapter();
|
|
132
|
+
if (!adapter) return 'MEM001';
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const row = adapter
|
|
136
|
+
.prepare('SELECT MAX(seq) as max_seq FROM memories')
|
|
137
|
+
.get();
|
|
138
|
+
const maxSeq = row ? (row['max_seq'] as number | null) : null;
|
|
139
|
+
if (maxSeq == null || isNaN(maxSeq)) return 'MEM001';
|
|
140
|
+
const next = maxSeq + 1;
|
|
141
|
+
return `MEM${String(next).padStart(3, '0')}`;
|
|
142
|
+
} catch {
|
|
143
|
+
return 'MEM001';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Mutation Functions ─────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Insert a new memory with auto-assigned ID.
|
|
151
|
+
* Returns the assigned ID, or null on failure.
|
|
152
|
+
*/
|
|
153
|
+
export function createMemory(fields: {
|
|
154
|
+
category: string;
|
|
155
|
+
content: string;
|
|
156
|
+
confidence?: number;
|
|
157
|
+
source_unit_type?: string;
|
|
158
|
+
source_unit_id?: string;
|
|
159
|
+
}): string | null {
|
|
160
|
+
if (!isDbAvailable()) return null;
|
|
161
|
+
const adapter = _getAdapter();
|
|
162
|
+
if (!adapter) return null;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const id = nextMemoryId();
|
|
166
|
+
const now = new Date().toISOString();
|
|
167
|
+
adapter.prepare(
|
|
168
|
+
`INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at)
|
|
169
|
+
VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at)`,
|
|
170
|
+
).run({
|
|
171
|
+
':id': id,
|
|
172
|
+
':category': fields.category,
|
|
173
|
+
':content': fields.content,
|
|
174
|
+
':confidence': fields.confidence ?? 0.8,
|
|
175
|
+
':source_unit_type': fields.source_unit_type ?? null,
|
|
176
|
+
':source_unit_id': fields.source_unit_id ?? null,
|
|
177
|
+
':created_at': now,
|
|
178
|
+
':updated_at': now,
|
|
179
|
+
});
|
|
180
|
+
return id;
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Update a memory's content and optionally its confidence.
|
|
188
|
+
*/
|
|
189
|
+
export function updateMemoryContent(id: string, content: string, confidence?: number): boolean {
|
|
190
|
+
if (!isDbAvailable()) return false;
|
|
191
|
+
const adapter = _getAdapter();
|
|
192
|
+
if (!adapter) return false;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const now = new Date().toISOString();
|
|
196
|
+
if (confidence != null) {
|
|
197
|
+
adapter.prepare(
|
|
198
|
+
'UPDATE memories SET content = :content, confidence = :confidence, updated_at = :updated_at WHERE id = :id',
|
|
199
|
+
).run({ ':content': content, ':confidence': confidence, ':updated_at': now, ':id': id });
|
|
200
|
+
} else {
|
|
201
|
+
adapter.prepare(
|
|
202
|
+
'UPDATE memories SET content = :content, updated_at = :updated_at WHERE id = :id',
|
|
203
|
+
).run({ ':content': content, ':updated_at': now, ':id': id });
|
|
204
|
+
}
|
|
205
|
+
return true;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Reinforce a memory: increment hit_count, update timestamp.
|
|
213
|
+
*/
|
|
214
|
+
export function reinforceMemory(id: string): boolean {
|
|
215
|
+
if (!isDbAvailable()) return false;
|
|
216
|
+
const adapter = _getAdapter();
|
|
217
|
+
if (!adapter) return false;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
adapter.prepare(
|
|
221
|
+
'UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id',
|
|
222
|
+
).run({ ':updated_at': new Date().toISOString(), ':id': id });
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Mark a memory as superseded by another.
|
|
231
|
+
*/
|
|
232
|
+
export function supersedeMemory(oldId: string, newId: string): boolean {
|
|
233
|
+
if (!isDbAvailable()) return false;
|
|
234
|
+
const adapter = _getAdapter();
|
|
235
|
+
if (!adapter) return false;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
adapter.prepare(
|
|
239
|
+
'UPDATE memories SET superseded_by = :new_id, updated_at = :updated_at WHERE id = :old_id',
|
|
240
|
+
).run({ ':new_id': newId, ':updated_at': new Date().toISOString(), ':old_id': oldId });
|
|
241
|
+
return true;
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Processed Unit Tracking ────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if a unit has already been processed for memory extraction.
|
|
251
|
+
*/
|
|
252
|
+
export function isUnitProcessed(unitKey: string): boolean {
|
|
253
|
+
if (!isDbAvailable()) return false;
|
|
254
|
+
const adapter = _getAdapter();
|
|
255
|
+
if (!adapter) return false;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const row = adapter.prepare(
|
|
259
|
+
'SELECT 1 FROM memory_processed_units WHERE unit_key = :key',
|
|
260
|
+
).get({ ':key': unitKey });
|
|
261
|
+
return row != null;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Record that a unit has been processed for memory extraction.
|
|
269
|
+
*/
|
|
270
|
+
export function markUnitProcessed(unitKey: string, activityFile: string): boolean {
|
|
271
|
+
if (!isDbAvailable()) return false;
|
|
272
|
+
const adapter = _getAdapter();
|
|
273
|
+
if (!adapter) return false;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
adapter.prepare(
|
|
277
|
+
`INSERT OR IGNORE INTO memory_processed_units (unit_key, activity_file, processed_at)
|
|
278
|
+
VALUES (:key, :file, :at)`,
|
|
279
|
+
).run({ ':key': unitKey, ':file': activityFile, ':at': new Date().toISOString() });
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ─── Maintenance ────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Reduce confidence for memories not updated within the last N processed units.
|
|
290
|
+
* "Stale" = updated_at is older than the Nth most recent processed_at.
|
|
291
|
+
*/
|
|
292
|
+
export function decayStaleMemories(thresholdUnits = 20): void {
|
|
293
|
+
if (!isDbAvailable()) return;
|
|
294
|
+
const adapter = _getAdapter();
|
|
295
|
+
if (!adapter) return;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
// Find the timestamp of the Nth most recent processed unit
|
|
299
|
+
const row = adapter.prepare(
|
|
300
|
+
`SELECT processed_at FROM memory_processed_units
|
|
301
|
+
ORDER BY processed_at DESC
|
|
302
|
+
LIMIT 1 OFFSET :offset`,
|
|
303
|
+
).get({ ':offset': thresholdUnits - 1 });
|
|
304
|
+
|
|
305
|
+
if (!row) return; // not enough processed units yet
|
|
306
|
+
|
|
307
|
+
const cutoff = row['processed_at'] as string;
|
|
308
|
+
adapter.prepare(
|
|
309
|
+
`UPDATE memories
|
|
310
|
+
SET confidence = MAX(0.1, confidence - 0.1), updated_at = :now
|
|
311
|
+
WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`,
|
|
312
|
+
).run({ ':now': new Date().toISOString(), ':cutoff': cutoff });
|
|
313
|
+
} catch {
|
|
314
|
+
// non-fatal
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Supersede lowest-ranked memories when count exceeds cap.
|
|
320
|
+
*/
|
|
321
|
+
export function enforceMemoryCap(max = 50): void {
|
|
322
|
+
if (!isDbAvailable()) return;
|
|
323
|
+
const adapter = _getAdapter();
|
|
324
|
+
if (!adapter) return;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const countRow = adapter.prepare(
|
|
328
|
+
'SELECT count(*) as cnt FROM memories WHERE superseded_by IS NULL',
|
|
329
|
+
).get();
|
|
330
|
+
const count = (countRow?.['cnt'] as number) ?? 0;
|
|
331
|
+
if (count <= max) return;
|
|
332
|
+
|
|
333
|
+
const excess = count - max;
|
|
334
|
+
// Find the IDs of the lowest-ranked active memories
|
|
335
|
+
const rows = adapter.prepare(
|
|
336
|
+
`SELECT id FROM memories
|
|
337
|
+
WHERE superseded_by IS NULL
|
|
338
|
+
ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
|
|
339
|
+
LIMIT :limit`,
|
|
340
|
+
).all({ ':limit': excess });
|
|
341
|
+
|
|
342
|
+
const now = new Date().toISOString();
|
|
343
|
+
for (const row of rows) {
|
|
344
|
+
adapter.prepare(
|
|
345
|
+
'UPDATE memories SET superseded_by = :reason, updated_at = :now WHERE id = :id',
|
|
346
|
+
).run({ ':reason': 'CAP_EXCEEDED', ':now': now, ':id': row['id'] as string });
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
// non-fatal
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Action Application ─────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Process an array of memory actions in a transaction.
|
|
357
|
+
* Calls enforceMemoryCap at the end.
|
|
358
|
+
*/
|
|
359
|
+
export function applyMemoryActions(
|
|
360
|
+
actions: MemoryAction[],
|
|
361
|
+
unitType?: string,
|
|
362
|
+
unitId?: string,
|
|
363
|
+
): void {
|
|
364
|
+
if (!isDbAvailable() || actions.length === 0) return;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
transaction(() => {
|
|
368
|
+
for (const action of actions) {
|
|
369
|
+
switch (action.action) {
|
|
370
|
+
case 'CREATE':
|
|
371
|
+
createMemory({
|
|
372
|
+
category: action.category,
|
|
373
|
+
content: action.content,
|
|
374
|
+
confidence: action.confidence,
|
|
375
|
+
source_unit_type: unitType,
|
|
376
|
+
source_unit_id: unitId,
|
|
377
|
+
});
|
|
378
|
+
break;
|
|
379
|
+
case 'UPDATE':
|
|
380
|
+
updateMemoryContent(action.id, action.content, action.confidence);
|
|
381
|
+
break;
|
|
382
|
+
case 'REINFORCE':
|
|
383
|
+
reinforceMemory(action.id);
|
|
384
|
+
break;
|
|
385
|
+
case 'SUPERSEDE':
|
|
386
|
+
supersedeMemory(action.id, action.superseded_by);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
enforceMemoryCap();
|
|
391
|
+
});
|
|
392
|
+
} catch {
|
|
393
|
+
// non-fatal — transaction will have rolled back
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─── Prompt Formatting ──────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Format memories as categorized markdown for system prompt injection.
|
|
401
|
+
* Truncates to token budget (~4 chars per token).
|
|
402
|
+
*/
|
|
403
|
+
export function formatMemoriesForPrompt(memories: Memory[], tokenBudget = 2000): string {
|
|
404
|
+
if (memories.length === 0) return '';
|
|
405
|
+
|
|
406
|
+
const charBudget = tokenBudget * 4;
|
|
407
|
+
const header = '## Project Memory (auto-learned)\n';
|
|
408
|
+
let output = header;
|
|
409
|
+
let remaining = charBudget - header.length;
|
|
410
|
+
|
|
411
|
+
// Group by category
|
|
412
|
+
const grouped = new Map<string, Memory[]>();
|
|
413
|
+
for (const m of memories) {
|
|
414
|
+
const list = grouped.get(m.category) ?? [];
|
|
415
|
+
list.push(m);
|
|
416
|
+
grouped.set(m.category, list);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Sort categories by priority
|
|
420
|
+
const sortedCategories = [...grouped.keys()].sort(
|
|
421
|
+
(a, b) => (CATEGORY_PRIORITY[a] ?? 99) - (CATEGORY_PRIORITY[b] ?? 99),
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
for (const category of sortedCategories) {
|
|
425
|
+
const items = grouped.get(category)!;
|
|
426
|
+
const catHeader = `\n### ${category.charAt(0).toUpperCase() + category.slice(1)}\n`;
|
|
427
|
+
|
|
428
|
+
if (remaining < catHeader.length + 10) break;
|
|
429
|
+
output += catHeader;
|
|
430
|
+
remaining -= catHeader.length;
|
|
431
|
+
|
|
432
|
+
for (const item of items) {
|
|
433
|
+
const bullet = `- ${item.content}\n`;
|
|
434
|
+
if (remaining < bullet.length) break;
|
|
435
|
+
output += bullet;
|
|
436
|
+
remaining -= bullet.length;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return output.trimEnd();
|
|
441
|
+
}
|
|
@@ -151,7 +151,7 @@ export async function handleMigrate(
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// ── Confirmation via showNextAction ────────────────────────────────────────
|
|
154
|
-
const choice = await showNextAction(ctx
|
|
154
|
+
const choice = await showNextAction(ctx, {
|
|
155
155
|
title: "Migration preview",
|
|
156
156
|
summary: lines,
|
|
157
157
|
actions: [
|
|
@@ -187,7 +187,7 @@ export async function handleMigrate(
|
|
|
187
187
|
);
|
|
188
188
|
|
|
189
189
|
// ── Post-write review offer ────────────────────────────────────────────────
|
|
190
|
-
const reviewChoice = await showNextAction(ctx
|
|
190
|
+
const reviewChoice = await showNextAction(ctx, {
|
|
191
191
|
title: "Migration written",
|
|
192
192
|
summary: [
|
|
193
193
|
`${result.paths.length} files written to .gsd/`,
|
|
@@ -28,7 +28,7 @@ Then:
|
|
|
28
28
|
7. Write `{{sliceUatPath}}` — a concrete UAT script with real test cases derived from the slice plan and task summaries. Include preconditions, numbered steps with expected outcomes, and edge cases. This must NOT be a placeholder or generic template — tailor every test case to what this slice actually built.
|
|
29
29
|
8. Review task summaries for `key_decisions`. Append any significant decisions to `.gsd/DECISIONS.md` if missing.
|
|
30
30
|
9. Mark {{sliceId}} done in `{{roadmapPath}}` (change `[ ]` to `[x]`)
|
|
31
|
-
10. Do not
|
|
31
|
+
10. Do not run git commands — the system commits your changes and handles any merge after this unit succeeds.
|
|
32
32
|
11. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
|
|
33
33
|
12. Update `.gsd/STATE.md`
|
|
34
34
|
|
|
@@ -51,7 +51,7 @@ Use these templates exactly:
|
|
|
51
51
|
5. Write `{{roadmapPath}}` (using Roadmap template) — decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice.
|
|
52
52
|
6. Seed `.gsd/DECISIONS.md` (using Decisions template)
|
|
53
53
|
7. Update `.gsd/STATE.md`
|
|
54
|
-
8.
|
|
54
|
+
8. {{commitInstruction}}
|
|
55
55
|
9. Say exactly: "Milestone {{milestoneId}} ready."
|
|
56
56
|
|
|
57
57
|
**For multi-milestone**, write in this order:
|
|
@@ -71,7 +71,7 @@ Use these templates exactly:
|
|
|
71
71
|
```
|
|
72
72
|
Each context file should be rich enough that a future agent — with no memory of this conversation — can understand the intent, constraints, dependencies, what the milestone unlocks, and what "done" looks like.
|
|
73
73
|
8. Update `.gsd/STATE.md`
|
|
74
|
-
9.
|
|
74
|
+
9. {{multiMilestoneCommitInstruction}}
|
|
75
75
|
10. Say exactly: "Milestone {{milestoneId}} ready."
|
|
76
76
|
|
|
77
77
|
## Critical Rules
|
|
@@ -201,9 +201,9 @@ When writing context.md, preserve the user's exact terminology, emphasis, and sp
|
|
|
201
201
|
5. Write `{{roadmapPath}}` — use the **Roadmap** output template below. Decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice that proves the assembled system works end-to-end in a real environment.
|
|
202
202
|
6. Seed `.gsd/DECISIONS.md` — use the **Decisions** output template below. Append rows for any architectural or pattern decisions made during discussion.
|
|
203
203
|
7. Update `.gsd/STATE.md`
|
|
204
|
-
8.
|
|
204
|
+
8. {{commitInstruction}}
|
|
205
205
|
|
|
206
|
-
After writing the files
|
|
206
|
+
After writing the files, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
|
|
207
207
|
|
|
208
208
|
### Multi-Milestone
|
|
209
209
|
|
|
@@ -271,8 +271,8 @@ For single-milestone projects, do NOT write this file — it is only for multi-m
|
|
|
271
271
|
#### Phase 4: Finalize
|
|
272
272
|
|
|
273
273
|
7. Update `.gsd/STATE.md`
|
|
274
|
-
8.
|
|
274
|
+
8. {{multiMilestoneCommitInstruction}}
|
|
275
275
|
|
|
276
|
-
After writing the files
|
|
276
|
+
After writing the files, say exactly: "Milestone M001 ready." — nothing else. Auto-mode will start automatically.
|
|
277
277
|
|
|
278
278
|
{{inlinedTemplates}}
|
|
@@ -63,7 +63,7 @@ Then:
|
|
|
63
63
|
14. Read the template at `~/.gsd/agent/extensions/gsd/templates/task-summary.md`
|
|
64
64
|
15. Write `{{taskSummaryPath}}`
|
|
65
65
|
16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
|
|
66
|
-
17. Do not
|
|
66
|
+
17. Do not run git commands — the system reads your task summary after completion and creates a meaningful commit from it (type inferred from title, message from your one-liner, key files from frontmatter). Write a clear, specific one-liner in the summary — it becomes the commit message.
|
|
67
67
|
18. Update `.gsd/STATE.md`
|
|
68
68
|
|
|
69
69
|
All work stays in your working directory: `{{workingDirectory}}`.
|
|
@@ -104,5 +104,5 @@ Once the user confirms depth:
|
|
|
104
104
|
1. Use the **Context** output template below
|
|
105
105
|
2. `mkdir -p` the milestone directory if needed
|
|
106
106
|
3. Write `{{milestoneId}}-CONTEXT.md` — preserve the user's exact terminology, emphasis, and framing. Do not paraphrase nuance into generic summaries. The context file is downstream agents' only window into this conversation.
|
|
107
|
-
4.
|
|
107
|
+
4. {{commitInstruction}}
|
|
108
108
|
5. Say exactly: `"{{milestoneId}} context written."` — nothing else.
|
|
@@ -55,7 +55,7 @@ Once the user is ready to wrap up:
|
|
|
55
55
|
- **Constraints** — anything the user flagged as a hard constraint
|
|
56
56
|
- **Integration Points** — what this slice consumes and produces
|
|
57
57
|
- **Open Questions** — anything still unresolved, with current thinking
|
|
58
|
-
4.
|
|
58
|
+
4. {{commitInstruction}}
|
|
59
59
|
5. Say exactly: `"{{sliceId}} context written."` — nothing else.
|
|
60
60
|
|
|
61
61
|
{{inlinedTemplates}}
|
|
@@ -59,7 +59,7 @@ Then:
|
|
|
59
59
|
- **Scope sanity:** Target 2–5 steps and 3–8 files per task. 10+ steps or 12+ files — must split. Each task must be completable in a single fresh context window.
|
|
60
60
|
- **Feature completeness:** Every task produces real, user-facing progress — not just internal scaffolding.
|
|
61
61
|
9. If planning produced structural decisions, append them to `.gsd/DECISIONS.md`
|
|
62
|
-
10.
|
|
62
|
+
10. {{commitInstruction}}
|
|
63
63
|
11. Update `.gsd/STATE.md`
|
|
64
64
|
|
|
65
65
|
The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. All work stays in your working directory: `{{workingDirectory}}`.
|
|
@@ -96,7 +96,7 @@ Then, after all milestone directories and context files are written:
|
|
|
96
96
|
4. If `.gsd/REQUIREMENTS.md` exists and the queued work introduces new in-scope capabilities or promotes Deferred items, update it.
|
|
97
97
|
5. If discussion produced decisions relevant to existing work, append to `.gsd/DECISIONS.md`.
|
|
98
98
|
6. Append to `.gsd/QUEUE.md`.
|
|
99
|
-
7.
|
|
99
|
+
7. {{commitInstruction}}
|
|
100
100
|
|
|
101
101
|
**Do NOT write roadmaps for queued milestones.**
|
|
102
102
|
**Do NOT update `.gsd/STATE.md`.**
|
|
@@ -57,7 +57,7 @@ Write `{{assessmentPath}}` with a brief confirmation that roadmap coverage still
|
|
|
57
57
|
1. Rewrite the remaining (unchecked) slices in `{{roadmapPath}}`. Keep completed slices exactly as they are (`[x]`). Update the boundary map for changed slices. Update the proof strategy if risks changed. Update requirement coverage if ownership or scope changed.
|
|
58
58
|
2. Write `{{assessmentPath}}` explaining what changed and why — keep it brief and concrete.
|
|
59
59
|
3. If `.gsd/REQUIREMENTS.md` exists and requirement ownership or status changed, update it.
|
|
60
|
-
4.
|
|
60
|
+
4. {{commitInstruction}}
|
|
61
61
|
|
|
62
62
|
**You MUST write the file `{{assessmentPath}}` before finishing.**
|
|
63
63
|
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
loadPersistedKeys,
|
|
18
18
|
} from "../auto-recovery.ts";
|
|
19
19
|
import { parseRoadmap, clearParseCache } from "../files.ts";
|
|
20
|
+
import { invalidateAllCaches } from "../cache.ts";
|
|
21
|
+
import { deriveState, invalidateStateCache } from "../state.ts";
|
|
20
22
|
|
|
21
23
|
function makeTmpBase(): string {
|
|
22
24
|
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
@@ -584,3 +586,55 @@ test("selfHealRuntimeRecords clears stale record when artifact exists at worktre
|
|
|
584
586
|
cleanup(mainBase);
|
|
585
587
|
}
|
|
586
588
|
});
|
|
589
|
+
|
|
590
|
+
// ─── #793: invalidateAllCaches unblocks skip-loop ─────────────────────────
|
|
591
|
+
// When the skip-loop breaker fires, it must call invalidateAllCaches() (not
|
|
592
|
+
// just invalidateStateCache()) to clear path/parse caches that deriveState
|
|
593
|
+
// depends on. Without this, even after cache invalidation, deriveState reads
|
|
594
|
+
// stale directory listings and returns the same unit, looping forever.
|
|
595
|
+
test("#793: invalidateAllCaches clears all caches so deriveState sees fresh disk state", async () => {
|
|
596
|
+
const base = makeTmpBase();
|
|
597
|
+
try {
|
|
598
|
+
const mid = "M001";
|
|
599
|
+
const sid = "S01";
|
|
600
|
+
const planDir = join(base, ".gsd", "milestones", mid, "slices", sid);
|
|
601
|
+
const tasksDir = join(planDir, "tasks");
|
|
602
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
603
|
+
mkdirSync(join(base, ".gsd", "milestones", mid), { recursive: true });
|
|
604
|
+
|
|
605
|
+
writeFileSync(
|
|
606
|
+
join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`),
|
|
607
|
+
`# M001: Test Milestone\n\n**Vision:** test.\n\n## Slices\n\n- [ ] **${sid}: Slice One** \`risk:low\` \`depends:[]\`\n > After this: done.\n`,
|
|
608
|
+
);
|
|
609
|
+
const planUnchecked = `# ${sid}: Slice One\n\n**Goal:** test.\n\n## Tasks\n\n- [ ] **T01: Task One** \`est:10m\`\n- [ ] **T02: Task Two** \`est:10m\`\n`;
|
|
610
|
+
writeFileSync(join(planDir, `${sid}-PLAN.md`), planUnchecked);
|
|
611
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01: Task One\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
|
|
612
|
+
writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02: Task Two\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
|
|
613
|
+
|
|
614
|
+
// Warm all caches
|
|
615
|
+
const state1 = await deriveState(base);
|
|
616
|
+
assert.equal(state1.activeTask?.id, "T01", "initial: T01 is active");
|
|
617
|
+
|
|
618
|
+
// Simulate task completion on disk (what the LLM does)
|
|
619
|
+
const planChecked = `# ${sid}: Slice One\n\n**Goal:** test.\n\n## Tasks\n\n- [x] **T01: Task One** \`est:10m\`\n- [ ] **T02: Task Two** \`est:10m\`\n`;
|
|
620
|
+
writeFileSync(join(planDir, `${sid}-PLAN.md`), planChecked);
|
|
621
|
+
writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "---\nid: T01\n---\n# Summary\n");
|
|
622
|
+
|
|
623
|
+
// invalidateStateCache alone: _stateCache cleared but path/parse caches warm
|
|
624
|
+
invalidateStateCache();
|
|
625
|
+
|
|
626
|
+
// invalidateAllCaches: all caches cleared — deriveState must re-read disk
|
|
627
|
+
invalidateAllCaches();
|
|
628
|
+
const state2 = await deriveState(base);
|
|
629
|
+
|
|
630
|
+
// After full invalidation, T01 should be complete and T02 should be next
|
|
631
|
+
assert.notEqual(state2.activeTask?.id, "T01", "#793: T01 not re-dispatched after full invalidation");
|
|
632
|
+
|
|
633
|
+
// Verify the caches are truly cleared by calling clearParseCache and clearPathCache
|
|
634
|
+
// do not throw (they should be no-ops after invalidateAllCaches already cleared them)
|
|
635
|
+
clearParseCache(); // no-op, but should not throw
|
|
636
|
+
assert.ok(true, "clearParseCache after invalidateAllCaches is safe");
|
|
637
|
+
} finally {
|
|
638
|
+
cleanup(base);
|
|
639
|
+
}
|
|
640
|
+
});
|