gsd-pi 2.73.1-dev.47ed32a → 2.73.1-dev.49397e5

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 (182) hide show
  1. package/dist/cli-web-branch.d.ts +4 -3
  2. package/dist/cli-web-branch.js +10 -7
  3. package/dist/cli.js +99 -206
  4. package/dist/headless-query.js +4 -1
  5. package/dist/logo.d.ts +1 -1
  6. package/dist/logo.js +1 -1
  7. package/dist/onboarding.js +59 -53
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +59 -1
  9. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  10. package/dist/resources/extensions/gsd/auto/phases.js +60 -10
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +93 -57
  13. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
  14. package/dist/resources/extensions/gsd/auto-verification.js +88 -3
  15. package/dist/resources/extensions/gsd/auto.js +29 -10
  16. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
  17. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
  19. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  20. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  21. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  22. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  23. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  24. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  25. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  26. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  27. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  28. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  29. package/dist/resources/extensions/gsd/gsd-db.js +321 -0
  30. package/dist/resources/extensions/gsd/index.js +15 -2
  31. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  32. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  33. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  34. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  35. package/dist/resources/extensions/gsd/notification-widget.js +2 -2
  36. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  37. package/dist/resources/extensions/gsd/state.js +66 -15
  38. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  39. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  40. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  41. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  42. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  43. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  44. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  73. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +3 -3
  79. package/packages/mcp-server/tsconfig.json +1 -0
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  81. package/packages/native/tsconfig.tsbuildinfo +1 -0
  82. package/packages/pi-agent-core/tsconfig.json +1 -0
  83. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  84. package/packages/pi-ai/tsconfig.json +1 -0
  85. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +10 -2
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +5 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/package.json +1 -1
  101. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +12 -6
  102. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
  103. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
  104. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -1
  105. package/packages/pi-coding-agent/tsconfig.json +1 -0
  106. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  107. package/packages/pi-tui/dist/__tests__/tui.test.js +30 -0
  108. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  109. package/packages/pi-tui/dist/tui.d.ts +1 -0
  110. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  111. package/packages/pi-tui/dist/tui.js +22 -3
  112. package/packages/pi-tui/dist/tui.js.map +1 -1
  113. package/packages/pi-tui/src/__tests__/tui.test.ts +38 -0
  114. package/packages/pi-tui/src/tui.ts +20 -3
  115. package/packages/pi-tui/tsconfig.json +1 -0
  116. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  117. package/packages/rpc-client/tsconfig.json +1 -0
  118. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  119. package/pkg/package.json +1 -1
  120. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +95 -1
  121. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +88 -0
  122. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  123. package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
  124. package/src/resources/extensions/gsd/auto/phases.ts +90 -10
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +107 -58
  127. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
  128. package/src/resources/extensions/gsd/auto-verification.ts +98 -3
  129. package/src/resources/extensions/gsd/auto.ts +33 -14
  130. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
  131. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  132. package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
  133. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  134. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  135. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  136. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  137. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  138. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  139. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  140. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  141. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  142. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  143. package/src/resources/extensions/gsd/gsd-db.ts +467 -0
  144. package/src/resources/extensions/gsd/index.ts +18 -2
  145. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  146. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  147. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  149. package/src/resources/extensions/gsd/notification-widget.ts +2 -2
  150. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  151. package/src/resources/extensions/gsd/state.ts +80 -17
  152. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
  153. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
  154. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  155. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  156. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  157. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  158. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  159. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  160. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
  161. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
  162. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
  163. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
  164. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
  165. package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
  166. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  167. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  168. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  169. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  170. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  171. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
  172. package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
  174. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  175. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  176. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  177. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  178. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  179. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  180. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  181. /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_ssgManifest.js +0 -0
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Storage layer for auto-learned project memories. Follows context-store.ts patterns.
4
4
  // All functions degrade gracefully: return empty results when DB unavailable, never throw.
5
- import { isDbAvailable, _getAdapter, transaction } from './gsd-db.js';
5
+ import { isDbAvailable, _getAdapter, transaction, insertMemoryRow, rewriteMemoryId, updateMemoryContentRow, incrementMemoryHitCount, supersedeMemoryRow, markMemoryUnitProcessed, decayMemoriesBefore, supersedeLowestRankedMemories, } from './gsd-db.js';
6
6
  // ─── Category Display Order ─────────────────────────────────────────────────
7
7
  const CATEGORY_PRIORITY = {
8
8
  gotcha: 0,
@@ -112,27 +112,23 @@ export function createMemory(fields) {
112
112
  const now = new Date().toISOString();
113
113
  // Insert with a temporary placeholder ID — seq is auto-assigned
114
114
  const placeholder = `_TMP_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
115
- adapter.prepare(`INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at)
116
- VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at)`).run({
117
- ':id': placeholder,
118
- ':category': fields.category,
119
- ':content': fields.content,
120
- ':confidence': fields.confidence ?? 0.8,
121
- ':source_unit_type': fields.source_unit_type ?? null,
122
- ':source_unit_id': fields.source_unit_id ?? null,
123
- ':created_at': now,
124
- ':updated_at': now,
115
+ insertMemoryRow({
116
+ id: placeholder,
117
+ category: fields.category,
118
+ content: fields.content,
119
+ confidence: fields.confidence ?? 0.8,
120
+ sourceUnitType: fields.source_unit_type ?? null,
121
+ sourceUnitId: fields.source_unit_id ?? null,
122
+ createdAt: now,
123
+ updatedAt: now,
125
124
  });
126
- // Derive the real ID from the assigned seq
125
+ // Derive the real ID from the assigned seq (SELECT is still fine via adapter)
127
126
  const row = adapter.prepare('SELECT seq FROM memories WHERE id = :id').get({ ':id': placeholder });
128
127
  if (!row)
129
128
  return placeholder; // fallback — should not happen
130
129
  const seq = row['seq'];
131
130
  const realId = `MEM${String(seq).padStart(3, '0')}`;
132
- adapter.prepare('UPDATE memories SET id = :real_id WHERE id = :placeholder').run({
133
- ':real_id': realId,
134
- ':placeholder': placeholder,
135
- });
131
+ rewriteMemoryId(placeholder, realId);
136
132
  return realId;
137
133
  }
138
134
  catch {
@@ -145,17 +141,8 @@ export function createMemory(fields) {
145
141
  export function updateMemoryContent(id, content, confidence) {
146
142
  if (!isDbAvailable())
147
143
  return false;
148
- const adapter = _getAdapter();
149
- if (!adapter)
150
- return false;
151
144
  try {
152
- const now = new Date().toISOString();
153
- if (confidence != null) {
154
- adapter.prepare('UPDATE memories SET content = :content, confidence = :confidence, updated_at = :updated_at WHERE id = :id').run({ ':content': content, ':confidence': confidence, ':updated_at': now, ':id': id });
155
- }
156
- else {
157
- adapter.prepare('UPDATE memories SET content = :content, updated_at = :updated_at WHERE id = :id').run({ ':content': content, ':updated_at': now, ':id': id });
158
- }
145
+ updateMemoryContentRow(id, content, confidence, new Date().toISOString());
159
146
  return true;
160
147
  }
161
148
  catch {
@@ -168,11 +155,8 @@ export function updateMemoryContent(id, content, confidence) {
168
155
  export function reinforceMemory(id) {
169
156
  if (!isDbAvailable())
170
157
  return false;
171
- const adapter = _getAdapter();
172
- if (!adapter)
173
- return false;
174
158
  try {
175
- adapter.prepare('UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id').run({ ':updated_at': new Date().toISOString(), ':id': id });
159
+ incrementMemoryHitCount(id, new Date().toISOString());
176
160
  return true;
177
161
  }
178
162
  catch {
@@ -185,11 +169,8 @@ export function reinforceMemory(id) {
185
169
  export function supersedeMemory(oldId, newId) {
186
170
  if (!isDbAvailable())
187
171
  return false;
188
- const adapter = _getAdapter();
189
- if (!adapter)
190
- return false;
191
172
  try {
192
- adapter.prepare('UPDATE memories SET superseded_by = :new_id, updated_at = :updated_at WHERE id = :old_id').run({ ':new_id': newId, ':updated_at': new Date().toISOString(), ':old_id': oldId });
173
+ supersedeMemoryRow(oldId, newId, new Date().toISOString());
193
174
  return true;
194
175
  }
195
176
  catch {
@@ -220,12 +201,8 @@ export function isUnitProcessed(unitKey) {
220
201
  export function markUnitProcessed(unitKey, activityFile) {
221
202
  if (!isDbAvailable())
222
203
  return false;
223
- const adapter = _getAdapter();
224
- if (!adapter)
225
- return false;
226
204
  try {
227
- adapter.prepare(`INSERT OR IGNORE INTO memory_processed_units (unit_key, activity_file, processed_at)
228
- VALUES (:key, :file, :at)`).run({ ':key': unitKey, ':file': activityFile, ':at': new Date().toISOString() });
205
+ markMemoryUnitProcessed(unitKey, activityFile, new Date().toISOString());
229
206
  return true;
230
207
  }
231
208
  catch {
@@ -244,16 +221,14 @@ export function decayStaleMemories(thresholdUnits = 20) {
244
221
  if (!adapter)
245
222
  return;
246
223
  try {
247
- // Find the timestamp of the Nth most recent processed unit
224
+ // Find the timestamp of the Nth most recent processed unit (read-only SELECT)
248
225
  const row = adapter.prepare(`SELECT processed_at FROM memory_processed_units
249
226
  ORDER BY processed_at DESC
250
227
  LIMIT 1 OFFSET :offset`).get({ ':offset': thresholdUnits - 1 });
251
228
  if (!row)
252
229
  return; // not enough processed units yet
253
230
  const cutoff = row['processed_at'];
254
- adapter.prepare(`UPDATE memories
255
- SET confidence = MAX(0.1, confidence - 0.1), updated_at = :now
256
- WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`).run({ ':now': new Date().toISOString(), ':cutoff': cutoff });
231
+ decayMemoriesBefore(cutoff, new Date().toISOString());
257
232
  }
258
233
  catch {
259
234
  // non-fatal
@@ -274,14 +249,7 @@ export function enforceMemoryCap(max = 50) {
274
249
  if (count <= max)
275
250
  return;
276
251
  const excess = count - max;
277
- // Batch update: supersede lowest-ranked active memories in a single statement
278
- adapter.prepare(`UPDATE memories SET superseded_by = 'CAP_EXCEEDED', updated_at = :now
279
- WHERE id IN (
280
- SELECT id FROM memories
281
- WHERE superseded_by IS NULL
282
- ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
283
- LIMIT :limit
284
- )`).run({ ':now': new Date().toISOString(), ':limit': excess });
252
+ supersedeLowestRankedMemories(excess, new Date().toISOString());
285
253
  }
286
254
  catch {
287
255
  // non-fatal
@@ -10,7 +10,7 @@
10
10
  * gate registry so the definitions stay in lockstep with prompt builders,
11
11
  * dispatch rules, and state derivation. See gate-registry.ts.
12
12
  */
13
- import { _getAdapter } from "./gsd-db.js";
13
+ import { isDbAvailable, upsertQualityGate } from "./gsd-db.js";
14
14
  import { getGatesForTurn } from "./gate-registry.js";
15
15
  /**
16
16
  * Insert milestone-level quality_gates records for a validation run.
@@ -24,21 +24,22 @@ import { getGatesForTurn } from "./gate-registry.js";
24
24
  * in gate-registry.ts automatically flows through here.
25
25
  */
26
26
  export function insertMilestoneValidationGates(milestoneId, sliceId, verdict, evaluatedAt) {
27
- const db = _getAdapter();
28
- if (!db)
27
+ if (!isDbAvailable())
29
28
  return;
30
29
  const gateVerdict = verdict === "pass" ? "pass" : "flag";
31
30
  const milestoneGates = getGatesForTurn("validate-milestone");
32
31
  for (const def of milestoneGates) {
33
- db.prepare(`INSERT OR REPLACE INTO quality_gates
34
- (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
35
- VALUES (:mid, :sid, :gid, 'milestone', '', 'complete', :verdict, :rationale, '', :evaluated_at)`).run({
36
- ":mid": milestoneId,
37
- ":sid": sliceId,
38
- ":gid": def.id,
39
- ":verdict": gateVerdict,
40
- ":rationale": `${def.promptSection} — milestone validation verdict: ${verdict}`,
41
- ":evaluated_at": evaluatedAt,
32
+ upsertQualityGate({
33
+ milestoneId,
34
+ sliceId,
35
+ gateId: def.id,
36
+ scope: "milestone",
37
+ taskId: "",
38
+ status: "complete",
39
+ verdict: gateVerdict,
40
+ rationale: `${def.promptSection} — milestone validation verdict: ${verdict}`,
41
+ findings: "",
42
+ evaluatedAt,
42
43
  });
43
44
  }
44
45
  }
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Both READ and WRITE operations are native — push operations remain as
6
6
  // execSync calls because git2 credential handling is too complex.
7
- import { execSync, execFileSync } from "node:child_process";
7
+ import { execFileSync } from "node:child_process";
8
8
  import { GSDError, GSD_GIT_ERROR } from "./errors.js";
9
9
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
10
10
  import { getErrorMessage } from "./error-utils.js";
@@ -206,7 +206,7 @@ export function nativeIsRepo(basePath) {
206
206
  return native.gitIsRepo(basePath);
207
207
  }
208
208
  try {
209
- execSync("git rev-parse --git-dir", { cwd: basePath, stdio: "pipe" });
209
+ execFileSync("git", ["rev-parse", "--git-dir"], { cwd: basePath, stdio: "pipe" });
210
210
  return true;
211
211
  }
212
212
  catch {
@@ -629,7 +629,10 @@ export function nativeCommit(basePath, message, options) {
629
629
  }
630
630
  // Fallback: use git commit with stdin pipe for safe multi-line messages
631
631
  try {
632
- const result = execSync(`git commit --no-verify -F -${options?.allowEmpty ? " --allow-empty" : ""}`, {
632
+ const args = ["commit", "--no-verify", "-F", "-"];
633
+ if (options?.allowEmpty)
634
+ args.push("--allow-empty");
635
+ const result = execFileSync("git", args, {
633
636
  cwd: basePath,
634
637
  stdio: ["pipe", "pipe", "pipe"],
635
638
  encoding: "utf-8",
@@ -767,7 +770,7 @@ export function nativeResetHard(basePath) {
767
770
  native.gitResetHard(basePath);
768
771
  return;
769
772
  }
770
- execSync("git reset --hard HEAD", { cwd: basePath, stdio: "pipe" });
773
+ execFileSync("git", ["reset", "--hard", "HEAD"], { cwd: basePath, stdio: "pipe" });
771
774
  }
772
775
  /**
773
776
  * Soft reset to a target ref (git reset --soft <ref>).
@@ -1,6 +1,6 @@
1
1
  // GSD Extension — Notification Widget
2
2
  // Always-on ambient widget rendered belowEditor showing unread count and
3
- // the most recent notification message. Refreshes every 5 seconds.
3
+ // the most recent notification message. Refreshes every 30 seconds.
4
4
  // Widget key: "gsd-notifications", placement: "belowEditor"
5
5
  import { getUnreadCount, onNotificationStoreChange } from "./notification-store.js";
6
6
  import { formattedShortcutPair } from "./shortcut-defs.js";
@@ -12,7 +12,7 @@ export function buildNotificationWidgetLines() {
12
12
  return [` 🔔 Notifications: ${unread} unread (${formattedShortcutPair("notifications")})`];
13
13
  }
14
14
  // ─── Widget init ────────────────────────────────────────────────────────
15
- const REFRESH_INTERVAL_MS = 5_000;
15
+ const REFRESH_INTERVAL_MS = 30_000;
16
16
  /**
17
17
  * Initialize the always-on notification widget (belowEditor).
18
18
  * Call once from session_start after the notification store is initialized.
@@ -0,0 +1,35 @@
1
+ You are generating tests for recently completed GSD work.
2
+
3
+ ## Slice: {{sliceId}} — {{sliceTitle}}
4
+
5
+ ### Summary
6
+
7
+ {{sliceSummary}}
8
+
9
+ ### Existing Test Patterns
10
+
11
+ {{existingTestPatterns}}
12
+
13
+ ## Working Directory
14
+
15
+ `{{workingDirectory}}`
16
+
17
+ ## Instructions
18
+
19
+ 1. Read the slice summary above to understand what was built
20
+ 2. Identify the source files that were created or modified for this slice
21
+ 3. Read the implementation code to understand behavior, edge cases, and error paths
22
+ 4. Write comprehensive tests following the project's existing test patterns and framework
23
+ 5. Run the tests to verify they pass
24
+ 6. Fix any failures
25
+
26
+ ### Rules
27
+
28
+ - Follow the project's existing test patterns (framework, assertions, file structure)
29
+ - Test behavior, not implementation details
30
+ - Cover: happy path, edge cases, error conditions, boundary values
31
+ - Do NOT modify implementation files — only create or update test files
32
+ - Name test files consistently with the project's conventions
33
+ - Keep tests focused and readable
34
+
35
+ {{skillActivation}}
@@ -271,7 +271,11 @@ function reconcileDiskToDb(basePath) {
271
271
  try {
272
272
  roadmapContent = readFileSync(roadmapPath, "utf-8");
273
273
  }
274
- catch {
274
+ catch (err) {
275
+ logWarning("state", "reconcileDiskToDb: roadmap read failed, skipping milestone", {
276
+ mid,
277
+ error: err.message,
278
+ });
275
279
  continue;
276
280
  }
277
281
  const parsed = parseRoadmap(roadmapContent);
@@ -312,6 +316,10 @@ function reconcileDiskToDb(basePath) {
312
316
  function buildCompletenessSet(basePath, milestones) {
313
317
  const completeMilestoneIds = new Set();
314
318
  const parkedMilestoneIds = new Set();
319
+ // DB-authoritative: a milestone is only "complete" when its DB row says so.
320
+ // SUMMARY-file presence is NOT a completion signal here — an orphan SUMMARY
321
+ // (crashed complete-milestone turn, partial merge, manual edit) must not
322
+ // flip derived state to complete and cascade into a false auto-merge (#4179).
315
323
  for (const m of milestones) {
316
324
  const parkedFile = resolveMilestoneFile(basePath, m.id, "PARKED");
317
325
  if (parkedFile || m.status === 'parked') {
@@ -322,11 +330,6 @@ function buildCompletenessSet(basePath, milestones) {
322
330
  completeMilestoneIds.add(m.id);
323
331
  continue;
324
332
  }
325
- const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
326
- if (summaryFile) {
327
- completeMilestoneIds.add(m.id);
328
- continue;
329
- }
330
333
  }
331
334
  return { completeMilestoneIds, parkedMilestoneIds };
332
335
  }
@@ -347,17 +350,22 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
347
350
  if (isGhostMilestone(basePath, m.id))
348
351
  continue;
349
352
  }
350
- const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
351
- if (completeMilestoneIds.has(m.id) || (summaryFile !== null)) {
353
+ // DB-authoritative completeness (#4179): only trust completeMilestoneIds,
354
+ // which is itself derived from DB status. SUMMARY-file presence alone must
355
+ // not imply completion. The summary file may still be consulted below as a
356
+ // title source for legitimately-complete milestones whose DB row has no title.
357
+ if (completeMilestoneIds.has(m.id)) {
352
358
  let title = stripMilestonePrefix(m.title) || m.id;
353
- if (summaryFile && !m.title) {
354
- const summaryContent = await loadFile(summaryFile);
355
- if (summaryContent) {
356
- title = parseSummary(summaryContent).title || m.id;
359
+ if (!m.title) {
360
+ const summaryFile = resolveMilestoneFile(basePath, m.id, "SUMMARY");
361
+ if (summaryFile) {
362
+ const summaryContent = await loadFile(summaryFile);
363
+ if (summaryContent) {
364
+ title = parseSummary(summaryContent).title || m.id;
365
+ }
357
366
  }
358
367
  }
359
368
  registry.push({ id: m.id, title, status: 'complete' });
360
- completeMilestoneIds.add(m.id);
361
369
  continue;
362
370
  }
363
371
  const allSlicesDone = slices.length > 0 && slices.every(s => isStatusDone(s.status));
@@ -391,7 +399,14 @@ async function buildRegistryAndFindActive(basePath, milestones, completeMileston
391
399
  const validationFile = resolveMilestoneFile(basePath, m.id, "VALIDATION");
392
400
  const validationContent = validationFile ? await loadFile(validationFile) : null;
393
401
  const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
394
- if (!validationTerminal || (validationTerminal && !summaryFile)) {
402
+ // DB-authoritative (#4179): completeness is already decided by
403
+ // completeMilestoneIds above. If we reached this branch, the DB says
404
+ // the milestone is NOT complete — so any SUMMARY file on disk is an
405
+ // orphan (crashed complete-milestone, partial merge, manual edit) and
406
+ // must not short-circuit this path. When validation is terminal, fall
407
+ // through to the default active-push below so `complete-milestone` can
408
+ // re-run idempotently.
409
+ if (!validationTerminal) {
395
410
  activeMilestone = { id: m.id, title };
396
411
  activeMilestoneSlices = slices;
397
412
  activeMilestoneFound = true;
@@ -516,6 +531,9 @@ function resolveSliceDependencies(activeMilestoneSlices) {
516
531
  return { activeSlice: null, activeSliceRow: null };
517
532
  }
518
533
  }
534
+ // First pass: find a slice with ALL dependencies satisfied (strict)
535
+ let bestFallback = null;
536
+ let bestFallbackSatisfied = -1;
519
537
  for (const s of activeMilestoneSlices) {
520
538
  if (isStatusDone(s.status))
521
539
  continue;
@@ -524,6 +542,23 @@ function resolveSliceDependencies(activeMilestoneSlices) {
524
542
  if (s.depends.every(dep => doneSliceIds.has(dep))) {
525
543
  return { activeSlice: { id: s.id, title: s.title }, activeSliceRow: s };
526
544
  }
545
+ // Track the slice with the most satisfied dependencies as fallback
546
+ const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
547
+ if (satisfied > bestFallbackSatisfied || (satisfied === bestFallbackSatisfied && !bestFallback)) {
548
+ bestFallback = s;
549
+ bestFallbackSatisfied = satisfied;
550
+ }
551
+ }
552
+ // Fallback: if no slice has all deps met but there ARE incomplete non-deferred
553
+ // slices, pick the one with the most deps satisfied. This prevents hard-blocking
554
+ // when dependency metadata is stale (e.g. after reassessment added/removed slices)
555
+ // or when deps reference slices from previous milestones.
556
+ if (bestFallback) {
557
+ const unmet = bestFallback.depends.filter(dep => !doneSliceIds.has(dep));
558
+ logWarning("state", `No slice has all deps satisfied — falling back to ${bestFallback.id} ` +
559
+ `(${bestFallbackSatisfied}/${bestFallback.depends.length} deps met, ` +
560
+ `unmet: ${unmet.join(", ")})`, { mid: activeMilestoneSlices[0]?.milestone_id, sid: bestFallback.id });
561
+ return { activeSlice: { id: bestFallback.id, title: bestFallback.title }, activeSliceRow: bestFallback };
527
562
  }
528
563
  return { activeSlice: null, activeSliceRow: null };
529
564
  }
@@ -567,7 +602,7 @@ async function reconcileSliceTasks(basePath, milestoneId, sliceId, planFile) {
567
602
  const summaryPath = resolveTaskFile(basePath, milestoneId, sliceId, t.id, "SUMMARY");
568
603
  if (summaryPath && existsSync(summaryPath)) {
569
604
  try {
570
- updateTaskStatus(milestoneId, sliceId, t.id, "complete");
605
+ updateTaskStatus(milestoneId, sliceId, t.id, "complete", new Date().toISOString());
571
606
  logWarning("reconcile", `task ${milestoneId}/${sliceId}/${t.id} status reconciled from "${t.status}" to "complete" (#2514)`, { mid: milestoneId, sid: sliceId, tid: t.id });
572
607
  reconciled = true;
573
608
  }
@@ -1269,6 +1304,8 @@ export async function _deriveStateImpl(basePath) {
1269
1304
  }
1270
1305
  }
1271
1306
  else {
1307
+ let bestFallbackLegacy = null;
1308
+ let bestFallbackLegacySatisfied = -1;
1272
1309
  for (const s of activeRoadmap.slices) {
1273
1310
  if (s.done)
1274
1311
  continue;
@@ -1276,6 +1313,20 @@ export async function _deriveStateImpl(basePath) {
1276
1313
  activeSlice = { id: s.id, title: s.title };
1277
1314
  break;
1278
1315
  }
1316
+ // Track best fallback
1317
+ const satisfied = s.depends.filter(dep => doneSliceIds.has(dep)).length;
1318
+ if (satisfied > bestFallbackLegacySatisfied) {
1319
+ bestFallbackLegacy = s;
1320
+ bestFallbackLegacySatisfied = satisfied;
1321
+ }
1322
+ }
1323
+ // Fallback: if no slice has all deps met, pick the one with the most deps satisfied
1324
+ if (!activeSlice && bestFallbackLegacy) {
1325
+ const unmet = bestFallbackLegacy.depends.filter(dep => !doneSliceIds.has(dep));
1326
+ logWarning("state", `No slice has all deps satisfied — falling back to ${bestFallbackLegacy.id} ` +
1327
+ `(${bestFallbackLegacySatisfied}/${bestFallbackLegacy.depends.length} deps met, ` +
1328
+ `unmet: ${unmet.join(", ")})`);
1329
+ activeSlice = { id: bestFallbackLegacy.id, title: bestFallbackLegacy.title };
1279
1330
  }
1280
1331
  }
1281
1332
  if (!activeSlice) {
@@ -1,7 +1,7 @@
1
1
  import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
2
2
  import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-milestone.js";
3
3
  import { loadWriteGateSnapshot, shouldBlockContextArtifactSaveInSnapshot } from "../bootstrap/write-gate.js";
4
- import { getMilestone, getSliceStatusSummary, getSliceTaskCounts, _getAdapter, saveGateResult, } from "../gsd-db.js";
4
+ import { getMilestone, getSliceStatusSummary, getSliceTaskCounts, readTransaction, saveGateResult, } from "../gsd-db.js";
5
5
  import { GATE_REGISTRY } from "../gate-registry.js";
6
6
  import { saveArtifactToDb } from "../db-writer.js";
7
7
  import { handleCompleteMilestone } from "./complete-milestone.js";
@@ -493,12 +493,9 @@ export async function executeMilestoneStatus(params, basePath = process.cwd()) {
493
493
  isError: true,
494
494
  };
495
495
  }
496
- const adapter = _getAdapter();
497
- adapter.exec("BEGIN");
498
- try {
496
+ return readTransaction(() => {
499
497
  const milestone = getMilestone(params.milestoneId);
500
498
  if (!milestone) {
501
- adapter.exec("COMMIT");
502
499
  return {
503
500
  content: [{ type: "text", text: `Milestone ${params.milestoneId} not found in database.` }],
504
501
  details: { operation: "milestone_status", milestoneId: params.milestoneId, found: false },
@@ -510,7 +507,6 @@ export async function executeMilestoneStatus(params, basePath = process.cwd()) {
510
507
  status: s.status,
511
508
  taskCounts: getSliceTaskCounts(params.milestoneId, s.id),
512
509
  }));
513
- adapter.exec("COMMIT");
514
510
  const result = {
515
511
  milestoneId: milestone.id,
516
512
  title: milestone.title,
@@ -524,14 +520,7 @@ export async function executeMilestoneStatus(params, basePath = process.cwd()) {
524
520
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
525
521
  details: { operation: "milestone_status", milestoneId: milestone.id, sliceCount: slices.length },
526
522
  };
527
- }
528
- catch (txErr) {
529
- try {
530
- adapter.exec("ROLLBACK");
531
- }
532
- catch { /* swallow */ }
533
- throw txErr;
534
- }
523
+ });
535
524
  }
536
525
  catch (err) {
537
526
  const msg = err instanceof Error ? err.message : String(err);
@@ -83,12 +83,9 @@ export function executeReplan(basePath, mid, sid, capture) {
83
83
  // Also write replan_triggered_at column for DB-backed detection
84
84
  try {
85
85
  const req = createRequire(import.meta.url);
86
- const { isDbAvailable, _getAdapter } = req("./gsd-db.js");
86
+ const { isDbAvailable, setSliceReplanTriggeredAt } = req("./gsd-db.js");
87
87
  if (isDbAvailable()) {
88
- const adapter = _getAdapter();
89
- if (adapter) {
90
- adapter.prepare("UPDATE slices SET replan_triggered_at = :ts WHERE milestone_id = :mid AND id = :sid").run({ ":ts": ts, ":mid": mid, ":sid": sid });
91
- }
88
+ setSliceReplanTriggeredAt(mid, sid, ts);
92
89
  }
93
90
  }
94
91
  catch {
@@ -1,4 +1,4 @@
1
- import { _getAdapter, transaction, } from "./gsd-db.js";
1
+ import { _getAdapter, readTransaction, restoreManifest, } from "./gsd-db.js";
2
2
  import { atomicWriteSync } from "./atomic-write.js";
3
3
  import { readFileSync, existsSync, mkdirSync } from "node:fs";
4
4
  import { join } from "node:path";
@@ -41,8 +41,7 @@ export function snapshotState() {
41
41
  const db = requireDb();
42
42
  // Wrap all reads in a deferred transaction so the snapshot is consistent
43
43
  // (all SELECTs see the same DB state even if a concurrent write lands between them).
44
- db.exec("BEGIN DEFERRED");
45
- try {
44
+ return readTransaction(() => {
46
45
  const rawMilestones = db.prepare("SELECT * FROM milestones ORDER BY id").all();
47
46
  const milestones = rawMilestones.map((r) => ({
48
47
  id: r["id"],
@@ -146,74 +145,14 @@ export function snapshotState() {
146
145
  decisions,
147
146
  verification_evidence,
148
147
  };
149
- db.exec("COMMIT");
150
148
  return result;
151
- }
152
- catch (err) {
153
- try {
154
- db.exec("ROLLBACK");
155
- }
156
- catch { /* ignore rollback failure */ }
157
- throw err;
158
- }
159
- }
160
- // ─── restore ─────────────────────────────────────────────────────────────
161
- /**
162
- * Atomically replace all workflow state from a manifest.
163
- * Runs inside a transaction — if any insert fails, no tables are modified.
164
- * Only touches engine tables + decisions. Does NOT modify artifacts or memories.
165
- */
166
- function restore(manifest) {
167
- const db = requireDb();
168
- transaction(() => {
169
- // Clear engine tables (order matters for foreign-key-like consistency)
170
- db.exec("DELETE FROM verification_evidence");
171
- db.exec("DELETE FROM tasks");
172
- db.exec("DELETE FROM slices");
173
- db.exec("DELETE FROM milestones");
174
- db.exec("DELETE FROM decisions WHERE 1=1");
175
- // Restore milestones
176
- const msStmt = db.prepare(`INSERT INTO milestones (id, title, status, depends_on, created_at, completed_at,
177
- vision, success_criteria, key_risks, proof_strategy,
178
- verification_contract, verification_integration, verification_operational, verification_uat,
179
- definition_of_done, requirement_coverage, boundary_map_markdown)
180
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
181
- for (const m of manifest.milestones) {
182
- msStmt.run(m.id, m.title, m.status, JSON.stringify(m.depends_on), m.created_at, m.completed_at, m.vision, JSON.stringify(m.success_criteria), JSON.stringify(m.key_risks), JSON.stringify(m.proof_strategy), m.verification_contract, m.verification_integration, m.verification_operational, m.verification_uat, JSON.stringify(m.definition_of_done), m.requirement_coverage, m.boundary_map_markdown);
183
- }
184
- // Restore slices
185
- const slStmt = db.prepare(`INSERT INTO slices (milestone_id, id, title, status, risk, depends, demo,
186
- created_at, completed_at, full_summary_md, full_uat_md,
187
- goal, success_criteria, proof_level, integration_closure, observability_impact,
188
- sequence, replan_triggered_at)
189
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
190
- for (const s of manifest.slices) {
191
- slStmt.run(s.milestone_id, s.id, s.title, s.status, s.risk, JSON.stringify(s.depends), s.demo, s.created_at, s.completed_at, s.full_summary_md, s.full_uat_md, s.goal, s.success_criteria, s.proof_level, s.integration_closure, s.observability_impact, s.sequence, s.replan_triggered_at);
192
- }
193
- // Restore tasks
194
- const tkStmt = db.prepare(`INSERT INTO tasks (milestone_id, slice_id, id, title, status,
195
- one_liner, narrative, verification_result, duration, completed_at,
196
- blocker_discovered, deviations, known_issues, key_files, key_decisions,
197
- full_summary_md, description, estimate, files, verify,
198
- inputs, expected_output, observability_impact, sequence)
199
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
200
- for (const t of manifest.tasks) {
201
- tkStmt.run(t.milestone_id, t.slice_id, t.id, t.title, t.status, t.one_liner, t.narrative, t.verification_result, t.duration, t.completed_at, t.blocker_discovered ? 1 : 0, t.deviations, t.known_issues, JSON.stringify(t.key_files), JSON.stringify(t.key_decisions), t.full_summary_md, t.description, t.estimate, JSON.stringify(t.files), t.verify, JSON.stringify(t.inputs), JSON.stringify(t.expected_output), t.observability_impact, t.sequence);
202
- }
203
- // Restore decisions
204
- const dcStmt = db.prepare(`INSERT INTO decisions (seq, id, when_context, scope, decision, choice, rationale, revisable, made_by, superseded_by)
205
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
206
- for (const d of manifest.decisions) {
207
- dcStmt.run(d.seq, d.id, d.when_context, d.scope, d.decision, d.choice, d.rationale, d.revisable, d.made_by, d.superseded_by);
208
- }
209
- // Restore verification evidence
210
- const evStmt = db.prepare(`INSERT INTO verification_evidence (task_id, slice_id, milestone_id, command, exit_code, verdict, duration_ms, created_at)
211
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
212
- for (const e of manifest.verification_evidence) {
213
- evStmt.run(e.task_id, e.slice_id, e.milestone_id, e.command, e.exit_code, e.verdict, e.duration_ms, e.created_at);
214
- }
215
149
  });
216
150
  }
151
+ // ─── restore ─────────────────────────────────────────────────────────────
152
+ //
153
+ // The actual restore() implementation lives in gsd-db.ts (single-writer
154
+ // invariant). This module only orchestrates reading the manifest file
155
+ // and handing it to the writer.
217
156
  // ─── writeManifest ───────────────────────────────────────────────────────
218
157
  /**
219
158
  * Write current DB state to .gsd/state-manifest.json via atomicWriteSync.
@@ -258,6 +197,6 @@ export function bootstrapFromManifest(basePath) {
258
197
  if (!manifest) {
259
198
  return false;
260
199
  }
261
- restore(manifest);
200
+ restoreManifest(manifest);
262
201
  return true;
263
202
  }