gsd-pi 2.46.1 → 2.47.0-dev.8cfe772

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 (220) hide show
  1. package/README.md +46 -29
  2. package/dist/resources/extensions/claude-code-cli/index.js +25 -0
  3. package/dist/resources/extensions/claude-code-cli/models.js +40 -0
  4. package/dist/resources/extensions/claude-code-cli/package.json +11 -0
  5. package/dist/resources/extensions/claude-code-cli/partial-builder.js +223 -0
  6. package/dist/resources/extensions/claude-code-cli/readiness.js +26 -0
  7. package/dist/resources/extensions/claude-code-cli/sdk-types.js +8 -0
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +309 -0
  9. package/dist/resources/extensions/gsd/auto-start.js +17 -9
  10. package/dist/resources/extensions/gsd/guided-flow.js +78 -2
  11. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  12. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
  13. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  14. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  16. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
  17. package/dist/resources/extensions/gsd/repo-identity.js +5 -2
  18. package/dist/resources/extensions/gsd/session-forensics.js +10 -1
  19. package/dist/resources/extensions/gsd/state.js +29 -2
  20. package/dist/resources/extensions/gsd/workflow-events.js +1 -1
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/required-server-files.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  91. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  97. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  111. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  113. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  115. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  117. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/index.html +1 -1
  127. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  128. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  129. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  130. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  132. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  133. package/dist/web/standalone/.next/server/app/page.js +2 -2
  134. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  136. package/dist/web/standalone/.next/server/chunks/229.js +1 -1
  137. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  138. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/middleware.js +2 -2
  140. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  142. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  143. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  144. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  145. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  146. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  147. package/dist/web/standalone/.next/static/chunks/app/page-6654a8cca61a3d1c.js +1 -0
  148. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  149. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  150. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  151. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  152. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  153. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  154. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  155. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  156. package/dist/web/standalone/server.js +1 -1
  157. package/package.json +3 -1
  158. package/packages/pi-agent-core/dist/agent-loop.js +27 -1
  159. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  160. package/packages/pi-agent-core/dist/agent.d.ts +7 -0
  161. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  162. package/packages/pi-agent-core/dist/agent.js +2 -0
  163. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  164. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  165. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  166. package/packages/pi-agent-core/dist/types.js.map +1 -1
  167. package/packages/pi-agent-core/src/agent-loop.ts +26 -1
  168. package/packages/pi-agent-core/src/agent.ts +10 -0
  169. package/packages/pi-agent-core/src/types.ts +10 -0
  170. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -2
  171. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +43 -0
  173. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/core/model-registry.js +26 -3
  176. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/sdk.js +1 -0
  179. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  180. package/packages/pi-coding-agent/package.json +1 -1
  181. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +27 -2
  182. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +70 -0
  183. package/packages/pi-coding-agent/src/core/model-registry.ts +29 -2
  184. package/packages/pi-coding-agent/src/core/sdk.ts +1 -0
  185. package/packages/pi-tui/dist/components/box.d.ts +1 -0
  186. package/packages/pi-tui/dist/components/box.d.ts.map +1 -1
  187. package/packages/pi-tui/dist/components/box.js +10 -0
  188. package/packages/pi-tui/dist/components/box.js.map +1 -1
  189. package/packages/pi-tui/src/components/box.ts +10 -0
  190. package/pkg/package.json +1 -1
  191. package/src/resources/extensions/claude-code-cli/index.ts +28 -0
  192. package/src/resources/extensions/claude-code-cli/models.ts +42 -0
  193. package/src/resources/extensions/claude-code-cli/package.json +11 -0
  194. package/src/resources/extensions/claude-code-cli/partial-builder.ts +258 -0
  195. package/src/resources/extensions/claude-code-cli/readiness.ts +30 -0
  196. package/src/resources/extensions/claude-code-cli/sdk-types.ts +149 -0
  197. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +370 -0
  198. package/src/resources/extensions/gsd/auto-start.ts +15 -8
  199. package/src/resources/extensions/gsd/guided-flow.ts +96 -2
  200. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  201. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
  202. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  203. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  204. package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  205. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
  206. package/src/resources/extensions/gsd/repo-identity.ts +5 -2
  207. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  208. package/src/resources/extensions/gsd/state.ts +33 -1
  209. package/src/resources/extensions/gsd/tests/discuss-queued-milestones.test.ts +241 -0
  210. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +121 -0
  211. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +70 -0
  212. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +40 -0
  213. package/src/resources/extensions/gsd/tests/preflight-context-draft-filter.test.ts +115 -0
  214. package/src/resources/extensions/gsd/tests/run-uat.test.ts +25 -0
  215. package/src/resources/extensions/gsd/workflow-events.ts +1 -1
  216. package/dist/web/standalone/.next/static/chunks/app/page-12dd5ece0df4badc.js +0 -1
  217. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  218. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  219. /package/dist/web/standalone/.next/static/{P4nF4UcdATjrbNMBH_Ulh → DyrX2zX_4v7KZDbUNxE2q}/_buildManifest.js +0 -0
  220. /package/dist/web/standalone/.next/static/{P4nF4UcdATjrbNMBH_Ulh → DyrX2zX_4v7KZDbUNxE2q}/_ssgManifest.js +0 -0
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Stream adapter: bridges the Claude Agent SDK into GSD's streamSimple contract.
3
+ *
4
+ * The SDK runs the full agentic loop (multi-turn, tool execution, compaction)
5
+ * in one call. This adapter translates the SDK's streaming output into
6
+ * AssistantMessageEvents for TUI rendering, then strips tool-call blocks from
7
+ * the final AssistantMessage so GSD's agent loop doesn't try to dispatch them.
8
+ */
9
+ import { EventStream } from "@gsd/pi-ai";
10
+ import { execSync } from "node:child_process";
11
+ import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.js";
12
+ // ---------------------------------------------------------------------------
13
+ // Stream factory
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Construct an AssistantMessageEventStream using EventStream directly.
17
+ * (The class itself is only re-exported as a type from the @gsd/pi-ai barrel.)
18
+ */
19
+ function createAssistantStream() {
20
+ return new EventStream((event) => event.type === "done" || event.type === "error", (event) => {
21
+ if (event.type === "done")
22
+ return event.message;
23
+ if (event.type === "error")
24
+ return event.error;
25
+ throw new Error("Unexpected event type for final result");
26
+ });
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Claude binary resolution
30
+ // ---------------------------------------------------------------------------
31
+ let cachedClaudePath = null;
32
+ /**
33
+ * Resolve the path to the system-installed `claude` binary.
34
+ * The SDK defaults to a bundled cli.js which doesn't exist when
35
+ * installed as a library — we need to point it at the real CLI.
36
+ */
37
+ function getClaudePath() {
38
+ if (cachedClaudePath)
39
+ return cachedClaudePath;
40
+ try {
41
+ cachedClaudePath = execSync("which claude", { timeout: 5_000, stdio: "pipe" })
42
+ .toString()
43
+ .trim();
44
+ }
45
+ catch {
46
+ cachedClaudePath = "claude"; // fall back to PATH resolution
47
+ }
48
+ return cachedClaudePath;
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Prompt extraction
52
+ // ---------------------------------------------------------------------------
53
+ /**
54
+ * Extract the last user prompt text from GSD's context messages.
55
+ * The SDK manages its own conversation history — we only send
56
+ * the latest user message as the prompt.
57
+ */
58
+ function extractLastUserPrompt(context) {
59
+ for (let i = context.messages.length - 1; i >= 0; i--) {
60
+ const msg = context.messages[i];
61
+ if (msg.role === "user") {
62
+ if (typeof msg.content === "string")
63
+ return msg.content;
64
+ if (Array.isArray(msg.content)) {
65
+ const textParts = msg.content
66
+ .filter((part) => part.type === "text")
67
+ .map((part) => part.text);
68
+ if (textParts.length > 0)
69
+ return textParts.join("\n");
70
+ }
71
+ }
72
+ }
73
+ return "";
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Error helper
77
+ // ---------------------------------------------------------------------------
78
+ function makeErrorMessage(model, errorMsg) {
79
+ return {
80
+ role: "assistant",
81
+ content: [{ type: "text", text: `Claude Code error: ${errorMsg}` }],
82
+ api: "anthropic-messages",
83
+ provider: "claude-code",
84
+ model,
85
+ usage: { ...ZERO_USAGE },
86
+ stopReason: "error",
87
+ errorMessage: errorMsg,
88
+ timestamp: Date.now(),
89
+ };
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // streamSimple implementation
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * GSD streamSimple function that delegates to the Claude Agent SDK.
96
+ *
97
+ * Emits AssistantMessageEvent deltas for real-time TUI rendering
98
+ * (thinking, text, tool calls). The final AssistantMessage has tool-call
99
+ * blocks stripped so the agent loop ends the turn without local dispatch.
100
+ */
101
+ export function streamViaClaudeCode(model, context, options) {
102
+ const stream = createAssistantStream();
103
+ void pumpSdkMessages(model, context, options, stream);
104
+ return stream;
105
+ }
106
+ async function pumpSdkMessages(model, context, options, stream) {
107
+ const modelId = model.id;
108
+ let builder = null;
109
+ /** Track the last text content seen across all assistant turns for the final message. */
110
+ let lastTextContent = "";
111
+ let lastThinkingContent = "";
112
+ /** Collect tool calls from intermediate SDK turns for tool_execution events. */
113
+ const intermediateToolCalls = [];
114
+ try {
115
+ // Dynamic import — the SDK is an optional dependency.
116
+ const sdkModule = "@anthropic-ai/claude-agent-sdk";
117
+ const sdk = (await import(/* webpackIgnore: true */ sdkModule));
118
+ // Bridge GSD's AbortSignal to SDK's AbortController
119
+ const controller = new AbortController();
120
+ if (options?.signal) {
121
+ options.signal.addEventListener("abort", () => controller.abort(), { once: true });
122
+ }
123
+ const prompt = extractLastUserPrompt(context);
124
+ const queryResult = sdk.query({
125
+ prompt,
126
+ options: {
127
+ pathToClaudeCodeExecutable: getClaudePath(),
128
+ model: modelId,
129
+ includePartialMessages: true,
130
+ persistSession: false,
131
+ abortController: controller,
132
+ cwd: process.cwd(),
133
+ permissionMode: "bypassPermissions",
134
+ allowDangerouslySkipPermissions: true,
135
+ settingSources: ["project"],
136
+ systemPrompt: { type: "preset", preset: "claude_code" },
137
+ betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
138
+ },
139
+ });
140
+ // Emit start with an empty partial
141
+ const initialPartial = {
142
+ role: "assistant",
143
+ content: [],
144
+ api: "anthropic-messages",
145
+ provider: "claude-code",
146
+ model: modelId,
147
+ usage: { ...ZERO_USAGE },
148
+ stopReason: "stop",
149
+ timestamp: Date.now(),
150
+ };
151
+ stream.push({ type: "start", partial: initialPartial });
152
+ for await (const msg of queryResult) {
153
+ if (options?.signal?.aborted)
154
+ break;
155
+ switch (msg.type) {
156
+ // -- Init --
157
+ case "system": {
158
+ // Nothing to emit — the stream is already started.
159
+ break;
160
+ }
161
+ // -- Streaming partial messages --
162
+ case "stream_event": {
163
+ const partial = msg;
164
+ if (partial.parent_tool_use_id !== null)
165
+ break; // skip subagent
166
+ const event = partial.event;
167
+ // New assistant turn starts with message_start
168
+ if (event.type === "message_start") {
169
+ builder = new PartialMessageBuilder(event.message?.model ?? modelId);
170
+ break;
171
+ }
172
+ if (!builder)
173
+ break;
174
+ const assistantEvent = builder.handleEvent(event);
175
+ if (assistantEvent) {
176
+ // Skip toolcall events — the agent loop's externalToolExecution
177
+ // path emits tool_execution_start/end events after streamSimple
178
+ // returns. Streaming toolcall events would render tool calls
179
+ // out of order in the TUI's accumulated message content.
180
+ const t = assistantEvent.type;
181
+ if (t !== "toolcall_start" && t !== "toolcall_delta" && t !== "toolcall_end") {
182
+ stream.push(assistantEvent);
183
+ }
184
+ }
185
+ break;
186
+ }
187
+ // -- Complete assistant message (non-streaming fallback) --
188
+ case "assistant": {
189
+ const sdkAssistant = msg;
190
+ if (sdkAssistant.parent_tool_use_id !== null)
191
+ break;
192
+ // Capture text content from complete messages
193
+ for (const block of sdkAssistant.message.content) {
194
+ if (block.type === "text") {
195
+ lastTextContent = block.text;
196
+ }
197
+ else if (block.type === "thinking") {
198
+ lastThinkingContent = block.thinking;
199
+ }
200
+ }
201
+ break;
202
+ }
203
+ // -- User message (synthetic tool result — signals turn boundary) --
204
+ case "user": {
205
+ const userMsg = msg;
206
+ if (userMsg.parent_tool_use_id !== null)
207
+ break;
208
+ // Capture content from the completed turn before resetting
209
+ if (builder) {
210
+ for (const block of builder.message.content) {
211
+ if (block.type === "text" && block.text) {
212
+ lastTextContent = block.text;
213
+ }
214
+ else if (block.type === "thinking" && block.thinking) {
215
+ lastThinkingContent = block.thinking;
216
+ }
217
+ else if (block.type === "toolCall") {
218
+ // Collect tool calls for externalToolExecution rendering
219
+ intermediateToolCalls.push(block);
220
+ }
221
+ }
222
+ }
223
+ builder = null;
224
+ break;
225
+ }
226
+ // -- Result (terminal) --
227
+ case "result": {
228
+ const result = msg;
229
+ // Build final message. Include intermediate tool calls so the
230
+ // agent loop's externalToolExecution path emits tool_execution
231
+ // events for proper TUI rendering, followed by the text response.
232
+ const finalContent = [];
233
+ // Add tool calls from intermediate turns first (renders above text)
234
+ finalContent.push(...intermediateToolCalls);
235
+ // Add text/thinking from the last turn
236
+ if (builder && builder.message.content.length > 0) {
237
+ for (const block of builder.message.content) {
238
+ if (block.type === "text" || block.type === "thinking") {
239
+ finalContent.push(block);
240
+ }
241
+ }
242
+ }
243
+ else {
244
+ if (lastThinkingContent) {
245
+ finalContent.push({ type: "thinking", thinking: lastThinkingContent });
246
+ }
247
+ if (lastTextContent) {
248
+ finalContent.push({ type: "text", text: lastTextContent });
249
+ }
250
+ }
251
+ // Fallback: use the SDK's result text if we have no content
252
+ if (finalContent.length === 0 && result.subtype === "success" && result.result) {
253
+ finalContent.push({ type: "text", text: result.result });
254
+ }
255
+ const finalMessage = {
256
+ role: "assistant",
257
+ content: finalContent,
258
+ api: "anthropic-messages",
259
+ provider: "claude-code",
260
+ model: modelId,
261
+ usage: mapUsage(result.usage, result.total_cost_usd),
262
+ stopReason: result.is_error ? "error" : "stop",
263
+ timestamp: Date.now(),
264
+ };
265
+ if (result.is_error) {
266
+ const errText = "errors" in result
267
+ ? result.errors?.join("; ")
268
+ : result.subtype;
269
+ finalMessage.errorMessage = errText;
270
+ stream.push({ type: "error", reason: "error", error: finalMessage });
271
+ }
272
+ else {
273
+ stream.push({ type: "done", reason: "stop", message: finalMessage });
274
+ }
275
+ return;
276
+ }
277
+ default:
278
+ break;
279
+ }
280
+ }
281
+ // Generator exhausted without a result message (unexpected)
282
+ const fallbackContent = [];
283
+ if (lastTextContent) {
284
+ fallbackContent.push({ type: "text", text: lastTextContent });
285
+ }
286
+ if (fallbackContent.length === 0) {
287
+ fallbackContent.push({ type: "text", text: "(Claude Code session ended without a response)" });
288
+ }
289
+ const fallback = {
290
+ role: "assistant",
291
+ content: fallbackContent,
292
+ api: "anthropic-messages",
293
+ provider: "claude-code",
294
+ model: modelId,
295
+ usage: { ...ZERO_USAGE },
296
+ stopReason: "stop",
297
+ timestamp: Date.now(),
298
+ };
299
+ stream.push({ type: "done", reason: "stop", message: fallback });
300
+ }
301
+ catch (err) {
302
+ const errorMsg = err instanceof Error ? err.message : String(err);
303
+ stream.push({
304
+ type: "error",
305
+ reason: "error",
306
+ error: makeErrorMessage(modelId, errorMsg),
307
+ });
308
+ }
309
+ }
@@ -20,7 +20,7 @@ import { synthesizeCrashRecovery } from "./session-forensics.js";
20
20
  import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive, } from "./crash-recovery.js";
21
21
  import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
22
22
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
23
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, } from "./native-git-bridge.js";
23
+ import { nativeInit, nativeAddAll, nativeCommit, } from "./native-git-bridge.js";
24
24
  import { GitServiceImpl } from "./git-service.js";
25
25
  import { captureIntegrationBranch, detectWorktreeName, setActiveMilestoneId, } from "./worktree.js";
26
26
  import { getAutoWorktreePath } from "./auto-worktree.js";
@@ -30,7 +30,7 @@ import { initRoutingHistory } from "./routing-history.js";
30
30
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
31
31
  import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
32
32
  import { snapshotSkills } from "./skill-discovery.js";
33
- import { isDbAvailable } from "./gsd-db.js";
33
+ import { isDbAvailable, getMilestone } from "./gsd-db.js";
34
34
  import { hideFooter } from "./auto-dashboard.js";
35
35
  import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
36
36
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:fs";
@@ -69,13 +69,14 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
69
69
  ctx.ui.notify(`GSD_PROJECT_ID must contain only alphanumeric characters, hyphens, and underscores. Got: "${customProjectId}"`, "error");
70
70
  return releaseLockAndReturn();
71
71
  }
72
- // Ensure git repo exists.
73
- // Guard against inherited repos: if `base` is a subdirectory of another
74
- // git repo that has no .gsd (i.e. the parent project was never initialised
75
- // with GSD), create a fresh git repo at `base` so it gets its own identity
76
- // hash. Without this, repoIdentity() resolves to the parent repo's hash
77
- // and loads milestones from an unrelated project (#1639).
78
- if (!nativeIsRepo(base) || isInheritedRepo(base)) {
72
+ // Ensure git repo exists *locally* at base.
73
+ // nativeIsRepo() uses `git rev-parse` which traverses up to parent dirs,
74
+ // so a parent repo can make it return true even when base has no .git of
75
+ // its own. Check for a local .git instead (defense-in-depth for the case
76
+ // where isInheritedRepo() returns a false negative, e.g. stale .gsd at
77
+ // the parent git root). See #2393 and related issue.
78
+ const hasLocalGit = existsSync(join(base, ".git"));
79
+ if (!hasLocalGit || isInheritedRepo(base)) {
79
80
  const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
80
81
  nativeInit(base, mainBranch);
81
82
  }
@@ -502,6 +503,13 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
502
503
  if (milestoneIds.length > 1) {
503
504
  const issues = [];
504
505
  for (const id of milestoneIds) {
506
+ // Skip completed/parked milestones — a leftover CONTEXT-DRAFT.md
507
+ // on a finished milestone is harmless residue, not an actionable warning.
508
+ if (isDbAvailable()) {
509
+ const ms = getMilestone(id);
510
+ if (ms?.status === "complete" || ms?.status === "parked")
511
+ continue;
512
+ }
505
513
  const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
506
514
  if (draft)
507
515
  issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
@@ -403,9 +403,14 @@ export async function showDiscuss(ctx, pi, basePath) {
403
403
  // Invalidate caches to pick up artifacts written by a just-completed discuss/plan
404
404
  invalidateAllCaches();
405
405
  const state = await deriveState(basePath);
406
- // Guard: no active milestone
406
+ // No active milestone — check for pending milestones to discuss instead
407
407
  if (!state.activeMilestone) {
408
- ctx.ui.notify("No active milestone. Run /gsd to create one first.", "warning");
408
+ const pendingMilestones = state.registry.filter(m => m.status === "pending");
409
+ if (pendingMilestones.length === 0) {
410
+ ctx.ui.notify("No active milestone. Run /gsd to create one first.", "warning");
411
+ return;
412
+ }
413
+ await showDiscussQueuedMilestone(ctx, pi, basePath, pendingMilestones);
409
414
  return;
410
415
  }
411
416
  const mid = state.activeMilestone.id;
@@ -526,6 +531,16 @@ export async function showDiscuss(ctx, pi, basePath) {
526
531
  recommended: s.id === firstUndiscussedId,
527
532
  };
528
533
  });
534
+ // Offer access to queued milestones when any exist
535
+ const pendingMilestones = state.registry.filter(m => m.status === "pending");
536
+ if (pendingMilestones.length > 0) {
537
+ actions.push({
538
+ id: "discuss_queued_milestone",
539
+ label: "Discuss a queued milestone",
540
+ description: `Refine context for ${pendingMilestones.length} queued milestone(s). Does not affect current execution.`,
541
+ recommended: false,
542
+ });
543
+ }
529
544
  const choice = await showNextAction(ctx, {
530
545
  title: "GSD — Discuss a slice",
531
546
  summary: [
@@ -537,6 +552,10 @@ export async function showDiscuss(ctx, pi, basePath) {
537
552
  });
538
553
  if (choice === "not_yet")
539
554
  return;
555
+ if (choice === "discuss_queued_milestone") {
556
+ await showDiscussQueuedMilestone(ctx, pi, basePath, pendingMilestones);
557
+ return;
558
+ }
540
559
  const chosen = pendingSlices.find(s => s.id === choice);
541
560
  if (!chosen)
542
561
  return;
@@ -564,6 +583,63 @@ export async function showDiscuss(ctx, pi, basePath) {
564
583
  invalidateAllCaches();
565
584
  }
566
585
  }
586
+ // ─── Queued Milestone Discussion ─────────────────────────────────────────────
587
+ /**
588
+ * Show a picker of queued (pending) milestones and dispatch a discuss flow for
589
+ * the chosen one. Discussing a queued milestone does NOT activate it — it only
590
+ * refines the CONTEXT.md artifact so it is better prepared when auto-mode
591
+ * eventually reaches it.
592
+ */
593
+ async function showDiscussQueuedMilestone(ctx, pi, basePath, pendingMilestones) {
594
+ const actions = pendingMilestones.map((m, i) => {
595
+ const hasContext = !!resolveMilestoneFile(basePath, m.id, "CONTEXT");
596
+ const hasDraft = !hasContext && !!resolveMilestoneFile(basePath, m.id, "CONTEXT-DRAFT");
597
+ const contextStatus = hasContext ? "context ✓" : hasDraft ? "draft context" : "no context yet";
598
+ return {
599
+ id: m.id,
600
+ label: `${m.id}: ${m.title}`,
601
+ description: `[queued] · ${contextStatus}`,
602
+ recommended: i === 0,
603
+ };
604
+ });
605
+ const choice = await showNextAction(ctx, {
606
+ title: "GSD — Discuss a queued milestone",
607
+ summary: [
608
+ "Select a queued milestone to discuss.",
609
+ "Discussing will update its context file. It will not be activated.",
610
+ ],
611
+ actions,
612
+ notYetMessage: "Run /gsd discuss when ready.",
613
+ });
614
+ if (choice === "not_yet")
615
+ return;
616
+ const chosen = pendingMilestones.find(m => m.id === choice);
617
+ if (!chosen)
618
+ return;
619
+ await dispatchDiscussForMilestone(ctx, pi, basePath, chosen.id, chosen.title);
620
+ }
621
+ /**
622
+ * Dispatch the guided-discuss-milestone prompt for a milestone without
623
+ * setting pendingAutoStart — so discussing a queued milestone does not
624
+ * implicitly activate it when the session ends.
625
+ */
626
+ async function dispatchDiscussForMilestone(ctx, pi, basePath, mid, milestoneTitle) {
627
+ const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
628
+ const draftContent = draftFile ? await loadFile(draftFile) : null;
629
+ const discussMilestoneTemplates = inlineTemplate("context", "Context");
630
+ const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
631
+ const basePrompt = loadPrompt("guided-discuss-milestone", {
632
+ milestoneId: mid,
633
+ milestoneTitle,
634
+ inlinedTemplates: discussMilestoneTemplates,
635
+ structuredQuestionsAvailable,
636
+ commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
637
+ });
638
+ const prompt = draftContent
639
+ ? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
640
+ : basePrompt;
641
+ await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-milestone");
642
+ }
567
643
  // ─── Smart Entry Point ────────────────────────────────────────────────────────
568
644
  /**
569
645
  * The one wizard. Reads state, shows contextual options, dispatches into the workflow doc.
@@ -32,6 +32,6 @@ Then:
32
32
  11. Do not run git commands — the system commits your changes and handles any merge after this unit succeeds.
33
33
  12. Update `.gsd/PROJECT.md` if it exists — refresh current state if needed.
34
34
 
35
- **You MUST do ALL THREE before finishing: (1) write `{{sliceSummaryPath}}`, (2) write `{{sliceUatPath}}`, (3) call `gsd_complete_slice`. The unit will not be marked complete if any of these are missing.**
35
+ **You MUST call `gsd_complete_slice` with the slice summary and UAT content before finishing. The tool persists to both DB and disk and renders `{{sliceSummaryPath}}` and `{{sliceUatPath}}` automatically.**
36
36
 
37
37
  When done, say: "Slice {{sliceId}} complete."
@@ -10,10 +10,10 @@ Plan milestone {{milestoneId}} ("{{milestoneTitle}}"). Read `.gsd/DECISIONS.md`
10
10
  ## Planning Doctrine
11
11
 
12
12
  - **Risk-first means proof-first.** The earliest slices should prove the hardest thing works by shipping the real feature through the uncertain path. If auth is the risk, the first slice ships a real login page with real session handling that a user can actually use — not a CLI command that returns "authenticated: true". Proof is the shipped feature working. There is no separate "proof" artifact. Do not plan spikes, proof-of-concept slices, or validation-only slices — the proof is the real feature, built through the risky path.
13
- - **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means you could show a stakeholder and they'd see real product progress not a developer showing a terminal command. If the only way to demonstrate the slice is through a test runner or a curl command, the slice is missing its UI/UX surface. Add it. A slice that only proves something but doesn't ship real working code is not a slice — restructure it.
13
+ - **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means the intended user can exercise the capability through its real interfacefor a web app that's the UI, for a CLI tool that's the terminal, for an API that's a consuming client or curl. The test is: can someone *use* it, not just *assert* it passes. A slice that only proves something but doesn't ship real working code is not a slice — restructure it.
14
14
  - **Brownfield bias.** When planning against an existing codebase, ground slices in existing modules, conventions, and seams. Prefer extending real patterns over inventing new ones.
15
15
  - **Each slice should establish something downstream slices can depend on.** Think about what stable surface this slice creates for later work — an API, a data shape, a proven integration path.
16
- - **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it.
16
+ - **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it. Exception: if the infrastructure *is* the product surface (a new protocol, extension API, or provider interface), the slice is vertical by definition — the downstream consumer is the demo.
17
17
  - **Verification-first.** When planning slices, know what "done" looks like before detailing implementation. Each slice's demo line should describe concrete, verifiable evidence — not vague "it works" claims.
18
18
  - **Plan for integrated reality, not just local proof.** Distinguish contract proof from live integration proof. If the milestone involves multiple runtime boundaries, one slice must explicitly prove the assembled system through the real entrypoint or runtime path.
19
19
  - **Truthful demo lines only.** If a slice is proven by fixtures or tests only, say so. Do not phrase harness-level proof as if the user can already perform the live end-to-end behavior unless that has actually been exercised.
@@ -64,10 +64,10 @@ Then:
64
64
  Apply these when decomposing and ordering slices:
65
65
 
66
66
  - **Risk-first means proof-first.** The earliest slices should prove the hardest thing works by shipping the real feature through the uncertain path. If auth is the risk, the first slice ships a real login page with real session handling that a user can actually use — not a CLI command that returns "authenticated: true". Proof is the shipped feature working. There is no separate "proof" artifact. Do not plan spikes, proof-of-concept slices, or validation-only slices — the proof is the real feature, built through the risky path.
67
- - **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means you could show a stakeholder and they'd see real product progress not a developer showing a terminal command. If the only way to demonstrate the slice is through a test runner or a curl command, the slice is missing its UI/UX surface. Add it. A slice that only proves something but doesn't ship real working code is not a slice — restructure it.
67
+ - **Every slice is vertical, demoable, and shippable.** Every slice ships real, user-facing functionality. "Demoable" means the intended user can exercise the capability through its real interfacefor a web app that's the UI, for a CLI tool that's the terminal, for an API that's a consuming client or curl. The test is: can someone *use* it, not just *assert* it passes. A slice that only proves something but doesn't ship real working code is not a slice — restructure it.
68
68
  - **Brownfield bias.** When planning against an existing codebase, ground slices in existing modules, conventions, and seams. Prefer extending real patterns over inventing new ones.
69
69
  - **Each slice should establish something downstream slices can depend on.** Think about what stable surface this slice creates for later work — an API, a data shape, a proven integration path.
70
- - **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it.
70
+ - **Avoid foundation-only slices.** If a slice doesn't produce something demoable end-to-end, it's probably a layer, not a vertical slice. Restructure it. Exception: if the infrastructure *is* the product surface (a new protocol, extension API, or provider interface), the slice is vertical by definition — the downstream consumer is the demo.
71
71
  - **Verification-first.** When planning slices, know what "done" looks like before detailing implementation. Each slice's demo line should describe concrete, verifiable evidence — not vague "it works" claims.
72
72
  - **Plan for integrated reality, not just local proof.** Distinguish contract proof from live integration proof. If the milestone involves multiple runtime boundaries, one slice must explicitly prove the assembled system through the real entrypoint or runtime path.
73
73
  - **Truthful demo lines only.** If a slice is proven by fixtures or tests only, say so. Do not phrase harness-level proof as if the user can already perform the live end-to-end behavior unless that has actually been exercised.
@@ -77,6 +77,6 @@ Then:
77
77
 
78
78
  The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. All work stays in your working directory: `{{workingDirectory}}`.
79
79
 
80
- **You MUST write the file `{{outputPath}}` before finishing.**
80
+ **You MUST call `gsd_plan_slice` to persist the planning state before finishing.**
81
81
 
82
82
  When done, say: "Slice {{sliceId}} planned."
@@ -28,7 +28,7 @@ Then research the codebase and relevant technologies. Narrate key findings and s
28
28
  5. **Web search budget:** You have a limited budget of web searches (max ~15 per session). Use them strategically — prefer `resolve_library` / `get_library_docs` for library documentation. Do NOT repeat the same or similar queries. If a search didn't find what you need, rephrase once or move on. Target 3-5 total web searches for a typical research unit.
29
29
  6. Use the **Research** output template from the inlined context above — include only sections that have real content
30
30
  7. If `.gsd/REQUIREMENTS.md` exists, research against it. Identify which Active requirements are table stakes, likely omissions, overbuilt risks, or domain-standard behaviors the user may or may not want.
31
- 8. Write `{{outputPath}}`
31
+ 8. Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `artifact_type: "RESEARCH"`, and the full research markdown as `content` — the tool computes the file path and persists to both DB and disk.
32
32
 
33
33
  ## Strategic Questions to Answer
34
34
 
@@ -42,6 +42,6 @@ Then research the codebase and relevant technologies. Narrate key findings and s
42
42
 
43
43
  **Research is advisory, not auto-binding.** Surface candidate requirements clearly instead of silently expanding scope.
44
44
 
45
- **You MUST write the file `{{outputPath}}` before finishing.**
45
+ **You MUST call `gsd_summary_save` with the research content before finishing.**
46
46
 
47
47
  When done, say: "Milestone {{milestoneId}} researched."
@@ -55,7 +55,7 @@ After running all checks, compute the **overall verdict**:
55
55
  - `FAIL` — one or more checks failed
56
56
  - `PARTIAL` — some checks passed, but one or more checks were skipped, inconclusive, or still require human judgment
57
57
 
58
- Write `{{uatResultPath}}` with:
58
+ Call `gsd_summary_save` with `milestone_id: {{milestoneId}}`, `slice_id: {{sliceId}}`, `artifact_type: "ASSESSMENT"`, and the full UAT result markdown as `content` — the tool computes the file path and persists to both DB and disk. The content should follow this format:
59
59
 
60
60
  ```markdown
61
61
  ---
@@ -84,6 +84,6 @@ date: <ISO 8601 timestamp>
84
84
 
85
85
  ---
86
86
 
87
- **You MUST write `{{uatResultPath}}` before finishing.**
87
+ **You MUST call `gsd_summary_save` with the UAT result content before finishing.**
88
88
 
89
89
  When done, say: "UAT {{sliceId}} complete."
@@ -112,8 +112,11 @@ export function isInheritedRepo(basePath) {
112
112
  // (i.e. the parent project was initialised with GSD).
113
113
  if (isProjectGsd(join(root, ".gsd")))
114
114
  return false;
115
- // Also walk up from basePath to the git root checking for .gsd
116
- let dir = normalizedBase;
115
+ // Walk up from basePath's parent to the git root checking for .gsd.
116
+ // Start at dirname(normalizedBase), NOT normalizedBase itself — finding
117
+ // .gsd at basePath means GSD state is set up for THIS project, which
118
+ // says nothing about whether the git repo is inherited from an ancestor.
119
+ let dir = dirname(normalizedBase);
117
120
  while (dir !== normalizedRoot && dir !== dirname(dir)) {
118
121
  if (isProjectGsd(join(dir, ".gsd")))
119
122
  return false;
@@ -126,7 +126,16 @@ export function extractTrace(entries) {
126
126
  }
127
127
  }
128
128
  if (isError && resultText) {
129
- errors.push(resultText.slice(0, 300));
129
+ // Filter out benign "errors" that are normal during code exploration:
130
+ // - grep/rg/find returning exit code 1 (no matches) is expected POSIX behavior
131
+ // - User interrupts (Escape/skip) are intentional, not failures
132
+ const trimmed = resultText.trim();
133
+ const isBenignNoMatch = pending?.name === "bash" &&
134
+ /^\(no output\)\s*\n\s*Command exited with code 1$/m.test(trimmed);
135
+ const isUserSkip = /^Skipped due to queued user message/i.test(trimmed);
136
+ if (!isBenignNoMatch && !isUserSkip) {
137
+ errors.push(resultText.slice(0, 300));
138
+ }
130
139
  }
131
140
  }
132
141
  }
@@ -9,7 +9,7 @@ import { nativeBatchParseGsdFiles } from './native-parser-bridge.js';
9
9
  import { join, resolve } from 'path';
10
10
  import { existsSync, readdirSync } from 'node:fs';
11
11
  import { debugCount, debugTime } from './debug-logger.js';
12
- import { isDbAvailable, getAllMilestones, getMilestoneSlices, getSliceTasks, getReplanHistory, getSlice, insertMilestone, } from './gsd-db.js';
12
+ import { isDbAvailable, getAllMilestones, getMilestoneSlices, getSliceTasks, getReplanHistory, getSlice, insertMilestone, updateTaskStatus, } from './gsd-db.js';
13
13
  /**
14
14
  * A "ghost" milestone directory contains only META.json (and no substantive
15
15
  * files like CONTEXT, CONTEXT-DRAFT, ROADMAP, or SUMMARY). These appear when
@@ -524,7 +524,34 @@ export async function deriveStateFromDb(basePath) {
524
524
  };
525
525
  }
526
526
  // ── Get tasks from DB ────────────────────────────────────────────────
527
- const tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
527
+ let tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
528
+ // ── Reconcile stale task status (#2514) ──────────────────────────────
529
+ // When a session disconnects after the agent writes SUMMARY + VERIFY
530
+ // artifacts but before postUnitPostVerification updates the DB, tasks
531
+ // remain "pending" in the DB despite being complete on disk. Without
532
+ // reconciliation, deriveState keeps returning the stale task as active,
533
+ // causing the dispatcher to re-dispatch the same completed task forever.
534
+ let reconciled = false;
535
+ for (const t of tasks) {
536
+ if (isStatusDone(t.status))
537
+ continue;
538
+ const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
539
+ if (summaryPath && existsSync(summaryPath)) {
540
+ try {
541
+ updateTaskStatus(activeMilestone.id, activeSlice.id, t.id, "complete");
542
+ process.stderr.write(`gsd-reconcile: task ${activeMilestone.id}/${activeSlice.id}/${t.id} had SUMMARY on disk but DB status was "${t.status}" — updated to "complete" (#2514)\n`);
543
+ reconciled = true;
544
+ }
545
+ catch (e) {
546
+ // DB write failed — continue with stale status rather than crash
547
+ process.stderr.write(`gsd-reconcile: failed to update task ${t.id}: ${e.message}\n`);
548
+ }
549
+ }
550
+ }
551
+ // Re-fetch tasks if any were reconciled so downstream logic sees fresh status
552
+ if (reconciled) {
553
+ tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
554
+ }
528
555
  const taskProgress = {
529
556
  done: tasks.filter(t => isStatusDone(t.status)).length,
530
557
  total: tasks.length,