gsd-pi 2.66.1-dev.0df32ec → 2.66.1-dev.3cea7ac

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 (230) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +79 -11
  2. package/dist/resources/extensions/claude-code-cli/partial-builder.js +4 -3
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -3
  4. package/dist/resources/extensions/gsd/auto/loop.js +13 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +10 -4
  6. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
  7. package/dist/resources/extensions/gsd/auto/session.js +1 -1
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +65 -15
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +30 -28
  10. package/dist/resources/extensions/gsd/auto-prompts.js +6 -6
  11. package/dist/resources/extensions/gsd/auto-recovery.js +11 -12
  12. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +18 -6
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +59 -5
  14. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +8 -5
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +186 -14
  16. package/dist/resources/extensions/gsd/codebase-generator.js +4 -0
  17. package/dist/resources/extensions/gsd/commands/handlers/core.js +3 -3
  18. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +10 -4
  19. package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -1
  20. package/dist/resources/extensions/gsd/detection.js +6 -0
  21. package/dist/resources/extensions/gsd/files.js +19 -2
  22. package/dist/resources/extensions/gsd/guided-flow.js +12 -8
  23. package/dist/resources/extensions/gsd/index.js +1 -1
  24. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +2 -0
  25. package/dist/resources/extensions/gsd/parsers-legacy.js +3 -1
  26. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  28. package/dist/resources/extensions/gsd/prompts/discuss.md +3 -3
  29. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
  30. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  31. package/dist/resources/extensions/gsd/prompts/rethink.md +6 -2
  32. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  33. package/dist/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  34. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  36. package/dist/resources/extensions/gsd/safety/file-change-validator.js +2 -1
  37. package/dist/resources/extensions/gsd/state.js +2 -1
  38. package/dist/resources/extensions/gsd/visualizer-overlay.js +27 -26
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +46 -7
  40. package/dist/resources/extensions/remote-questions/manager.js +8 -0
  41. package/dist/resources/extensions/shared/interview-ui.js +10 -0
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/required-server-files.json +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  48. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.html +1 -1
  64. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  71. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  72. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  73. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/dist/web/standalone/server.js +1 -1
  75. package/package.json +1 -1
  76. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic-shared.js +4 -3
  78. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  79. package/packages/pi-ai/dist/utils/json-parse.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/utils/json-parse.js +11 -1
  81. package/packages/pi-ai/dist/utils/json-parse.js.map +1 -1
  82. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/utils/repair-tool-json.js +60 -1
  84. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  85. package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts +2 -0
  86. package/packages/pi-ai/dist/utils/tests/json-parse.test.d.ts.map +1 -0
  87. package/packages/pi-ai/dist/utils/tests/json-parse.test.js +14 -0
  88. package/packages/pi-ai/dist/utils/tests/json-parse.test.js.map +1 -0
  89. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +10 -0
  90. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  91. package/packages/pi-ai/src/providers/anthropic-shared.ts +4 -3
  92. package/packages/pi-ai/src/utils/json-parse.ts +11 -1
  93. package/packages/pi-ai/src/utils/repair-tool-json.ts +69 -1
  94. package/packages/pi-ai/src/utils/tests/json-parse.test.ts +17 -0
  95. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +13 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +17 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +2 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +2 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js +2 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +2 -2
  114. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  115. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +18 -0
  116. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +2 -1
  117. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +11 -2
  118. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +2 -1
  119. package/packages/pi-coding-agent/src/modes/interactive/components/scoped-models-selector.ts +2 -1
  120. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +2 -2
  121. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +13 -0
  122. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
  123. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts +2 -0
  124. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.d.ts.map +1 -0
  125. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +35 -0
  126. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -0
  127. package/packages/pi-tui/dist/__tests__/tui.test.d.ts +2 -0
  128. package/packages/pi-tui/dist/__tests__/tui.test.d.ts.map +1 -0
  129. package/packages/pi-tui/dist/__tests__/tui.test.js +43 -0
  130. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -0
  131. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  132. package/packages/pi-tui/dist/autocomplete.js +9 -7
  133. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  134. package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts +2 -0
  135. package/packages/pi-tui/dist/components/__tests__/editor.test.d.ts.map +1 -0
  136. package/packages/pi-tui/dist/components/__tests__/editor.test.js +54 -0
  137. package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -0
  138. package/packages/pi-tui/dist/components/editor.d.ts +3 -1
  139. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  140. package/packages/pi-tui/dist/components/editor.js +14 -3
  141. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  142. package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
  143. package/packages/pi-tui/dist/stdin-buffer.js +6 -0
  144. package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
  145. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  146. package/packages/pi-tui/dist/tui.js +8 -0
  147. package/packages/pi-tui/dist/tui.js.map +1 -1
  148. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +15 -0
  149. package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +43 -0
  150. package/packages/pi-tui/src/__tests__/tui.test.ts +50 -0
  151. package/packages/pi-tui/src/autocomplete.ts +9 -7
  152. package/packages/pi-tui/src/components/__tests__/editor.test.ts +64 -0
  153. package/packages/pi-tui/src/components/editor.ts +14 -3
  154. package/packages/pi-tui/src/stdin-buffer.ts +7 -0
  155. package/packages/pi-tui/src/tui.ts +9 -0
  156. package/src/resources/extensions/ask-user-questions.ts +103 -11
  157. package/src/resources/extensions/claude-code-cli/partial-builder.ts +4 -3
  158. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +12 -3
  159. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +17 -0
  160. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +18 -0
  161. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -1
  162. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  163. package/src/resources/extensions/gsd/auto/phases.ts +10 -5
  164. package/src/resources/extensions/gsd/auto/run-unit.ts +14 -2
  165. package/src/resources/extensions/gsd/auto/session.ts +1 -1
  166. package/src/resources/extensions/gsd/auto-dashboard.ts +76 -16
  167. package/src/resources/extensions/gsd/auto-dispatch.ts +36 -35
  168. package/src/resources/extensions/gsd/auto-prompts.ts +5 -6
  169. package/src/resources/extensions/gsd/auto-recovery.ts +15 -15
  170. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +27 -6
  171. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +67 -6
  172. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +11 -8
  173. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +209 -16
  174. package/src/resources/extensions/gsd/codebase-generator.ts +4 -0
  175. package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -6
  176. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +11 -4
  177. package/src/resources/extensions/gsd/custom-workflow-engine.ts +3 -1
  178. package/src/resources/extensions/gsd/detection.ts +6 -0
  179. package/src/resources/extensions/gsd/files.ts +21 -2
  180. package/src/resources/extensions/gsd/guided-flow.ts +15 -8
  181. package/src/resources/extensions/gsd/index.ts +6 -0
  182. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +2 -0
  183. package/src/resources/extensions/gsd/parsers-legacy.ts +3 -1
  184. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  185. package/src/resources/extensions/gsd/prompts/discuss-prepared.md +7 -7
  186. package/src/resources/extensions/gsd/prompts/discuss.md +3 -3
  187. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -3
  188. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  189. package/src/resources/extensions/gsd/prompts/rethink.md +6 -2
  190. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  191. package/src/resources/extensions/gsd/prompts/triage-captures.md +1 -1
  192. package/src/resources/extensions/gsd/prompts/validate-milestone.md +4 -4
  193. package/src/resources/extensions/gsd/prompts/worktree-merge.md +3 -1
  194. package/src/resources/extensions/gsd/safety/file-change-validator.ts +4 -1
  195. package/src/resources/extensions/gsd/state.ts +2 -1
  196. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +52 -1
  197. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +50 -2
  198. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +48 -0
  199. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  200. package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +44 -0
  201. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +7 -1
  202. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +31 -0
  203. package/src/resources/extensions/gsd/tests/detection.test.ts +37 -0
  204. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +35 -0
  206. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +34 -0
  207. package/src/resources/extensions/gsd/tests/health-widget.test.ts +45 -0
  208. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +53 -13
  209. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -2
  210. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +1 -1
  211. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +3 -4
  212. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +21 -0
  213. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +71 -2
  214. package/src/resources/extensions/gsd/tests/parsers.test.ts +25 -0
  215. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -1
  216. package/src/resources/extensions/gsd/tests/queue-execution-guard.test.ts +9 -0
  217. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +19 -0
  218. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +73 -0
  219. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +98 -0
  220. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +2 -2
  221. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +26 -0
  222. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +59 -0
  223. package/src/resources/extensions/gsd/tests/workflow-reconcile.test.ts +91 -0
  224. package/src/resources/extensions/gsd/tests/write-gate.test.ts +210 -35
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -27
  226. package/src/resources/extensions/gsd/workflow-reconcile.ts +59 -8
  227. package/src/resources/extensions/remote-questions/manager.ts +9 -0
  228. package/src/resources/extensions/shared/interview-ui.ts +13 -0
  229. /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_buildManifest.js +0 -0
  230. /package/dist/web/standalone/.next/static/{Zw5aZFHFtOwjJSOsINh1m → HxFcJ8GrYNPsg9ARz7GPz}/_ssgManifest.js +0 -0
@@ -9,6 +9,7 @@ import {
9
9
  formatRelativeTime,
10
10
  type HealthWidgetData,
11
11
  } from "../health-widget-core.ts";
12
+ import { registerHooks } from "../bootstrap/register-hooks.ts";
12
13
 
13
14
  function makeTempDir(prefix: string): string {
14
15
  const dir = join(
@@ -177,3 +178,47 @@ test("detectHealthWidgetProjectState: metrics file alone does not imply project"
177
178
  );
178
179
  assert.equal(detectHealthWidgetProjectState(dir), "initialized");
179
180
  });
181
+
182
+ test("session_start bootstraps the health widget alongside notifications", async (t) => {
183
+ const dir = makeTempDir("bootstrap");
184
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
185
+
186
+ const originalCwd = process.cwd();
187
+ process.chdir(dir);
188
+ t.after(() => {
189
+ process.chdir(originalCwd);
190
+ cleanup(dir);
191
+ });
192
+
193
+ const widgets: string[] = [];
194
+ const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
195
+ const pi = {
196
+ on(event: string, handler: (event: unknown, ctx: any) => Promise<void> | void) {
197
+ handlers.set(event, handler);
198
+ },
199
+ } as any;
200
+
201
+ registerHooks(pi);
202
+ const sessionStart = handlers.get("session_start");
203
+ assert.ok(sessionStart, "session_start handler is registered");
204
+
205
+ await sessionStart!({}, {
206
+ hasUI: true,
207
+ ui: {
208
+ notify: () => {},
209
+ setStatus: () => {},
210
+ setWorkingMessage: () => {},
211
+ onTerminalInput: () => () => {},
212
+ setWidget: (key: string) => {
213
+ widgets.push(key);
214
+ },
215
+ },
216
+ sessionManager: {
217
+ getSessionId: () => null,
218
+ },
219
+ model: null,
220
+ } as any);
221
+
222
+ assert.ok(widgets.includes("gsd-health"), "health widget is bootstrapped");
223
+ assert.ok(widgets.includes("gsd-notifications"), "notification widget still boots");
224
+ });
@@ -1,9 +1,10 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
3
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, chmodSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
+ import { execFileSync } from "node:child_process";
7
8
 
8
9
  import {
9
10
  resolveExpectedArtifactPath,
@@ -11,6 +12,7 @@ import {
11
12
  diagnoseExpectedArtifact,
12
13
  buildLoopRemediationSteps,
13
14
  hasImplementationArtifacts,
15
+ reconcileMergeState,
14
16
  } from "../../auto-recovery.ts";
15
17
  import { parseRoadmap, parsePlan } from "../../parsers-legacy.ts";
16
18
  import { parseTaskPlanFile, clearParseCache } from "../../files.ts";
@@ -669,8 +671,6 @@ test("#793: invalidateAllCaches clears all caches so deriveState sees fresh disk
669
671
 
670
672
  // ─── hasImplementationArtifacts (#1703) ───────────────────────────────────
671
673
 
672
- import { execFileSync } from "node:child_process";
673
-
674
674
  function makeGitBase(): string {
675
675
  const base = join(tmpdir(), `gsd-test-git-${randomUUID()}`);
676
676
  mkdirSync(base, { recursive: true });
@@ -745,9 +745,6 @@ test("verifyExpectedArtifact complete-milestone fails with only .gsd/ files (#17
745
745
 
746
746
  // ─── reconcileMergeState: silent nativeCommit failure (#2542) ─────────────
747
747
 
748
- import { reconcileMergeState } from "../../auto-recovery.ts";
749
- import { chmodSync } from "node:fs";
750
-
751
748
  function makeMockCtx(): { ctx: any; notifications: Array<{ msg: string; level: string }> } {
752
749
  const notifications: Array<{ msg: string; level: string }> = [];
753
750
  const ctx = {
@@ -760,7 +757,7 @@ function makeMockCtx(): { ctx: any; notifications: Array<{ msg: string; level: s
760
757
  return { ctx, notifications };
761
758
  }
762
759
 
763
- test("reconcileMergeState returns false and notifies error when nativeCommit fails (#2542)", (t) => {
760
+ test("reconcileMergeState returns blocked and notifies error when nativeCommit fails (#2542)", (t) => {
764
761
  const base = makeGitBase();
765
762
  t.after(() => cleanup(base));
766
763
 
@@ -786,9 +783,7 @@ test("reconcileMergeState returns false and notifies error when nativeCommit fai
786
783
  const { ctx, notifications } = makeMockCtx();
787
784
  const result = reconcileMergeState(base, ctx);
788
785
 
789
- // The function should return false to signal reconciliation failure
790
- // (Currently it silently swallows the error and returns true — this test should FAIL before the fix)
791
- assert.equal(result, false, "reconcileMergeState should return false when nativeCommit fails");
786
+ assert.equal(result, "blocked", "reconcileMergeState should return blocked when nativeCommit fails");
792
787
  const errorNotifications = notifications.filter(n => n.level === "error");
793
788
  assert.ok(errorNotifications.length > 0, "should notify an error when nativeCommit fails");
794
789
  assert.ok(
@@ -797,18 +792,63 @@ test("reconcileMergeState returns false and notifies error when nativeCommit fai
797
792
  );
798
793
  });
799
794
 
800
- test("reconcileMergeState returns true when no merge state present", (t) => {
801
- // When there's no MERGE_HEAD or SQUASH_MSG, reconcileMergeState returns false (no dirty state)
795
+ test("reconcileMergeState returns clean when no merge state present", (t) => {
802
796
  const base = makeGitBase();
803
797
  t.after(() => cleanup(base));
804
798
 
805
799
  const { ctx, notifications } = makeMockCtx();
806
800
  const result = reconcileMergeState(base, ctx);
807
801
 
808
- assert.equal(result, false, "should return false when no merge state exists");
802
+ assert.equal(result, "clean", "should return clean when no merge state exists");
809
803
  assert.equal(notifications.length, 0, "should not notify when no merge state present");
810
804
  });
811
805
 
806
+ test("reconcileMergeState blocks and preserves unresolved code conflicts", (t) => {
807
+ const base = makeGitBase();
808
+ t.after(() => cleanup(base));
809
+
810
+ writeFileSync(join(base, "conflict.txt"), "base\n");
811
+ execFileSync("git", ["add", "conflict.txt"], { cwd: base, stdio: "ignore" });
812
+ execFileSync("git", ["commit", "-m", "add conflict base"], { cwd: base, stdio: "ignore" });
813
+
814
+ execFileSync("git", ["checkout", "-b", "feature"], { cwd: base, stdio: "ignore" });
815
+ writeFileSync(join(base, "conflict.txt"), "feature\n");
816
+ execFileSync("git", ["add", "conflict.txt"], { cwd: base, stdio: "ignore" });
817
+ execFileSync("git", ["commit", "-m", "feature change"], { cwd: base, stdio: "ignore" });
818
+
819
+ execFileSync("git", ["checkout", "main"], { cwd: base, stdio: "ignore" });
820
+ writeFileSync(join(base, "conflict.txt"), "main\n");
821
+ execFileSync("git", ["add", "conflict.txt"], { cwd: base, stdio: "ignore" });
822
+ execFileSync("git", ["commit", "-m", "main change"], { cwd: base, stdio: "ignore" });
823
+
824
+ let mergeFailed = false;
825
+ try {
826
+ execFileSync("git", ["merge", "--no-ff", "feature"], { cwd: base, stdio: "ignore" });
827
+ } catch {
828
+ mergeFailed = true;
829
+ }
830
+ assert.equal(mergeFailed, true, "merge should produce a conflict");
831
+ assert.ok(existsSync(join(base, ".git", "MERGE_HEAD")), "MERGE_HEAD should remain present before reconcile");
832
+
833
+ const beforeContents = readFileSync(join(base, "conflict.txt"), "utf8");
834
+ assert.match(beforeContents, /<<<<<<<|=======|>>>>>>>/, "fixture should contain conflict markers");
835
+
836
+ const { ctx, notifications } = makeMockCtx();
837
+ const result = reconcileMergeState(base, ctx);
838
+
839
+ assert.equal(result, "blocked", "code conflicts should block reconciliation");
840
+ assert.ok(existsSync(join(base, ".git", "MERGE_HEAD")), "MERGE_HEAD should be preserved for manual resolution");
841
+ assert.equal(
842
+ readFileSync(join(base, "conflict.txt"), "utf8"),
843
+ beforeContents,
844
+ "reconcile should preserve the conflicted file contents",
845
+ );
846
+ assert.ok(
847
+ notifications.some((n) => n.level === "error" && n.msg.includes("manual conflict resolution is preserved")),
848
+ "should notify that auto-mode paused and preserved manual work",
849
+ );
850
+ });
851
+
812
852
  test("verifyExpectedArtifact complete-milestone passes with impl files (#1703)", (t) => {
813
853
  const base = makeGitBase();
814
854
  t.after(() => cleanup(base));
@@ -360,8 +360,8 @@ describe("session management", () => {
360
360
  assert.equal(s.unitRecoveryCount.size, 0, "recovery counts cleared");
361
361
  });
362
362
 
363
- test("NEW_SESSION_TIMEOUT_MS is 30 seconds", () => {
364
- assert.equal(NEW_SESSION_TIMEOUT_MS, 30_000, "session timeout should be 30s");
363
+ test("NEW_SESSION_TIMEOUT_MS is 120 seconds", () => {
364
+ assert.equal(NEW_SESSION_TIMEOUT_MS, 120_000, "session timeout should be 120s");
365
365
  });
366
366
 
367
367
  test("MAX_UNIT_DISPATCHES limits retries for a single unit", () => {
@@ -72,7 +72,7 @@ function makeMockDeps(
72
72
  getCurrentBranch: () => "main",
73
73
  autoWorktreeBranch: () => "auto/M001",
74
74
  resolveMilestoneFile: () => null,
75
- reconcileMergeState: () => false,
75
+ reconcileMergeState: () => "clean",
76
76
  getLedger: () => ({ units: [] }),
77
77
  getProjectTotals: () => ({ cost: 0 }),
78
78
  formatCost: (c: number) => `$${c.toFixed(2)}`,
@@ -273,9 +273,9 @@ test('Scenario 2: Fully complete project — deriveState phase', async () => {
273
273
  invalidateAllCaches();
274
274
  const state = await deriveState(base);
275
275
  assert.deepStrictEqual(state.phase, 'complete', 'complete: deriveState phase is complete (validation + summary written by migration)');
276
- // When all milestones are complete, activeMilestone points to the last entry (for display)
277
- assert.ok(state.activeMilestone !== null, 'complete: deriveState has activeMilestone (last entry)');
278
- assert.deepStrictEqual(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001');
276
+ assert.equal(state.activeMilestone, null, 'complete: deriveState has no activeMilestone');
277
+ assert.ok(state.lastCompletedMilestone !== null, 'complete: deriveState exposes lastCompletedMilestone');
278
+ assert.deepStrictEqual(state.lastCompletedMilestone!.id, 'M001', 'complete: deriveState lastCompletedMilestone is M001');
279
279
 
280
280
  // generatePreview for complete project
281
281
  const preview = generatePreview(project);
@@ -292,4 +292,3 @@ test('Scenario 2: Fully complete project — deriveState phase', async () => {
292
292
  rmSync(base, { recursive: true, force: true });
293
293
  }
294
294
  });
295
-
@@ -57,4 +57,25 @@ describe("parallel-monitor-overlay", () => {
57
57
  assert.ok(closed, "pressing q should trigger onClose");
58
58
  overlay2.dispose();
59
59
  });
60
+
61
+ it("ParallelMonitorOverlay clamps scrollOffset during render", async () => {
62
+ const mod = await import("../parallel-monitor-overlay.js");
63
+
64
+ const mockTui = { requestRender: () => {} };
65
+ const mockTheme = {
66
+ fg: (_color: string, text: string) => text,
67
+ bold: (text: string) => text,
68
+ };
69
+ const overlay = new mod.ParallelMonitorOverlay(
70
+ mockTui,
71
+ mockTheme as any,
72
+ () => {},
73
+ "/nonexistent/path",
74
+ );
75
+
76
+ (overlay as any).scrollOffset = 999;
77
+ overlay.render(80);
78
+ assert.equal((overlay as any).scrollOffset, 0, "empty overlays clamp scroll to zero");
79
+ overlay.dispose();
80
+ });
60
81
  });
@@ -4,12 +4,15 @@
4
4
  * Verifies the dispatch rule and prompt builder exist with correct structure.
5
5
  */
6
6
 
7
- import test from "node:test";
7
+ import test, { afterEach } from "node:test";
8
8
  import assert from "node:assert/strict";
9
- import { readFileSync } from "node:fs";
9
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
10
  import { join, dirname } from "node:path";
11
+ import { tmpdir } from "node:os";
11
12
  import { fileURLToPath } from "node:url";
12
13
 
14
+ import { resolveDispatch } from "../auto-dispatch.ts";
15
+
13
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
17
 
15
18
  const dispatchSrc = readFileSync(join(__dirname, "..", "auto-dispatch.ts"), "utf-8");
@@ -17,6 +20,47 @@ const promptsSrc = readFileSync(join(__dirname, "..", "auto-prompts.ts"), "utf-8
17
20
  const templatePath = join(__dirname, "..", "prompts", "parallel-research-slices.md");
18
21
  const templateSrc = readFileSync(templatePath, "utf-8");
19
22
 
23
+ const tmpDirs: string[] = [];
24
+
25
+ function makeTmpProject(): string {
26
+ const base = mkdtempSync(join(tmpdir(), "parallel-research-"));
27
+ tmpDirs.push(base);
28
+ const milestoneDir = join(base, ".gsd", "milestones", "M001");
29
+ mkdirSync(milestoneDir, { recursive: true });
30
+ writeFileSync(
31
+ join(milestoneDir, "M001-ROADMAP.md"),
32
+ [
33
+ "# M001: Parallel Research Milestone",
34
+ "",
35
+ "**Vision:** Research-ready slices.",
36
+ "",
37
+ "**Success Criteria:**",
38
+ "- Research both slices",
39
+ "",
40
+ "## Slices",
41
+ "",
42
+ "- [ ] **S01: Alpha** `risk:low` `depends:[]`",
43
+ "- [ ] **S02: Beta** `risk:low` `depends:[]`",
44
+ "",
45
+ "## Boundary Map",
46
+ "",
47
+ ].join("\n"),
48
+ "utf-8",
49
+ );
50
+ return base;
51
+ }
52
+
53
+ afterEach(() => {
54
+ for (const dir of tmpDirs) {
55
+ try {
56
+ rmSync(dir, { recursive: true, force: true });
57
+ } catch {
58
+ // Best-effort cleanup only.
59
+ }
60
+ }
61
+ tmpDirs.length = 0;
62
+ });
63
+
20
64
  // ─── Dispatch rule ────────────────────────────────────────────────────────
21
65
 
22
66
  test("dispatch: parallel-research-slices rule exists", () => {
@@ -75,3 +119,28 @@ test("template: validate-milestone uses parallel reviewers", () => {
75
119
  "validate-milestone should dispatch 3 parallel reviewers",
76
120
  );
77
121
  });
122
+
123
+ test("resolveDispatch prefers parallel research when multiple slices are ready", async () => {
124
+ const base = makeTmpProject();
125
+
126
+ const action = await resolveDispatch({
127
+ basePath: base,
128
+ mid: "M001",
129
+ midTitle: "Parallel Research Milestone",
130
+ state: {
131
+ phase: "planning",
132
+ activeMilestone: { id: "M001", title: "Parallel Research Milestone", status: "active" },
133
+ activeSlice: { id: "S01", title: "Alpha" },
134
+ activeTask: null,
135
+ registry: [],
136
+ blockers: [],
137
+ } as any,
138
+ prefs: undefined,
139
+ });
140
+
141
+ assert.equal(action.action, "dispatch");
142
+ if (action.action === "dispatch") {
143
+ assert.equal(action.unitType, "research-slice");
144
+ assert.equal(action.unitId, "M001/parallel-research");
145
+ }
146
+ });
@@ -703,6 +703,31 @@ Widget description.
703
703
  assert.deepStrictEqual(p.tasks[0].title, 'Build the widget', 'em-dash heading T01 title');
704
704
  });
705
705
 
706
+ test('parsePlan: filename subheadings do not become task ids', () => {
707
+ const content = `# S15: Filename Headings
708
+
709
+ **Goal:** Ignore file-reference subheadings inside task descriptions.
710
+ **Demo:** Only real task ids are parsed.
711
+
712
+ ## Tasks
713
+
714
+ - [ ] **T01: First task** \`est:10m\`
715
+ Implement the feature.
716
+
717
+ ### constraints.py — \`add_off_request_tiered()\`
718
+ - preserve behavior
719
+
720
+ ### annotations.py — \`annotate()\`
721
+ - keep metadata
722
+ `;
723
+
724
+ const p = parsePlan(content);
725
+ assert.deepStrictEqual(p.tasks.map((task) => task.id), ['T01'], 'filename subheadings should not create extra tasks');
726
+ assert.deepStrictEqual(p.tasks[0].title, 'First task', 'real task should still parse normally');
727
+ assert.ok(p.tasks[0].description.includes('preserve behavior'), 'detail lines under filename subheadings should remain attached to the task');
728
+ assert.ok(p.tasks[0].description.includes('keep metadata'), 'later detail lines should also remain attached to the task');
729
+ });
730
+
706
731
  test('parsePlan: mixed checkbox and heading-style tasks', () => {
707
732
  const content = `# S14: Mixed Format
708
733
 
@@ -51,6 +51,12 @@ test("guided discussion prompts avoid wrap-up prompts after every round", () =>
51
51
  assert.doesNotMatch(slicePrompt, /I think I have a solid picture of this slice\. Ready to wrap up/i);
52
52
  });
53
53
 
54
+ test("guided milestone discussion scopes depth verification to the milestone id", () => {
55
+ const prompt = readPrompt("guided-discuss-milestone");
56
+ assert.match(prompt, /depth_verification_\{\{milestoneId\}\}/, "depth verification id should include the milestone id");
57
+ assert.doesNotMatch(prompt, /depth_verification_confirm" — this enables the write-gate downstream/i, "legacy global depth gate wording should be gone");
58
+ });
59
+
54
60
  test("guided-resume-task prompt preserves recovery state until work is superseded", () => {
55
61
  const prompt = readPrompt("guided-resume-task");
56
62
  assert.match(prompt, /Do \*\*not\*\* delete the continue file immediately/i);
@@ -188,7 +194,8 @@ test("validate-milestone prompt dispatches parallel reviewers", () => {
188
194
  assert.match(prompt, /Reviewer C/);
189
195
  assert.match(prompt, /Requirements Coverage/);
190
196
  assert.match(prompt, /Cross-Slice Integration/);
191
- assert.match(prompt, /UAT/);
197
+ assert.match(prompt, /Assessment & Acceptance Criteria/);
198
+ assert.match(prompt, /assessment evidence/i);
192
199
  });
193
200
 
194
201
  // ─── Prompt migration: replan-slice → gsd_replan_slice ────────────────
@@ -13,6 +13,7 @@
13
13
  * (e) write/edit to source path → block
14
14
  * (f) bash command → block (could execute work)
15
15
  * (g) registered GSD tools (gsd_milestone_generate_id, gsd_summary_save) → pass
16
+ * (h) unknown custom tools → block
16
17
  */
17
18
 
18
19
  import test from 'node:test';
@@ -155,3 +156,11 @@ test('queue-guard: allows web search and library tools during queue mode', () =>
155
156
  const r4 = shouldBlockQueueExecution('fetch_page', '', true);
156
157
  assert.strictEqual(r4.block, false, 'fetch_page should pass');
157
158
  });
159
+
160
+ // ─── Scenario 10: Unknown custom tools are blocked during queue mode ──
161
+
162
+ test('queue-guard: blocks unknown custom tools during queue mode', () => {
163
+ const result = shouldBlockQueueExecution('custom_codegen_tool', '', true);
164
+ assert.strictEqual(result.block, true, 'unknown custom tools should be blocked');
165
+ assert.ok(result.reason, 'should explain the queue restriction');
166
+ });
@@ -101,6 +101,25 @@ test("parseTaskPlanIO handles multiple backtick tokens on one line", () => {
101
101
  assert.deepEqual(io.outputFiles, ["src/c.ts"]);
102
102
  });
103
103
 
104
+ test("parseTaskPlanIO strips inline descriptions from backtick-wrapped file references", () => {
105
+ const content = `# T01: Described Paths
106
+
107
+ ## Inputs
108
+
109
+ - \`src/config.ts — existing configuration\`
110
+ - \`src/flags.ts - feature flags\`
111
+
112
+ ## Expected Output
113
+
114
+ - \`definitions/ac-audit.md — current state of AC CRM\`
115
+ - \`docs/runbook.md - update deployment notes\`
116
+ `;
117
+
118
+ const io = parseTaskPlanIO(content);
119
+ assert.deepEqual(io.inputFiles, ["src/config.ts", "src/flags.ts"]);
120
+ assert.deepEqual(io.outputFiles, ["definitions/ac-audit.md", "docs/runbook.md"]);
121
+ });
122
+
104
123
  // ─── deriveTaskGraph ──────────────────────────────────────────────────────
105
124
 
106
125
  test("deriveTaskGraph: linear chain T01→T02→T03", () => {
@@ -0,0 +1,73 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { registerShortcuts } from "../bootstrap/register-shortcuts.ts";
8
+
9
+ function makeTempDir(prefix: string): string {
10
+ const dir = join(
11
+ tmpdir(),
12
+ `gsd-register-shortcuts-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
13
+ );
14
+ mkdirSync(dir, { recursive: true });
15
+ return dir;
16
+ }
17
+
18
+ function cleanup(dir: string): void {
19
+ try {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ } catch {
22
+ // best-effort
23
+ }
24
+ }
25
+
26
+ test("dashboard shortcut resolves the project root instead of the current worktree path", async (t) => {
27
+ const projectRoot = makeTempDir("project");
28
+ const worktreeRoot = join(projectRoot, ".gsd", "worktrees", "M001");
29
+ mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
30
+ mkdirSync(worktreeRoot, { recursive: true });
31
+
32
+ const originalCwd = process.cwd();
33
+ process.chdir(worktreeRoot);
34
+ t.after(() => {
35
+ process.chdir(originalCwd);
36
+ cleanup(projectRoot);
37
+ });
38
+
39
+ let capturedHandler: ((ctx: any) => Promise<void>) | null = null;
40
+ const shortcuts: Array<{ description: string; handler: (ctx: any) => Promise<void> }> = [];
41
+ const pi = {
42
+ registerShortcut: (_key: unknown, shortcut: { description: string; handler: (ctx: any) => Promise<void> }) => {
43
+ shortcuts.push(shortcut);
44
+ if (!capturedHandler) {
45
+ capturedHandler = shortcut.handler;
46
+ }
47
+ },
48
+ } as any;
49
+
50
+ registerShortcuts(pi);
51
+ assert.ok(capturedHandler, "dashboard shortcut is registered");
52
+ const dashboardShortcut = shortcuts[0];
53
+ assert.ok(dashboardShortcut, "dashboard shortcut is captured");
54
+
55
+ let customCalls = 0;
56
+ const notices: Array<{ message: string; type?: string }> = [];
57
+ await dashboardShortcut.handler({
58
+ hasUI: true,
59
+ ui: {
60
+ custom: async () => {
61
+ customCalls++;
62
+ return true;
63
+ },
64
+ notify: (message: string, type?: string) => {
65
+ notices.push({ message, type });
66
+ },
67
+ },
68
+ });
69
+
70
+ assert.ok(customCalls > 0, "shortcut opens the dashboard overlay when project root is resolved");
71
+ assert.equal(notices.length, 0, "shortcut does not fall back to the missing-.gsd warning");
72
+ assert.equal(shortcuts.length, 3, "all GSD shortcuts are still registered");
73
+ });
@@ -760,6 +760,104 @@ test("ask-user-questions source-level: tryRemoteQuestions is called before the h
760
760
  );
761
761
  });
762
762
 
763
+ // ═══════════════════════════════════════════════════════════════════════════
764
+ // Race model tests (#3810) — local TUI races against remote channel
765
+ // ═══════════════════════════════════════════════════════════════════════════
766
+
767
+ test("ask-user-questions source-level: raceRemoteAndLocal function exists", () => {
768
+ const src = readFileSync(
769
+ join(__dirname, "..", "..", "ask-user-questions.ts"),
770
+ "utf-8",
771
+ );
772
+ assert.ok(
773
+ src.includes("async function raceRemoteAndLocal("),
774
+ "raceRemoteAndLocal helper should exist for racing local TUI against remote channel",
775
+ );
776
+ });
777
+
778
+ test("ask-user-questions source-level: race path uses isRemoteConfigured for routing", () => {
779
+ const src = readFileSync(
780
+ join(__dirname, "..", "..", "ask-user-questions.ts"),
781
+ "utf-8",
782
+ );
783
+ assert.ok(
784
+ src.includes("isRemoteConfigured()"),
785
+ "execute() should call isRemoteConfigured() for lightweight routing decision",
786
+ );
787
+ });
788
+
789
+ test("ask-user-questions source-level: race path checks both hasRemote and ctx.hasUI", () => {
790
+ // Regression: #3810 — the race should only activate when BOTH remote and local UI
791
+ // are available. Headless mode should still use remote-only, and no-remote should
792
+ // use local-only.
793
+ const src = readFileSync(
794
+ join(__dirname, "..", "..", "ask-user-questions.ts"),
795
+ "utf-8",
796
+ );
797
+ assert.ok(
798
+ src.includes("hasRemote && ctx.hasUI"),
799
+ "Race path should require both remote configured and local UI available",
800
+ );
801
+ assert.ok(
802
+ src.includes("hasRemote && !ctx.hasUI"),
803
+ "Headless path should handle remote-only when no local UI",
804
+ );
805
+ });
806
+
807
+ test("ask-user-questions source-level: race treats remote timeout as non-win", () => {
808
+ // Regression: the whole point of the race is that a remote timeout should NOT
809
+ // block the local TUI. The race helper must filter out timed_out results.
810
+ const src = readFileSync(
811
+ join(__dirname, "..", "..", "ask-user-questions.ts"),
812
+ "utf-8",
813
+ );
814
+ const raceFnStart = src.indexOf("async function raceRemoteAndLocal(");
815
+ const raceFnEnd = src.indexOf("\n}", raceFnStart);
816
+ const raceFnBody = src.slice(raceFnStart, raceFnEnd);
817
+ assert.ok(
818
+ raceFnBody.includes("timed_out"),
819
+ "raceRemoteAndLocal should check for timed_out in remote results",
820
+ );
821
+ assert.ok(
822
+ raceFnBody.includes("details?.error"),
823
+ "raceRemoteAndLocal should check for error in remote results",
824
+ );
825
+ });
826
+
827
+ test("ask-user-questions source-level: race uses AbortController to cancel loser", () => {
828
+ const src = readFileSync(
829
+ join(__dirname, "..", "..", "ask-user-questions.ts"),
830
+ "utf-8",
831
+ );
832
+ assert.ok(
833
+ src.includes("new AbortController()"),
834
+ "Race path should create an AbortController for cancellation",
835
+ );
836
+ assert.ok(
837
+ src.includes("controller.abort()"),
838
+ "raceRemoteAndLocal should abort the controller to cancel the losing side",
839
+ );
840
+ });
841
+
842
+ test("manager source-level: isRemoteConfigured export exists", () => {
843
+ const src = readFileSync(
844
+ join(__dirname, "..", "..", "remote-questions", "manager.ts"),
845
+ "utf-8",
846
+ );
847
+ assert.ok(
848
+ src.includes("export function isRemoteConfigured()"),
849
+ "manager.ts should export isRemoteConfigured for lightweight config checking",
850
+ );
851
+ // Must delegate to resolveRemoteConfig — no separate config parsing
852
+ const fnStart = src.indexOf("export function isRemoteConfigured()");
853
+ const fnEnd = src.indexOf("\n}", fnStart);
854
+ const fnBody = src.slice(fnStart, fnEnd);
855
+ assert.ok(
856
+ fnBody.includes("resolveRemoteConfig()"),
857
+ "isRemoteConfigured should delegate to resolveRemoteConfig",
858
+ );
859
+ });
860
+
763
861
  test("config source-level: removeProviderToken uses auth.remove not auth.set with empty key", () => {
764
862
  const commandSrc = readFileSync(
765
863
  join(__dirname, "..", "..", "remote-questions", "remote-command.ts"),
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
6
6
 
7
7
  const { deriveState } = await import("../state.js");
8
8
 
9
- test("deriveState reports complete when all milestone slices are done", async () => {
9
+ test("deriveState reports the last completed milestone when all milestone slices are done", async () => {
10
10
  const base = mkdtempSync(join(tmpdir(), "gsd-smart-entry-complete-"));
11
11
 
12
12
  try {
@@ -31,7 +31,7 @@ test("deriveState reports complete when all milestone slices are done", async ()
31
31
 
32
32
  const state = await deriveState(base);
33
33
  assert.equal(state.phase, "complete");
34
- assert.equal(state.activeMilestone?.id, "M001");
34
+ assert.equal(state.lastCompletedMilestone?.id, "M001");
35
35
  } finally {
36
36
  rmSync(base, { recursive: true, force: true });
37
37
  }