@opengsd/gsd-pi 1.0.2-dev.5961fbf → 1.0.2-dev.5f7864c

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 (223) hide show
  1. package/README.md +63 -12
  2. package/dist/onboarding.js +22 -3
  3. package/dist/resource-loader.d.ts +2 -0
  4. package/dist/resource-loader.js +18 -1
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/context7/index.js +12 -2
  7. package/dist/resources/extensions/get-secrets-from-user.js +16 -16
  8. package/dist/resources/extensions/google-cli/index.js +30 -0
  9. package/dist/resources/extensions/google-cli/models.js +55 -0
  10. package/dist/resources/extensions/google-cli/package.json +11 -0
  11. package/dist/resources/extensions/google-cli/readiness.js +12 -0
  12. package/dist/resources/extensions/google-cli/stream-adapter.js +191 -0
  13. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  14. package/dist/resources/extensions/gsd/auto-start.js +232 -49
  15. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -15
  17. package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
  19. package/dist/resources/extensions/gsd/commands-handlers.js +3 -0
  20. package/dist/resources/extensions/gsd/commands-usage.js +105 -1
  21. package/dist/resources/extensions/gsd/config-overlay.js +20 -14
  22. package/dist/resources/extensions/gsd/context-overlay.js +22 -16
  23. package/dist/resources/extensions/gsd/dashboard-overlay.js +10 -23
  24. package/dist/resources/extensions/gsd/doctor-providers.js +54 -24
  25. package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
  26. package/dist/resources/extensions/gsd/guided-flow.js +1 -1
  27. package/dist/resources/extensions/gsd/key-manager.js +45 -13
  28. package/dist/resources/extensions/gsd/notification-overlay.js +8 -9
  29. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +15 -13
  30. package/dist/resources/extensions/gsd/prompt-loader.js +2 -0
  31. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -2
  32. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  33. package/dist/resources/extensions/gsd/queue-reorder-ui.js +28 -18
  34. package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
  35. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -1
  36. package/dist/resources/extensions/gsd/tui/render-kit.js +51 -0
  37. package/dist/resources/extensions/gsd/vision-ask.js +22 -0
  38. package/dist/resources/extensions/gsd/visualizer-overlay.js +8 -36
  39. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
  40. package/dist/resources/extensions/search-the-web/native-search.js +57 -8
  41. package/dist/resources/extensions/shared/confirm-ui.js +9 -6
  42. package/dist/resources/extensions/shared/dialog-frame.js +42 -0
  43. package/dist/resources/extensions/shared/interview-ui.js +42 -30
  44. package/dist/resources/extensions/shared/next-action-ui.js +6 -6
  45. package/dist/resources/shared/package-manager-detection.js +36 -0
  46. package/dist/update-check.d.ts +6 -2
  47. package/dist/update-check.js +7 -3
  48. package/dist/web/standalone/.next/BUILD_ID +1 -1
  49. package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
  50. package/dist/web/standalone/.next/build-manifest.json +2 -2
  51. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/index.html +1 -1
  70. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
  77. package/dist/web/standalone/.next/server/chunks/1834.js +2 -2
  78. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  80. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  81. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  82. package/package.json +1 -1
  83. package/packages/cloud-mcp-gateway/package.json +2 -2
  84. package/packages/contracts/package.json +1 -1
  85. package/packages/daemon/package.json +4 -4
  86. package/packages/gsd-agent-core/package.json +5 -5
  87. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts +12 -0
  88. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts.map +1 -0
  89. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js +45 -0
  90. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js.map +1 -0
  91. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts +3 -2
  92. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js +11 -11
  94. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js.map +1 -1
  95. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts +3 -3
  96. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js +13 -11
  98. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts +3 -3
  100. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js +12 -10
  102. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js.map +1 -1
  103. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts +1 -0
  104. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts.map +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js +1 -0
  106. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +1 -1
  108. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +2 -2
  110. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts +6 -1
  112. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  113. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js +9 -6
  114. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  115. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  116. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  117. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  118. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts +3 -0
  119. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  120. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +144 -2
  121. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  122. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.d.ts.map +1 -1
  123. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js +2 -14
  124. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js.map +1 -1
  125. package/packages/gsd-agent-modes/package.json +7 -7
  126. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  127. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  128. package/packages/mcp-server/package.json +3 -3
  129. package/packages/native/package.json +1 -1
  130. package/packages/pi-agent-core/dist/agent-loop.js +13 -13
  131. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  132. package/packages/pi-agent-core/package.json +1 -1
  133. package/packages/pi-ai/dist/models.generated.d.ts +57 -17
  134. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  135. package/packages/pi-ai/dist/models.generated.js +64 -28
  136. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  137. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  138. package/packages/pi-ai/dist/providers/anthropic.js +50 -0
  139. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  140. package/packages/pi-ai/dist/types.d.ts +2 -0
  141. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  142. package/packages/pi-ai/dist/types.js.map +1 -1
  143. package/packages/pi-ai/package.json +1 -1
  144. package/packages/pi-coding-agent/package.json +7 -7
  145. package/packages/pi-tui/package.json +1 -1
  146. package/packages/rpc-client/package.json +2 -2
  147. package/pkg/package.json +1 -1
  148. package/scripts/install/detect-existing.js +17 -3
  149. package/scripts/install/npm-global.js +103 -33
  150. package/scripts/install.js +1 -0
  151. package/src/resources/extensions/context7/index.ts +15 -2
  152. package/src/resources/extensions/get-secrets-from-user.ts +17 -16
  153. package/src/resources/extensions/google-cli/index.ts +34 -0
  154. package/src/resources/extensions/google-cli/models.ts +57 -0
  155. package/src/resources/extensions/google-cli/package.json +11 -0
  156. package/src/resources/extensions/google-cli/readiness.ts +15 -0
  157. package/src/resources/extensions/google-cli/stream-adapter.ts +245 -0
  158. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  159. package/src/resources/extensions/gsd/auto-start.ts +307 -56
  160. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
  161. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -15
  162. package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
  163. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
  164. package/src/resources/extensions/gsd/commands-handlers.ts +2 -0
  165. package/src/resources/extensions/gsd/commands-usage.ts +110 -5
  166. package/src/resources/extensions/gsd/config-overlay.ts +19 -16
  167. package/src/resources/extensions/gsd/context-overlay.ts +24 -19
  168. package/src/resources/extensions/gsd/dashboard-overlay.ts +14 -27
  169. package/src/resources/extensions/gsd/doctor-providers.ts +55 -27
  170. package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
  171. package/src/resources/extensions/gsd/guided-flow.ts +1 -1
  172. package/src/resources/extensions/gsd/key-manager.ts +57 -14
  173. package/src/resources/extensions/gsd/notification-overlay.ts +12 -11
  174. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +16 -12
  175. package/src/resources/extensions/gsd/prompt-loader.ts +2 -0
  176. package/src/resources/extensions/gsd/prompts/discuss.md +4 -2
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  178. package/src/resources/extensions/gsd/queue-reorder-ui.ts +29 -20
  179. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
  180. package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
  181. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +31 -0
  182. package/src/resources/extensions/gsd/tests/commands-context.test.ts +5 -3
  183. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
  184. package/src/resources/extensions/gsd/tests/commands-usage.test.ts +97 -0
  185. package/src/resources/extensions/gsd/tests/context-chart.test.ts +9 -0
  186. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +25 -0
  187. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +4 -2
  188. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
  189. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +6 -0
  190. package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
  191. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +6 -1
  192. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
  193. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +7 -1
  194. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +46 -0
  195. package/src/resources/extensions/gsd/tests/show-config-command.test.ts +4 -0
  196. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
  197. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
  198. package/src/resources/extensions/gsd/tests/tui-border-assertions.ts +28 -0
  199. package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +14 -0
  200. package/src/resources/extensions/gsd/tests/vision-ask.test.ts +23 -0
  201. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +6 -1
  202. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +60 -0
  203. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +54 -0
  204. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
  205. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
  206. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
  207. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
  208. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +56 -4
  209. package/src/resources/extensions/gsd/tui/render-kit.ts +82 -0
  210. package/src/resources/extensions/gsd/vision-ask.ts +28 -0
  211. package/src/resources/extensions/gsd/visualizer-overlay.ts +12 -40
  212. package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
  213. package/src/resources/extensions/search-the-web/native-search.ts +60 -8
  214. package/src/resources/extensions/shared/confirm-ui.ts +8 -12
  215. package/src/resources/extensions/shared/dialog-frame.ts +71 -0
  216. package/src/resources/extensions/shared/interview-ui.ts +43 -42
  217. package/src/resources/extensions/shared/next-action-ui.ts +6 -6
  218. package/src/resources/extensions/shared/tests/confirm-ui.test.ts +57 -0
  219. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +163 -0
  220. package/src/resources/extensions/shared/tests/next-action-ui-hasui.test.ts +55 -0
  221. package/src/resources/shared/package-manager-detection.ts +39 -0
  222. /package/dist/web/standalone/.next/static/{spUYLkQXoHJyxYOMH9VQy → IjxvcC7sl_MHNKXsUZrAy}/_buildManifest.js +0 -0
  223. /package/dist/web/standalone/.next/static/{spUYLkQXoHJyxYOMH9VQy → IjxvcC7sl_MHNKXsUZrAy}/_ssgManifest.js +0 -0
@@ -235,12 +235,16 @@ test("gsd_task_complete — enrichment arrays are optional", () => {
235
235
  "milestoneId",
236
236
  "oneLiner",
237
237
  "narrative",
238
- "verification",
239
238
  ];
240
239
  for (const field of coreRequired) {
241
240
  assert.ok(required.has(field), `core field "${field}" must be required`);
242
241
  }
243
242
 
243
+ assert.ok(
244
+ !required.has("verification"),
245
+ "verification must be optional at the schema layer so step-mode can recover when verificationEvidence is present",
246
+ );
247
+
244
248
  // Enrichment fields must be optional
245
249
  const enrichmentFields = [
246
250
  "keyFiles",
@@ -272,6 +276,25 @@ test("gsd_task_complete — validates with only core params", () => {
272
276
  assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.join(", ")}`);
273
277
  });
274
278
 
279
+ test("gsd_task_complete — accepts evidence-only verification at schema layer", () => {
280
+ const tool = getTool("gsd_task_complete");
281
+ assert.ok(tool, "gsd_task_complete must be registered");
282
+
283
+ const params = {
284
+ taskId: "T01",
285
+ sliceId: "S01",
286
+ milestoneId: "M001",
287
+ oneLiner: "Implemented the feature",
288
+ narrative: "Created the module and wired it up.",
289
+ verificationEvidence: [
290
+ { command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
291
+ ],
292
+ };
293
+
294
+ const errors = validateSchema(tool, params);
295
+ assert.strictEqual(errors.length, 0, `Evidence-only params should validate but got errors: ${errors.join(", ")}`);
296
+ });
297
+
275
298
  // ─── gsd_complete_milestone: enrichment arrays must be optional ──────────────
276
299
 
277
300
  test("gsd_complete_milestone — enrichment arrays are optional", () => {
@@ -0,0 +1,28 @@
1
+ import assert from "node:assert/strict";
2
+
3
+ import { visibleWidth } from "@gsd/pi-tui";
4
+
5
+ const ANSI_PATTERN = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
6
+
7
+ function stripAnsi(text: string): string {
8
+ return text.replace(ANSI_PATTERN, "");
9
+ }
10
+
11
+ export function assertFullOuterBorder(lines: string[], width: number): void {
12
+ assert.ok(lines.length >= 2, "dialog must include top and bottom borders");
13
+
14
+ for (const [index, line] of lines.entries()) {
15
+ assert.equal(visibleWidth(line), width, `line ${index} must fill dialog width`);
16
+ }
17
+
18
+ const top = stripAnsi(lines[0] ?? "");
19
+ const bottom = stripAnsi(lines.at(-1) ?? "");
20
+ assert.match(top, /^[╭┌].*[╮┐]$/, `top border missing full corners: ${top}`);
21
+ assert.match(bottom, /^[╰└].*[╯┘]$/, `bottom border missing full corners: ${bottom}`);
22
+
23
+ for (let index = 1; index < lines.length - 1; index++) {
24
+ const line = stripAnsi(lines[index] ?? "");
25
+ assert.match(line, /^[│┃├]/, `line ${index} missing left border: ${line}`);
26
+ assert.match(line, /[│┃┤]$/, `line ${index} missing right border: ${line}`);
27
+ }
28
+ }
@@ -5,8 +5,10 @@ import { describe, test } from "node:test";
5
5
  import assert from "node:assert/strict";
6
6
 
7
7
  import { visibleWidth } from "@gsd/pi-tui";
8
+ import { assertFullOuterBorder } from "./tui-border-assertions.ts";
8
9
  import {
9
10
  padRightVisible,
11
+ renderDialogFrame,
10
12
  renderFrame,
11
13
  renderKeyHints,
12
14
  renderPanel,
@@ -60,6 +62,18 @@ describe("tui render kit", () => {
60
62
  }
61
63
  });
62
64
 
65
+ test("renderDialogFrame draws a full titled modal border with footer", () => {
66
+ const lines = renderDialogFrame(theme, "Dialog", ["row", "long ".repeat(40)], 40, {
67
+ footer: renderKeyHints(theme, ["esc close"], 36),
68
+ });
69
+ assertWidth(lines, 40);
70
+ assertFullOuterBorder(lines, 40);
71
+ assert.match(lines[0] ?? "", /^╭─ Dialog ─+╮$/);
72
+ assert.ok(lines.some((line) => line.startsWith("│") && line.endsWith("│")));
73
+ assert.ok(lines.some((line) => line.startsWith("├") && line.endsWith("┤")));
74
+ assert.match(lines.at(-1) ?? "", /^╰─+╯$/);
75
+ });
76
+
63
77
  test("renderPanel stays within width and draws no vertical borders", () => {
64
78
  for (const width of [3, 40, 80]) {
65
79
  const lines = renderPanel(theme, "Title", ["row", "long ".repeat(40)], width);
@@ -0,0 +1,23 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { chooseVisionAskVariant, VISION_ASK_VARIANTS } from "../vision-ask.ts";
4
+
5
+ test("vision ask variants stay varied and conversational", () => {
6
+ assert.ok(VISION_ASK_VARIANTS.length >= 6, "keep enough openers to avoid repetition");
7
+ assert.equal(new Set(VISION_ASK_VARIANTS).size, VISION_ASK_VARIANTS.length, "openers should be unique");
8
+
9
+ for (const opener of VISION_ASK_VARIANTS) {
10
+ assert.ok(opener.length <= 72, `opener should stay short: ${opener}`);
11
+ assert.doesNotMatch(opener, /\n/, "opener should be a single line");
12
+ assert.doesNotMatch(opener, /\bstakeholders?|key success metrics?|business objectives?\b/i, "avoid corporate wording");
13
+ assert.notEqual(opener, "What's the vision?", "do not keep the old fixed opener in rotation");
14
+ }
15
+ });
16
+
17
+ test("chooseVisionAskVariant picks from the configured opener list", () => {
18
+ assert.equal(chooseVisionAskVariant(() => 0), VISION_ASK_VARIANTS[0]);
19
+ assert.equal(
20
+ chooseVisionAskVariant((exclusiveMax) => exclusiveMax - 1),
21
+ VISION_ASK_VARIANTS[VISION_ASK_VARIANTS.length - 1],
22
+ );
23
+ });
@@ -14,6 +14,7 @@ import { test } from "node:test";
14
14
  import assert from "node:assert/strict";
15
15
 
16
16
  import { GSDVisualizerOverlay, TAB_COUNT } from "../visualizer-overlay.ts";
17
+ import { assertFullOuterBorder } from "./tui-border-assertions.ts";
17
18
 
18
19
  function makeTui() {
19
20
  const renders: number[] = [];
@@ -50,7 +51,11 @@ test("overlay renders 10 tabs (Progress, Timeline, Deps, Metrics, Health, Agent,
50
51
  overlay.loading = true; // body shows loading text, but tab bar renders regardless
51
52
 
52
53
  // Use a very wide terminal so the tab bar is not truncated.
53
- const lines = overlay.render(200).map(stripAnsi);
54
+ const rawLines = overlay.render(200);
55
+ assertFullOuterBorder(rawLines, 200);
56
+ const lines = rawLines.map(stripAnsi);
57
+ assert.match(lines[0] ?? "", /^╭─ GSD Visualizer /);
58
+ assert.match(lines.at(-1) ?? "", /^╰─+╯$/);
54
59
  const tabBar = lines.find((l) => l.includes("Progress") && l.includes("Export"));
55
60
  assert.ok(tabBar, `expected a tab-bar line containing all labels, got:\n${lines.slice(0, 5).join("\n")}`);
56
61
  for (const label of ["Progress", "Timeline", "Deps", "Metrics", "Health", "Agent", "Changes", "Knowledge", "Captures", "Export"]) {
@@ -1,6 +1,11 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
3
6
 
7
+ import { registerHooks } from "../bootstrap/register-hooks.ts";
8
+ import { GSD_WORKFLOW_MCP_SERVER_NAME } from "../mcp-project-config.ts";
4
9
  import { prepareWorkflowMcpForProject, shouldAutoPrepareWorkflowMcp } from "../workflow-mcp-auto-prep.ts";
5
10
 
6
11
  test("shouldAutoPrepareWorkflowMcp enables prep for externalCli local transport", () => {
@@ -74,3 +79,58 @@ test("prepareWorkflowMcpForProject warns with /gsd mcp init guidance when prep f
74
79
  assert.equal(notifications[0].level, "warning");
75
80
  assert.match(notifications[0].message, /Please run \/gsd mcp init \./);
76
81
  });
82
+
83
+ test("before_agent_start auto-prepares project workflow MCP for Claude Code CLI", async (t) => {
84
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-before-agent-"));
85
+ const originalCwd = process.cwd();
86
+ const notifications: string[] = [];
87
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any> | any>>();
88
+ const pi = {
89
+ on(event: string, handler: (event: any, ctx?: any) => Promise<any> | any) {
90
+ const existing = handlers.get(event) ?? [];
91
+ existing.push(handler);
92
+ handlers.set(event, existing);
93
+ },
94
+ getActiveTools: () => [],
95
+ getAllTools: () => [],
96
+ setActiveTools() {},
97
+ };
98
+
99
+ t.after(() => {
100
+ process.chdir(originalCwd);
101
+ rmSync(projectRoot, { recursive: true, force: true });
102
+ });
103
+
104
+ process.chdir(projectRoot);
105
+ registerHooks(pi as any, []);
106
+
107
+ const beforeAgentStart = handlers.get("before_agent_start")?.[0];
108
+ assert.ok(beforeAgentStart, "before_agent_start hook should be registered");
109
+
110
+ await beforeAgentStart(
111
+ { prompt: "hello", systemPrompt: "base" },
112
+ {
113
+ cwd: projectRoot,
114
+ model: { provider: "claude-code", baseUrl: "local://claude-code" },
115
+ modelRegistry: {
116
+ getProviderAuthMode: () => "externalCli",
117
+ isProviderRequestReady: () => true,
118
+ },
119
+ ui: {
120
+ notify(message: string) {
121
+ notifications.push(message);
122
+ },
123
+ setWidget() {},
124
+ },
125
+ },
126
+ );
127
+
128
+ const configPath = join(projectRoot, ".mcp.json");
129
+ assert.equal(existsSync(configPath), true, "Claude Code CLI turns should create project MCP config");
130
+
131
+ const parsed = JSON.parse(readFileSync(configPath, "utf-8")) as {
132
+ mcpServers?: Record<string, unknown>;
133
+ };
134
+ assert.ok(parsed.mcpServers?.[GSD_WORKFLOW_MCP_SERVER_NAME]);
135
+ assert.match(notifications.join("\n"), /Claude Code MCP prepared/);
136
+ });
@@ -148,6 +148,60 @@ test("executeTaskComplete coerces string verificationEvidence entries", async ()
148
148
  }
149
149
  });
150
150
 
151
+ test("executeTaskComplete derives missing verification from evidence", async () => {
152
+ const base = makeTmpBase();
153
+ try {
154
+ openTestDb(base);
155
+ const planDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
156
+ mkdirSync(planDir, { recursive: true });
157
+ writeFileSync(join(planDir, "S01-PLAN.md"), "# S01\n\n- [ ] **T01: Demo** `est:5m`\n");
158
+
159
+ const result = await inProjectDir(base, () => executeTaskComplete({
160
+ milestoneId: "M001",
161
+ sliceId: "S01",
162
+ taskId: "T01",
163
+ oneLiner: "Completed task",
164
+ narrative: "Did the work",
165
+ verificationEvidence: [
166
+ { command: "npm test", exitCode: 0, verdict: "pass", durationMs: 1234 },
167
+ ],
168
+ }, base));
169
+
170
+ assert.equal(result.details.operation, "complete_task");
171
+ const db = _getAdapter();
172
+ assert.ok(db, "DB should be open");
173
+ const row = db!.prepare(
174
+ "SELECT verification_result FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?",
175
+ ).get("M001", "S01", "T01") as Record<string, unknown> | undefined;
176
+
177
+ assert.match(String(row?.verification_result), /Verification evidence recorded/);
178
+ assert.match(String(row?.verification_result), /`npm test` exited 0 \(pass\)/);
179
+ } finally {
180
+ closeDatabase();
181
+ cleanup(base);
182
+ }
183
+ });
184
+
185
+ test("executeTaskComplete returns a tool error when verification cannot be derived", async () => {
186
+ const base = makeTmpBase();
187
+ try {
188
+ openTestDb(base);
189
+ const result = await inProjectDir(base, () => executeTaskComplete({
190
+ milestoneId: "M001",
191
+ sliceId: "S01",
192
+ taskId: "T01",
193
+ oneLiner: "Completed task",
194
+ narrative: "Did the work",
195
+ }, base));
196
+
197
+ assert.equal(result.isError, true);
198
+ assert.match(String(result.content[0]?.text), /verification is required/);
199
+ } finally {
200
+ closeDatabase();
201
+ cleanup(base);
202
+ }
203
+ });
204
+
151
205
  test("executeSliceComplete preserves omitted optional requirement arrays", async () => {
152
206
  const base = makeTmpBase();
153
207
  try {
@@ -9,7 +9,7 @@ import { join } from "node:path";
9
9
  import { probeGitConflictState } from "../git-conflict-state.js";
10
10
  import { ensureWorkspaceGitReadyForPath } from "../workspace-git-preflight.js";
11
11
  import { isWorkspaceGitAllowedCommand } from "../workspace-git-guard.js";
12
- import { cleanup, git, makeTempRepo } from "./test-utils.ts";
12
+ import { cleanup, git, makeTempDir, makeTempRepo } from "./test-utils.ts";
13
13
 
14
14
  function seedGsdConflict(base: string): void {
15
15
  mkdirSync(join(base, ".gsd"), { recursive: true });
@@ -60,6 +60,21 @@ test("probeGitConflictState reports clean repo", () => {
60
60
  }
61
61
  });
62
62
 
63
+ test("ensureWorkspaceGitReadyForPath allows fresh non-git project setup folders", async () => {
64
+ const base = makeTempDir("gsd-ws-git-non-repo-");
65
+ try {
66
+ mkdirSync(join(base, ".gsd"), { recursive: true });
67
+
68
+ const probe = probeGitConflictState(base);
69
+ assert.equal(probe.status, "clean");
70
+
71
+ const ready = await ensureWorkspaceGitReadyForPath(base);
72
+ assert.equal(ready.ok, true);
73
+ } finally {
74
+ cleanup(base);
75
+ }
76
+ });
77
+
63
78
  test("ensureWorkspaceGitReadyForPath auto-resolves .gsd/ conflicts", async () => {
64
79
  const base = makeTempRepo("gsd-ws-git-heal-");
65
80
  try {
@@ -212,6 +212,34 @@ test("enterMilestone returns ok:true mode:none when isolation disabled", () => {
212
212
  assert.equal(s.basePath, "/project");
213
213
  });
214
214
 
215
+ test("adoptStrandedMilestone forces branch recovery even when normal preferences differ", (t) => {
216
+ const previousCwd = process.cwd();
217
+ const base = makeGitRepoBase({ isolation: "worktree" });
218
+ t.after(() => cleanupRepoBase(base, previousCwd));
219
+
220
+ const s = makeSession({ basePath: base, originalBasePath: base });
221
+ const deps = makeDeps();
222
+ const ctx = makeCtx();
223
+ const lifecycle = new WorktreeLifecycle(s, deps);
224
+
225
+ const result = lifecycle.adoptStrandedMilestone("M001", base, ctx, {
226
+ mode: "branch",
227
+ });
228
+
229
+ assert.equal(result.ok, true, `expected ok:true, got: ${JSON.stringify(result)}`);
230
+ if (result.ok) {
231
+ assert.equal(result.mode, "branch");
232
+ assert.equal(result.path, base);
233
+ }
234
+ assert.equal(s.basePath, base);
235
+ assert.equal(s.strandedRecoveryIsolationMode, "branch");
236
+ const currentBranch = execFileSync("git", ["branch", "--show-current"], {
237
+ cwd: base,
238
+ encoding: "utf-8",
239
+ }).trim();
240
+ assert.equal(currentBranch, "milestone/M001");
241
+ });
242
+
215
243
  test("enterMilestone returns ok:false reason:isolation-degraded when session degraded", () => {
216
244
  const s = makeSession({ isolationDegraded: true });
217
245
  const deps = makeDeps({ getIsolationMode: () => "branch" });
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { test } from "node:test";
16
16
  import assert from "node:assert/strict";
17
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
17
+ import { existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { tmpdir } from "node:os";
20
20
 
@@ -79,3 +79,47 @@ test("#2942: injected existsFn — milestones/ alone is enough", () => {
79
79
  p === "/proj/.gsd" || p === "/proj/.gsd/milestones";
80
80
  assert.equal(hasGsdBootstrapArtifacts("/proj/.gsd", existsFn), true);
81
81
  });
82
+
83
+ test("bare /gsd routes zombie .gsd folders to project init before closeout/db checks", async (t) => {
84
+ const base = mkdtempSync(join(tmpdir(), "gsd-zombie-bare-command-"));
85
+ t.after(() => rmSync(base, { recursive: true, force: true }));
86
+ mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
87
+
88
+ const previousCwd = process.cwd();
89
+ const previousGsdHome = process.env.GSD_HOME;
90
+ const previousProjectRoot = process.env.GSD_PROJECT_ROOT;
91
+ try {
92
+ process.chdir(base);
93
+ process.env.GSD_HOME = join(base, ".test-gsd-home");
94
+ delete process.env.GSD_PROJECT_ROOT;
95
+
96
+ const notifications: string[] = [];
97
+ const ctx = {
98
+ hasUI: false,
99
+ ui: {
100
+ notify: (content: unknown) => notifications.push(String(content)),
101
+ setStatus: () => {},
102
+ setWidget: () => {},
103
+ },
104
+ };
105
+ const { handleAutoCommand } = await import("../commands/handlers/auto.ts");
106
+
107
+ await handleAutoCommand("", ctx as any, {} as any);
108
+
109
+ assert.ok(
110
+ notifications.some((message) => message.includes("/gsd init did not start")),
111
+ "bare /gsd should route unbootstrapped zombie folders to the init wizard",
112
+ );
113
+ assert.equal(
114
+ existsSync(join(base, ".gsd", "gsd.db")),
115
+ false,
116
+ "bare /gsd should not create the project DB before init has bootstrapped .gsd/",
117
+ );
118
+ } finally {
119
+ process.chdir(previousCwd);
120
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
121
+ else process.env.GSD_HOME = previousGsdHome;
122
+ if (previousProjectRoot === undefined) delete process.env.GSD_PROJECT_ROOT;
123
+ else process.env.GSD_PROJECT_ROOT = previousProjectRoot;
124
+ }
125
+ });
@@ -173,6 +173,15 @@ export async function handleCompleteTask(
173
173
  if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
174
174
  return { error: "milestoneId is required and must be a non-empty string" };
175
175
  }
176
+ if (!params.oneLiner || typeof params.oneLiner !== "string" || params.oneLiner.trim() === "") {
177
+ return { error: "oneLiner is required and must be a non-empty string" };
178
+ }
179
+ if (!params.narrative || typeof params.narrative !== "string" || params.narrative.trim() === "") {
180
+ return { error: "narrative is required and must be a non-empty string" };
181
+ }
182
+ if (!params.verification || typeof params.verification !== "string" || params.verification.trim() === "") {
183
+ return { error: "verification is required and must be a non-empty string" };
184
+ }
176
185
 
177
186
  const artifactBasePath = resolveCanonicalMilestoneRoot(basePath, params.milestoneId);
178
187
 
@@ -306,7 +306,7 @@ export interface TaskCompleteParams {
306
306
  milestoneId: string;
307
307
  oneLiner: string;
308
308
  narrative: string;
309
- verification: string;
309
+ verification?: string;
310
310
  deviations?: string;
311
311
  knownIssues?: string;
312
312
  keyFiles?: string[];
@@ -315,6 +315,40 @@ export interface TaskCompleteParams {
315
315
  verificationEvidence?: VerificationEvidenceInput[];
316
316
  }
317
317
 
318
+ type NormalizedVerificationEvidence = {
319
+ command: string;
320
+ exitCode: number;
321
+ verdict: string;
322
+ durationMs: number;
323
+ };
324
+
325
+ function normalizeVerificationEvidence(
326
+ evidence: VerificationEvidenceInput[] | undefined,
327
+ ): NormalizedVerificationEvidence[] {
328
+ return (evidence ?? []).map((entry) =>
329
+ typeof entry === "string"
330
+ ? { command: entry, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 }
331
+ : entry,
332
+ );
333
+ }
334
+
335
+ function deriveVerificationSummary(
336
+ evidence: NormalizedVerificationEvidence[],
337
+ ): string | null {
338
+ if (evidence.length === 0) return null;
339
+
340
+ const rendered = evidence.slice(0, 3).map((entry) => {
341
+ const command = entry.command.trim() || "(unspecified command)";
342
+ const verdict = entry.verdict.trim() || "recorded";
343
+ return `\`${command}\` exited ${entry.exitCode} (${verdict})`;
344
+ });
345
+ const suffix = evidence.length > rendered.length
346
+ ? `; ${evidence.length - rendered.length} more check(s) recorded`
347
+ : "";
348
+
349
+ return `Verification evidence recorded: ${rendered.join("; ")}${suffix}.`;
350
+ }
351
+
318
352
  export type CompleteMilestoneExecutorParams = Partial<CompleteMilestoneParams> & Record<string, unknown>;
319
353
  export type SliceCompleteExecutorParams = CompleteSliceParams;
320
354
  export type PlanMilestoneExecutorParams = PlanMilestoneParams;
@@ -350,9 +384,27 @@ export async function executeTaskComplete(
350
384
  }
351
385
  try {
352
386
  const coerced = { ...params };
353
- coerced.verificationEvidence = (params.verificationEvidence ?? []).map((v) =>
354
- typeof v === "string" ? { command: v, exitCode: -1, verdict: "unknown (coerced from string)", durationMs: 0 } : v,
355
- );
387
+ const verificationEvidence = normalizeVerificationEvidence(params.verificationEvidence);
388
+ coerced.verificationEvidence = verificationEvidence;
389
+
390
+ const verification = typeof params.verification === "string" ? params.verification.trim() : "";
391
+ if (verification.length === 0) {
392
+ const derived = deriveVerificationSummary(verificationEvidence);
393
+ if (derived) {
394
+ coerced.verification = derived;
395
+ } else if (params.blockerDiscovered === true) {
396
+ coerced.verification = "Not run: blocker discovered before verification.";
397
+ } else {
398
+ return {
399
+ content: [{
400
+ type: "text",
401
+ text: "Error completing task: verification is required unless verificationEvidence is provided or blockerDiscovered is true.",
402
+ }],
403
+ details: { operation: "complete_task", error: "verification_required" },
404
+ isError: true,
405
+ };
406
+ }
407
+ }
356
408
 
357
409
  const result = await handleCompleteTask(coerced as any, basePath);
358
410
  if ("error" in result) {
@@ -151,3 +151,85 @@ export function renderFrame(
151
151
  lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
152
152
  return lines.map((line) => safeLine(line, width, ""));
153
153
  }
154
+
155
+ export interface DialogFrameOptions {
156
+ borderColor?: string;
157
+ paddingX?: number;
158
+ footer?: string | string[];
159
+ scroll?: {
160
+ offset: number;
161
+ visibleRows: number;
162
+ totalRows: number;
163
+ trackOffset?: number;
164
+ trackRows?: number;
165
+ };
166
+ }
167
+
168
+ function renderTitledTopBorder(
169
+ theme: ThemeLike,
170
+ title: string,
171
+ width: number,
172
+ border: (text: string) => string,
173
+ ): string {
174
+ const trimmedTitle = title.trim();
175
+ if (!trimmedTitle || width < 10) {
176
+ return border("╭" + "─".repeat(width - 2) + "╮");
177
+ }
178
+
179
+ const maxTitleWidth = Math.max(0, width - 7);
180
+ const safeTitle = safeLine(trimmedTitle, maxTitleWidth);
181
+ const fill = Math.max(0, width - visibleWidth(safeTitle) - 5);
182
+ return border("╭─ ") + theme.bold(theme.fg("accent", safeTitle)) + border(" " + "─".repeat(fill) + "╮");
183
+ }
184
+
185
+ export function renderDialogFrame(
186
+ theme: ThemeLike,
187
+ title: string,
188
+ inner: string[],
189
+ width: number,
190
+ options: DialogFrameOptions = {},
191
+ ): string[] {
192
+ if (width < 4) return inner.map((line) => safeLine(line, width));
193
+
194
+ const borderColor = options.borderColor ?? "borderAccent";
195
+ const paddingX = Math.max(0, options.paddingX ?? 1);
196
+ const contentWidth = Math.max(0, width - 2 - paddingX * 2);
197
+ const border = (text: string) => theme.fg(borderColor, text);
198
+ const pad = " ".repeat(paddingX);
199
+ const lines = [renderTitledTopBorder(theme, title, width, border)];
200
+
201
+ const scroll = options.scroll;
202
+ const bodyRows = inner.length;
203
+ const trackOffset = Math.max(0, Math.min(scroll?.trackOffset ?? 0, bodyRows));
204
+ const trackRows = Math.max(0, Math.min(scroll?.trackRows ?? bodyRows, bodyRows - trackOffset));
205
+ const scrollable = !!scroll && scroll.totalRows > scroll.visibleRows && trackRows > 0;
206
+ const thumbLen = scrollable
207
+ ? Math.max(1, Math.round((scroll.visibleRows / scroll.totalRows) * trackRows))
208
+ : 0;
209
+ const maxThumbStart = Math.max(0, trackRows - thumbLen);
210
+ const maxScrollOffset = scrollable ? Math.max(1, scroll.totalRows - scroll.visibleRows) : 1;
211
+ const thumbStart = scrollable
212
+ ? trackOffset + Math.min(maxThumbStart, Math.round((scroll.offset / maxScrollOffset) * maxThumbStart))
213
+ : -1;
214
+
215
+ for (let i = 0; i < inner.length; i++) {
216
+ const line = inner[i] ?? "";
217
+ const rightBorder = scrollable && i >= thumbStart && i < thumbStart + thumbLen ? "┃" : "│";
218
+ lines.push(border("│") + pad + padRightVisible(line, contentWidth) + pad + border(rightBorder));
219
+ }
220
+
221
+ const footer = Array.isArray(options.footer)
222
+ ? options.footer
223
+ : options.footer
224
+ ? [options.footer]
225
+ : [];
226
+ if (footer.length > 0) {
227
+ lines.push(border("├" + "─".repeat(width - 2) + "┤"));
228
+ for (const line of footer) {
229
+ lines.push(border("│") + pad + padRightVisible(line, contentWidth) + pad + border("│"));
230
+ }
231
+ }
232
+
233
+ lines.push(border("╰" + "─".repeat(width - 2) + "╯"));
234
+ return lines.map((line) => safeLine(line, width, ""));
235
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Natural-language openers for milestone discussion.
3
+ *
4
+ * Keep these short and conversational. They are often the user's first prompt
5
+ * when GSD starts shaping a project or milestone, so they should feel like a
6
+ * collaborator starting a working session rather than a form field.
7
+ */
8
+ import { randomInt } from "node:crypto";
9
+
10
+ export const VISION_ASK_VARIANTS = [
11
+ "What are we building?",
12
+ "What do you want to make next?",
13
+ "What should this become?",
14
+ "What are you picturing?",
15
+ "Where should we take this?",
16
+ "What should this milestone unlock?",
17
+ "Tell me what you want to build.",
18
+ "What should GSD help you shape?",
19
+ ] as const;
20
+
21
+ export type VisionAskVariant = typeof VISION_ASK_VARIANTS[number];
22
+
23
+ export function chooseVisionAskVariant(
24
+ pickIndex: (exclusiveMax: number) => number = randomInt,
25
+ ): VisionAskVariant {
26
+ const index = pickIndex(VISION_ASK_VARIANTS.length);
27
+ return VISION_ASK_VARIANTS[index] ?? VISION_ASK_VARIANTS[0];
28
+ }