@opengsd/gsd-pi 1.1.1-dev.616a1a1 → 1.1.1-dev.74e8dd1

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 (232) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +167 -16
  3. package/dist/resources/extensions/gsd/auto/phases.js +4 -3
  4. package/dist/resources/extensions/gsd/auto-dashboard.js +15 -4
  5. package/dist/resources/extensions/gsd/auto-dispatch.js +39 -0
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +113 -7
  7. package/dist/resources/extensions/gsd/auto-prompts.js +9 -0
  8. package/dist/resources/extensions/gsd/auto-recovery.js +4 -4
  9. package/dist/resources/extensions/gsd/auto-start.js +94 -15
  10. package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +2 -1
  11. package/dist/resources/extensions/gsd/auto.js +22 -4
  12. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +79 -0
  13. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +43 -0
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +30 -9
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +16 -10
  16. package/dist/resources/extensions/gsd/commands/catalog.js +6 -1
  17. package/dist/resources/extensions/gsd/commands/handlers/core.js +6 -2
  18. package/dist/resources/extensions/gsd/commands/handlers/ops.js +7 -3
  19. package/dist/resources/extensions/gsd/commands-maintenance.js +172 -2
  20. package/dist/resources/extensions/gsd/commands-mcp-status.js +107 -59
  21. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +3 -1
  22. package/dist/resources/extensions/gsd/commands-verdict.js +1 -1
  23. package/dist/resources/extensions/gsd/config-overlay.js +2 -1
  24. package/dist/resources/extensions/gsd/error-classifier.js +2 -1
  25. package/dist/resources/extensions/gsd/exec-sandbox.js +2 -0
  26. package/dist/resources/extensions/gsd/gsd-db.js +37 -4
  27. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  28. package/dist/resources/extensions/gsd/mcp-filter.js +3 -0
  29. package/dist/resources/extensions/gsd/mcp-project-config.js +67 -8
  30. package/dist/resources/extensions/gsd/migration-auto-check.js +2 -2
  31. package/dist/resources/extensions/gsd/prompts/run-uat.md +10 -4
  32. package/dist/resources/extensions/gsd/prompts/system.md +3 -1
  33. package/dist/resources/extensions/gsd/safety/destructive-guard.js +3 -0
  34. package/dist/resources/extensions/gsd/skill-activation.js +20 -3
  35. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +4 -2
  36. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +1 -1
  37. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +18 -1
  38. package/dist/resources/extensions/gsd/state-reconciliation/index.js +6 -0
  39. package/dist/resources/extensions/gsd/state.js +15 -12
  40. package/dist/resources/extensions/gsd/tool-presentation-plan.js +120 -0
  41. package/dist/resources/extensions/gsd/tools/exec-tool.js +109 -0
  42. package/dist/resources/extensions/gsd/tools/plan-slice.js +14 -9
  43. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +2 -2
  44. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +366 -3
  45. package/dist/resources/extensions/gsd/unit-context-manifest.js +8 -3
  46. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -0
  47. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -1
  48. package/dist/resources/extensions/gsd/workflow-mcp.js +5 -1
  49. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -0
  50. package/dist/resources/extensions/mcp-client/manager.js +31 -1
  51. package/dist/web/standalone/.next/BUILD_ID +1 -1
  52. package/dist/web/standalone/.next/app-path-routes-manifest.json +4 -4
  53. package/dist/web/standalone/.next/build-manifest.json +2 -2
  54. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  55. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.html +1 -1
  72. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app-paths-manifest.json +4 -4
  79. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  80. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  82. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  83. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  84. package/package.json +2 -2
  85. package/packages/cloud-mcp-gateway/package.json +2 -2
  86. package/packages/contracts/dist/workflow.d.ts +14 -0
  87. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  88. package/packages/contracts/dist/workflow.js +16 -0
  89. package/packages/contracts/dist/workflow.js.map +1 -1
  90. package/packages/contracts/package.json +1 -1
  91. package/packages/daemon/package.json +4 -4
  92. package/packages/gsd-agent-core/package.json +5 -5
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  94. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  95. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
  96. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
  98. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +72 -31
  100. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.d.ts.map +1 -1
  102. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.js +2 -0
  103. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-extension-dialogs.js.map +1 -1
  104. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.d.ts.map +1 -1
  106. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-class-constants.js.map +1 -1
  108. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +1 -0
  110. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
  112. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +5 -0
  113. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
  114. package/packages/gsd-agent-modes/package.json +7 -7
  115. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  116. package/packages/mcp-server/dist/workflow-tools.js +82 -0
  117. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  118. package/packages/mcp-server/package.json +3 -3
  119. package/packages/native/package.json +1 -1
  120. package/packages/pi-agent-core/package.json +1 -1
  121. package/packages/pi-ai/dist/image-models.generated.d.ts +15 -0
  122. package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  123. package/packages/pi-ai/dist/image-models.generated.js +15 -0
  124. package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
  125. package/packages/pi-ai/dist/models.generated.d.ts +338 -17
  126. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  127. package/packages/pi-ai/dist/models.generated.js +412 -112
  128. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  129. package/packages/pi-ai/package.json +1 -1
  130. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  131. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  133. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  134. package/packages/pi-coding-agent/package.json +7 -7
  135. package/packages/pi-tui/dist/terminal.d.ts +1 -0
  136. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  137. package/packages/pi-tui/dist/terminal.js +8 -4
  138. package/packages/pi-tui/dist/terminal.js.map +1 -1
  139. package/packages/pi-tui/package.json +1 -1
  140. package/packages/rpc-client/package.json +2 -2
  141. package/pkg/package.json +1 -1
  142. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +196 -16
  143. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +239 -63
  144. package/src/resources/extensions/gsd/auto/phases.ts +5 -3
  145. package/src/resources/extensions/gsd/auto-dashboard.ts +16 -4
  146. package/src/resources/extensions/gsd/auto-dispatch.ts +48 -0
  147. package/src/resources/extensions/gsd/auto-post-unit.ts +138 -7
  148. package/src/resources/extensions/gsd/auto-prompts.ts +9 -0
  149. package/src/resources/extensions/gsd/auto-recovery.ts +4 -4
  150. package/src/resources/extensions/gsd/auto-start.ts +112 -17
  151. package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +2 -1
  152. package/src/resources/extensions/gsd/auto.ts +35 -3
  153. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +86 -0
  154. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +51 -0
  155. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +51 -14
  156. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +21 -10
  157. package/src/resources/extensions/gsd/commands/catalog.ts +6 -1
  158. package/src/resources/extensions/gsd/commands/handlers/core.ts +6 -2
  159. package/src/resources/extensions/gsd/commands/handlers/ops.ts +7 -3
  160. package/src/resources/extensions/gsd/commands-maintenance.ts +197 -2
  161. package/src/resources/extensions/gsd/commands-mcp-status.ts +134 -57
  162. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +4 -1
  163. package/src/resources/extensions/gsd/commands-verdict.ts +1 -1
  164. package/src/resources/extensions/gsd/config-overlay.ts +3 -1
  165. package/src/resources/extensions/gsd/error-classifier.ts +2 -1
  166. package/src/resources/extensions/gsd/exec-sandbox.ts +4 -0
  167. package/src/resources/extensions/gsd/gsd-db.ts +41 -6
  168. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  169. package/src/resources/extensions/gsd/mcp-filter.ts +3 -0
  170. package/src/resources/extensions/gsd/mcp-project-config.ts +92 -10
  171. package/src/resources/extensions/gsd/migration-auto-check.ts +2 -2
  172. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  173. package/src/resources/extensions/gsd/prompts/run-uat.md +10 -4
  174. package/src/resources/extensions/gsd/prompts/system.md +3 -1
  175. package/src/resources/extensions/gsd/safety/destructive-guard.ts +3 -0
  176. package/src/resources/extensions/gsd/skill-activation.ts +20 -2
  177. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +4 -2
  178. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +1 -1
  179. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +20 -0
  180. package/src/resources/extensions/gsd/state-reconciliation/index.ts +6 -0
  181. package/src/resources/extensions/gsd/state-reconciliation/types.ts +1 -0
  182. package/src/resources/extensions/gsd/state.ts +16 -12
  183. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +51 -0
  184. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +86 -0
  185. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +143 -2
  186. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +24 -2
  187. package/src/resources/extensions/gsd/tests/commands-dispatcher-validation-block.test.ts +38 -3
  188. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +6 -2
  189. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +8 -0
  190. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +50 -13
  191. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +60 -0
  192. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +18 -0
  193. package/src/resources/extensions/gsd/tests/exec-tool.test.ts +69 -0
  194. package/src/resources/extensions/gsd/tests/gsd-rebuild.test.ts +199 -0
  195. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +75 -0
  196. package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +13 -6
  197. package/src/resources/extensions/gsd/tests/mcp-filter.test.ts +15 -0
  198. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +68 -0
  199. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +177 -0
  200. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +3 -3
  201. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +54 -7
  202. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +39 -1
  203. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -0
  204. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  205. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +36 -0
  206. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +35 -0
  207. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +1 -1
  208. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +55 -0
  209. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +6 -2
  210. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +52 -0
  211. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +84 -10
  212. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +12 -2
  213. package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +29 -6
  214. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +29 -6
  215. package/src/resources/extensions/gsd/tests/validation-block-guard.test.ts +21 -0
  216. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +17 -2
  217. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +83 -0
  218. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +25 -0
  219. package/src/resources/extensions/gsd/tool-presentation-plan.ts +167 -0
  220. package/src/resources/extensions/gsd/tools/exec-tool.ts +130 -0
  221. package/src/resources/extensions/gsd/tools/plan-slice.ts +14 -9
  222. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +2 -2
  223. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +440 -2
  224. package/src/resources/extensions/gsd/unit-context-manifest.ts +14 -5
  225. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -0
  226. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -1
  227. package/src/resources/extensions/gsd/workflow-mcp.ts +5 -1
  228. package/src/resources/extensions/gsd/worktree-lifecycle.ts +26 -0
  229. package/src/resources/extensions/mcp-client/manager.ts +33 -1
  230. package/src/resources/extensions/mcp-client/tests/manager.test.ts +35 -0
  231. /package/dist/web/standalone/.next/static/{L9N5SPFi7f-Ne4u2uXzCe → eRWf-RI9bzbrwEurm_3uI}/_buildManifest.js +0 -0
  232. /package/dist/web/standalone/.next/static/{L9N5SPFi7f-Ne4u2uXzCe → eRWf-RI9bzbrwEurm_3uI}/_ssgManifest.js +0 -0
@@ -0,0 +1,199 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir } from "node:os";
14
+
15
+ import { handleRebuild } from "../commands-maintenance.ts";
16
+ import {
17
+ closeDatabase,
18
+ getTask,
19
+ insertMilestone,
20
+ insertSlice,
21
+ insertTask,
22
+ openDatabase,
23
+ } from "../gsd-db.ts";
24
+ import { invalidateStateCache } from "../state.ts";
25
+
26
+ type Note = { message: string; kind: string };
27
+
28
+ function makeBase(): string {
29
+ const base = mkdtempSync(join(tmpdir(), "gsd-rebuild-"));
30
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), {
31
+ recursive: true,
32
+ });
33
+ return base;
34
+ }
35
+
36
+ function cleanup(base: string): void {
37
+ closeDatabase();
38
+ invalidateStateCache();
39
+ rmSync(base, { recursive: true, force: true });
40
+ }
41
+
42
+ function makeCtx(): { ctx: any; notes: Note[] } {
43
+ const notes: Note[] = [];
44
+ return {
45
+ ctx: {
46
+ ui: {
47
+ notify: (message: string, kind: string) => notes.push({ message, kind }),
48
+ },
49
+ },
50
+ notes,
51
+ };
52
+ }
53
+
54
+ function seedOpenTask(): void {
55
+ insertMilestone({ id: "M001", title: "Milestone", status: "active" });
56
+ insertSlice({
57
+ id: "S01",
58
+ milestoneId: "M001",
59
+ title: "Slice",
60
+ status: "in_progress",
61
+ risk: "low",
62
+ depends: [],
63
+ });
64
+ insertTask({
65
+ id: "T01",
66
+ sliceId: "S01",
67
+ milestoneId: "M001",
68
+ title: "Task",
69
+ status: "pending",
70
+ });
71
+ }
72
+
73
+ function listFiles(dir: string): string[] {
74
+ if (!existsSync(dir)) return [];
75
+ const out: string[] = [];
76
+ const stack = [dir];
77
+ while (stack.length > 0) {
78
+ const current = stack.pop()!;
79
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
80
+ const full = join(current, entry.name);
81
+ if (entry.isDirectory()) {
82
+ stack.push(full);
83
+ } else {
84
+ out.push(full);
85
+ }
86
+ }
87
+ }
88
+ return out.sort();
89
+ }
90
+
91
+ test("handleRebuild quarantines stale completion projections without mutating DB state", async () => {
92
+ const base = makeBase();
93
+ try {
94
+ openDatabase(join(base, ".gsd", "gsd.db"));
95
+ seedOpenTask();
96
+
97
+ const summaryPath = join(
98
+ base,
99
+ ".gsd",
100
+ "milestones",
101
+ "M001",
102
+ "slices",
103
+ "S01",
104
+ "tasks",
105
+ "T01-SUMMARY.md",
106
+ );
107
+ writeFileSync(summaryPath, "# T01 Summary\n\nDisk-only completion.\n", "utf-8");
108
+
109
+ const { ctx, notes } = makeCtx();
110
+ await handleRebuild(ctx, base, "markdown");
111
+
112
+ assert.equal(existsSync(summaryPath), false, "stale SUMMARY projection should be moved aside");
113
+ const task = getTask("M001", "S01", "T01");
114
+ assert.equal(task?.status, "pending", "DB task status remains authoritative");
115
+ assert.equal(task?.full_summary_md, "", "disk summary must not be imported into DB");
116
+
117
+ const quarantined = listFiles(join(base, ".gsd", "quarantine", "projections"));
118
+ assert.equal(quarantined.length, 1);
119
+ assert.match(readFileSync(quarantined[0]!, "utf-8"), /Disk-only completion/);
120
+ assert.match(notes.at(-1)?.message ?? "", /Quarantined:\s+1/);
121
+ assert.equal(notes.at(-1)?.kind, "success");
122
+ } finally {
123
+ cleanup(base);
124
+ }
125
+ });
126
+
127
+ test("handleRebuild re-renders missing task summary projections from DB", async () => {
128
+ const base = makeBase();
129
+ try {
130
+ openDatabase(join(base, ".gsd", "gsd.db"));
131
+ seedOpenTask();
132
+ insertTask({
133
+ id: "T01",
134
+ sliceId: "S01",
135
+ milestoneId: "M001",
136
+ title: "Task",
137
+ status: "complete",
138
+ oneLiner: "Task complete",
139
+ narrative: "Finished through the DB.",
140
+ verificationResult: "passed",
141
+ fullSummaryMd: "# T01 Summary\n\nRendered from DB.\n",
142
+ });
143
+
144
+ const summaryPath = join(
145
+ base,
146
+ ".gsd",
147
+ "milestones",
148
+ "M001",
149
+ "slices",
150
+ "S01",
151
+ "tasks",
152
+ "T01-SUMMARY.md",
153
+ );
154
+ rmSync(summaryPath, { force: true });
155
+
156
+ const { ctx, notes } = makeCtx();
157
+ await handleRebuild(ctx, base);
158
+
159
+ assert.equal(existsSync(summaryPath), true, "missing SUMMARY projection should be regenerated");
160
+ assert.equal(readFileSync(summaryPath, "utf-8"), "# T01 Summary\n\nRendered from DB.\n");
161
+ assert.match(notes.at(-1)?.message ?? "", /rebuilt markdown projections from the canonical DB/);
162
+ assert.match(notes.at(-1)?.message ?? "", /Quarantined:\s+0/);
163
+ } finally {
164
+ cleanup(base);
165
+ }
166
+ });
167
+
168
+ test("handleRebuild database target is reserved and does not import markdown", async () => {
169
+ const base = makeBase();
170
+ try {
171
+ openDatabase(join(base, ".gsd", "gsd.db"));
172
+ seedOpenTask();
173
+
174
+ const summaryPath = join(
175
+ base,
176
+ ".gsd",
177
+ "milestones",
178
+ "M001",
179
+ "slices",
180
+ "S01",
181
+ "tasks",
182
+ "T01-SUMMARY.md",
183
+ );
184
+ writeFileSync(summaryPath, "# T01 Summary\n\nShould not import.\n", "utf-8");
185
+
186
+ const { ctx, notes } = makeCtx();
187
+ await handleRebuild(ctx, base, "database");
188
+
189
+ assert.equal(existsSync(summaryPath), true, "reserved DB rebuild must not move projection files");
190
+ const task = getTask("M001", "S01", "T01");
191
+ assert.equal(task?.status, "pending", "reserved DB rebuild must not mutate task status");
192
+ assert.equal(task?.full_summary_md, "", "reserved DB rebuild must not import markdown");
193
+ assert.match(notes.at(-1)?.message ?? "", /reserved/);
194
+ assert.match(notes.at(-1)?.message ?? "", /\/gsd recover --confirm/);
195
+ assert.equal(notes.at(-1)?.kind, "warning");
196
+ } finally {
197
+ cleanup(base);
198
+ }
199
+ });
@@ -24,6 +24,7 @@ import {
24
24
  } from '../gsd-db.ts';
25
25
  import { migrateHierarchyToDb } from '../md-importer.ts';
26
26
  import { deriveStateFromDb, invalidateStateCache } from '../state.ts';
27
+ import { handleRecover } from '../commands-maintenance.ts';
27
28
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
28
29
 
29
30
  function createFixtureBase(): string {
@@ -42,6 +43,22 @@ function cleanup(base: string): void {
42
43
  rmSync(base, { recursive: true, force: true });
43
44
  }
44
45
 
46
+ function makeCtx(confirm?: () => Promise<boolean>): {
47
+ ctx: any;
48
+ notes: Array<{ message: string; kind: string }>;
49
+ } {
50
+ const notes: Array<{ message: string; kind: string }> = [];
51
+ return {
52
+ ctx: {
53
+ ui: {
54
+ notify: (message: string, kind: string) => notes.push({ message, kind }),
55
+ ...(confirm ? { confirm: async () => confirm() } : {}),
56
+ },
57
+ },
58
+ notes,
59
+ };
60
+ }
61
+
45
62
  // ─── Fixture Content ──────────────────────────────────────────────────────
46
63
 
47
64
  const ROADMAP_M001 = `# M001: Recovery Test
@@ -437,4 +454,62 @@ describe('gsd-recover', async () => {
437
454
  cleanup(base);
438
455
  }
439
456
  });
457
+
458
+ test('handleRecover warns and does not import markdown without confirmation', async () => {
459
+ const base = createFixtureBase();
460
+ try {
461
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
462
+ openDatabase(':memory:');
463
+ insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
464
+
465
+ const { ctx, notes } = makeCtx();
466
+ await handleRecover(ctx, base);
467
+
468
+ assert.ok(getMilestone('M999'), 'existing DB row remains when recover is unconfirmed');
469
+ assert.equal(getMilestone('M001'), null, 'markdown milestone is not imported without confirmation');
470
+ assert.equal(notes.at(-1)?.kind, 'warning');
471
+ assert.match(notes.at(-1)?.message ?? '', /\/gsd recover --confirm/);
472
+ } finally {
473
+ closeDatabase();
474
+ cleanup(base);
475
+ }
476
+ });
477
+
478
+ test('handleRecover interactive cancellation leaves DB unchanged', async () => {
479
+ const base = createFixtureBase();
480
+ try {
481
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
482
+ openDatabase(':memory:');
483
+ insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
484
+
485
+ const { ctx, notes } = makeCtx(async () => false);
486
+ await handleRecover(ctx, base);
487
+
488
+ assert.ok(getMilestone('M999'), 'existing DB row remains when recover is cancelled');
489
+ assert.equal(getMilestone('M001'), null, 'markdown milestone is not imported after cancellation');
490
+ assert.match(notes.at(-1)?.message ?? '', /cancelled/);
491
+ } finally {
492
+ closeDatabase();
493
+ cleanup(base);
494
+ }
495
+ });
496
+
497
+ test('handleRecover imports markdown after explicit confirmation', async () => {
498
+ const base = createFixtureBase();
499
+ try {
500
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
501
+ openDatabase(':memory:');
502
+ insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
503
+
504
+ const { ctx, notes } = makeCtx();
505
+ await handleRecover(ctx, base, '--confirm');
506
+
507
+ assert.equal(getMilestone('M999'), null, 'confirmed recover clears old hierarchy rows');
508
+ assert.ok(getMilestone('M001'), 'confirmed recover imports markdown hierarchy');
509
+ assert.equal(notes.at(-1)?.kind, 'success');
510
+ } finally {
511
+ closeDatabase();
512
+ cleanup(base);
513
+ }
514
+ });
440
515
  });
@@ -50,6 +50,7 @@ import { handleCompleteSlice } from "../../tools/complete-slice.ts";
50
50
  import { handleCompleteMilestone } from "../../tools/complete-milestone.ts";
51
51
  import { handleReopenTask } from "../../tools/reopen-task.ts";
52
52
  import { handleReopenSlice } from "../../tools/reopen-slice.ts";
53
+ import { handleReopenMilestone } from "../../tools/reopen-milestone.ts";
53
54
 
54
55
  // ── State derivation ──────────────────────────────────────────────────────
55
56
  import {
@@ -700,9 +701,7 @@ describe("state-machine-live-validation", () => {
700
701
  assert.match((result as any).error, /closed milestone/);
701
702
  });
702
703
 
703
- test("no reopen-milestone tool exists milestone completion is irrevocable (H5)", async () => {
704
- // This test documents the H5 finding: there is no handleReopenMilestone function.
705
- // A completed milestone can only be undone via direct DB manipulation.
704
+ test("closed milestone cannot be reopened by generic DB update", async () => {
706
705
  base = createFullFixture();
707
706
  openDatabase(join(base, ".gsd", "gsd.db"));
708
707
  insertMilestone({ id: "M001", title: "Done", status: "complete" });
@@ -710,10 +709,18 @@ describe("state-machine-live-validation", () => {
710
709
  const milestone = getMilestone("M001");
711
710
  assert.ok(isClosedStatus(milestone!.status), "milestone is closed");
712
711
 
713
- // The only escape is direct DB manipulation — no handler exists
714
- updateMilestoneStatus("M001", "active", null);
712
+ assert.throws(
713
+ () => updateMilestoneStatus("M001", "active", null),
714
+ /use gsd_milestone_reopen for an explicit reopen/,
715
+ );
716
+
717
+ const result = await handleReopenMilestone(
718
+ { milestoneId: "M001", reason: "regression surfaced after closure" },
719
+ base,
720
+ );
721
+ assert.ok(!("error" in result), `unexpected reopen error: ${"error" in result ? result.error : ""}`);
715
722
  const reopened = getMilestone("M001");
716
- assert.equal(reopened!.status, "active", "direct DB manipulation can reopen, but no tool exposes this");
723
+ assert.equal(reopened!.status, "active", "explicit reopen handler reopens the milestone");
717
724
  });
718
725
  });
719
726
 
@@ -48,6 +48,21 @@ describe("discoverMcpServerNames", () => {
48
48
  assert.deepEqual(result.sort(), ["server-a", "server-b", "shared"]);
49
49
  });
50
50
 
51
+ it("reads from .claude/settings.local.json for Claude Code project-local servers", () => {
52
+ const dir = mkdtempSync(join(tmpdir(), "mcp-filter-test-"));
53
+ mkdirSync(join(dir, ".claude"), { recursive: true });
54
+ writeFileSync(
55
+ join(dir, ".claude", "settings.local.json"),
56
+ JSON.stringify({ mcpServers: { "local-server": {}, "shared": {} } }),
57
+ );
58
+ writeFileSync(
59
+ join(dir, ".claude", "settings.json"),
60
+ JSON.stringify({ mcpServers: { "project-server": {}, "shared": {} } }),
61
+ );
62
+ const result = discoverMcpServerNames(dir);
63
+ assert.deepEqual(result.sort(), ["local-server", "project-server", "shared"]);
64
+ });
65
+
51
66
  it("handles .claude/settings.json missing gracefully", () => {
52
67
  const dir = mkdtempSync(join(tmpdir(), "mcp-filter-test-"));
53
68
  writeFileSync(
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
7
  import {
8
+ ensureClaudeCodeMcpJsonServerEnabled,
8
9
  ensureProjectWorkflowMcpConfig,
9
10
  GSD_BROWSER_MCP_SERVER_NAME,
10
11
  GSD_WORKFLOW_MCP_SERVER_NAME,
@@ -54,6 +55,14 @@ test("ensureProjectWorkflowMcpConfig creates .mcp.json with workflow and browser
54
55
  ]);
55
56
  assert.equal(browserArgs[mcpArgIndex + 6], projectRoot);
56
57
  assert.equal((browserServer as { cwd?: string })?.cwd, projectRoot);
58
+
59
+ const settings = JSON.parse(readFileSync(join(projectRoot, ".claude", "settings.local.json"), "utf-8")) as {
60
+ enabledMcpjsonServers?: string[];
61
+ };
62
+ assert.deepEqual(settings.enabledMcpjsonServers, [
63
+ GSD_WORKFLOW_MCP_SERVER_NAME,
64
+ GSD_BROWSER_MCP_SERVER_NAME,
65
+ ]);
57
66
  } finally {
58
67
  rmSync(projectRoot, { recursive: true, force: true });
59
68
  }
@@ -115,6 +124,11 @@ test("ensureProjectWorkflowMcpConfig uses custom workflow server name from env",
115
124
  assert.ok(parsed.mcpServers?.["custom-workflow"]);
116
125
  assert.ok(parsed.mcpServers?.[GSD_BROWSER_MCP_SERVER_NAME]);
117
126
  assert.equal(parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME], undefined);
127
+
128
+ const settings = JSON.parse(readFileSync(join(projectRoot, ".claude", "settings.local.json"), "utf-8")) as {
129
+ enabledMcpjsonServers?: string[];
130
+ };
131
+ assert.deepEqual(settings.enabledMcpjsonServers, ["custom-workflow", GSD_BROWSER_MCP_SERVER_NAME]);
118
132
  } finally {
119
133
  rmSync(projectRoot, { recursive: true, force: true });
120
134
  }
@@ -136,6 +150,11 @@ test("ensureProjectWorkflowMcpConfig can disable the default browser MCP server"
136
150
  };
137
151
  assert.ok(parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME]);
138
152
  assert.equal(parsed.mcpServers?.[GSD_BROWSER_MCP_SERVER_NAME], undefined);
153
+
154
+ const settings = JSON.parse(readFileSync(join(projectRoot, ".claude", "settings.local.json"), "utf-8")) as {
155
+ enabledMcpjsonServers?: string[];
156
+ };
157
+ assert.deepEqual(settings.enabledMcpjsonServers, [GSD_WORKFLOW_MCP_SERVER_NAME]);
139
158
  } finally {
140
159
  rmSync(projectRoot, { recursive: true, force: true });
141
160
  }
@@ -156,3 +175,52 @@ test("ensureProjectWorkflowMcpConfig is idempotent when config is already curren
156
175
  rmSync(projectRoot, { recursive: true, force: true });
157
176
  }
158
177
  });
178
+
179
+ test("ensureProjectWorkflowMcpConfig updates stale Claude Code MCP approval state", () => {
180
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-init-"));
181
+ mkdirSync(join(projectRoot, ".gsd"), { recursive: true });
182
+ const settingsPath = join(projectRoot, ".claude", "settings.local.json");
183
+
184
+ try {
185
+ const first = ensureProjectWorkflowMcpConfig(projectRoot);
186
+ assert.equal(first.status, "created");
187
+
188
+ writeFileSync(
189
+ settingsPath,
190
+ `${JSON.stringify({
191
+ permissions: { allow: ["Bash(gh issue *)"] },
192
+ enabledMcpjsonServers: [],
193
+ disabledMcpjsonServers: [GSD_WORKFLOW_MCP_SERVER_NAME, GSD_BROWSER_MCP_SERVER_NAME],
194
+ }, null, 2)}\n`,
195
+ "utf-8",
196
+ );
197
+
198
+ const second = ensureProjectWorkflowMcpConfig(projectRoot);
199
+ assert.equal(second.status, "updated");
200
+
201
+ const settings = JSON.parse(readFileSync(settingsPath, "utf-8")) as {
202
+ permissions?: { allow?: string[] };
203
+ enabledMcpjsonServers?: string[];
204
+ disabledMcpjsonServers?: string[];
205
+ };
206
+ assert.deepEqual(settings.permissions?.allow, ["Bash(gh issue *)"]);
207
+ assert.deepEqual(settings.enabledMcpjsonServers, [
208
+ GSD_WORKFLOW_MCP_SERVER_NAME,
209
+ GSD_BROWSER_MCP_SERVER_NAME,
210
+ ]);
211
+ assert.deepEqual(settings.disabledMcpjsonServers, []);
212
+ } finally {
213
+ rmSync(projectRoot, { recursive: true, force: true });
214
+ }
215
+ });
216
+
217
+ test("ensureClaudeCodeMcpJsonServerEnabled is idempotent", () => {
218
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-init-"));
219
+
220
+ try {
221
+ assert.equal(ensureClaudeCodeMcpJsonServerEnabled(projectRoot, "gsd-workflow"), true);
222
+ assert.equal(ensureClaudeCodeMcpJsonServerEnabled(projectRoot, "gsd-workflow"), false);
223
+ } finally {
224
+ rmSync(projectRoot, { recursive: true, force: true });
225
+ }
226
+ });
@@ -1,14 +1,23 @@
1
1
  import test, { describe } from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
3
9
 
4
10
  import {
11
+ formatMcpDiscoveryResult,
5
12
  formatMcpInitResult,
6
13
  formatMcpConnectionTestResult,
7
14
  formatMcpStatusReport,
8
15
  formatMcpServerDetail,
9
16
  hasHostMcpTool,
17
+ handleMcpStatus,
10
18
  type McpServerStatus,
11
19
  } from "../commands-mcp-status.ts";
20
+ import { clearMcpConfigCache } from "../../mcp-client/manager.ts";
12
21
 
13
22
  // ─── formatMcpStatusReport ──────────────────────────────────────────────────
14
23
 
@@ -49,6 +58,16 @@ describe("formatMcpStatusReport", () => {
49
58
  assert.match(result, /disabled/i);
50
59
  });
51
60
 
61
+ test("shows available state for servers that pass a status probe", () => {
62
+ const servers: McpServerStatus[] = [
63
+ { name: "gsd-workflow", transport: "stdio", connected: false, available: true, toolCount: 62, error: undefined },
64
+ ];
65
+ const result = formatMcpStatusReport(servers);
66
+ assert.match(result, /gsd-workflow/);
67
+ assert.match(result, /available — 62 tools/);
68
+ assert.doesNotMatch(result, /disconnected/);
69
+ });
70
+
52
71
  test("includes server count in header", () => {
53
72
  const servers: McpServerStatus[] = [
54
73
  { name: "a", transport: "stdio", connected: true, toolCount: 3, error: undefined },
@@ -113,6 +132,20 @@ describe("formatMcpServerDetail", () => {
113
132
  assert.match(result, /disconnected/i);
114
133
  });
115
134
 
135
+ test("shows available status with tool names", () => {
136
+ const result = formatMcpServerDetail({
137
+ name: "gsd-workflow",
138
+ transport: "stdio",
139
+ connected: false,
140
+ available: true,
141
+ toolCount: 1,
142
+ tools: ["gsd_milestone_status"],
143
+ error: undefined,
144
+ });
145
+ assert.match(result, /available/i);
146
+ assert.match(result, /gsd_milestone_status/);
147
+ });
148
+
116
149
  test("shows env warnings for server detail", () => {
117
150
  const result = formatMcpServerDetail({
118
151
  name: "warned",
@@ -128,6 +161,150 @@ describe("formatMcpServerDetail", () => {
128
161
  });
129
162
  });
130
163
 
164
+ describe("handleMcpStatus", () => {
165
+ test("probes configured stdio servers before reporting disconnected", async () => {
166
+ const previousGsdHome = process.env.GSD_HOME;
167
+ const originalCwd = process.cwd();
168
+ const projectDir = mkdtempSync(join(tmpdir(), "gsd-mcp-status-project-"));
169
+ const gsdHomeDir = mkdtempSync(join(tmpdir(), "gsd-mcp-status-home-"));
170
+ try {
171
+ process.env.GSD_HOME = gsdHomeDir;
172
+ process.chdir(projectDir);
173
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
174
+
175
+ const require = createRequire(import.meta.url);
176
+ const mcpModuleUrl = pathToFileURL(require.resolve("@modelcontextprotocol/sdk/server/mcp.js")).href;
177
+ const stdioModuleUrl = pathToFileURL(require.resolve("@modelcontextprotocol/sdk/server/stdio.js")).href;
178
+ const serverPath = join(projectDir, "fake-mcp-server.mjs");
179
+ writeFileSync(
180
+ serverPath,
181
+ [
182
+ `const { McpServer } = await import(${JSON.stringify(mcpModuleUrl)});`,
183
+ `const { StdioServerTransport } = await import(${JSON.stringify(stdioModuleUrl)});`,
184
+ 'const server = new McpServer({ name: "fake", version: "1.0.0" }, { capabilities: { tools: {} } });',
185
+ 'server.tool("fake_tool", "Probe-visible tool", {}, async () => ({ content: [{ type: "text", text: "ok" }] }));',
186
+ 'await server.connect(new StdioServerTransport());',
187
+ ].join("\n"),
188
+ "utf-8",
189
+ );
190
+ writeFileSync(
191
+ join(projectDir, ".mcp.json"),
192
+ JSON.stringify({ mcpServers: { "gsd-workflow": { command: process.execPath, args: [serverPath] } } }),
193
+ "utf-8",
194
+ );
195
+
196
+ let message = "";
197
+ const ctx = {
198
+ getSystemPrompt: () => "",
199
+ ui: {
200
+ notify: (text: string) => {
201
+ message = text;
202
+ },
203
+ },
204
+ };
205
+
206
+ await handleMcpStatus("status", ctx as unknown as ExtensionCommandContext);
207
+
208
+ assert.match(message, /gsd-workflow/);
209
+ assert.match(message, /available — 1 tools/);
210
+ assert.doesNotMatch(message, /disconnected/);
211
+ } finally {
212
+ process.chdir(originalCwd);
213
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
214
+ else process.env.GSD_HOME = previousGsdHome;
215
+ rmSync(projectDir, { recursive: true, force: true });
216
+ rmSync(gsdHomeDir, { recursive: true, force: true });
217
+ clearMcpConfigCache();
218
+ }
219
+ });
220
+
221
+ test("discovers the only configured server when no server name is provided", async () => {
222
+ const previousGsdHome = process.env.GSD_HOME;
223
+ const originalCwd = process.cwd();
224
+ const projectDir = mkdtempSync(join(tmpdir(), "gsd-mcp-discover-project-"));
225
+ const gsdHomeDir = mkdtempSync(join(tmpdir(), "gsd-mcp-discover-home-"));
226
+ try {
227
+ process.env.GSD_HOME = gsdHomeDir;
228
+ process.chdir(projectDir);
229
+
230
+ const require = createRequire(import.meta.url);
231
+ const mcpModuleUrl = pathToFileURL(require.resolve("@modelcontextprotocol/sdk/server/mcp.js")).href;
232
+ const stdioModuleUrl = pathToFileURL(require.resolve("@modelcontextprotocol/sdk/server/stdio.js")).href;
233
+ const serverPath = join(projectDir, "discover-mcp-server.mjs");
234
+ writeFileSync(
235
+ serverPath,
236
+ [
237
+ `const { McpServer } = await import(${JSON.stringify(mcpModuleUrl)});`,
238
+ `const { StdioServerTransport } = await import(${JSON.stringify(stdioModuleUrl)});`,
239
+ 'const server = new McpServer({ name: "fake", version: "1.0.0" }, { capabilities: { tools: {} } });',
240
+ 'server.tool("discover_tool", "Discover-visible tool", {}, async () => ({ content: [{ type: "text", text: "ok" }] }));',
241
+ 'await server.connect(new StdioServerTransport());',
242
+ ].join("\n"),
243
+ "utf-8",
244
+ );
245
+ writeFileSync(
246
+ join(projectDir, ".mcp.json"),
247
+ JSON.stringify({ mcpServers: { "gsd-workflow": { command: process.execPath, args: [serverPath] } } }),
248
+ "utf-8",
249
+ );
250
+
251
+ let message = "";
252
+ const ctx = {
253
+ getSystemPrompt: () => "",
254
+ ui: {
255
+ notify: (text: string) => {
256
+ message = text;
257
+ },
258
+ },
259
+ };
260
+
261
+ await handleMcpStatus("discover", ctx as unknown as ExtensionCommandContext);
262
+
263
+ assert.match(message, /MCP discovery completed for gsd-workflow/);
264
+ assert.match(message, /discover_tool/);
265
+ assert.doesNotMatch(message, /Usage: \/gsd mcp/);
266
+ } finally {
267
+ process.chdir(originalCwd);
268
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
269
+ else process.env.GSD_HOME = previousGsdHome;
270
+ rmSync(projectDir, { recursive: true, force: true });
271
+ rmSync(gsdHomeDir, { recursive: true, force: true });
272
+ clearMcpConfigCache();
273
+ }
274
+ });
275
+ });
276
+
277
+ describe("formatMcpDiscoveryResult", () => {
278
+ test("summarizes discovered tools", () => {
279
+ const result = formatMcpDiscoveryResult({
280
+ ok: true,
281
+ server: "demo",
282
+ transport: "stdio",
283
+ toolCount: 1,
284
+ tools: ["ping"],
285
+ warnings: [],
286
+ });
287
+ assert.match(result, /discovery completed/i);
288
+ assert.match(result, /ping/);
289
+ assert.match(result, /mcp_call/);
290
+ });
291
+
292
+ test("summarizes discovery failures", () => {
293
+ const result = formatMcpDiscoveryResult({
294
+ ok: false,
295
+ server: "demo",
296
+ transport: "http",
297
+ toolCount: 0,
298
+ tools: [],
299
+ warnings: ["url references unset environment variable TOKEN."],
300
+ error: "bad config",
301
+ });
302
+ assert.match(result, /discovery failed/i);
303
+ assert.match(result, /bad config/);
304
+ assert.match(result, /TOKEN/);
305
+ });
306
+ });
307
+
131
308
  describe("formatMcpConnectionTestResult", () => {
132
309
  test("summarizes successful tools/list", () => {
133
310
  const result = formatMcpConnectionTestResult({
@@ -102,8 +102,8 @@ test("migration auto-check preserves empty DB and reports explicit recovery", as
102
102
  assert.equal(result.action, "recovery-required");
103
103
  assert.equal(result.reason, "db-empty");
104
104
  assert.deepEqual(result.afterDb, { milestones: 0, slices: 0, tasks: 0 });
105
- assert.equal(result.recoveryCommand, "/gsd recover");
106
- assert.match(result.message ?? "", /run `\/gsd recover`/);
105
+ assert.equal(result.recoveryCommand, "/gsd recover --confirm");
106
+ assert.match(result.message ?? "", /run `\/gsd recover --confirm`/);
107
107
  assert.equal(getAllMilestones().length, 0);
108
108
  assert.equal(getSliceTasks("M001", "S01").length, 0);
109
109
  } finally {
@@ -125,7 +125,7 @@ test("migration auto-check preserves DB on hierarchy count mismatch", async () =
125
125
  assert.equal(result.reason, "count-mismatch");
126
126
  assert.deepEqual(result.beforeDb, { milestones: 1, slices: 1, tasks: 0 });
127
127
  assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 0 });
128
- assert.equal(result.recoveryCommand, "/gsd recover");
128
+ assert.equal(result.recoveryCommand, "/gsd recover --confirm");
129
129
  assert.equal(getSliceTasks("M001", "S01").length, 0);
130
130
  } finally {
131
131
  cleanup(base);