gsd-pi 2.24.0 → 2.26.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 (206) hide show
  1. package/README.md +13 -3
  2. package/dist/headless.js +24 -4
  3. package/dist/models-resolver.d.ts +0 -11
  4. package/dist/models-resolver.js +0 -15
  5. package/dist/resource-loader.d.ts +0 -1
  6. package/dist/resource-loader.js +0 -9
  7. package/dist/resources/GSD-WORKFLOW.md +12 -9
  8. package/dist/resources/extensions/async-jobs/index.ts +9 -1
  9. package/dist/resources/extensions/bg-shell/index.ts +3 -2
  10. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  11. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  12. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  14. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
  15. package/dist/resources/extensions/gsd/auto-worktree.ts +132 -3
  16. package/dist/resources/extensions/gsd/auto.ts +265 -48
  17. package/dist/resources/extensions/gsd/cache.ts +3 -1
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
  19. package/dist/resources/extensions/gsd/doctor.ts +26 -1
  20. package/dist/resources/extensions/gsd/files.ts +13 -2
  21. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  22. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  23. package/dist/resources/extensions/gsd/guided-flow.ts +54 -22
  24. package/dist/resources/extensions/gsd/index.ts +62 -8
  25. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  26. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  27. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  28. package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
  29. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  30. package/dist/resources/extensions/gsd/preferences.ts +2 -1
  31. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  33. package/dist/resources/extensions/gsd/prompts/discuss.md +5 -5
  34. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/queue.md +3 -3
  39. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  40. package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
  41. package/dist/resources/extensions/gsd/state.ts +17 -6
  42. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  43. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  44. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  45. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  46. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  47. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  48. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  49. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  50. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  51. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  52. package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  53. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  54. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  56. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  57. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  58. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  59. package/dist/resources/extensions/gsd/types.ts +2 -0
  60. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  61. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  62. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  63. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  64. package/dist/resources/extensions/search-the-web/native-search.ts +19 -5
  65. package/dist/resources/extensions/shared/path-display.ts +19 -0
  66. package/package.json +1 -6
  67. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  68. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  69. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  70. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/providers/anthropic.js +64 -0
  72. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  73. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  74. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  75. package/packages/pi-ai/dist/types.d.ts +23 -1
  76. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/types.js.map +1 -1
  78. package/packages/pi-ai/src/providers/anthropic.ts +65 -1
  79. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  80. package/packages/pi-ai/src/types.ts +19 -1
  81. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  82. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
  84. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
  86. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
  94. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
  96. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  98. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  100. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  103. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
  106. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  108. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/index.js +5 -1
  110. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +135 -30
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
  123. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
  125. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
  128. package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
  130. package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
  132. package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
  133. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
  134. package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
  135. package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
  136. package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
  137. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  138. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  139. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  140. package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
  141. package/packages/pi-coding-agent/src/index.ts +15 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
  143. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  144. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +124 -4
  145. package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
  146. package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
  147. package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
  148. package/src/resources/GSD-WORKFLOW.md +12 -9
  149. package/src/resources/extensions/async-jobs/index.ts +9 -1
  150. package/src/resources/extensions/bg-shell/index.ts +3 -2
  151. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  152. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  153. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  154. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  155. package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
  156. package/src/resources/extensions/gsd/auto-worktree.ts +132 -3
  157. package/src/resources/extensions/gsd/auto.ts +265 -48
  158. package/src/resources/extensions/gsd/cache.ts +3 -1
  159. package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
  160. package/src/resources/extensions/gsd/doctor.ts +26 -1
  161. package/src/resources/extensions/gsd/files.ts +13 -2
  162. package/src/resources/extensions/gsd/git-service.ts +74 -14
  163. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  164. package/src/resources/extensions/gsd/guided-flow.ts +54 -22
  165. package/src/resources/extensions/gsd/index.ts +62 -8
  166. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  167. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  168. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  169. package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
  170. package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  171. package/src/resources/extensions/gsd/preferences.ts +2 -1
  172. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  174. package/src/resources/extensions/gsd/prompts/discuss.md +5 -5
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  178. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/queue.md +3 -3
  180. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  181. package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
  182. package/src/resources/extensions/gsd/state.ts +17 -6
  183. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  184. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  186. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  187. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  188. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  189. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  190. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  191. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  192. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  193. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  194. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  196. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  197. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  198. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  199. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  200. package/src/resources/extensions/gsd/types.ts +2 -0
  201. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  202. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  203. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  204. package/src/resources/extensions/gsd/worktree.ts +9 -2
  205. package/src/resources/extensions/search-the-web/native-search.ts +19 -5
  206. package/src/resources/extensions/shared/path-display.ts +19 -0
@@ -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/`,
@@ -483,6 +483,45 @@ export async function writeGSDDirectory(
483
483
  counts.research++;
484
484
  }
485
485
 
486
+ // For fully-completed milestones (all slices done), write a pass-through
487
+ // validation file so deriveState() doesn't enter validating-milestone
488
+ // phase for historical milestones that predate the validation gate (#819).
489
+ const allSlicesDone = milestone.slices.length > 0 && milestone.slices.every(s => s.done);
490
+ if (allSlicesDone) {
491
+ const validationPath = join(mDir, `${milestone.id}-VALIDATION.md`);
492
+ const validationContent = [
493
+ `---`,
494
+ `verdict: pass`,
495
+ `migrated: true`,
496
+ `---`,
497
+ ``,
498
+ `# ${milestone.id} Validation`,
499
+ ``,
500
+ `Migrated milestone — all slices were completed in the original project.`,
501
+ ``,
502
+ ].join('\n');
503
+ await saveFile(validationPath, validationContent);
504
+ paths.push(validationPath);
505
+ counts.other++;
506
+
507
+ // Also write a milestone summary if one doesn't exist
508
+ const summaryPath = join(mDir, `${milestone.id}-SUMMARY.md`);
509
+ const summaryContent = [
510
+ `---`,
511
+ `status: done`,
512
+ `migrated: true`,
513
+ `---`,
514
+ ``,
515
+ `# ${milestone.id}: ${milestone.title}`,
516
+ ``,
517
+ `Migrated from .planning — ${milestone.slices.length} slices completed.`,
518
+ ``,
519
+ ].join('\n');
520
+ await saveFile(summaryPath, summaryContent);
521
+ paths.push(summaryPath);
522
+ counts.other++;
523
+ }
524
+
486
525
  // Slices
487
526
  for (const slice of milestone.slices) {
488
527
  const sDir = join(mDir, 'slices', slice.id);
@@ -124,6 +124,12 @@ export async function startParallel(
124
124
  const toStart = milestoneIds.slice(0, config.max_workers);
125
125
 
126
126
  for (const mid of toStart) {
127
+ // Check budget ceiling before each spawn
128
+ if (isBudgetExceeded()) {
129
+ errors.push({ mid, error: `Budget ceiling ($${config.budget_ceiling}) reached — skipping` });
130
+ continue;
131
+ }
132
+
127
133
  try {
128
134
  // Create the worktree (without chdir — coordinator stays in project root)
129
135
  let wtPath: string;
@@ -233,7 +239,7 @@ export function spawnWorker(
233
239
 
234
240
  let child: ChildProcess;
235
241
  try {
236
- child = spawn(process.execPath, [binPath, "--print", "/gsd auto"], {
242
+ child = spawn(process.execPath, [binPath, "--mode", "json", "--print", "/gsd auto"], {
237
243
  cwd: worker.worktreePath,
238
244
  env: {
239
245
  ...process.env,
@@ -267,6 +273,28 @@ export function spawnWorker(
267
273
  return false;
268
274
  }
269
275
 
276
+ // ── NDJSON stdout monitoring ────────────────────────────────────────
277
+ // Workers run with --mode json, emitting one JSON event per line.
278
+ // We parse message_end events to extract cost/token usage, keeping
279
+ // the coordinator's cost tracking in sync with actual API spend.
280
+ if (child.stdout) {
281
+ let stdoutBuffer = "";
282
+ child.stdout.on("data", (data: Buffer) => {
283
+ stdoutBuffer += data.toString();
284
+ const lines = stdoutBuffer.split("\n");
285
+ stdoutBuffer = lines.pop() || "";
286
+ for (const line of lines) {
287
+ processWorkerLine(basePath, milestoneId, line);
288
+ }
289
+ });
290
+ // Flush remaining buffer on close
291
+ child.stdout.on("close", () => {
292
+ if (stdoutBuffer.trim()) {
293
+ processWorkerLine(basePath, milestoneId, stdoutBuffer);
294
+ }
295
+ });
296
+ }
297
+
270
298
  // Update session status with real PID
271
299
  writeSessionStatus(basePath, {
272
300
  milestoneId,
@@ -343,6 +371,90 @@ function resolveGsdBin(): string | null {
343
371
  return null;
344
372
  }
345
373
 
374
+ // ─── NDJSON Processing ──────────────────────────────────────────────────────
375
+
376
+ /**
377
+ * Process a single NDJSON line from a worker's stdout.
378
+ * Extracts cost and token usage from message_end events and updates
379
+ * the worker's tracking state + session status file.
380
+ */
381
+ function processWorkerLine(basePath: string, milestoneId: string, line: string): void {
382
+ if (!line.trim() || !state) return;
383
+
384
+ let event: Record<string, unknown>;
385
+ try {
386
+ event = JSON.parse(line);
387
+ } catch {
388
+ return; // Not valid JSON — skip (stderr leakage, debug output, etc.)
389
+ }
390
+
391
+ const type = String(event.type ?? "");
392
+
393
+ // message_end carries usage data with cost
394
+ if (type === "message_end" && event.message) {
395
+ const msg = event.message as Record<string, unknown>;
396
+ const usage = msg.usage as Record<string, unknown> | undefined;
397
+
398
+ if (usage) {
399
+ const cost = (usage.cost as Record<string, unknown>)?.total;
400
+ if (typeof cost === "number") {
401
+ const worker = state.workers.get(milestoneId);
402
+ if (worker) {
403
+ worker.cost += cost;
404
+ // Update aggregate
405
+ state.totalCost = 0;
406
+ for (const w of state.workers.values()) {
407
+ state.totalCost += w.cost;
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ // Track completed units (each message_end from assistant = progress)
414
+ if (msg.role === "assistant") {
415
+ const worker = state.workers.get(milestoneId);
416
+ if (worker) {
417
+ worker.completedUnits++;
418
+ }
419
+ }
420
+
421
+ // Update session status file so dashboard sees live cost
422
+ const worker = state.workers.get(milestoneId);
423
+ if (worker) {
424
+ writeSessionStatus(basePath, {
425
+ milestoneId,
426
+ pid: worker.pid,
427
+ state: worker.state,
428
+ currentUnit: null,
429
+ completedUnits: worker.completedUnits,
430
+ cost: worker.cost,
431
+ lastHeartbeat: Date.now(),
432
+ startedAt: worker.startedAt,
433
+ worktreePath: worker.worktreePath,
434
+ });
435
+ }
436
+ }
437
+
438
+ // tool_execution_start can track current unit
439
+ if (type === "extension_ui_request" && event.method === "notify") {
440
+ // GSD auto-mode sends notifications about current unit
441
+ const worker = state.workers.get(milestoneId);
442
+ if (worker) {
443
+ writeSessionStatus(basePath, {
444
+ milestoneId,
445
+ pid: worker.pid,
446
+ state: worker.state,
447
+ currentUnit: null,
448
+ completedUnits: worker.completedUnits,
449
+ cost: worker.cost,
450
+ lastHeartbeat: Date.now(),
451
+ startedAt: worker.startedAt,
452
+ worktreePath: worker.worktreePath,
453
+ });
454
+ }
455
+ }
456
+ }
457
+
346
458
  // ─── Stop ──────────────────────────────────────────────────────────────────
347
459
 
348
460
  /**
@@ -366,10 +478,16 @@ export async function stopParallel(
366
478
  // Send stop signal via file-based IPC (worker checks on next dispatch)
367
479
  sendSignal(basePath, mid, "stop");
368
480
 
369
- // Also send SIGTERM to the process for immediate response
370
- if (worker.process && worker.pid > 0) {
481
+ // Send SIGTERM to the process for immediate response.
482
+ // Use process handle when available, fall back to PID-based kill
483
+ // (handles are null after coordinator restart / deserialization).
484
+ if (worker.pid > 0) {
371
485
  try {
372
- worker.process.kill("SIGTERM");
486
+ if (worker.process) {
487
+ worker.process.kill("SIGTERM");
488
+ } else {
489
+ process.kill(worker.pid, "SIGTERM");
490
+ }
373
491
  } catch { /* process may already be dead */ }
374
492
  }
375
493
 
@@ -916,8 +916,9 @@ export function validatePreferences(preferences: GSDPreferences): {
916
916
  if (p.skip_reassess !== undefined) validatedPhases.skip_reassess = !!p.skip_reassess;
917
917
  if (p.skip_slice_research !== undefined) validatedPhases.skip_slice_research = !!p.skip_slice_research;
918
918
  if (p.skip_milestone_validation !== undefined) validatedPhases.skip_milestone_validation = !!p.skip_milestone_validation;
919
+ if ((p as any).require_slice_discussion !== undefined) (validatedPhases as any).require_slice_discussion = !!(p as any).require_slice_discussion;
919
920
  // Warn on unknown phase keys
920
- const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research", "skip_milestone_validation"]);
921
+ const knownPhaseKeys = new Set(["skip_research", "skip_reassess", "skip_slice_research", "skip_milestone_validation", "require_slice_discussion"]);
921
922
  for (const key of Object.keys(p)) {
922
923
  if (!knownPhaseKeys.has(key)) {
923
924
  warnings.push(`unknown phases key "${key}" — ignored`);
@@ -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