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
@@ -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 as any, {
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 as any, {
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 commit or squash-merge manually — the system auto-commits your changes and handles the merge after this unit succeeds.
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. Commit: `docs({{milestoneId}}): context, requirements, and roadmap`
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. Commit: `docs: project plan — N milestones`
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. Commit: `docs({{milestoneId}}): context, requirements, and roadmap`
204
+ 8. {{commitInstruction}}
205
205
 
206
- After writing the files and committing, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
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. Commit: `docs: project plan — N milestones` (replace N with the actual milestone count)
274
+ 8. {{multiMilestoneCommitInstruction}}
275
275
 
276
- After writing the files and committing, say exactly: "Milestone M001 ready." — nothing else. Auto-mode will start automatically.
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 commit manually — the system auto-commits your changes after this unit completes.
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. Commit: `git add {{milestoneId}}-CONTEXT.md && git commit -m "docs({{milestoneId}}): milestone context from discuss"`
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. Commit: `git -C {{projectRoot}} add {{contextPath}} && git -C {{projectRoot}} commit -m "docs({{milestoneId}}/{{sliceId}}): slice context from discuss"`
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. Commit: `docs({{sliceId}}): add slice plan`
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. Commit: `docs: queue <milestone list>`
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. Commit: `docs({{milestoneId}}): reassess roadmap after {{completedSliceId}}`
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
+ });