gsd-pi 2.72.0-dev.de4c4b3 → 2.73.0-dev.1cfd50c

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 (259) hide show
  1. package/README.md +12 -2
  2. package/dist/cli.js +59 -3
  3. package/dist/onboarding.js +10 -0
  4. package/dist/resources/extensions/async-jobs/await-tool.js +7 -4
  5. package/dist/resources/extensions/async-jobs/job-manager.js +28 -3
  6. package/dist/resources/extensions/claude-code-cli/partial-builder.js +40 -12
  7. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +48 -23
  8. package/dist/resources/extensions/gsd/auto/loop.js +84 -1
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +6 -0
  10. package/dist/resources/extensions/gsd/auto-recovery.js +11 -0
  11. package/dist/resources/extensions/gsd/auto.js +25 -19
  12. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +9 -11
  13. package/dist/resources/extensions/gsd/commands-handlers.js +4 -1
  14. package/dist/resources/extensions/gsd/context-injector.js +1 -1
  15. package/dist/resources/extensions/gsd/custom-workflow-engine.js +3 -7
  16. package/dist/resources/extensions/gsd/definition-io.js +15 -0
  17. package/dist/resources/extensions/gsd/dispatch-guard.js +4 -0
  18. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +6 -3
  19. package/dist/resources/extensions/gsd/git-service.js +11 -8
  20. package/dist/resources/extensions/gsd/gitignore.js +12 -6
  21. package/dist/resources/extensions/gsd/gsd-db.js +49 -6
  22. package/dist/resources/extensions/gsd/key-manager.js +2 -0
  23. package/dist/resources/extensions/gsd/preferences-skills.js +2 -34
  24. package/dist/resources/extensions/gsd/preferences-types.js +15 -0
  25. package/dist/resources/extensions/gsd/preferences.js +16 -3
  26. package/dist/resources/extensions/gsd/prompt-loader.js +4 -1
  27. package/dist/resources/extensions/gsd/prompts/discuss.md +122 -13
  28. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  29. package/dist/resources/extensions/gsd/state.js +21 -1
  30. package/dist/resources/extensions/gsd/workflow-projections.js +7 -0
  31. package/dist/resources/extensions/gsd/worktree-manager.js +30 -3
  32. package/dist/resources/extensions/gsd/write-intercept.js +10 -1
  33. package/dist/resources/extensions/ollama/index.js +4 -5
  34. package/dist/resources/extensions/ollama/ollama-client.js +35 -6
  35. package/dist/resources/extensions/ollama/ollama-discovery.js +32 -6
  36. package/dist/web/standalone/.next/BUILD_ID +1 -1
  37. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  38. package/dist/web/standalone/.next/build-manifest.json +2 -2
  39. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  40. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  41. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  51. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  64. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  70. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/hooks/route.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/knowledge/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  80. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  85. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  88. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  95. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  96. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  97. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +3 -3
  98. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/index.html +1 -1
  103. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  108. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  109. package/dist/web/standalone/.next/server/app/page.js +2 -2
  110. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  112. package/dist/web/standalone/.next/server/chunks/2331.js +16 -16
  113. package/dist/web/standalone/.next/server/chunks/4741.js +12 -12
  114. package/dist/web/standalone/.next/server/chunks/5822.js +2 -2
  115. package/dist/web/standalone/.next/server/chunks/63.js +8 -8
  116. package/dist/web/standalone/.next/server/chunks/6897.js +3 -3
  117. package/dist/web/standalone/.next/server/edge-runtime-webpack.js +2 -0
  118. package/dist/web/standalone/.next/server/functions-config-manifest.json +0 -9
  119. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/middleware-manifest.json +29 -2
  121. package/dist/web/standalone/.next/server/middleware.js +4 -12
  122. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  123. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  124. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  125. package/dist/web/standalone/.next/server/webpack-runtime.js +1 -1
  126. package/package.json +1 -1
  127. package/packages/pi-ai/dist/env-api-keys.js +1 -0
  128. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  129. package/packages/pi-ai/dist/models.custom.d.ts +105 -0
  130. package/packages/pi-ai/dist/models.custom.d.ts.map +1 -1
  131. package/packages/pi-ai/dist/models.custom.js +97 -0
  132. package/packages/pi-ai/dist/models.custom.js.map +1 -1
  133. package/packages/pi-ai/dist/models.generated.d.ts +648 -140
  134. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  135. package/packages/pi-ai/dist/models.generated.js +867 -370
  136. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  137. package/packages/pi-ai/dist/models.generated.test.d.ts +2 -0
  138. package/packages/pi-ai/dist/models.generated.test.d.ts.map +1 -0
  139. package/packages/pi-ai/dist/models.generated.test.js +334 -0
  140. package/packages/pi-ai/dist/models.generated.test.js.map +1 -0
  141. package/packages/pi-ai/dist/models.test.js +105 -0
  142. package/packages/pi-ai/dist/models.test.js.map +1 -1
  143. package/packages/pi-ai/dist/types.d.ts +1 -1
  144. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  145. package/packages/pi-ai/dist/types.js.map +1 -1
  146. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  147. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +5 -1
  148. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  149. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.d.ts +2 -0
  150. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.d.ts.map +1 -0
  151. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js +57 -0
  152. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js.map +1 -0
  153. package/packages/pi-ai/src/env-api-keys.ts +1 -0
  154. package/packages/pi-ai/src/models.custom.ts +98 -0
  155. package/packages/pi-ai/src/models.generated.test.ts +373 -0
  156. package/packages/pi-ai/src/models.generated.ts +867 -370
  157. package/packages/pi-ai/src/models.test.ts +135 -0
  158. package/packages/pi-ai/src/types.ts +1 -0
  159. package/packages/pi-ai/src/utils/oauth/github-copilot.test.ts +71 -0
  160. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +4 -1
  161. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  163. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  165. package/packages/pi-coding-agent/dist/core/sdk.js +9 -0
  166. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -0
  168. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  169. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +87 -12
  171. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +1 -0
  173. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  174. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +22 -9
  175. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  176. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts +2 -0
  177. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.d.ts.map +1 -0
  178. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +63 -0
  179. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -0
  180. package/packages/pi-coding-agent/package.json +1 -1
  181. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  182. package/packages/pi-coding-agent/src/core/sdk.ts +10 -0
  183. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +72 -0
  184. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -12
  185. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +71 -0
  186. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +23 -9
  187. package/packages/pi-tui/dist/components/__tests__/editor.test.js +12 -0
  188. package/packages/pi-tui/dist/components/__tests__/editor.test.js.map +1 -1
  189. package/packages/pi-tui/dist/components/__tests__/input.test.js +12 -0
  190. package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
  191. package/packages/pi-tui/dist/keys.d.ts.map +1 -1
  192. package/packages/pi-tui/dist/keys.js +27 -0
  193. package/packages/pi-tui/dist/keys.js.map +1 -1
  194. package/packages/pi-tui/src/components/__tests__/editor.test.ts +18 -0
  195. package/packages/pi-tui/src/components/__tests__/input.test.ts +18 -0
  196. package/packages/pi-tui/src/keys.ts +32 -0
  197. package/pkg/package.json +1 -1
  198. package/src/resources/extensions/async-jobs/await-tool.test.ts +40 -7
  199. package/src/resources/extensions/async-jobs/await-tool.ts +7 -4
  200. package/src/resources/extensions/async-jobs/job-manager.ts +33 -3
  201. package/src/resources/extensions/claude-code-cli/partial-builder.ts +45 -12
  202. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +49 -24
  203. package/src/resources/extensions/claude-code-cli/tests/partial-builder.test.ts +91 -2
  204. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +112 -0
  205. package/src/resources/extensions/gsd/auto/loop.ts +89 -1
  206. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -0
  207. package/src/resources/extensions/gsd/auto-recovery.ts +10 -0
  208. package/src/resources/extensions/gsd/auto.ts +25 -20
  209. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -10
  210. package/src/resources/extensions/gsd/commands-handlers.ts +5 -1
  211. package/src/resources/extensions/gsd/context-injector.ts +1 -1
  212. package/src/resources/extensions/gsd/custom-workflow-engine.ts +4 -8
  213. package/src/resources/extensions/gsd/definition-io.ts +18 -0
  214. package/src/resources/extensions/gsd/dispatch-guard.ts +5 -0
  215. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +6 -3
  216. package/src/resources/extensions/gsd/git-service.ts +11 -8
  217. package/src/resources/extensions/gsd/gitignore.ts +12 -6
  218. package/src/resources/extensions/gsd/gsd-db.ts +54 -6
  219. package/src/resources/extensions/gsd/key-manager.ts +2 -0
  220. package/src/resources/extensions/gsd/preferences-skills.ts +2 -36
  221. package/src/resources/extensions/gsd/preferences-types.ts +16 -0
  222. package/src/resources/extensions/gsd/preferences.ts +19 -6
  223. package/src/resources/extensions/gsd/prompt-loader.ts +6 -1
  224. package/src/resources/extensions/gsd/prompts/discuss.md +122 -13
  225. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  226. package/src/resources/extensions/gsd/state.ts +20 -0
  227. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +27 -0
  228. package/src/resources/extensions/gsd/tests/block-db-writes.test.ts +63 -0
  229. package/src/resources/extensions/gsd/tests/definition-io.test.ts +57 -0
  230. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +26 -0
  231. package/src/resources/extensions/gsd/tests/doctor-heal-fixable-warnings.test.ts +14 -0
  232. package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +104 -0
  233. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +107 -5
  234. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +8 -6
  235. package/src/resources/extensions/gsd/tests/key-manager.test.ts +63 -0
  236. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +54 -0
  237. package/src/resources/extensions/gsd/tests/plan-milestone-artifact-verification.test.ts +62 -0
  238. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +34 -0
  239. package/src/resources/extensions/gsd/tests/preferences-formatting.test.ts +87 -0
  240. package/src/resources/extensions/gsd/tests/preferences.test.ts +53 -0
  241. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +96 -1
  242. package/src/resources/extensions/gsd/tests/prompt-loader-working-directory.test.ts +19 -0
  243. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +97 -0
  244. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +41 -0
  245. package/src/resources/extensions/gsd/workflow-projections.ts +8 -0
  246. package/src/resources/extensions/gsd/worktree-manager.ts +29 -3
  247. package/src/resources/extensions/gsd/write-intercept.ts +10 -1
  248. package/src/resources/extensions/ollama/index.ts +4 -5
  249. package/src/resources/extensions/ollama/ollama-client.ts +35 -6
  250. package/src/resources/extensions/ollama/ollama-discovery.ts +37 -6
  251. package/src/resources/extensions/ollama/tests/ollama-discovery.test.ts +54 -0
  252. package/dist/resources/extensions/gsd/auto-observability.js +0 -54
  253. package/dist/resources/extensions/gsd/file-watcher.js +0 -80
  254. package/dist/resources/extensions/gsd/rtk-status.js +0 -43
  255. package/src/resources/extensions/gsd/auto-observability.ts +0 -72
  256. package/src/resources/extensions/gsd/file-watcher.ts +0 -100
  257. package/src/resources/extensions/gsd/rtk-status.ts +0 -53
  258. /package/dist/web/standalone/.next/static/{f-Gremw0nLxxFUySaHRPw → uNGVqSkAnszMl0okA4nnp}/_buildManifest.js +0 -0
  259. /package/dist/web/standalone/.next/static/{f-Gremw0nLxxFUySaHRPw → uNGVqSkAnszMl0okA4nnp}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -623,8 +623,10 @@ The best practice for working in teams is to ensure unique milestone names acros
623
623
  # ── GSD: Runtime / Ephemeral (per-developer, per-session) ──────────────────
624
624
  # Crash detection sentinel — PID lock, written per auto-mode session
625
625
  .gsd/auto.lock
626
- # Auto-mode dispatch tracker — prevents re-running completed units
627
- .gsd/completed-units.json
626
+ # Auto-mode dispatch tracker — prevents re-running completed units (includes archived per-milestone files)
627
+ .gsd/completed-units*.json
628
+ # State manifest — workflow state for recovery
629
+ .gsd/state-manifest.json
628
630
  # Derived state cache — regenerated from plan/roadmap files on disk
629
631
  .gsd/STATE.md
630
632
  # Per-developer token/cost accumulator
@@ -637,6 +639,14 @@ The best practice for working in teams is to ensure unique milestone names acros
637
639
  .gsd/worktrees/
638
640
  # Parallel orchestration IPC and worker status
639
641
  .gsd/parallel/
642
+ # SQLite database and WAL sidecars — checkpoint state, forensics data
643
+ .gsd/gsd.db*
644
+ # Daily-rotated event journal — structured event log for forensics
645
+ .gsd/journal/
646
+ # Doctor run history — diagnostic check results
647
+ .gsd/doctor-history.jsonl
648
+ # Workflow event log — structured event stream
649
+ .gsd/event-log.jsonl
640
650
  # Generated HTML reports (regenerable via /gsd export --html)
641
651
  .gsd/reports/
642
652
  # Session-specific interrupted-work markers
package/dist/cli.js CHANGED
@@ -5,8 +5,7 @@ import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
5
5
  import { initResources, buildResourceLoader, getNewerManagedResourceVersion } from './resource-loader.js';
6
6
  import { ensureManagedTools } from './tool-bootstrap.js';
7
7
  import { loadStoredEnvKeys } from './wizard.js';
8
- import { migratePiCredentials } from './pi-migration.js';
9
- import { validateConfiguredModel } from './startup-model-validation.js';
8
+ import { migratePiCredentials, getPiDefaultModelAndProvider } from './pi-migration.js';
10
9
  import { shouldMigrateAnthropicToClaudeCode } from './provider-migrations.js';
11
10
  import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
12
11
  import chalk from 'chalk';
@@ -102,6 +101,41 @@ function parseCliArgs(argv) {
102
101
  }
103
102
  return flags;
104
103
  }
104
+ /**
105
+ * Validate the configured default model against the registry and reset it if
106
+ * it no longer exists. Must run AFTER extensions have registered their
107
+ * providers so that extension models (e.g. pi-claude-cli) are visible.
108
+ */
109
+ function validateConfiguredModel(modelRegistry, settingsManager) {
110
+ const configuredProvider = settingsManager.getDefaultProvider();
111
+ const configuredModel = settingsManager.getDefaultModel();
112
+ const allModels = modelRegistry.getAll();
113
+ const availableModels = modelRegistry.getAvailable();
114
+ const configuredExists = configuredProvider && configuredModel &&
115
+ allModels.some((m) => m.provider === configuredProvider && m.id === configuredModel);
116
+ const configuredAvailable = configuredProvider && configuredModel &&
117
+ availableModels.some((m) => m.provider === configuredProvider && m.id === configuredModel);
118
+ if (!configuredModel || !configuredExists) {
119
+ // Model not configured at all, or removed from registry — pick a fallback.
120
+ // Only fires when the model is genuinely unknown (not just temporarily unavailable).
121
+ const piDefault = getPiDefaultModelAndProvider();
122
+ const preferred = (piDefault
123
+ ? availableModels.find((m) => m.provider === piDefault.provider && m.id === piDefault.model)
124
+ : undefined) ||
125
+ availableModels.find((m) => m.provider === 'openai' && m.id === 'gpt-5.4') ||
126
+ availableModels.find((m) => m.provider === 'openai') ||
127
+ availableModels.find((m) => m.provider === 'anthropic' && m.id === 'claude-opus-4-6') ||
128
+ availableModels.find((m) => m.provider === 'anthropic' && m.id.includes('opus')) ||
129
+ availableModels.find((m) => m.provider === 'anthropic') ||
130
+ availableModels[0];
131
+ if (preferred) {
132
+ settingsManager.setDefaultModelAndProvider(preferred.provider, preferred.id);
133
+ }
134
+ }
135
+ if (settingsManager.getDefaultThinkingLevel() !== 'off' && !configuredExists) {
136
+ settingsManager.setDefaultThinkingLevel('off');
137
+ }
138
+ }
105
139
  const cliFlags = parseCliArgs(process.argv);
106
140
  const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
107
141
  // Early resource-skew check — must run before TTY gate so version mismatch
@@ -320,8 +354,22 @@ if (!isPrintMode) {
320
354
  if (!isPrintMode && process.stdout.columns && process.stdout.columns < 40) {
321
355
  process.stderr.write(chalk.yellow(`[gsd] Terminal width is ${process.stdout.columns} columns (minimum recommended: 40). Output may be unreadable.\n`));
322
356
  }
323
- // --list-models: print available models and exit (no TTY needed)
357
+ // --list-models: load extensions so that extension-registered providers (e.g.
358
+ // pi-claude-cli) appear in the listing, then flush their pending registrations
359
+ // into the model registry before printing.
324
360
  if (cliFlags.listModels !== undefined) {
361
+ exitIfManagedResourcesAreNewer(agentDir);
362
+ initResources(agentDir);
363
+ const listModelsLoader = new DefaultResourceLoader({
364
+ agentDir,
365
+ additionalExtensionPaths: cliFlags.extensions.length > 0 ? cliFlags.extensions : undefined,
366
+ });
367
+ await listModelsLoader.reload();
368
+ const listModelsExtensions = listModelsLoader.getExtensions();
369
+ for (const { name, config } of listModelsExtensions.runtime.pendingProviderRegistrations) {
370
+ modelRegistry.registerProvider(name, config);
371
+ }
372
+ listModelsExtensions.runtime.pendingProviderRegistrations = [];
325
373
  const models = modelRegistry.getAvailable();
326
374
  if (models.length === 0) {
327
375
  console.log('No models available. Set API keys in environment variables.');
@@ -461,6 +509,10 @@ if (isPrintMode) {
461
509
  process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
462
510
  }
463
511
  }
512
+ // Validate configured model now that extension providers are registered.
513
+ // Must run after createAgentSession() which flushes pendingProviderRegistrations
514
+ // so extension models (e.g. pi-claude-cli) are visible in the registry.
515
+ validateConfiguredModel(modelRegistry, settingsManager);
464
516
  // Apply --model override if specified
465
517
  if (cliFlags.model) {
466
518
  const available = modelRegistry.getAvailable();
@@ -648,6 +700,10 @@ if (extensionsResult.errors.length > 0) {
648
700
  process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
649
701
  }
650
702
  }
703
+ // Validate configured model now that extension providers are registered.
704
+ // Must run after createAgentSession() which flushes pendingProviderRegistrations
705
+ // so extension models (e.g. pi-claude-cli) are visible in the registry.
706
+ validateConfiguredModel(modelRegistry, settingsManager);
651
707
  // Restore scoped models from settings on startup.
652
708
  // The upstream InteractiveMode reads enabledModels from settings when /scoped-models is opened,
653
709
  // but doesn't apply them to the session at startup — so Ctrl+P cycles all models instead of
@@ -277,6 +277,16 @@ async function runLlmStep(p, pc, authStorage) {
277
277
  p.log.info('Your Claude subscription will be used for inference. No API key needed.');
278
278
  // Store sentinel so hasAuth('claude-code') returns true on future boots
279
279
  authStorage.set('claude-code', { type: 'api_key', key: 'cli' });
280
+ // Persist claude-code as the default provider so the startup migration in
281
+ // cli.ts does not need to fire and the user is not left on "anthropic".
282
+ const settingsPath = join(agentDir, 'settings.json');
283
+ try {
284
+ const raw = existsSync(settingsPath) ? JSON.parse(readFileSync(settingsPath, 'utf-8')) : {};
285
+ raw.defaultProvider = 'claude-code';
286
+ mkdirSync(dirname(settingsPath), { recursive: true });
287
+ writeFileSync(settingsPath, JSON.stringify(raw, null, 2), 'utf-8');
288
+ }
289
+ catch { /* non-fatal — startup migration will catch it */ }
280
290
  return true;
281
291
  }
282
292
  // ── Step 2: Which provider? ──────────────────────────────────────────────
@@ -54,11 +54,14 @@ export function createAwaitTool(getManager) {
54
54
  };
55
55
  }
56
56
  }
57
- // Mark all watched jobs as awaited upfront so the onJobComplete
58
- // callback (which fires synchronously in the promise .then()) knows
59
- // to suppress the follow-up message.
57
+ // Suppress follow-up notifications for all watched jobs upfront.
58
+ // suppressFollowUp() cancels the pending delivery timer (if any), which
59
+ // handles both the within-turn case (job completes while we await) and
60
+ // the cross-turn case (job already completed before await_job was called).
61
+ // Previously this only set j.awaited = true, which missed the cross-turn
62
+ // case because the queueMicrotask had already fired (#3787).
60
63
  for (const j of watched)
61
- j.awaited = true;
64
+ manager.suppressFollowUp(j.id);
62
65
  // If all watched jobs are already done, return immediately
63
66
  const running = watched.filter((j) => j.status === "running");
64
67
  if (running.length === 0) {
@@ -118,13 +118,38 @@ export class AsyncJobManager {
118
118
  }
119
119
  }
120
120
  // ── Private ────────────────────────────────────────────────────────────
121
+ /**
122
+ * Suppress follow-up notification for a job — cancels any pending delivery
123
+ * timer and marks the job as awaited. Safe to call at any time, including
124
+ * before or after the job completes (#3787).
125
+ */
126
+ suppressFollowUp(id) {
127
+ const job = this.jobs.get(id);
128
+ if (!job)
129
+ return;
130
+ job.awaited = true;
131
+ if (job.deliveryTimer !== undefined) {
132
+ clearTimeout(job.deliveryTimer);
133
+ job.deliveryTimer = undefined;
134
+ }
135
+ }
121
136
  deliverResult(job) {
122
137
  if (!this.onJobComplete)
123
138
  return;
124
- // Defer delivery by one microtask so await_job's .then() chain runs first
125
- // and can set job.awaited = true before onJobComplete checks it (#2762).
139
+ // Use setTimeout(0) instead of queueMicrotask so the handle is cancellable.
140
+ // suppressFollowUp() can clear this timer even when await_job is called in
141
+ // a later LLM turn (after the job already completed). queueMicrotask ran
142
+ // immediately and could not be cancelled (#2762, #3787).
126
143
  const cb = this.onJobComplete;
127
- queueMicrotask(() => cb(job));
144
+ job.deliveryTimer = setTimeout(() => {
145
+ job.deliveryTimer = undefined;
146
+ if (!job.awaited)
147
+ cb(job);
148
+ }, 0);
149
+ // Allow process to exit even if timer is pending
150
+ if (typeof job.deliveryTimer === "object" && "unref" in job.deliveryTimer) {
151
+ job.deliveryTimer.unref();
152
+ }
128
153
  }
129
154
  scheduleEviction(id) {
130
155
  const existing = this.evictionTimers.get(id);
@@ -6,6 +6,44 @@
6
6
  */
7
7
  import { hasXmlParameterTags, repairToolJson } from "@gsd/pi-ai";
8
8
  // ---------------------------------------------------------------------------
9
+ // MCP tool name parsing
10
+ // ---------------------------------------------------------------------------
11
+ /**
12
+ * Split a Claude Code MCP tool name (`mcp__<server>__<tool>`) into its parts.
13
+ * Returns null for non-prefixed names so callers can fall through unchanged.
14
+ *
15
+ * Server names may contain hyphens (`gsd-workflow`); the SDK uses the literal
16
+ * `__` delimiter between the server name and the tool name.
17
+ */
18
+ export function parseMcpToolName(name) {
19
+ if (!name.startsWith("mcp__"))
20
+ return null;
21
+ const rest = name.slice("mcp__".length);
22
+ const delim = rest.indexOf("__");
23
+ if (delim <= 0 || delim === rest.length - 2)
24
+ return null;
25
+ return { server: rest.slice(0, delim), tool: rest.slice(delim + 2) };
26
+ }
27
+ /**
28
+ * Build a GSD ToolCall block from a Claude Code SDK tool_use block, stripping
29
+ * the `mcp__<server>__` prefix from the name so registered extension renderers
30
+ * (which use the unprefixed canonical names) can match. The original server
31
+ * name is preserved on the block for diagnostics and rendering.
32
+ */
33
+ function toolCallFromBlock(id, rawName, input) {
34
+ const parsed = parseMcpToolName(rawName);
35
+ const toolCall = {
36
+ type: "toolCall",
37
+ id,
38
+ name: parsed ? parsed.tool : rawName,
39
+ arguments: input,
40
+ };
41
+ if (parsed) {
42
+ toolCall.mcpServer = parsed.server;
43
+ }
44
+ return toolCall;
45
+ }
46
+ // ---------------------------------------------------------------------------
9
47
  // Content-block mapping helpers
10
48
  // ---------------------------------------------------------------------------
11
49
  /**
@@ -22,12 +60,7 @@ export function mapContentBlock(block) {
22
60
  ...(block.signature ? { thinkingSignature: block.signature } : {}),
23
61
  };
24
62
  case "tool_use":
25
- return {
26
- type: "toolCall",
27
- id: block.id,
28
- name: block.name,
29
- arguments: block.input,
30
- };
63
+ return toolCallFromBlock(block.id, block.name, block.input);
31
64
  case "server_tool_use":
32
65
  return {
33
66
  type: "serverToolUse",
@@ -149,12 +182,7 @@ export class PartialMessageBuilder {
149
182
  }
150
183
  if (block.type === "tool_use") {
151
184
  this.toolJsonAccum.set(streamIndex, "");
152
- this.partial.content.push({
153
- type: "toolCall",
154
- id: block.id,
155
- name: block.name,
156
- arguments: {},
157
- });
185
+ this.partial.content.push(toolCallFromBlock(block.id, block.name, {}));
158
186
  return { type: "toolcall_start", contentIndex, partial: this.partial };
159
187
  }
160
188
  if (block.type === "server_tool_use") {
@@ -92,18 +92,34 @@ function extractMessageText(msg) {
92
92
  * call effectively stateless. This version serialises the complete
93
93
  * conversation history (system prompt + all user/assistant turns) so
94
94
  * Claude Code has full context for multi-turn continuity.
95
+ *
96
+ * History is wrapped in XML-tag structure rather than `[User]`/`[Assistant]`
97
+ * bracket headers. Bracket headers read to the model as an in-context
98
+ * demonstration of how turns are delimited, causing it to fabricate fake
99
+ * user turns in its own output. XML tags read as document structure and
100
+ * don't get mirrored in free text.
95
101
  */
96
102
  export function buildPromptFromContext(context) {
97
- const parts = [];
103
+ const hasContent = Boolean(context.systemPrompt) || context.messages.some((m) => extractMessageText(m));
104
+ if (!hasContent)
105
+ return "";
106
+ const parts = [
107
+ "Respond only to the final user message below. " +
108
+ "Do not emit <user_message>, <assistant_message>, or <prior_system_context> tags in your response.",
109
+ ];
98
110
  if (context.systemPrompt) {
99
- parts.push(`[System]\n${context.systemPrompt}`);
111
+ parts.push(`<prior_system_context>\n${context.systemPrompt}\n</prior_system_context>`);
100
112
  }
113
+ const turns = [];
101
114
  for (const msg of context.messages) {
102
115
  const text = extractMessageText(msg);
103
116
  if (!text)
104
117
  continue;
105
- const label = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "System";
106
- parts.push(`[${label}]\n${text}`);
118
+ const tag = msg.role === "user" ? "user_message" : msg.role === "assistant" ? "assistant_message" : "system_message";
119
+ turns.push(`<${tag}>\n${text}\n</${tag}>`);
120
+ }
121
+ if (turns.length > 0) {
122
+ parts.push(`<conversation_history>\n${turns.join("\n")}\n</conversation_history>`);
107
123
  }
108
124
  return parts.join("\n\n");
109
125
  }
@@ -389,32 +405,25 @@ export function makeAbortedMessage(model, lastTextContent) {
389
405
  /**
390
406
  * Resolve the Claude Code permission mode for the current run.
391
407
  *
392
- * - Auto-mode / headless runs bypass permissions so tool calls don't block
393
- * on prompts the user isn't watching.
394
- * - Interactive runs default to `acceptEdits` so file/bash writes still
395
- * land quickly but the SDK retains a permission gate.
396
- * - `GSD_CLAUDE_CODE_PERMISSION_MODE` forces a specific mode when set.
408
+ * GSD subagents run underneath a host Claude Code session the user has
409
+ * already consented to, and their work (edits, shell inspection, MCP calls)
410
+ * spans the full workflow toolset. Defaulting the inner SDK to
411
+ * `bypassPermissions` avoids per-tool approval prompts that offer no
412
+ * meaningful safety beyond what the host session and the subagent prompts
413
+ * already enforce. `GSD_CLAUDE_CODE_PERMISSION_MODE` lets security-conscious
414
+ * users opt into a stricter mode (`acceptEdits`, `default`, `plan`).
397
415
  *
398
- * Cross-extension coupling is kept minimal by dynamically importing
399
- * `isAutoActive` and falling back to the bypass default if the import
400
- * fails (e.g. in unit tests that load stream-adapter in isolation).
416
+ * Tradeoff: bypass means a prompt-injection payload read from an untrusted
417
+ * file could trigger tool calls without a second gate. Accepted for GSD
418
+ * because the workflow is explicit user intent and the alternative
419
+ * (#4099) is continuous approval fatigue that blocks real work.
401
420
  */
402
421
  export async function resolveClaudePermissionMode(env = process.env) {
403
422
  const override = env.GSD_CLAUDE_CODE_PERMISSION_MODE?.trim();
404
423
  if (override === "bypassPermissions" || override === "acceptEdits" || override === "default" || override === "plan") {
405
424
  return override;
406
425
  }
407
- try {
408
- const autoMod = (await import("../gsd/auto.js"));
409
- if (typeof autoMod.isAutoActive === "function" && autoMod.isAutoActive()) {
410
- return "bypassPermissions";
411
- }
412
- return "acceptEdits";
413
- }
414
- catch {
415
- // auto.ts unavailable (tests, non-GSD contexts) — stay permissive.
416
- return "bypassPermissions";
417
- }
426
+ return "bypassPermissions";
418
427
  }
419
428
  /**
420
429
  * Build the options object passed to the Claude Agent SDK's `query()` call.
@@ -431,6 +440,21 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
431
440
  const mcpServers = buildWorkflowMcpServers();
432
441
  const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
433
442
  const disallowedTools = ["AskUserQuestion"];
443
+ // Pre-authorize the safe built-ins and every registered workflow MCP
444
+ // server's tools. `acceptEdits` mode (the interactive default) only
445
+ // auto-approves file edits — Read/Glob/Grep, basic shell inspection, and
446
+ // every `mcp__gsd-workflow__*` call still surface as "This command
447
+ // requires approval" and block GSD actions (#4099).
448
+ const allowedTools = [
449
+ "Read",
450
+ "Write",
451
+ "Edit",
452
+ "Glob",
453
+ "Grep",
454
+ "Bash(ls:*)",
455
+ "Bash(pwd)",
456
+ ...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
457
+ ];
434
458
  return {
435
459
  pathToClaudeCodeExecutable: getClaudePath(),
436
460
  model: modelId,
@@ -442,6 +466,7 @@ export function buildSdkOptions(modelId, prompt, overrides, extraOptions = {}) {
442
466
  settingSources: ["project"],
443
467
  systemPrompt: { type: "preset", preset: "claude_code" },
444
468
  disallowedTools,
469
+ ...(allowedTools.length > 0 ? { allowedTools } : {}),
445
470
  ...(mcpServers ? { mcpServers } : {}),
446
471
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
447
472
  ...extraOptions,
@@ -13,6 +13,69 @@ import { runPreDispatch, runDispatch, runGuards, runUnitPhase, runFinalize, } fr
13
13
  import { debugLog } from "../debug-logger.js";
14
14
  import { isInfrastructureError, isTransientCooldownError, getCooldownRetryAfterMs, COOLDOWN_FALLBACK_WAIT_MS, MAX_COOLDOWN_RETRIES } from "./infra-errors.js";
15
15
  import { resolveEngine } from "../engine-resolver.js";
16
+ import { logWarning } from "../workflow-logger.js";
17
+ import { gsdRoot } from "../paths.js";
18
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ // ── Stuck detection persistence (#3704) ──────────────────────────────────
21
+ // Persist stuck detection state to disk so it survives session restarts.
22
+ // Without this, restarting auto-mode resets all counters, allowing the
23
+ // same blocked unit to burn a full retry budget each session.
24
+ function stuckStatePath(basePath) {
25
+ return join(gsdRoot(basePath), "runtime", "stuck-state.json");
26
+ }
27
+ function loadStuckState(basePath) {
28
+ try {
29
+ const data = JSON.parse(readFileSync(stuckStatePath(basePath), "utf-8"));
30
+ return {
31
+ recentUnits: Array.isArray(data.recentUnits) ? data.recentUnits : [],
32
+ stuckRecoveryAttempts: typeof data.stuckRecoveryAttempts === "number" ? data.stuckRecoveryAttempts : 0,
33
+ };
34
+ }
35
+ catch (err) {
36
+ debugLog("autoLoop", { phase: "load-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
37
+ return { recentUnits: [], stuckRecoveryAttempts: 0 };
38
+ }
39
+ }
40
+ function saveStuckState(basePath, state) {
41
+ try {
42
+ const filePath = stuckStatePath(basePath);
43
+ mkdirSync(join(gsdRoot(basePath), "runtime"), { recursive: true });
44
+ writeFileSync(filePath, JSON.stringify({
45
+ recentUnits: state.recentUnits.slice(-20), // keep last 20 entries
46
+ stuckRecoveryAttempts: state.stuckRecoveryAttempts,
47
+ updatedAt: new Date().toISOString(),
48
+ }) + "\n");
49
+ }
50
+ catch (err) {
51
+ debugLog("autoLoop", { phase: "save-stuck-state-failed", error: err instanceof Error ? err.message : String(err) });
52
+ }
53
+ }
54
+ // ── Memory pressure monitoring (#3331) ──────────────────────────────────
55
+ // Check heap usage every N iterations and trigger graceful shutdown before
56
+ // the OS OOM killer sends SIGKILL. The threshold is 90% of the V8 heap
57
+ // limit (--max-old-space-size or default ~1.5-4GB depending on platform).
58
+ const MEMORY_CHECK_INTERVAL = 5; // check every 5 iterations
59
+ const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% of heap limit
60
+ function checkMemoryPressure() {
61
+ const mem = process.memoryUsage();
62
+ // v8.getHeapStatistics() gives heap_size_limit but requires import
63
+ // Use a conservative estimate: RSS > 3GB is danger zone on most systems
64
+ const heapMB = Math.round(mem.heapUsed / 1024 / 1024);
65
+ const rssMB = Math.round(mem.rss / 1024 / 1024);
66
+ // Try to get the actual V8 heap limit
67
+ let limitMB = 4096; // conservative default
68
+ try {
69
+ const v8 = require("node:v8");
70
+ const stats = v8.getHeapStatistics();
71
+ limitMB = Math.round(stats.heap_size_limit / 1024 / 1024);
72
+ }
73
+ catch {
74
+ limitMB = 4096; /* v8 stats unavailable — use conservative default */
75
+ }
76
+ const pct = heapMB / limitMB;
77
+ return { pressured: pct > MEMORY_PRESSURE_THRESHOLD, heapMB, limitMB, pct };
78
+ }
16
79
  /**
17
80
  * Main auto-mode execution loop. Iterates: derive → dispatch → guards →
18
81
  * runUnit → finalize → repeat. Exits when s.active becomes false or a
@@ -24,7 +87,13 @@ import { resolveEngine } from "../engine-resolver.js";
24
87
  export async function autoLoop(ctx, pi, s, deps) {
25
88
  debugLog("autoLoop", { phase: "enter" });
26
89
  let iteration = 0;
27
- const loopState = { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
90
+ // Load persisted stuck state so counters survive session restarts (#3704)
91
+ const persisted = loadStuckState(s.basePath);
92
+ const loopState = {
93
+ recentUnits: persisted.recentUnits,
94
+ stuckRecoveryAttempts: persisted.stuckRecoveryAttempts,
95
+ consecutiveFinalizeTimeouts: 0,
96
+ };
28
97
  let consecutiveErrors = 0;
29
98
  let consecutiveCooldowns = 0;
30
99
  const recentErrorMessages = [];
@@ -44,6 +113,19 @@ export async function autoLoop(ctx, pi, s, deps) {
44
113
  await deps.stopAuto(ctx, pi, `Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`);
45
114
  break;
46
115
  }
116
+ // ── Memory pressure check (#3331) ──
117
+ // Graceful shutdown before OOM killer sends SIGKILL.
118
+ if (iteration % MEMORY_CHECK_INTERVAL === 0) {
119
+ const mem = checkMemoryPressure();
120
+ debugLog("autoLoop", { phase: "memory-check", ...mem });
121
+ if (mem.pressured) {
122
+ logWarning("dispatch", `Memory pressure: ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%) — stopping auto-mode to prevent OOM kill`);
123
+ await deps.stopAuto(ctx, pi, `Memory pressure: heap at ${mem.heapMB}MB / ${mem.limitMB}MB (${Math.round(mem.pct * 100)}%). ` +
124
+ `Stopping gracefully to prevent OOM kill after ${iteration} iterations. ` +
125
+ `Resume with /gsd auto to continue from where you left off.`);
126
+ break;
127
+ }
128
+ }
47
129
  if (!s.cmdCtx) {
48
130
  debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
49
131
  break;
@@ -162,6 +244,7 @@ export async function autoLoop(ctx, pi, s, deps) {
162
244
  consecutiveCooldowns = 0;
163
245
  recentErrorMessages.length = 0;
164
246
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId, seq: nextSeq(), eventType: "iteration-end", data: { iteration } });
247
+ saveStuckState(s.basePath, loopState); // persist across session restarts (#3704)
165
248
  debugLog("autoLoop", { phase: "iteration-complete", iteration });
166
249
  if (reconcileResult.outcome === "milestone-complete") {
167
250
  await deps.stopAuto(ctx, pi, "Workflow complete");
@@ -16,6 +16,7 @@ import { loadFile, parseSummary, resolveAllOverrides } from "./files.js";
16
16
  import { loadPrompt } from "./prompt-loader.js";
17
17
  import { resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveMilestoneFile, resolveTasksDir, buildTaskFileName, } from "./paths.js";
18
18
  import { invalidateAllCaches } from "./cache.js";
19
+ import { rebuildState } from "./doctor.js";
19
20
  import { parseUnitId } from "./unit-id.js";
20
21
  import { closeoutUnit } from "./auto-unit-closeout.js";
21
22
  import { autoCommitCurrentBranch, } from "./worktree.js";
@@ -288,6 +289,11 @@ export async function postUnitPreVerification(pctx, opts) {
288
289
  debugLog("postUnit", { phase: "browser-teardown", status: "closed" });
289
290
  }
290
291
  });
292
+ // Keep the on-disk STATE.md aligned with the live derived state after
293
+ // ordinary unit completion, before any worktree state is synced back.
294
+ await runSafely("postUnit", "state-rebuild", async () => {
295
+ await rebuildState(s.basePath);
296
+ });
291
297
  // Sync worktree state back to project root (skipped for lightweight sidecars)
292
298
  if (!opts?.skipWorktreeSync && s.originalBasePath && s.originalBasePath !== s.basePath) {
293
299
  await runSafely("postUnit", "worktree-sync", () => {
@@ -224,6 +224,17 @@ export function verifyExpectedArtifact(unitType, unitId, base) {
224
224
  if (!isValidationTerminal(validationContent))
225
225
  return false;
226
226
  }
227
+ if (unitType === "plan-milestone") {
228
+ try {
229
+ const roadmap = parseLegacyRoadmap(readFileSync(absPath, "utf-8"));
230
+ if (roadmap.slices.length === 0)
231
+ return false;
232
+ }
233
+ catch (err) {
234
+ logWarning("recovery", `plan-milestone roadmap verification failed: ${err instanceof Error ? err.message : String(err)}`);
235
+ return false;
236
+ }
237
+ }
227
238
  // plan-slice must produce a plan with actual task entries, not just a scaffold.
228
239
  // The plan file may exist from a prior discussion/context step with only headings
229
240
  // but no tasks. Without this check the artifact is considered "complete" and the
@@ -425,9 +425,13 @@ function cleanupAfterLoopExit(ctx) {
425
425
  /* best-effort — mirror stopAuto cleanup */
426
426
  logWarning("session", `lock cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
427
427
  }
428
- ctx.ui.setStatus("gsd-auto", undefined);
429
- ctx.ui.setWidget("gsd-progress", undefined);
430
- ctx.ui.setFooter(undefined);
428
+ // A transient provider-error pause intentionally leaves the paused badge
429
+ // visible so the user still has a resumable auto-mode signal on screen.
430
+ if (!s.paused) {
431
+ ctx.ui.setStatus("gsd-auto", undefined);
432
+ ctx.ui.setWidget("gsd-progress", undefined);
433
+ ctx.ui.setFooter(undefined);
434
+ }
431
435
  // Restore CWD out of worktree back to original project root
432
436
  if (s.originalBasePath) {
433
437
  s.basePath = s.originalBasePath;
@@ -529,7 +533,22 @@ export async function stopAuto(ctx, pi, reason) {
529
533
  catch (e) {
530
534
  debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
531
535
  }
532
- // ── Step 5: DB cleanup ──
536
+ // ── Step 5: Rebuild state while DB is still open (#3599) ──
537
+ // rebuildState() calls deriveState() which needs the DB for authoritative
538
+ // state. Previously this ran after closeDatabase(), forcing a filesystem
539
+ // fallback that could disagree with the DB-backed dispatch decisions —
540
+ // a split-brain where dispatch says "blocked" but STATE.md shows work.
541
+ if (s.basePath) {
542
+ try {
543
+ await rebuildState(s.basePath);
544
+ }
545
+ catch (e) {
546
+ debugLog("stop-rebuild-state-failed", {
547
+ error: e instanceof Error ? e.message : String(e),
548
+ });
549
+ }
550
+ }
551
+ // ── Step 6: DB cleanup ──
533
552
  if (isDbAvailable()) {
534
553
  try {
535
554
  const { closeDatabase } = await import("./gsd-db.js");
@@ -541,7 +560,7 @@ export async function stopAuto(ctx, pi, reason) {
541
560
  });
542
561
  }
543
562
  }
544
- // ── Step 6: Restore basePath and chdir ──
563
+ // ── Step 7: Restore basePath and chdir ──
545
564
  try {
546
565
  if (s.originalBasePath) {
547
566
  s.basePath = s.originalBasePath;
@@ -557,7 +576,7 @@ export async function stopAuto(ctx, pi, reason) {
557
576
  catch (e) {
558
577
  debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
559
578
  }
560
- // ── Step 7: Ledger notification ──
579
+ // ── Step 8: Ledger notification ──
561
580
  try {
562
581
  const ledger = getLedger();
563
582
  if (ledger && ledger.units.length > 0) {
@@ -571,17 +590,6 @@ export async function stopAuto(ctx, pi, reason) {
571
590
  catch (e) {
572
591
  debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
573
592
  }
574
- // ── Step 8: Rebuild state ──
575
- if (s.basePath) {
576
- try {
577
- await rebuildState(s.basePath);
578
- }
579
- catch (e) {
580
- debugLog("stop-rebuild-state-failed", {
581
- error: e instanceof Error ? e.message : String(e),
582
- });
583
- }
584
- }
585
593
  // ── Step 9: Cmux sidebar / event log ──
586
594
  try {
587
595
  clearCmuxSidebar(loadedPreferences);
@@ -1294,8 +1302,6 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
1294
1302
  pi.sendMessage({ customType: "gsd-auto", content: hookPrompt, display: true }, { triggerTurn: true });
1295
1303
  return true;
1296
1304
  }
1297
- // Direct phase dispatch → auto-direct-dispatch.ts
1298
- export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
1299
1305
  // Re-export recovery functions for external consumers
1300
1306
  export { buildLoopRemediationSteps, } from "./auto-recovery.js";
1301
1307
  export { resolveExpectedArtifactPath } from "./auto-artifact-paths.js";