gsd-pi 2.74.0-dev.2b524c3 → 2.74.0-dev.b741afb

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 (159) hide show
  1. package/dist/cli.js +85 -0
  2. package/dist/headless-query.js +4 -1
  3. package/dist/help-text.js +23 -0
  4. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  5. package/dist/resources/extensions/gsd/auto/phases.js +45 -1
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +52 -56
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -0
  8. package/dist/resources/extensions/gsd/auto.js +8 -2
  9. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
  10. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  11. package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
  12. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  13. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  14. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  15. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  16. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  17. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  18. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  19. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  20. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  21. package/dist/resources/extensions/gsd/graph-context.js +66 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +321 -0
  23. package/dist/resources/extensions/gsd/index.js +15 -2
  24. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  25. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  26. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  27. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  28. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  29. package/dist/resources/extensions/gsd/state.js +5 -1
  30. package/dist/resources/extensions/gsd/tools/complete-slice.js +15 -0
  31. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  32. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  33. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  34. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  35. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  36. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.html +1 -1
  59. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  68. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  69. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  70. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  71. package/package.json +3 -2
  72. package/packages/daemon/package.json +2 -2
  73. package/packages/mcp-server/dist/index.d.ts +3 -0
  74. package/packages/mcp-server/dist/index.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/index.js +3 -0
  76. package/packages/mcp-server/dist/index.js.map +1 -1
  77. package/packages/mcp-server/dist/readers/graph.d.ts +87 -0
  78. package/packages/mcp-server/dist/readers/graph.d.ts.map +1 -0
  79. package/packages/mcp-server/dist/readers/graph.js +548 -0
  80. package/packages/mcp-server/dist/readers/graph.js.map +1 -0
  81. package/packages/mcp-server/dist/readers/index.d.ts +2 -0
  82. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -1
  83. package/packages/mcp-server/dist/readers/index.js +1 -0
  84. package/packages/mcp-server/dist/readers/index.js.map +1 -1
  85. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  86. package/packages/mcp-server/dist/server.js +65 -0
  87. package/packages/mcp-server/dist/server.js.map +1 -1
  88. package/packages/mcp-server/package.json +2 -2
  89. package/packages/mcp-server/src/index.ts +15 -0
  90. package/packages/mcp-server/src/readers/graph.test.ts +426 -0
  91. package/packages/mcp-server/src/readers/graph.ts +708 -0
  92. package/packages/mcp-server/src/readers/index.ts +12 -0
  93. package/packages/mcp-server/src/server.ts +83 -0
  94. package/packages/mcp-server/tsconfig.json +1 -0
  95. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  96. package/packages/native/package.json +2 -2
  97. package/packages/native/tsconfig.tsbuildinfo +1 -0
  98. package/packages/pi-agent-core/package.json +1 -1
  99. package/packages/pi-agent-core/tsconfig.json +1 -0
  100. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  101. package/packages/pi-ai/package.json +1 -1
  102. package/packages/pi-ai/tsconfig.json +1 -0
  103. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  104. package/packages/pi-coding-agent/tsconfig.json +1 -0
  105. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  106. package/packages/pi-tui/package.json +1 -1
  107. package/packages/pi-tui/tsconfig.json +1 -0
  108. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  109. package/packages/rpc-client/package.json +1 -1
  110. package/packages/rpc-client/tsconfig.json +1 -0
  111. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  112. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  113. package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
  114. package/src/resources/extensions/gsd/auto/phases.ts +68 -1
  115. package/src/resources/extensions/gsd/auto-post-unit.ts +60 -57
  116. package/src/resources/extensions/gsd/auto-prompts.ts +13 -0
  117. package/src/resources/extensions/gsd/auto.ts +7 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
  119. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  120. package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
  121. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  122. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  123. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  124. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  125. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  126. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  127. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  128. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  129. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  130. package/src/resources/extensions/gsd/graph-context.ts +85 -0
  131. package/src/resources/extensions/gsd/gsd-db.ts +467 -0
  132. package/src/resources/extensions/gsd/index.ts +18 -2
  133. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  134. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  135. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  136. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  137. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  138. package/src/resources/extensions/gsd/state.ts +9 -2
  139. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  140. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  141. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  142. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  143. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  146. package/src/resources/extensions/gsd/tests/graph-context.test.ts +337 -0
  147. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  148. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  149. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  150. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  151. package/src/resources/extensions/gsd/tools/complete-slice.ts +19 -0
  152. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  153. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  154. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  155. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  156. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  157. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  158. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_buildManifest.js +0 -0
  159. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_ssgManifest.js +0 -0
@@ -3,7 +3,19 @@
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
5
 
6
- import { isDbAvailable, _getAdapter, transaction } from './gsd-db.js';
6
+ import {
7
+ isDbAvailable,
8
+ _getAdapter,
9
+ transaction,
10
+ insertMemoryRow,
11
+ rewriteMemoryId,
12
+ updateMemoryContentRow,
13
+ incrementMemoryHitCount,
14
+ supersedeMemoryRow,
15
+ markMemoryUnitProcessed,
16
+ decayMemoriesBefore,
17
+ supersedeLowestRankedMemories,
18
+ } from './gsd-db.js';
7
19
 
8
20
  // ─── Types ──────────────────────────────────────────────────────────────────
9
21
 
@@ -170,28 +182,22 @@ export function createMemory(fields: {
170
182
  const now = new Date().toISOString();
171
183
  // Insert with a temporary placeholder ID — seq is auto-assigned
172
184
  const placeholder = `_TMP_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
173
- adapter.prepare(
174
- `INSERT INTO memories (id, category, content, confidence, source_unit_type, source_unit_id, created_at, updated_at)
175
- VALUES (:id, :category, :content, :confidence, :source_unit_type, :source_unit_id, :created_at, :updated_at)`,
176
- ).run({
177
- ':id': placeholder,
178
- ':category': fields.category,
179
- ':content': fields.content,
180
- ':confidence': fields.confidence ?? 0.8,
181
- ':source_unit_type': fields.source_unit_type ?? null,
182
- ':source_unit_id': fields.source_unit_id ?? null,
183
- ':created_at': now,
184
- ':updated_at': now,
185
+ insertMemoryRow({
186
+ id: placeholder,
187
+ category: fields.category,
188
+ content: fields.content,
189
+ confidence: fields.confidence ?? 0.8,
190
+ sourceUnitType: fields.source_unit_type ?? null,
191
+ sourceUnitId: fields.source_unit_id ?? null,
192
+ createdAt: now,
193
+ updatedAt: now,
185
194
  });
186
- // Derive the real ID from the assigned seq
195
+ // Derive the real ID from the assigned seq (SELECT is still fine via adapter)
187
196
  const row = adapter.prepare('SELECT seq FROM memories WHERE id = :id').get({ ':id': placeholder });
188
197
  if (!row) return placeholder; // fallback — should not happen
189
198
  const seq = row['seq'] as number;
190
199
  const realId = `MEM${String(seq).padStart(3, '0')}`;
191
- adapter.prepare('UPDATE memories SET id = :real_id WHERE id = :placeholder').run({
192
- ':real_id': realId,
193
- ':placeholder': placeholder,
194
- });
200
+ rewriteMemoryId(placeholder, realId);
195
201
  return realId;
196
202
  } catch {
197
203
  return null;
@@ -203,20 +209,9 @@ export function createMemory(fields: {
203
209
  */
204
210
  export function updateMemoryContent(id: string, content: string, confidence?: number): boolean {
205
211
  if (!isDbAvailable()) return false;
206
- const adapter = _getAdapter();
207
- if (!adapter) return false;
208
212
 
209
213
  try {
210
- const now = new Date().toISOString();
211
- if (confidence != null) {
212
- adapter.prepare(
213
- 'UPDATE memories SET content = :content, confidence = :confidence, updated_at = :updated_at WHERE id = :id',
214
- ).run({ ':content': content, ':confidence': confidence, ':updated_at': now, ':id': id });
215
- } else {
216
- adapter.prepare(
217
- 'UPDATE memories SET content = :content, updated_at = :updated_at WHERE id = :id',
218
- ).run({ ':content': content, ':updated_at': now, ':id': id });
219
- }
214
+ updateMemoryContentRow(id, content, confidence, new Date().toISOString());
220
215
  return true;
221
216
  } catch {
222
217
  return false;
@@ -228,13 +223,9 @@ export function updateMemoryContent(id: string, content: string, confidence?: nu
228
223
  */
229
224
  export function reinforceMemory(id: string): boolean {
230
225
  if (!isDbAvailable()) return false;
231
- const adapter = _getAdapter();
232
- if (!adapter) return false;
233
226
 
234
227
  try {
235
- adapter.prepare(
236
- 'UPDATE memories SET hit_count = hit_count + 1, updated_at = :updated_at WHERE id = :id',
237
- ).run({ ':updated_at': new Date().toISOString(), ':id': id });
228
+ incrementMemoryHitCount(id, new Date().toISOString());
238
229
  return true;
239
230
  } catch {
240
231
  return false;
@@ -246,13 +237,9 @@ export function reinforceMemory(id: string): boolean {
246
237
  */
247
238
  export function supersedeMemory(oldId: string, newId: string): boolean {
248
239
  if (!isDbAvailable()) return false;
249
- const adapter = _getAdapter();
250
- if (!adapter) return false;
251
240
 
252
241
  try {
253
- adapter.prepare(
254
- 'UPDATE memories SET superseded_by = :new_id, updated_at = :updated_at WHERE id = :old_id',
255
- ).run({ ':new_id': newId, ':updated_at': new Date().toISOString(), ':old_id': oldId });
242
+ supersedeMemoryRow(oldId, newId, new Date().toISOString());
256
243
  return true;
257
244
  } catch {
258
245
  return false;
@@ -284,14 +271,9 @@ export function isUnitProcessed(unitKey: string): boolean {
284
271
  */
285
272
  export function markUnitProcessed(unitKey: string, activityFile: string): boolean {
286
273
  if (!isDbAvailable()) return false;
287
- const adapter = _getAdapter();
288
- if (!adapter) return false;
289
274
 
290
275
  try {
291
- adapter.prepare(
292
- `INSERT OR IGNORE INTO memory_processed_units (unit_key, activity_file, processed_at)
293
- VALUES (:key, :file, :at)`,
294
- ).run({ ':key': unitKey, ':file': activityFile, ':at': new Date().toISOString() });
276
+ markMemoryUnitProcessed(unitKey, activityFile, new Date().toISOString());
295
277
  return true;
296
278
  } catch {
297
279
  return false;
@@ -310,7 +292,7 @@ export function decayStaleMemories(thresholdUnits = 20): void {
310
292
  if (!adapter) return;
311
293
 
312
294
  try {
313
- // Find the timestamp of the Nth most recent processed unit
295
+ // Find the timestamp of the Nth most recent processed unit (read-only SELECT)
314
296
  const row = adapter.prepare(
315
297
  `SELECT processed_at FROM memory_processed_units
316
298
  ORDER BY processed_at DESC
@@ -320,11 +302,7 @@ export function decayStaleMemories(thresholdUnits = 20): void {
320
302
  if (!row) return; // not enough processed units yet
321
303
 
322
304
  const cutoff = row['processed_at'] as string;
323
- adapter.prepare(
324
- `UPDATE memories
325
- SET confidence = MAX(0.1, confidence - 0.1), updated_at = :now
326
- WHERE superseded_by IS NULL AND updated_at < :cutoff AND confidence > 0.1`,
327
- ).run({ ':now': new Date().toISOString(), ':cutoff': cutoff });
305
+ decayMemoriesBefore(cutoff, new Date().toISOString());
328
306
  } catch {
329
307
  // non-fatal
330
308
  }
@@ -346,16 +324,7 @@ export function enforceMemoryCap(max = 50): void {
346
324
  if (count <= max) return;
347
325
 
348
326
  const excess = count - max;
349
- // Batch update: supersede lowest-ranked active memories in a single statement
350
- adapter.prepare(
351
- `UPDATE memories SET superseded_by = 'CAP_EXCEEDED', updated_at = :now
352
- WHERE id IN (
353
- SELECT id FROM memories
354
- WHERE superseded_by IS NULL
355
- ORDER BY (confidence * (1.0 + hit_count * 0.1)) ASC
356
- LIMIT :limit
357
- )`,
358
- ).run({ ':now': new Date().toISOString(), ':limit': excess });
327
+ supersedeLowestRankedMemories(excess, new Date().toISOString());
359
328
  } catch {
360
329
  // non-fatal
361
330
  }
@@ -11,7 +11,7 @@
11
11
  * dispatch rules, and state derivation. See gate-registry.ts.
12
12
  */
13
13
 
14
- import { _getAdapter } from "./gsd-db.js";
14
+ import { isDbAvailable, upsertQualityGate } from "./gsd-db.js";
15
15
  import { getGatesForTurn } from "./gate-registry.js";
16
16
 
17
17
  /**
@@ -31,24 +31,23 @@ export function insertMilestoneValidationGates(
31
31
  verdict: string,
32
32
  evaluatedAt: string,
33
33
  ): void {
34
- const db = _getAdapter();
35
- if (!db) return;
34
+ if (!isDbAvailable()) return;
36
35
 
37
36
  const gateVerdict = verdict === "pass" ? "pass" : "flag";
38
37
  const milestoneGates = getGatesForTurn("validate-milestone");
39
38
 
40
39
  for (const def of milestoneGates) {
41
- db.prepare(
42
- `INSERT OR REPLACE INTO quality_gates
43
- (milestone_id, slice_id, gate_id, scope, task_id, status, verdict, rationale, findings, evaluated_at)
44
- VALUES (:mid, :sid, :gid, 'milestone', '', 'complete', :verdict, :rationale, '', :evaluated_at)`,
45
- ).run({
46
- ":mid": milestoneId,
47
- ":sid": sliceId,
48
- ":gid": def.id,
49
- ":verdict": gateVerdict,
50
- ":rationale": `${def.promptSection} — milestone validation verdict: ${verdict}`,
51
- ":evaluated_at": evaluatedAt,
40
+ upsertQualityGate({
41
+ milestoneId,
42
+ sliceId,
43
+ gateId: def.id,
44
+ scope: "milestone",
45
+ taskId: "",
46
+ status: "complete",
47
+ verdict: gateVerdict,
48
+ rationale: `${def.promptSection} — milestone validation verdict: ${verdict}`,
49
+ findings: "",
50
+ evaluatedAt,
52
51
  });
53
52
  }
54
53
  }
@@ -323,7 +323,7 @@ export function nativeIsRepo(basePath: string): boolean {
323
323
  return native.gitIsRepo(basePath);
324
324
  }
325
325
  try {
326
- execSync("git rev-parse --git-dir", { cwd: basePath, stdio: "pipe" });
326
+ execFileSync("git", ["rev-parse", "--git-dir"], { cwd: basePath, stdio: "pipe" });
327
327
  return true;
328
328
  } catch {
329
329
  return false;
@@ -790,16 +790,15 @@ export function nativeCommit(
790
790
 
791
791
  // Fallback: use git commit with stdin pipe for safe multi-line messages
792
792
  try {
793
- const result = execSync(
794
- `git commit --no-verify -F -${options?.allowEmpty ? " --allow-empty" : ""}`,
795
- {
796
- cwd: basePath,
797
- stdio: ["pipe", "pipe", "pipe"],
798
- encoding: "utf-8",
799
- env: GIT_NO_PROMPT_ENV,
800
- input: message,
801
- },
802
- ).trim();
793
+ const args = ["commit", "--no-verify", "-F", "-"];
794
+ if (options?.allowEmpty) args.push("--allow-empty");
795
+ const result = execFileSync("git", args, {
796
+ cwd: basePath,
797
+ stdio: ["pipe", "pipe", "pipe"],
798
+ encoding: "utf-8",
799
+ env: GIT_NO_PROMPT_ENV,
800
+ input: message,
801
+ }).trim();
803
802
  return result;
804
803
  } catch (err: unknown) {
805
804
  const errObj = err as { stdout?: string; stderr?: string; message?: string };
@@ -940,7 +939,7 @@ export function nativeResetHard(basePath: string): void {
940
939
  native.gitResetHard(basePath);
941
940
  return;
942
941
  }
943
- execSync("git reset --hard HEAD", { cwd: basePath, stdio: "pipe" });
942
+ execFileSync("git", ["reset", "--hard", "HEAD"], { cwd: basePath, stdio: "pipe" });
944
943
  }
945
944
 
946
945
  /**
@@ -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}}
@@ -345,8 +345,15 @@ function reconcileDiskToDb(basePath: string): MilestoneRow[] {
345
345
  const dbSliceIds = new Set(dbSlices.map(s => s.id));
346
346
 
347
347
  let roadmapContent: string;
348
- try { roadmapContent = readFileSync(roadmapPath, "utf-8"); }
349
- catch { continue; }
348
+ try {
349
+ roadmapContent = readFileSync(roadmapPath, "utf-8");
350
+ } catch (err) {
351
+ logWarning("state", "reconcileDiskToDb: roadmap read failed, skipping milestone", {
352
+ mid,
353
+ error: (err as Error).message,
354
+ });
355
+ continue;
356
+ }
350
357
 
351
358
  const parsed = parseRoadmap(roadmapContent);
352
359
  for (const s of parsed.slices) {
@@ -0,0 +1,158 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ // ─── Helpers ──────────────────────────────────────────────────────────────
9
+
10
+ function makeTmpBase(): string {
11
+ const base = join(tmpdir(), `gsd-backlog-test-${randomUUID()}`);
12
+ mkdirSync(join(base, ".gsd"), { recursive: true });
13
+ return base;
14
+ }
15
+
16
+ function cleanup(base: string): void {
17
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
18
+ }
19
+
20
+ function backlogPath(base: string): string {
21
+ return join(base, ".gsd", "BACKLOG.md");
22
+ }
23
+
24
+ function writeBacklog(base: string, content: string): void {
25
+ writeFileSync(backlogPath(base), content, "utf-8");
26
+ }
27
+
28
+ function readBacklog(base: string): string {
29
+ return readFileSync(backlogPath(base), "utf-8");
30
+ }
31
+
32
+ // Test the parsing/writing logic inline since the handler requires runtime context
33
+
34
+ interface BacklogItem {
35
+ id: string;
36
+ title: string;
37
+ done: boolean;
38
+ note: string;
39
+ }
40
+
41
+ function parseBacklog(content: string): BacklogItem[] {
42
+ const items: BacklogItem[] = [];
43
+ for (const line of content.split("\n")) {
44
+ const match = line.match(/^- \[([ x])\] (999\.\d+) — (.+?)(?:\s*\((.+)\))?$/);
45
+ if (match) {
46
+ items.push({
47
+ id: match[2],
48
+ title: match[3].trim(),
49
+ done: match[1] === "x",
50
+ note: match[4] ?? "",
51
+ });
52
+ }
53
+ }
54
+ return items;
55
+ }
56
+
57
+ function formatBacklog(items: BacklogItem[]): string {
58
+ const lines = ["# Backlog\n"];
59
+ for (const item of items) {
60
+ const check = item.done ? "x" : " ";
61
+ const note = item.note ? ` (${item.note})` : "";
62
+ lines.push(`- [${check}] ${item.id} — ${item.title}${note}`);
63
+ }
64
+ lines.push("");
65
+ return lines.join("\n");
66
+ }
67
+
68
+ // ─── Tests ──────────────────────────────────────────────────────────────
69
+
70
+ test("backlog: parse empty file returns empty array", () => {
71
+ const items = parseBacklog("");
72
+ assert.equal(items.length, 0);
73
+ });
74
+
75
+ test("backlog: parse valid entries", () => {
76
+ const content = `# Backlog
77
+
78
+ - [ ] 999.1 — OAuth support (added 2026-03-23)
79
+ - [x] 999.2 — Rate limiting (promoted 2026-03-24)
80
+ - [ ] 999.3 — Dark mode`;
81
+
82
+ const items = parseBacklog(content);
83
+ assert.equal(items.length, 3);
84
+ assert.equal(items[0].id, "999.1");
85
+ assert.equal(items[0].title, "OAuth support");
86
+ assert.equal(items[0].done, false);
87
+ assert.equal(items[0].note, "added 2026-03-23");
88
+
89
+ assert.equal(items[1].id, "999.2");
90
+ assert.equal(items[1].done, true);
91
+ assert.equal(items[1].note, "promoted 2026-03-24");
92
+
93
+ assert.equal(items[2].id, "999.3");
94
+ assert.equal(items[2].title, "Dark mode");
95
+ assert.equal(items[2].note, "");
96
+ });
97
+
98
+ test("backlog: format roundtrips correctly", () => {
99
+ const items: BacklogItem[] = [
100
+ { id: "999.1", title: "OAuth support", done: false, note: "added 2026-03-23" },
101
+ { id: "999.2", title: "Rate limiting", done: true, note: "promoted 2026-03-24" },
102
+ ];
103
+
104
+ const formatted = formatBacklog(items);
105
+ const parsed = parseBacklog(formatted);
106
+
107
+ assert.equal(parsed.length, 2);
108
+ assert.equal(parsed[0].id, "999.1");
109
+ assert.equal(parsed[0].title, "OAuth support");
110
+ assert.equal(parsed[1].done, true);
111
+ });
112
+
113
+ test("backlog: write and read from disk", () => {
114
+ const base = makeTmpBase();
115
+ try {
116
+ const items: BacklogItem[] = [
117
+ { id: "999.1", title: "Test item", done: false, note: "added 2026-03-23" },
118
+ ];
119
+ writeBacklog(base, formatBacklog(items));
120
+
121
+ assert.ok(existsSync(backlogPath(base)));
122
+ const content = readBacklog(base);
123
+ assert.ok(content.includes("999.1"));
124
+ assert.ok(content.includes("Test item"));
125
+ } finally {
126
+ cleanup(base);
127
+ }
128
+ });
129
+
130
+ test("backlog: next ID increments correctly", () => {
131
+ const items: BacklogItem[] = [
132
+ { id: "999.1", title: "First", done: false, note: "" },
133
+ { id: "999.2", title: "Second", done: false, note: "" },
134
+ { id: "999.5", title: "Fifth", done: false, note: "" },
135
+ ];
136
+
137
+ let maxNum = 0;
138
+ for (const item of items) {
139
+ const match = item.id.match(/^999\.(\d+)$/);
140
+ if (match) {
141
+ const num = parseInt(match[1], 10);
142
+ if (num > maxNum) maxNum = num;
143
+ }
144
+ }
145
+ const nextId = `999.${maxNum + 1}`;
146
+ assert.equal(nextId, "999.6");
147
+ });
148
+
149
+ test("backlog: empty backlog returns no items", () => {
150
+ const base = makeTmpBase();
151
+ try {
152
+ // No BACKLOG.md exists
153
+ assert.ok(!existsSync(backlogPath(base)));
154
+ // Would return empty array
155
+ } finally {
156
+ cleanup(base);
157
+ }
158
+ });
@@ -0,0 +1,127 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ // ─── Mock dispatcher to capture routed commands ─────────────────────────
5
+
6
+ let lastRouted: string | null = null;
7
+ let lastQuick: string | null = null;
8
+
9
+ const mockCtx = {
10
+ ui: {
11
+ notify: (_msg: string, _level: string) => {},
12
+ },
13
+ } as any;
14
+
15
+ // We test the keyword matching logic directly since the handler imports
16
+ // the dispatcher dynamically (which requires the full extension runtime).
17
+
18
+ // Inline the route-matching logic from commands-do.ts for unit testing.
19
+ interface Route {
20
+ keywords: string[];
21
+ command: string;
22
+ }
23
+
24
+ const ROUTES: Route[] = [
25
+ { keywords: ["progress", "status", "dashboard", "how far", "where are we"], command: "status" },
26
+ { keywords: ["auto", "autonomous", "run all", "keep going", "start auto"], command: "auto" },
27
+ { keywords: ["stop", "halt", "abort"], command: "stop" },
28
+ { keywords: ["pause", "break", "take a break"], command: "pause" },
29
+ { keywords: ["history", "past", "what happened", "previous"], command: "history" },
30
+ { keywords: ["doctor", "health", "diagnose", "check health"], command: "doctor" },
31
+ { keywords: ["clean up", "cleanup", "remove old", "prune", "tidy"], command: "cleanup" },
32
+ { keywords: ["ship", "pull request", "create pr", "open pr", "merge"], command: "ship" },
33
+ { keywords: ["discuss", "talk about", "architecture", "design"], command: "discuss" },
34
+ { keywords: ["undo", "revert", "rollback", "take back"], command: "undo" },
35
+ { keywords: ["skip", "skip task", "skip this"], command: "skip" },
36
+ { keywords: ["visualize", "viz", "graph", "chart", "show graph"], command: "visualize" },
37
+ { keywords: ["capture", "note", "idea", "thought", "remember"], command: "capture" },
38
+ { keywords: ["inspect", "database", "sqlite", "db state"], command: "inspect" },
39
+ { keywords: ["session report", "session summary", "cost summary", "how much"], command: "session-report" },
40
+ { keywords: ["backlog", "parking lot", "later", "someday"], command: "backlog" },
41
+ { keywords: ["add tests", "write tests", "generate tests", "test coverage"], command: "add-tests" },
42
+ { keywords: ["next", "step", "next step", "what's next"], command: "next" },
43
+ ];
44
+
45
+ interface MatchResult {
46
+ command: string;
47
+ remainingArgs: string;
48
+ score: number;
49
+ }
50
+
51
+ function matchRoute(input: string): MatchResult | null {
52
+ const lower = input.toLowerCase();
53
+ let bestMatch: MatchResult | null = null;
54
+
55
+ for (const route of ROUTES) {
56
+ for (const keyword of route.keywords) {
57
+ if (lower.includes(keyword)) {
58
+ const score = keyword.length;
59
+ if (!bestMatch || score > bestMatch.score) {
60
+ const idx = lower.indexOf(keyword);
61
+ const remaining = (input.slice(0, idx) + input.slice(idx + keyword.length)).trim();
62
+ bestMatch = { command: route.command, remainingArgs: remaining, score };
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ return bestMatch;
69
+ }
70
+
71
+ // ─── Tests ──────────────────────────────────────────────────────────────
72
+
73
+ test("/gsd do: routes 'show me progress' to status", () => {
74
+ const match = matchRoute("show me progress");
75
+ assert.ok(match);
76
+ assert.equal(match.command, "status");
77
+ });
78
+
79
+ test("/gsd do: routes 'run autonomously' to auto", () => {
80
+ const match = matchRoute("run autonomously");
81
+ assert.ok(match);
82
+ assert.equal(match.command, "auto");
83
+ });
84
+
85
+ test("/gsd do: routes 'clean up old branches' to cleanup", () => {
86
+ const match = matchRoute("clean up old branches");
87
+ assert.ok(match);
88
+ assert.equal(match.command, "cleanup");
89
+ assert.equal(match.remainingArgs, "old branches");
90
+ });
91
+
92
+ test("/gsd do: routes 'create pr for milestone' to ship", () => {
93
+ const match = matchRoute("create pr for milestone");
94
+ assert.ok(match);
95
+ assert.equal(match.command, "ship");
96
+ });
97
+
98
+ test("/gsd do: routes 'add tests for S03' to add-tests", () => {
99
+ const match = matchRoute("add tests for S03");
100
+ assert.ok(match);
101
+ assert.equal(match.command, "add-tests");
102
+ });
103
+
104
+ test("/gsd do: routes 'what is next' to next", () => {
105
+ const match = matchRoute("what's next");
106
+ assert.ok(match);
107
+ assert.equal(match.command, "next");
108
+ });
109
+
110
+ test("/gsd do: returns null for unrecognized input", () => {
111
+ const match = matchRoute("florbinate the gizmo");
112
+ assert.equal(match, null);
113
+ });
114
+
115
+ test("/gsd do: prefers longer keyword match", () => {
116
+ // "check health" (12 chars) should beat "health" (6 chars)
117
+ const match = matchRoute("check health of the system");
118
+ assert.ok(match);
119
+ assert.equal(match.command, "doctor");
120
+ assert.ok(match.score >= 12);
121
+ });
122
+
123
+ test("/gsd do: routes 'session report' to session-report", () => {
124
+ const match = matchRoute("show me the session report");
125
+ assert.ok(match);
126
+ assert.equal(match.command, "session-report");
127
+ });
@@ -0,0 +1,68 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ // Test the filtering logic used by /gsd pr-branch.
5
+ // Full integration requires git operations, so we test the path filtering.
6
+
7
+ test("pr-branch: identifies .gsd/ paths", () => {
8
+ const files = [
9
+ ".gsd/milestones/M001/ROADMAP.md",
10
+ ".gsd/metrics.json",
11
+ "src/main.ts",
12
+ "package.json",
13
+ ".planning/PLAN.md",
14
+ "PLAN.md",
15
+ ];
16
+
17
+ const codeFiles = files.filter(
18
+ (f) => !f.startsWith(".gsd/") && !f.startsWith(".planning/") && f !== "PLAN.md",
19
+ );
20
+
21
+ assert.deepEqual(codeFiles, ["src/main.ts", "package.json"]);
22
+ });
23
+
24
+ test("pr-branch: all .gsd/ files returns empty", () => {
25
+ const files = [
26
+ ".gsd/milestones/M001/ROADMAP.md",
27
+ ".gsd/metrics.json",
28
+ ".gsd/BACKLOG.md",
29
+ ];
30
+
31
+ const codeFiles = files.filter(
32
+ (f) => !f.startsWith(".gsd/") && !f.startsWith(".planning/") && f !== "PLAN.md",
33
+ );
34
+
35
+ assert.equal(codeFiles.length, 0);
36
+ });
37
+
38
+ test("pr-branch: mixed commits with code changes", () => {
39
+ const files = [
40
+ ".gsd/milestones/M001/ROADMAP.md",
41
+ "src/auth.ts",
42
+ "src/auth.test.ts",
43
+ ];
44
+
45
+ const hasCodeChanges = files.some(
46
+ (f) => !f.startsWith(".gsd/") && !f.startsWith(".planning/") && f !== "PLAN.md",
47
+ );
48
+
49
+ assert.ok(hasCodeChanges);
50
+ });
51
+
52
+ test("pr-branch: --dry-run flag", () => {
53
+ assert.ok("--dry-run".includes("--dry-run"));
54
+ assert.ok(!"--name my-branch".includes("--dry-run"));
55
+ });
56
+
57
+ test("pr-branch: --name flag parsing", () => {
58
+ const args = "--name my-clean-pr";
59
+ const nameMatch = args.match(/--name\s+(\S+)/);
60
+ assert.ok(nameMatch);
61
+ assert.equal(nameMatch[1], "my-clean-pr");
62
+ });
63
+
64
+ test("pr-branch: default branch name", () => {
65
+ const currentBranch = "feat/add-auth";
66
+ const prBranch = `pr/${currentBranch}`;
67
+ assert.equal(prBranch, "pr/feat/add-auth");
68
+ });