gsd-pi 2.23.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 (212) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +12 -3
  3. package/dist/headless.d.ts +4 -0
  4. package/dist/headless.js +118 -10
  5. package/dist/help-text.js +22 -7
  6. package/dist/models-resolver.d.ts +0 -11
  7. package/dist/models-resolver.js +0 -15
  8. package/dist/resource-loader.d.ts +0 -1
  9. package/dist/resource-loader.js +64 -18
  10. package/dist/resources/GSD-WORKFLOW.md +12 -9
  11. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  12. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  13. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  15. package/dist/resources/extensions/gsd/auto-prompts.ts +87 -0
  16. package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
  17. package/dist/resources/extensions/gsd/auto-worktree.ts +134 -4
  18. package/dist/resources/extensions/gsd/auto.ts +307 -77
  19. package/dist/resources/extensions/gsd/cache.ts +3 -1
  20. package/dist/resources/extensions/gsd/commands.ts +176 -10
  21. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  22. package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  23. package/dist/resources/extensions/gsd/doctor.ts +58 -11
  24. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  25. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  26. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  27. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  28. package/dist/resources/extensions/gsd/guided-flow.ts +109 -12
  29. package/dist/resources/extensions/gsd/index.ts +48 -2
  30. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  31. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  32. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  33. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  34. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  35. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  36. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  39. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
  40. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  43. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  45. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  47. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  48. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  49. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  50. package/dist/resources/extensions/gsd/state.ts +72 -30
  51. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  52. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  53. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  54. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
  55. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  56. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  57. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  58. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  59. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  60. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  62. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  63. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  64. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  65. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  66. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  67. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  68. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  69. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  70. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  71. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  72. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  73. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  74. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  75. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  76. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  77. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  78. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  79. package/dist/resources/extensions/gsd/types.ts +15 -1
  80. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  81. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  82. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  83. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  84. package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
  85. package/dist/resources/extensions/subagent/index.ts +5 -0
  86. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  87. package/dist/update-check.d.ts +9 -0
  88. package/dist/update-check.js +97 -0
  89. package/package.json +6 -1
  90. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  91. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  92. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  93. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/anthropic.js +55 -7
  95. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  96. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  98. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  99. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  100. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  101. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  102. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  103. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  106. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  110. package/packages/pi-ai/dist/types.d.ts +23 -1
  111. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  112. package/packages/pi-ai/dist/types.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +59 -9
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  117. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  118. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  119. package/packages/pi-ai/src/types.ts +19 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  132. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  133. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
  135. package/scripts/postinstall.js +7 -109
  136. package/src/resources/GSD-WORKFLOW.md +12 -9
  137. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  138. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  139. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  140. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  141. package/src/resources/extensions/gsd/auto-prompts.ts +87 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
  143. package/src/resources/extensions/gsd/auto-worktree.ts +134 -4
  144. package/src/resources/extensions/gsd/auto.ts +307 -77
  145. package/src/resources/extensions/gsd/cache.ts +3 -1
  146. package/src/resources/extensions/gsd/commands.ts +176 -10
  147. package/src/resources/extensions/gsd/complexity.ts +1 -0
  148. package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  149. package/src/resources/extensions/gsd/doctor.ts +58 -11
  150. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  151. package/src/resources/extensions/gsd/git-service.ts +74 -14
  152. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  153. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  154. package/src/resources/extensions/gsd/guided-flow.ts +109 -12
  155. package/src/resources/extensions/gsd/index.ts +48 -2
  156. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  157. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  158. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  159. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  160. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  161. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  162. package/src/resources/extensions/gsd/preferences.ts +65 -1
  163. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  165. package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
  166. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  170. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  174. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  175. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  176. package/src/resources/extensions/gsd/state.ts +72 -30
  177. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  178. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  179. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
  181. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  182. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  183. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  184. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  185. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  186. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  187. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  188. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  189. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  190. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  191. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  192. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  193. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  194. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  195. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  196. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  197. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  198. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  199. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  200. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  201. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  202. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  203. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  204. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  205. package/src/resources/extensions/gsd/types.ts +15 -1
  206. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  207. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  208. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  209. package/src/resources/extensions/gsd/worktree.ts +9 -2
  210. package/src/resources/extensions/search-the-web/native-search.ts +15 -5
  211. package/src/resources/extensions/subagent/index.ts +5 -0
  212. package/src/resources/extensions/subagent/worker-registry.ts +99 -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/`,
@@ -0,0 +1,233 @@
1
+ /**
2
+ * GSD Parallel Eligibility — Milestone parallelism analysis.
3
+ *
4
+ * Analyzes which milestones can safely run in parallel by checking
5
+ * dependency satisfaction and file overlap across slice plans.
6
+ */
7
+
8
+ import { deriveState } from "./state.js";
9
+ import { parseRoadmap, parsePlan, loadFile } from "./files.js";
10
+ import { resolveMilestoneFile, resolveSliceFile } from "./paths.js";
11
+ import { findMilestoneIds } from "./guided-flow.js";
12
+ import type { MilestoneRegistryEntry } from "./types.js";
13
+
14
+ // ─── Types ───────────────────────────────────────────────────────────────────
15
+
16
+ export interface EligibilityResult {
17
+ milestoneId: string;
18
+ title: string;
19
+ eligible: boolean;
20
+ reason: string;
21
+ }
22
+
23
+ export interface ParallelCandidates {
24
+ eligible: EligibilityResult[];
25
+ ineligible: EligibilityResult[];
26
+ fileOverlaps: Array<{ mid1: string; mid2: string; files: string[] }>;
27
+ }
28
+
29
+ // ─── File Collection ─────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Collect all `filesLikelyTouched` across every slice plan in a milestone.
33
+ * Returns a deduplicated list of file paths.
34
+ */
35
+ async function collectTouchedFiles(
36
+ basePath: string,
37
+ milestoneId: string,
38
+ ): Promise<string[]> {
39
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
40
+ if (!roadmapPath) return [];
41
+
42
+ const roadmapContent = await loadFile(roadmapPath);
43
+ if (!roadmapContent) return [];
44
+
45
+ const roadmap = parseRoadmap(roadmapContent);
46
+ const files = new Set<string>();
47
+
48
+ for (const slice of roadmap.slices) {
49
+ const planPath = resolveSliceFile(basePath, milestoneId, slice.id, "PLAN");
50
+ if (!planPath) continue;
51
+
52
+ const planContent = await loadFile(planPath);
53
+ if (!planContent) continue;
54
+
55
+ const plan = parsePlan(planContent);
56
+ for (const f of plan.filesLikelyTouched) {
57
+ files.add(f);
58
+ }
59
+ }
60
+
61
+ return [...files];
62
+ }
63
+
64
+ // ─── Overlap Detection ──────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Compare file sets across milestones and return pairs with overlapping files.
68
+ */
69
+ function detectFileOverlaps(
70
+ fileSets: Map<string, string[]>,
71
+ ): Array<{ mid1: string; mid2: string; files: string[] }> {
72
+ const overlaps: Array<{ mid1: string; mid2: string; files: string[] }> = [];
73
+ const ids = [...fileSets.keys()];
74
+
75
+ for (let i = 0; i < ids.length; i++) {
76
+ const files1 = new Set(fileSets.get(ids[i])!);
77
+ for (let j = i + 1; j < ids.length; j++) {
78
+ const files2 = fileSets.get(ids[j])!;
79
+ const shared = files2.filter(f => files1.has(f));
80
+ if (shared.length > 0) {
81
+ overlaps.push({ mid1: ids[i], mid2: ids[j], files: shared.sort() });
82
+ }
83
+ }
84
+ }
85
+
86
+ return overlaps;
87
+ }
88
+
89
+ // ─── Analysis ────────────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Analyze milestones for parallel execution eligibility.
93
+ *
94
+ * A milestone is eligible if:
95
+ * 1. It is not complete
96
+ * 2. Its dependencies (`dependsOn`) are all complete
97
+ * 3. It does not have file overlap with other eligible milestones
98
+ * (overlaps are flagged as warnings but do not disqualify)
99
+ */
100
+ export async function analyzeParallelEligibility(
101
+ basePath: string,
102
+ ): Promise<ParallelCandidates> {
103
+ const milestoneIds = findMilestoneIds(basePath);
104
+ const state = await deriveState(basePath);
105
+ const registry = state.registry;
106
+
107
+ // Build a lookup for quick status checks
108
+ const registryMap = new Map<string, MilestoneRegistryEntry>();
109
+ for (const entry of registry) {
110
+ registryMap.set(entry.id, entry);
111
+ }
112
+
113
+ const eligible: EligibilityResult[] = [];
114
+ const ineligible: EligibilityResult[] = [];
115
+
116
+ for (const mid of milestoneIds) {
117
+ const entry = registryMap.get(mid);
118
+ const title = entry?.title ?? mid;
119
+ const status = entry?.status ?? "pending";
120
+
121
+ // Rule 1: skip complete milestones
122
+ if (status === "complete") {
123
+ ineligible.push({
124
+ milestoneId: mid,
125
+ title,
126
+ eligible: false,
127
+ reason: "Already complete.",
128
+ });
129
+ continue;
130
+ }
131
+
132
+ // Rule 2: check dependency satisfaction
133
+ const deps = entry?.dependsOn ?? [];
134
+ const unsatisfied = deps.filter(dep => {
135
+ const depEntry = registryMap.get(dep);
136
+ return !depEntry || depEntry.status !== "complete";
137
+ });
138
+
139
+ if (unsatisfied.length > 0) {
140
+ ineligible.push({
141
+ milestoneId: mid,
142
+ title,
143
+ eligible: false,
144
+ reason: `Blocked by incomplete dependencies: ${unsatisfied.join(", ")}.`,
145
+ });
146
+ continue;
147
+ }
148
+
149
+ eligible.push({
150
+ milestoneId: mid,
151
+ title,
152
+ eligible: true,
153
+ reason: "All dependencies satisfied.",
154
+ });
155
+ }
156
+
157
+ // Rule 3: check file overlap among eligible milestones
158
+ const fileSets = new Map<string, string[]>();
159
+ for (const result of eligible) {
160
+ const files = await collectTouchedFiles(basePath, result.milestoneId);
161
+ fileSets.set(result.milestoneId, files);
162
+ }
163
+
164
+ const fileOverlaps = detectFileOverlaps(fileSets);
165
+
166
+ // Annotate eligible milestones that have file overlaps
167
+ const overlappingIds = new Set<string>();
168
+ for (const overlap of fileOverlaps) {
169
+ overlappingIds.add(overlap.mid1);
170
+ overlappingIds.add(overlap.mid2);
171
+ }
172
+
173
+ for (const result of eligible) {
174
+ if (overlappingIds.has(result.milestoneId)) {
175
+ result.reason = "All dependencies satisfied. WARNING: has file overlap with another eligible milestone.";
176
+ }
177
+ }
178
+
179
+ return { eligible, ineligible, fileOverlaps };
180
+ }
181
+
182
+ // ─── Formatting ──────────────────────────────────────────────────────────────
183
+
184
+ /**
185
+ * Produce a human-readable report of parallel eligibility analysis.
186
+ */
187
+ export function formatEligibilityReport(candidates: ParallelCandidates): string {
188
+ const lines: string[] = [];
189
+
190
+ lines.push("# Parallel Eligibility Report");
191
+ lines.push("");
192
+
193
+ // Eligible milestones
194
+ lines.push(`## Eligible for Parallel Execution (${candidates.eligible.length})`);
195
+ lines.push("");
196
+ if (candidates.eligible.length === 0) {
197
+ lines.push("No milestones are currently eligible for parallel execution.");
198
+ } else {
199
+ for (const e of candidates.eligible) {
200
+ lines.push(`- **${e.milestoneId}** — ${e.title}`);
201
+ lines.push(` ${e.reason}`);
202
+ }
203
+ }
204
+ lines.push("");
205
+
206
+ // Ineligible milestones
207
+ lines.push(`## Ineligible (${candidates.ineligible.length})`);
208
+ lines.push("");
209
+ if (candidates.ineligible.length === 0) {
210
+ lines.push("All milestones are eligible.");
211
+ } else {
212
+ for (const e of candidates.ineligible) {
213
+ lines.push(`- **${e.milestoneId}** — ${e.title}`);
214
+ lines.push(` ${e.reason}`);
215
+ }
216
+ }
217
+ lines.push("");
218
+
219
+ // File overlap warnings
220
+ if (candidates.fileOverlaps.length > 0) {
221
+ lines.push(`## File Overlap Warnings (${candidates.fileOverlaps.length})`);
222
+ lines.push("");
223
+ for (const overlap of candidates.fileOverlaps) {
224
+ lines.push(`- **${overlap.mid1}** <-> **${overlap.mid2}** — ${overlap.files.length} shared file(s):`);
225
+ for (const f of overlap.files) {
226
+ lines.push(` - \`${f}\``);
227
+ }
228
+ }
229
+ lines.push("");
230
+ }
231
+
232
+ return lines.join("\n");
233
+ }