gsd-pi 2.80.0-dev.cf9433f56 → 2.80.0-dev.d4fc28e6b

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 (237) hide show
  1. package/dist/cli.js +0 -19
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
  4. package/dist/resources/extensions/gsd/auto/loop.js +71 -8
  5. package/dist/resources/extensions/gsd/auto/phases.js +150 -94
  6. package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
  7. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
  8. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  9. package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
  10. package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
  11. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
  14. package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
  15. package/dist/resources/extensions/gsd/auto-start.js +197 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
  17. package/dist/resources/extensions/gsd/auto.js +18 -22
  18. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
  20. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
  21. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
  22. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
  23. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
  24. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
  26. package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
  27. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  28. package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
  29. package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
  30. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
  31. package/dist/resources/extensions/gsd/guided-flow.js +47 -28
  32. package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
  33. package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
  35. package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
  36. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  39. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  41. package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
  42. package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
  43. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  44. package/dist/web/standalone/.next/BUILD_ID +1 -1
  45. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  46. package/dist/web/standalone/.next/build-manifest.json +2 -2
  47. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.html +1 -1
  65. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  72. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  74. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  75. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  76. package/dist/welcome-screen.d.ts +2 -0
  77. package/dist/welcome-screen.js +9 -7
  78. package/package.json +1 -1
  79. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  80. package/packages/pi-agent-core/dist/agent-loop.js +4 -1
  81. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  82. package/packages/pi-agent-core/dist/agent.d.ts +5 -0
  83. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  84. package/packages/pi-agent-core/dist/agent.js +2 -0
  85. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  86. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  87. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  88. package/packages/pi-agent-core/dist/index.js +2 -0
  89. package/packages/pi-agent-core/dist/index.js.map +1 -1
  90. package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
  91. package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
  92. package/packages/pi-agent-core/dist/token-audit.js +221 -0
  93. package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
  94. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  95. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  96. package/packages/pi-agent-core/dist/types.js.map +1 -1
  97. package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
  98. package/packages/pi-agent-core/src/agent-loop.ts +4 -1
  99. package/packages/pi-agent-core/src/agent.ts +8 -0
  100. package/packages/pi-agent-core/src/index.ts +2 -0
  101. package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
  102. package/packages/pi-agent-core/src/token-audit.ts +287 -0
  103. package/packages/pi-agent-core/src/types.ts +14 -0
  104. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
  106. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  108. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
  110. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
  113. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
  117. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
  119. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
  121. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
  124. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
  127. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
  128. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
  129. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
  130. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
  132. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
  134. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
  136. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
  138. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  139. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
  140. package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
  141. package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
  142. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
  143. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
  144. package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
  145. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
  146. package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
  147. package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
  148. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
  149. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
  150. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  151. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
  152. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
  153. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  154. package/src/resources/extensions/gsd/auto/loop.ts +84 -8
  155. package/src/resources/extensions/gsd/auto/phases.ts +218 -154
  156. package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
  157. package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
  158. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  159. package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
  160. package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
  161. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
  163. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
  164. package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
  165. package/src/resources/extensions/gsd/auto-start.ts +230 -9
  166. package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
  167. package/src/resources/extensions/gsd/auto.ts +18 -18
  168. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
  169. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
  170. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
  171. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
  172. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
  173. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
  174. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
  175. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
  176. package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
  177. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  178. package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
  179. package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
  180. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
  181. package/src/resources/extensions/gsd/guided-flow.ts +52 -35
  182. package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
  183. package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
  184. package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
  185. package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
  186. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
  187. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  188. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  189. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  190. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  191. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
  192. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
  193. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
  194. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  195. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
  196. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
  197. package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
  198. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  199. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
  200. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
  201. package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
  202. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
  203. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
  204. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
  205. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
  206. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
  207. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
  208. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
  209. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
  210. package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
  211. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
  212. package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
  213. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
  214. package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
  215. package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
  216. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
  217. package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
  218. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
  219. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
  220. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  221. package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
  222. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
  223. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
  224. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
  225. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
  226. package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
  227. package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
  228. package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
  229. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
  230. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
  231. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
  232. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
  233. package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
  234. package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
  235. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
  236. /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
  237. /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: System prompt and hidden context bootstrap for GSD sessions.
1
3
  import { existsSync, readFileSync, unlinkSync } from "node:fs";
2
4
  import { join } from "node:path";
3
5
  import { logWarning } from "../workflow-logger.js";
@@ -17,6 +19,11 @@ import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile,
17
19
  import { toPosixPath } from "../../shared/mod.js";
18
20
  import { autoEnableCmuxPreferences } from "../commands-cmux.js";
19
21
  import { gsdHome } from "../gsd-home.js";
22
+ const DEFAULT_CONTEXT_MESSAGE_MAX_CHARS = 4_000;
23
+ const DEFAULT_KNOWLEDGE_MAX_CHARS = 12_000;
24
+ const DEFAULT_CODEBASE_MAX_CHARS = 8_000;
25
+ const MIN_CONTEXT_MESSAGE_MAX_CHARS = 1_000;
26
+ const MIN_KNOWLEDGE_MAX_CHARS = 1_000;
20
27
  /**
21
28
  * Bundled skill triggers — resolved dynamically at runtime instead of
22
29
  * hardcoding absolute paths in the system prompt template. Only skills
@@ -135,7 +142,6 @@ export async function buildBeforeAgentStartResult(event, ctx) {
135
142
  catch (e) {
136
143
  logWarning("bootstrap", `decisions backfill failed: ${e.message}`);
137
144
  }
138
- const memoryBlock = await loadMemoryBlock(event.prompt ?? "");
139
145
  let newSkillsBlock = "";
140
146
  if (hasSkillSnapshot()) {
141
147
  const newSkills = detectNewSkills();
@@ -165,11 +171,10 @@ export async function buildBeforeAgentStartResult(event, ctx) {
165
171
  if (rawContent) {
166
172
  // Cap injection size to ~2 000 tokens to avoid bloating every request.
167
173
  // Full map is always available at .gsd/CODEBASE.md.
168
- const MAX_CODEBASE_CHARS = 8_000;
169
174
  const generatedMatch = rawContent.match(/Generated: (\S+)/);
170
175
  const generatedAt = generatedMatch?.[1] ?? "unknown";
171
- const content = rawContent.length > MAX_CODEBASE_CHARS
172
- ? rawContent.slice(0, MAX_CODEBASE_CHARS) + "\n\n*(truncated — see .gsd/CODEBASE.md for full map)*"
176
+ const content = rawContent.length > DEFAULT_CODEBASE_MAX_CHARS
177
+ ? rawContent.slice(0, DEFAULT_CODEBASE_MAX_CHARS) + "\n\n*(truncated — see .gsd/CODEBASE.md for full map)*"
173
178
  : rawContent;
174
179
  codebaseBlock = `\n\n[PROJECT CODEBASE — File structure and descriptions (generated ${generatedAt}, auto-refreshed when GSD detects tracked file changes; use /gsd codebase stats for status)]\n\n${content}`;
175
180
  }
@@ -180,6 +185,9 @@ export async function buildBeforeAgentStartResult(event, ctx) {
180
185
  }
181
186
  warnDeprecatedAgentInstructions();
182
187
  const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
188
+ const memoryBlock = await loadMemoryBlock(event.prompt ?? "", {
189
+ includePromptRelevant: !(injection && isLowEntropyResumePrompt(event.prompt ?? "")),
190
+ });
183
191
  // Re-inject forensics context on follow-up turns (#2941)
184
192
  const forensicsInjection = !injection ? buildForensicsContextInjection(process.cwd(), event.prompt) : null;
185
193
  const worktreeBlock = buildWorktreeContextBlock();
@@ -188,12 +196,9 @@ export async function buildBeforeAgentStartResult(event, ctx) {
188
196
  ? `\n\n## Subagent Model\n\nWhen spawning subagents via the \`subagent\` tool, always pass \`model: "${subagentModelConfig.primary}"\` in the tool call parameters. Never omit this — always specify it explicitly.`
189
197
  : "";
190
198
  // memoryBlock is FTS-queried against the user prompt and changes per call.
191
- // Removing it from `fullSystem` keeps the system-prompt cache breakpoint
192
- // stable across calls the only scoped goal of this fix. The pi-ai
193
- // Anthropic adapter additionally cache-marks the last user turn, so the
194
- // memoryBlock injected via the context message may itself be cached up to
195
- // that boundary; that's orthogonal and unchanged from prior behavior. The
196
- // load-bearing win here is preserving the system+tools cache hit. (#5019)
199
+ // Keeping it out of `fullSystem` preserves provider prompt-cache stability
200
+ // for the static system/tool prefix. The dynamic memory block rides the
201
+ // volatile context message instead. (#5019)
197
202
  const fullSystem = `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${knowledgeBlock}${codebaseBlock}${newSkillsBlock}${worktreeBlock}${subagentModelBlock}`;
198
203
  stopContextTimer({
199
204
  systemPromptSize: fullSystem.length,
@@ -217,20 +222,49 @@ export async function buildBeforeAgentStartResult(event, ctx) {
217
222
  * filesystem and DB dependencies to exercise this routing logic in-place.
218
223
  */
219
224
  export function buildContextMessage(opts) {
220
- const memoryContent = opts.memoryBlock.trim();
225
+ const contextCharLimit = getContextMessageCharLimit();
226
+ const memoryContent = markMemoryContextSupplied(opts.memoryBlock.trim());
221
227
  if (opts.injection) {
222
- const content = memoryContent ? `${memoryContent}\n\n${opts.injection}` : opts.injection;
228
+ const content = limitContextMessageContent(memoryContent ? `${memoryContent}\n\n${opts.injection}` : opts.injection, contextCharLimit);
223
229
  return { customType: "gsd-guided-context", content, display: false };
224
230
  }
225
231
  if (opts.forensicsInjection) {
226
- const content = memoryContent ? `${memoryContent}\n\n${opts.forensicsInjection}` : opts.forensicsInjection;
232
+ const content = limitContextMessageContent(memoryContent ? `${memoryContent}\n\n${opts.forensicsInjection}` : opts.forensicsInjection, contextCharLimit);
227
233
  return { customType: "gsd-forensics", content, display: false };
228
234
  }
229
235
  if (memoryContent) {
230
- return { customType: "gsd-memory", content: memoryContent, display: false };
236
+ return {
237
+ customType: "gsd-memory",
238
+ content: limitContextMessageContent(memoryContent, contextCharLimit),
239
+ display: false,
240
+ };
231
241
  }
232
242
  return null;
233
243
  }
244
+ function getContextMessageCharLimit() {
245
+ const raw = process.env.PI_GSD_CONTEXT_MAX_CHARS;
246
+ if (!raw)
247
+ return DEFAULT_CONTEXT_MESSAGE_MAX_CHARS;
248
+ if (raw === "0")
249
+ return null;
250
+ const parsed = Number(raw);
251
+ if (!Number.isFinite(parsed) || parsed < MIN_CONTEXT_MESSAGE_MAX_CHARS) {
252
+ return DEFAULT_CONTEXT_MESSAGE_MAX_CHARS;
253
+ }
254
+ return Math.floor(parsed);
255
+ }
256
+ function limitContextMessageContent(content, limit) {
257
+ if (!limit || content.length <= limit)
258
+ return content;
259
+ const suffix = "\n\n[GSD Context Truncated]\nFull context is available from the referenced .gsd files and tools; read on demand only if this excerpt lacks required evidence.";
260
+ const headBudget = Math.max(0, limit - suffix.length);
261
+ return `${content.slice(0, headBudget).trimEnd()}${suffix}`;
262
+ }
263
+ function markMemoryContextSupplied(memoryContent) {
264
+ if (!memoryContent)
265
+ return "";
266
+ return `[GSD Context Metadata]\n- Memory supplied: yes\n\n${memoryContent}`;
267
+ }
234
268
  /**
235
269
  * ADR-013 step 4 — auto-injection parity for the memories table.
236
270
  *
@@ -249,7 +283,7 @@ export function buildContextMessage(opts) {
249
283
  * with a token-budget cap. Failures degrade gracefully — the function never
250
284
  * throws and returns "" so the system prompt construction continues.
251
285
  */
252
- export async function loadMemoryBlock(userPrompt) {
286
+ export async function loadMemoryBlock(userPrompt, opts = {}) {
253
287
  try {
254
288
  const { formatMemoriesForPrompt, getActiveMemoriesRanked, queryMemoriesRanked } = await import("../memory-store.js");
255
289
  // Categories that belong in every turn. Pre-ADR-013 this was just
@@ -268,7 +302,7 @@ export async function loadMemoryBlock(userPrompt) {
268
302
  const criticalIds = new Set(critical.map((m) => m.id));
269
303
  let relevant = [];
270
304
  const trimmed = userPrompt.trim();
271
- if (trimmed) {
305
+ if (trimmed && opts.includePromptRelevant !== false) {
272
306
  const hits = queryMemoriesRanked({ query: trimmed, k: QUERY_K });
273
307
  relevant = hits.map((h) => h.memory).filter((m) => !criticalIds.has(m.id));
274
308
  }
@@ -319,15 +353,37 @@ export function loadKnowledgeBlock(gsdHomeDir, cwd) {
319
353
  return { block: "", globalSizeKb: 0 };
320
354
  }
321
355
  const parts = [];
322
- if (globalKnowledge)
323
- parts.push(`## Global Knowledge\n\n${globalKnowledge}`);
324
- if (projectKnowledge)
325
- parts.push(`## Project Knowledge\n\n${projectKnowledge}`);
356
+ if (globalKnowledge) {
357
+ parts.push(`## Global Knowledge\nSource: \`${globalKnowledgePath}\`\n\n${globalKnowledge}`);
358
+ }
359
+ if (projectKnowledge) {
360
+ parts.push(`## Project Knowledge\nSource: \`${knowledgePath}\`\n\n${projectKnowledge}`);
361
+ }
362
+ const body = limitKnowledgeBlock(parts.join("\n\n"), getKnowledgeCharLimit());
326
363
  return {
327
- block: `\n\n[KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${parts.join("\n\n")}`,
364
+ block: `\n\n[KNOWLEDGE — Rules, patterns, and lessons learned]\n\n${body}`,
328
365
  globalSizeKb,
329
366
  };
330
367
  }
368
+ function getKnowledgeCharLimit() {
369
+ const raw = process.env.PI_GSD_KNOWLEDGE_MAX_CHARS;
370
+ if (!raw)
371
+ return DEFAULT_KNOWLEDGE_MAX_CHARS;
372
+ if (raw === "0")
373
+ return null;
374
+ const parsed = Number(raw);
375
+ if (!Number.isFinite(parsed) || parsed < MIN_KNOWLEDGE_MAX_CHARS) {
376
+ return DEFAULT_KNOWLEDGE_MAX_CHARS;
377
+ }
378
+ return Math.floor(parsed);
379
+ }
380
+ function limitKnowledgeBlock(content, limit) {
381
+ if (!limit || content.length <= limit)
382
+ return content;
383
+ const suffix = "\n\n[Knowledge Truncated]\nFull KNOWLEDGE.md content remains available at the source path(s) above; read on demand only if this excerpt lacks a required rule.";
384
+ const headBudget = Math.max(0, limit - suffix.length);
385
+ return `${content.slice(0, headBudget).trimEnd()}${suffix}`;
386
+ }
331
387
  function buildWorktreeContextBlock() {
332
388
  const worktreeName = getActiveWorktreeName();
333
389
  const worktreeMainCwd = getWorktreeOriginalCwd();
@@ -376,6 +432,10 @@ function buildWorktreeContextBlock() {
376
432
  * Tested against the trimmed, lowercased prompt with trailing punctuation stripped.
377
433
  */
378
434
  const RESUME_INTENT_PATTERNS = /^(continue|resume|ok|go|go ahead|proceed|keep going|carry on|next|yes|yeah|yep|sure|do it|let's go|pick up where you left off)$/;
435
+ export function isLowEntropyResumePrompt(prompt) {
436
+ const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
437
+ return RESUME_INTENT_PATTERNS.test(trimmed);
438
+ }
379
439
  async function buildGuidedExecuteContextInjection(prompt, basePath) {
380
440
  const ensureStateDbOpen = async () => {
381
441
  const { ensureDbOpen } = await import("./dynamic-tools.js");
@@ -402,8 +462,7 @@ async function buildGuidedExecuteContextInjection(prompt, basePath) {
402
462
  // control/help/diagnostic prompts with unrelated execution context.
403
463
  // Phase-gated: only fire during "executing" to avoid misrouting during
404
464
  // replanning, gate evaluation, or other non-execution phases.
405
- const trimmed = prompt.trim().toLowerCase().replace(/[.!?,]+$/g, "");
406
- if (RESUME_INTENT_PATTERNS.test(trimmed)) {
465
+ if (isLowEntropyResumePrompt(prompt)) {
407
466
  await ensureStateDbOpen();
408
467
  const state = await deriveState(basePath);
409
468
  if (state.phase === "executing" && state.activeTask && state.activeMilestone && state.activeSlice) {
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * #2909: Adds a fast-path git status check before milestone completion merges.
5
5
  * When the working tree is dirty the user is warned and changes are auto-stashed
6
- * so the merge can proceed cleanly. After the merge completes, postflightPopStash
7
- * restores the stashed changes.
6
+ * so the merge can proceed cleanly. After the merge completes, postflightPopStash
7
+ * restores the stashed changes and reports whether manual recovery is needed.
8
8
  *
9
9
  * Design constraints (from Trek-e approval):
10
10
  * - Warn the user before stashing (no silent surprises)
11
11
  * - git stash push / git stash pop only — no custom stash management layer
12
- * - Stash/pop errors are logged but MUST NOT block the merge
12
+ * - Stash/pop errors are logged but MUST NOT block the merge itself
13
13
  * - Fast-path status check — clean trees pay no extra cost
14
14
  */
15
15
  import { execFileSync } from "node:child_process";
@@ -98,7 +98,8 @@ export function preflightCleanRoot(basePath, milestoneId, notify) {
98
98
  *
99
99
  * Only called when preflightCleanRoot returned stashPushed=true.
100
100
  * Any pop error (e.g. conflict) is logged and notified but does NOT throw —
101
- * the merge already completed successfully.
101
+ * the merge already completed successfully. Callers must treat
102
+ * needsManualRecovery=true as a dirty workspace stop, not a clean completion.
102
103
  */
103
104
  export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
104
105
  let stashRef = null;
@@ -108,7 +109,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
108
109
  const msg = `No matching GSD preflight stash found for milestone ${milestoneId}; leaving stash list untouched.`;
109
110
  logWarning("preflight", msg);
110
111
  notify(msg, "warning");
111
- return;
112
+ return {
113
+ restored: false,
114
+ needsManualRecovery: true,
115
+ message: msg,
116
+ };
112
117
  }
113
118
  execFileSync("git", ["stash", "pop", stashRef], {
114
119
  cwd: basePath,
@@ -116,7 +121,14 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
116
121
  encoding: "utf-8",
117
122
  env: GIT_NO_PROMPT_ENV,
118
123
  });
119
- notify(`Restored stashed changes after milestone ${milestoneId} merge.`, "info");
124
+ const msg = `Restored stashed changes after milestone ${milestoneId} merge.`;
125
+ notify(msg, "info");
126
+ return {
127
+ restored: true,
128
+ needsManualRecovery: false,
129
+ message: msg,
130
+ stashRef,
131
+ };
120
132
  }
121
133
  catch (err) {
122
134
  // Pop conflicts mean the merged code collides with the stashed changes.
@@ -127,5 +139,11 @@ export function postflightPopStash(basePath, milestoneId, stashMarker, notify) {
127
139
  const msg = `git stash pop ${stashRef ?? ""}`.trim() + ` failed after merge of milestone ${milestoneId}: ${err instanceof Error ? err.message : String(err)}. ${restoreHint}`;
128
140
  logWarning("preflight", msg);
129
141
  notify(msg, "warning");
142
+ return {
143
+ restored: false,
144
+ needsManualRecovery: true,
145
+ message: msg,
146
+ ...(stashRef ? { stashRef } : {}),
147
+ };
130
148
  }
131
149
  }
@@ -17,6 +17,8 @@ import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
17
17
  import { getAutoWorktreePath } from "./auto-worktree.js";
18
18
  import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
19
19
  import { loadPrompt } from "./prompt-loader.js";
20
+ import { buildDoctorHealIssuePayload, buildDoctorHealSummary, buildWorkflowDispatchContent, } from "./workflow-protocol.js";
21
+ import { restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch, } from "./bootstrap/register-hooks.js";
20
22
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
21
23
  const UPDATE_FETCH_TIMEOUT_MS = 5000;
22
24
  // Detects a bun-installed gsd via `process.argv[1]`. Mirrors isBunInstall in
@@ -64,13 +66,19 @@ export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
64
66
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(gsdHome(), "agent", "GSD-WORKFLOW.md");
65
67
  const workflow = readFileSync(workflowPath, "utf-8");
66
68
  const prompt = loadPrompt("doctor-heal", {
67
- doctorSummary: reportText,
68
- structuredIssues,
69
+ doctorSummary: buildDoctorHealSummary(reportText),
70
+ structuredIssues: buildDoctorHealIssuePayload(structuredIssues),
69
71
  scopeLabel: scope ?? "active milestone / blocking scope",
70
72
  doctorCommandSuffix: scope ? ` ${scope}` : "",
71
73
  });
72
- const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
73
- pi.sendMessage({ customType: "gsd-doctor-heal", content, display: false }, { triggerTurn: true });
74
+ const content = buildWorkflowDispatchContent({ workflow, workflowPath, task: prompt });
75
+ const savedTools = scopeGsdWorkflowToolsForDispatch(pi);
76
+ try {
77
+ pi.sendMessage({ customType: "gsd-doctor-heal", content, display: false }, { triggerTurn: true });
78
+ }
79
+ finally {
80
+ restoreGsdWorkflowTools(pi, savedTools);
81
+ }
74
82
  }
75
83
  /** Parse doctor command args into structured flags and positionals (pure, no I/O). */
76
84
  export function parseDoctorArgs(args) {
@@ -213,11 +221,17 @@ export async function handleTriage(ctx, pi, basePath) {
213
221
  });
214
222
  const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(gsdHome(), "agent", "GSD-WORKFLOW.md");
215
223
  const workflow = readFileSync(workflowPath, "utf-8");
216
- pi.sendMessage({
217
- customType: "gsd-triage",
218
- content: `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`,
219
- display: false,
220
- }, { triggerTurn: true });
224
+ const savedTools = scopeGsdWorkflowToolsForDispatch(pi);
225
+ try {
226
+ pi.sendMessage({
227
+ customType: "gsd-triage",
228
+ content: buildWorkflowDispatchContent({ workflow, workflowPath, task: prompt }),
229
+ display: false,
230
+ }, { triggerTurn: true });
231
+ }
232
+ finally {
233
+ restoreGsdWorkflowTools(pi, savedTools);
234
+ }
221
235
  }
222
236
  export async function handleSteer(change, ctx, pi) {
223
237
  const basePath = currentDirectoryRoot();
@@ -26,6 +26,58 @@ function isAlreadyActiveConstraintError(err) {
26
26
  }
27
27
  return /\bUNIQUE\b|\bconstraint failed\b/i.test(msg);
28
28
  }
29
+ function settleStaleActiveDispatchForUnit(input, now) {
30
+ const db = _getAdapter();
31
+ const active = db.prepare(`SELECT id, status, worker_id, milestone_lease_token
32
+ FROM unit_dispatches
33
+ WHERE unit_id = :unit_id
34
+ AND status IN ('claimed','running')
35
+ ORDER BY id DESC
36
+ LIMIT 1`).get({ ":unit_id": input.unitId });
37
+ if (!active)
38
+ return;
39
+ if (active.worker_id === input.workerId &&
40
+ active.milestone_lease_token === input.milestoneLeaseToken) {
41
+ return;
42
+ }
43
+ const reason = "stale-dispatch-lease-takeover";
44
+ const result = db.prepare(`UPDATE unit_dispatches
45
+ SET status = 'canceled',
46
+ ended_at = :ended_at,
47
+ exit_reason = :reason
48
+ WHERE id = :id
49
+ AND status IN ('claimed','running')
50
+ AND (worker_id != :worker_id OR milestone_lease_token != :token)`).run({
51
+ ":id": active.id,
52
+ ":ended_at": now,
53
+ ":reason": reason,
54
+ ":worker_id": input.workerId,
55
+ ":token": input.milestoneLeaseToken,
56
+ });
57
+ const changes = typeof result.changes === "number"
58
+ ? result.changes
59
+ : 0;
60
+ if (changes < 1)
61
+ return;
62
+ insertAuditEvent({
63
+ eventId: randomUUID(),
64
+ traceId: input.traceId,
65
+ turnId: input.turnId ?? undefined,
66
+ category: "orchestration",
67
+ type: "dispatch-stale-canceled",
68
+ ts: now,
69
+ payload: {
70
+ dispatchId: active.id,
71
+ unitId: input.unitId,
72
+ priorStatus: active.status,
73
+ priorWorkerId: active.worker_id,
74
+ priorMilestoneLeaseToken: active.milestone_lease_token,
75
+ takeoverWorkerId: input.workerId,
76
+ takeoverMilestoneLeaseToken: input.milestoneLeaseToken,
77
+ reason,
78
+ },
79
+ });
80
+ }
29
81
  /**
30
82
  * Insert a new dispatch row in `claimed` state. Atomic guard against
31
83
  * double-claim (B2): the partial unique index
@@ -58,6 +110,7 @@ export function recordDispatchClaim(input) {
58
110
  milestoneLeaseToken: input.milestoneLeaseToken,
59
111
  };
60
112
  }
113
+ settleStaleActiveDispatchForUnit(input, now);
61
114
  try {
62
115
  const result = db.prepare(`INSERT INTO unit_dispatches (
63
116
  trace_id, turn_id, worker_id, milestone_lease_token,
@@ -127,6 +127,8 @@ export function createGSDExtensionAPI(pi, sharedHandlers) {
127
127
  getActiveTools: () => pi.getActiveTools(),
128
128
  getAllTools: () => pi.getAllTools(),
129
129
  setActiveTools: (...args) => pi.setActiveTools(...args),
130
+ getVisibleSkills: () => pi.getVisibleSkills(),
131
+ setVisibleSkills: (...args) => pi.setVisibleSkills(...args),
130
132
  getCommands: () => pi.getCommands(),
131
133
  // ── Model & thinking ───────────────────────────────────────────────
132
134
  setModel: (...args) => pi.setModel(...args),
@@ -51,6 +51,8 @@ export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesCon
51
51
  import { logWarning } from "./workflow-logger.js";
52
52
  import { deleteRuntimeKv } from "./db/runtime-kv.js";
53
53
  import { PAUSED_SESSION_KV_KEY } from "./interrupted-session.js";
54
+ import { buildWorkflowDispatchContent } from "./workflow-protocol.js";
55
+ import { isFullGsdToolSurfaceRequested, restoreGsdWorkflowTools, scopeGsdWorkflowToolsForDispatch } from "./bootstrap/register-hooks.js";
54
56
  function scheduleAutoStartAfterIdle(ctx, pi, basePath, verboseMode, options, launch = startAutoDetached) {
55
57
  const waitForIdle = typeof ctx.waitForIdle === "function"
56
58
  ? ctx.waitForIdle.bind(ctx)
@@ -845,38 +847,55 @@ async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType)
845
847
  return;
846
848
  }
847
849
  }
848
- // Scope tools for discuss flows (#2949).
850
+ // Scope tools for guided workflow turns (#2949, token-consumption savings).
849
851
  // Providers with grammar-based constrained decoding (xAI/Grok) return
850
852
  // "Grammar is too complex" when the combined tool schema is too large.
851
- // Discuss flows only need a small subset of GSD tools — strip the heavy
852
- // planning/execution/completion tools to keep the grammar within limits.
853
+ // Guided workflow turns only need the active unit's tool surface; strip
854
+ // unrelated GSD tools and broad non-GSD tools for this queued turn, then
855
+ // restore so the narrowed surface does not leak into future dispatches.
853
856
  let savedTools = null;
854
- if (unitType?.startsWith("discuss-")) {
857
+ try {
855
858
  const currentTools = pi.getActiveTools();
856
- savedTools = currentTools;
857
- // Keep all non-GSD tools (builtins, other extensions) and only the
858
- // GSD tools on the discuss allowlist.
859
- const scopedTools = currentTools.filter((t) => !t.startsWith("gsd_") || DISCUSS_TOOLS_ALLOWLIST.includes(t));
860
- pi.setActiveTools(scopedTools);
861
- debugLog("discuss-tool-scoping", {
862
- unitType,
863
- before: currentTools.length,
864
- after: scopedTools.length,
865
- removed: currentTools.length - scopedTools.length,
866
- });
867
- }
868
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(gsdHome(), "agent", "GSD-WORKFLOW.md");
869
- const workflow = readFileSync(workflowPath, "utf-8");
870
- pi.sendMessage({
871
- customType,
872
- content: `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${note}`,
873
- display: false,
874
- }, { triggerTurn: true });
875
- // Restore full tool set after the message is queued. The LLM turn has
876
- // already captured the scoped set — restoring prevents the narrowed
877
- // tools from leaking into subsequent dispatches (#3628).
878
- if (savedTools) {
879
- pi.setActiveTools(savedTools);
859
+ savedTools = {
860
+ tools: currentTools,
861
+ visibleSkills: typeof pi.getVisibleSkills === "function" ? pi.getVisibleSkills() : undefined,
862
+ restoreVisibleSkills: typeof pi.setVisibleSkills === "function",
863
+ };
864
+ if (unitType?.startsWith("discuss-") && !isFullGsdToolSurfaceRequested()) {
865
+ // Keep all non-GSD tools (builtins, other extensions) and only the
866
+ // GSD tools on the discuss allowlist.
867
+ const scopedTools = currentTools.filter((t) => !t.startsWith("gsd_") || DISCUSS_TOOLS_ALLOWLIST.includes(t));
868
+ pi.setActiveTools(scopedTools);
869
+ const scopedState = scopeGsdWorkflowToolsForDispatch(pi, unitType);
870
+ savedTools = {
871
+ tools: currentTools,
872
+ visibleSkills: scopedState?.visibleSkills ?? savedTools.visibleSkills,
873
+ restoreVisibleSkills: scopedState?.restoreVisibleSkills ?? savedTools.restoreVisibleSkills,
874
+ };
875
+ debugLog("discuss-tool-scoping", {
876
+ unitType,
877
+ before: currentTools.length,
878
+ after: pi.getActiveTools().length,
879
+ removed: currentTools.length - pi.getActiveTools().length,
880
+ });
881
+ }
882
+ else {
883
+ savedTools = scopeGsdWorkflowToolsForDispatch(pi, unitType) ?? savedTools;
884
+ }
885
+ const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(gsdHome(), "agent", "GSD-WORKFLOW.md");
886
+ const workflow = readFileSync(workflowPath, "utf-8");
887
+ pi.sendMessage({
888
+ customType,
889
+ content: buildWorkflowDispatchContent({ workflow, workflowPath, task: note }),
890
+ display: false,
891
+ }, { triggerTurn: true });
892
+ }
893
+ finally {
894
+ // Restore full tool set after the message is queued. The LLM turn has
895
+ // already captured the scoped set — restoring prevents the narrowed
896
+ // tools from leaking into subsequent dispatches (#3628). The finally
897
+ // block ensures restoration even if sendMessage throws.
898
+ restoreGsdWorkflowTools(pi, savedTools);
880
899
  }
881
900
  }
882
901
  function getStructuredQuestionsAvailability(pi, ctx) {
@@ -5,7 +5,7 @@
5
5
  // Both READ and WRITE operations are native — push operations remain as
6
6
  // execSync calls because git2 credential handling is too complex.
7
7
  import { execFileSync } from "node:child_process";
8
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { GSDError, GSD_GIT_ERROR } from "./errors.js";
11
11
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
@@ -1007,6 +1007,24 @@ export function nativeRmForce(basePath, paths) {
1007
1007
  gitFileExec(basePath, ["rm", "--force", "--", path], true);
1008
1008
  }
1009
1009
  }
1010
+ function runGitWorktreeAdd(basePath, wtPath, branch, createBranch, startPoint) {
1011
+ if (createBranch) {
1012
+ const branchRef = gitExec(basePath, ["show-ref", "--verify", `refs/heads/${branch}`], true);
1013
+ if (branchRef) {
1014
+ gitExec(basePath, ["worktree", "add", wtPath, branch]);
1015
+ return;
1016
+ }
1017
+ gitExec(basePath, ["worktree", "add", "-b", branch, wtPath, startPoint ?? "HEAD"]);
1018
+ }
1019
+ else {
1020
+ gitExec(basePath, ["worktree", "add", wtPath, branch]);
1021
+ }
1022
+ }
1023
+ export function assertWorktreeMaterialized(wtPath) {
1024
+ if (existsSync(join(wtPath, ".git")))
1025
+ return;
1026
+ throw new GSDError(GSD_GIT_ERROR, `git worktree add did not materialize a valid worktree at ${wtPath}: missing .git file`);
1027
+ }
1010
1028
  /**
1011
1029
  * Add a new git worktree.
1012
1030
  * Native: libgit2 worktree API.
@@ -1016,14 +1034,20 @@ export function nativeWorktreeAdd(basePath, wtPath, branch, createBranch, startP
1016
1034
  const native = loadNative();
1017
1035
  if (native) {
1018
1036
  native.gitWorktreeAdd(basePath, wtPath, branch, createBranch, startPoint);
1019
- return;
1020
- }
1021
- if (createBranch) {
1022
- gitExec(basePath, ["worktree", "add", "-b", branch, wtPath, startPoint ?? "HEAD"]);
1023
- }
1024
- else {
1025
- gitExec(basePath, ["worktree", "add", wtPath, branch]);
1037
+ try {
1038
+ assertWorktreeMaterialized(wtPath);
1039
+ return;
1040
+ }
1041
+ catch {
1042
+ rmSync(wtPath, { recursive: true, force: true });
1043
+ gitExec(basePath, ["worktree", "prune"], true);
1044
+ runGitWorktreeAdd(basePath, wtPath, branch, createBranch, startPoint);
1045
+ assertWorktreeMaterialized(wtPath);
1046
+ return;
1047
+ }
1026
1048
  }
1049
+ runGitWorktreeAdd(basePath, wtPath, branch, createBranch, startPoint);
1050
+ assertWorktreeMaterialized(wtPath);
1027
1051
  }
1028
1052
  /**
1029
1053
  * Remove a git worktree.
@@ -0,0 +1,101 @@
1
+ // GSD-2 + src/resources/extensions/gsd/orphan-stash-audit.ts
2
+ // Startup sweep for orphaned gsd-preflight-stash entries left behind by
3
+ // interrupted milestone merges (#5538-followup).
4
+ import { execFileSync } from "node:child_process";
5
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
6
+ /**
7
+ * Recognize the "already restored" failure mode of `git stash apply`.
8
+ *
9
+ * When a preflight stash captured untracked files via `--include-untracked`
10
+ * and those files are now present in the working tree (e.g. a prior audit
11
+ * run already applied this stash), `git stash apply` aborts with
12
+ * `<path> already exists, no checkout` and exits non-zero. That is the
13
+ * idempotent steady state for this audit, not a recovery failure — treat
14
+ * it as a no-op so repeated GSD startups stop spamming the user with
15
+ * warnings about stashes that have already been restored (#5538-followup
16
+ * peer-review feedback).
17
+ */
18
+ function _isAlreadyRestoredApplyError(err) {
19
+ if (!err || typeof err !== "object")
20
+ return false;
21
+ const stderr = err.stderr;
22
+ const stderrText = typeof stderr === "string" ? stderr : stderr instanceof Uint8Array ? Buffer.from(stderr).toString("utf-8") : "";
23
+ if (stderrText && /already exists, no checkout/i.test(stderrText))
24
+ return true;
25
+ const message = err instanceof Error ? err.message : String(err);
26
+ return /already exists, no checkout/i.test(message);
27
+ }
28
+ export { _isAlreadyRestoredApplyError };
29
+ /**
30
+ * Audit `git stash list` for orphaned `gsd-preflight-stash:M00x:*` entries.
31
+ *
32
+ * The matching merge code in `phases.ts` previously skipped the postflight
33
+ * pop whenever `mergeAndExit` threw, leaking the user's pre-merge working
34
+ * tree into the stash list. For every preflight-stash entry whose milestone
35
+ * is now complete (per the supplied callback), `git stash apply` is invoked —
36
+ * NOT `pop`. The stash entry stays in the list so the user retains a backup
37
+ * if the apply produces unexpected merge results. Idempotent across repeated
38
+ * startup runs.
39
+ *
40
+ * Failures are best-effort: a list error (no repo, git unavailable) returns
41
+ * an empty result. An apply error becomes a warning the user sees alongside
42
+ * the existing orphan-branch audit messages — startup continues.
43
+ */
44
+ export function auditOrphanedPreflightStashes(basePath, isMilestoneComplete) {
45
+ const result = { applied: [], warnings: [] };
46
+ let listOutput;
47
+ try {
48
+ listOutput = execFileSync("git", ["stash", "list", "--format=%gd%x00%s"], {
49
+ cwd: basePath,
50
+ stdio: ["ignore", "pipe", "pipe"],
51
+ encoding: "utf-8",
52
+ env: GIT_NO_PROMPT_ENV,
53
+ });
54
+ }
55
+ catch {
56
+ return result;
57
+ }
58
+ const MARKER_RE = /\bgsd-preflight-stash:([A-Za-z0-9_-]+):/;
59
+ for (const line of listOutput.split("\n")) {
60
+ const sep = line.indexOf("\x00");
61
+ if (sep < 0)
62
+ continue;
63
+ const ref = line.slice(0, sep);
64
+ const subject = line.slice(sep + 1);
65
+ if (!ref || !subject)
66
+ continue;
67
+ const match = MARKER_RE.exec(subject);
68
+ if (!match)
69
+ continue;
70
+ const milestoneId = match[1];
71
+ let complete = false;
72
+ try {
73
+ complete = isMilestoneComplete(milestoneId);
74
+ }
75
+ catch (err) {
76
+ result.warnings.push(`Could not determine completion status for ${milestoneId} during preflight-stash audit: ${err instanceof Error ? err.message : String(err)}.`);
77
+ continue;
78
+ }
79
+ if (!complete)
80
+ continue;
81
+ try {
82
+ execFileSync("git", ["stash", "apply", "--quiet", ref], {
83
+ cwd: basePath,
84
+ stdio: ["ignore", "pipe", "pipe"],
85
+ encoding: "utf-8",
86
+ env: GIT_NO_PROMPT_ENV,
87
+ });
88
+ result.applied.push({ milestoneId, stashRef: ref });
89
+ }
90
+ catch (err) {
91
+ // Idempotent steady state: stash was already applied in a prior audit
92
+ // run; the files exist and `git stash apply` refuses to overwrite.
93
+ // Skip silently so repeat runs are no-ops.
94
+ if (_isAlreadyRestoredApplyError(err))
95
+ continue;
96
+ result.warnings.push(`Could not apply orphaned preflight stash ${ref} (milestone ${milestoneId}): ${err instanceof Error ? err.message : String(err)}. ` +
97
+ `Run \`git stash apply ${ref}\` manually to restore your pre-merge changes.`);
98
+ }
99
+ }
100
+ return result;
101
+ }