gsd-pi 2.67.0-dev.1cd1e0f → 2.67.0-dev.2367d7e

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 (257) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +155 -70
  3. package/dist/resources/extensions/gsd/auto/phases.js +17 -0
  4. package/dist/resources/extensions/gsd/auto/session.js +10 -0
  5. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +12 -0
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-start.js +16 -30
  8. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  9. package/dist/resources/extensions/gsd/auto.js +121 -59
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +11 -435
  11. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -4
  12. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +7 -64
  13. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +88 -8
  15. package/dist/resources/extensions/gsd/commands/catalog.js +2 -1
  16. package/dist/resources/extensions/gsd/commands/handlers/core.js +39 -25
  17. package/dist/resources/extensions/gsd/commands/index.js +8 -1
  18. package/dist/resources/extensions/gsd/commands-mcp-status.js +43 -7
  19. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -4
  20. package/dist/resources/extensions/gsd/doctor-proactive.js +3 -3
  21. package/dist/resources/extensions/gsd/doctor.js +8 -4
  22. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  23. package/dist/resources/extensions/gsd/guided-flow.js +56 -31
  24. package/dist/resources/extensions/gsd/init-wizard.js +37 -0
  25. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  26. package/dist/resources/extensions/gsd/mcp-project-config.js +83 -0
  27. package/dist/resources/extensions/gsd/state.js +7 -2
  28. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +508 -0
  29. package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
  30. package/dist/resources/extensions/gsd/workflow-mcp.js +261 -0
  31. package/dist/web/standalone/.next/BUILD_ID +1 -1
  32. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  33. package/dist/web/standalone/.next/build-manifest.json +3 -3
  34. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  35. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  36. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.html +1 -1
  55. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  56. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  57. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  58. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  63. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  66. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  67. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  68. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  69. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  70. package/dist/web/standalone/.next/static/chunks/{webpack-b49b09f97429b5d0.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  71. package/package.json +4 -2
  72. package/packages/mcp-server/README.md +38 -0
  73. package/packages/mcp-server/dist/cli.d.ts +9 -0
  74. package/packages/mcp-server/dist/cli.d.ts.map +1 -0
  75. package/packages/mcp-server/dist/cli.js +58 -0
  76. package/packages/mcp-server/dist/cli.js.map +1 -0
  77. package/packages/mcp-server/dist/index.d.ts +20 -0
  78. package/packages/mcp-server/dist/index.d.ts.map +1 -0
  79. package/packages/mcp-server/dist/index.js +14 -0
  80. package/packages/mcp-server/dist/index.js.map +1 -0
  81. package/packages/mcp-server/dist/readers/captures.d.ts +25 -0
  82. package/packages/mcp-server/dist/readers/captures.d.ts.map +1 -0
  83. package/packages/mcp-server/dist/readers/captures.js +67 -0
  84. package/packages/mcp-server/dist/readers/captures.js.map +1 -0
  85. package/packages/mcp-server/dist/readers/doctor-lite.d.ts +20 -0
  86. package/packages/mcp-server/dist/readers/doctor-lite.d.ts.map +1 -0
  87. package/packages/mcp-server/dist/readers/doctor-lite.js +173 -0
  88. package/packages/mcp-server/dist/readers/doctor-lite.js.map +1 -0
  89. package/packages/mcp-server/dist/readers/index.d.ts +14 -0
  90. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -0
  91. package/packages/mcp-server/dist/readers/index.js +10 -0
  92. package/packages/mcp-server/dist/readers/index.js.map +1 -0
  93. package/packages/mcp-server/dist/readers/knowledge.d.ts +18 -0
  94. package/packages/mcp-server/dist/readers/knowledge.d.ts.map +1 -0
  95. package/packages/mcp-server/dist/readers/knowledge.js +82 -0
  96. package/packages/mcp-server/dist/readers/knowledge.js.map +1 -0
  97. package/packages/mcp-server/dist/readers/metrics.d.ts +32 -0
  98. package/packages/mcp-server/dist/readers/metrics.d.ts.map +1 -0
  99. package/packages/mcp-server/dist/readers/metrics.js +74 -0
  100. package/packages/mcp-server/dist/readers/metrics.js.map +1 -0
  101. package/packages/mcp-server/dist/readers/paths.d.ts +42 -0
  102. package/packages/mcp-server/dist/readers/paths.d.ts.map +1 -0
  103. package/packages/mcp-server/dist/readers/paths.js +199 -0
  104. package/packages/mcp-server/dist/readers/paths.js.map +1 -0
  105. package/packages/mcp-server/dist/readers/roadmap.d.ts +26 -0
  106. package/packages/mcp-server/dist/readers/roadmap.d.ts.map +1 -0
  107. package/packages/mcp-server/dist/readers/roadmap.js +194 -0
  108. package/packages/mcp-server/dist/readers/roadmap.js.map +1 -0
  109. package/packages/mcp-server/dist/readers/state.d.ts +43 -0
  110. package/packages/mcp-server/dist/readers/state.d.ts.map +1 -0
  111. package/packages/mcp-server/dist/readers/state.js +184 -0
  112. package/packages/mcp-server/dist/readers/state.js.map +1 -0
  113. package/packages/mcp-server/dist/server.d.ts +28 -0
  114. package/packages/mcp-server/dist/server.d.ts.map +1 -0
  115. package/packages/mcp-server/dist/server.js +319 -0
  116. package/packages/mcp-server/dist/server.js.map +1 -0
  117. package/packages/mcp-server/dist/session-manager.d.ts +54 -0
  118. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -0
  119. package/packages/mcp-server/dist/session-manager.js +284 -0
  120. package/packages/mcp-server/dist/session-manager.js.map +1 -0
  121. package/packages/mcp-server/dist/types.d.ts +61 -0
  122. package/packages/mcp-server/dist/types.d.ts.map +1 -0
  123. package/packages/mcp-server/dist/types.js +11 -0
  124. package/packages/mcp-server/dist/types.js.map +1 -0
  125. package/packages/mcp-server/dist/workflow-tools.d.ts +9 -0
  126. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -0
  127. package/packages/mcp-server/dist/workflow-tools.js +532 -0
  128. package/packages/mcp-server/dist/workflow-tools.js.map +1 -0
  129. package/packages/mcp-server/src/server.ts +6 -2
  130. package/packages/mcp-server/src/workflow-tools.test.ts +976 -0
  131. package/packages/mcp-server/src/workflow-tools.ts +997 -0
  132. package/packages/mcp-server/tsconfig.json +1 -1
  133. package/packages/pi-agent-core/dist/agent-loop.js +14 -6
  134. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  135. package/packages/pi-agent-core/src/agent-loop.test.ts +53 -0
  136. package/packages/pi-agent-core/src/agent-loop.ts +20 -6
  137. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  138. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  139. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  140. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  141. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  142. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  143. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  144. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  145. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  146. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  148. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts +2 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.d.ts.map +1 -0
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +28 -0
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -0
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -12
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  158. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +19 -0
  159. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  160. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  161. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  163. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  165. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  166. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -12
  167. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  168. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  169. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  170. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +54 -0
  172. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -12
  173. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +21 -0
  174. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  175. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +19 -15
  176. package/packages/rpc-client/dist/index.d.ts +10 -0
  177. package/packages/rpc-client/dist/index.d.ts.map +1 -0
  178. package/packages/rpc-client/dist/index.js +9 -0
  179. package/packages/rpc-client/dist/index.js.map +1 -0
  180. package/packages/rpc-client/dist/jsonl.d.ts +17 -0
  181. package/packages/rpc-client/dist/jsonl.d.ts.map +1 -0
  182. package/packages/rpc-client/dist/jsonl.js +54 -0
  183. package/packages/rpc-client/dist/jsonl.js.map +1 -0
  184. package/packages/rpc-client/dist/rpc-client.d.ts +259 -0
  185. package/packages/rpc-client/dist/rpc-client.d.ts.map +1 -0
  186. package/packages/rpc-client/dist/rpc-client.js +541 -0
  187. package/packages/rpc-client/dist/rpc-client.js.map +1 -0
  188. package/packages/rpc-client/dist/rpc-client.test.d.ts +2 -0
  189. package/packages/rpc-client/dist/rpc-client.test.d.ts.map +1 -0
  190. package/packages/rpc-client/dist/rpc-client.test.js +477 -0
  191. package/packages/rpc-client/dist/rpc-client.test.js.map +1 -0
  192. package/packages/rpc-client/dist/rpc-types.d.ts +566 -0
  193. package/packages/rpc-client/dist/rpc-types.d.ts.map +1 -0
  194. package/packages/rpc-client/dist/rpc-types.js +12 -0
  195. package/packages/rpc-client/dist/rpc-types.js.map +1 -0
  196. package/scripts/ensure-workspace-builds.cjs +2 -0
  197. package/scripts/link-workspace-packages.cjs +21 -14
  198. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +193 -93
  199. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +173 -79
  200. package/src/resources/extensions/gsd/auto/phases.ts +25 -0
  201. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  202. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +20 -0
  203. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  204. package/src/resources/extensions/gsd/auto-start.ts +23 -55
  205. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  206. package/src/resources/extensions/gsd/auto.ts +133 -64
  207. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +22 -435
  208. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -5
  209. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +7 -72
  210. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  211. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +122 -6
  212. package/src/resources/extensions/gsd/commands/catalog.ts +2 -1
  213. package/src/resources/extensions/gsd/commands/handlers/core.ts +53 -26
  214. package/src/resources/extensions/gsd/commands/index.ts +7 -1
  215. package/src/resources/extensions/gsd/commands-mcp-status.ts +53 -7
  216. package/src/resources/extensions/gsd/doctor-git-checks.ts +4 -4
  217. package/src/resources/extensions/gsd/doctor-proactive.ts +3 -3
  218. package/src/resources/extensions/gsd/doctor.ts +9 -5
  219. package/src/resources/extensions/gsd/gsd-db.ts +12 -0
  220. package/src/resources/extensions/gsd/guided-flow.ts +66 -36
  221. package/src/resources/extensions/gsd/init-wizard.ts +40 -0
  222. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  223. package/src/resources/extensions/gsd/mcp-project-config.ts +128 -0
  224. package/src/resources/extensions/gsd/state.ts +7 -1
  225. package/src/resources/extensions/gsd/tests/auto-project-root-env.test.ts +29 -0
  226. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  227. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  228. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  229. package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
  230. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  231. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +66 -0
  232. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  233. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
  234. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  235. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  236. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +2 -9
  237. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +0 -33
  238. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  239. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  240. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  241. package/src/resources/extensions/gsd/tests/mcp-project-config.test.ts +85 -0
  242. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +15 -0
  243. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  244. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
  245. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +500 -0
  246. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +625 -0
  247. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +629 -0
  248. package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
  249. package/src/resources/extensions/gsd/workflow-mcp.ts +320 -0
  250. package/dist/web/standalone/.next/static/chunks/6502.b804e48b7919f55e.js +0 -9
  251. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts +0 -13
  252. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.d.ts.map +0 -1
  253. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js +0 -27
  254. package/packages/pi-coding-agent/dist/modes/interactive/provider-auth-setup.js.map +0 -1
  255. package/packages/pi-coding-agent/src/modes/interactive/provider-auth-setup.ts +0 -40
  256. /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → WMDT_0C0XDkBKtsAI_AX4}/_buildManifest.js +0 -0
  257. /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → WMDT_0C0XDkBKtsAI_AX4}/_ssgManifest.js +0 -0
@@ -1,3 +1,6 @@
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
1
4
  const MILESTONE_CONTEXT_RE = /M\d+(?:-[a-z0-9]{6})?-CONTEXT\.md$/;
2
5
  const CONTEXT_MILESTONE_RE = /(?:^|[/\\])(M\d+(?:-[a-z0-9]{6})?)-CONTEXT\.md$/i;
3
6
  const DEPTH_VERIFICATION_MILESTONE_RE = /depth_verification[_-](M\d+(?:-[a-z0-9]{6})?)/i;
@@ -65,6 +68,69 @@ const GATE_SAFE_TOOLS = new Set([
65
68
  "search_and_read",
66
69
  ]);
67
70
 
71
+ export interface WriteGateSnapshot {
72
+ verifiedDepthMilestones: string[];
73
+ activeQueuePhase: boolean;
74
+ pendingGateId: string | null;
75
+ }
76
+
77
+ function shouldPersistWriteGateSnapshot(env: NodeJS.ProcessEnv = process.env): boolean {
78
+ return env.GSD_PERSIST_WRITE_GATE_STATE === "1";
79
+ }
80
+
81
+ function writeGateSnapshotPath(basePath: string = process.cwd()): string {
82
+ return join(basePath, ".gsd", "runtime", "write-gate-state.json");
83
+ }
84
+
85
+ function currentWriteGateSnapshot(): WriteGateSnapshot {
86
+ return {
87
+ verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
88
+ activeQueuePhase,
89
+ pendingGateId,
90
+ };
91
+ }
92
+
93
+ function persistWriteGateSnapshot(basePath: string = process.cwd()): void {
94
+ if (!shouldPersistWriteGateSnapshot()) return;
95
+ const path = writeGateSnapshotPath(basePath);
96
+ mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
97
+ const tempPath = `${path}.tmp`;
98
+ writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
99
+ renameSync(tempPath, path);
100
+ }
101
+
102
+ function clearPersistedWriteGateSnapshot(basePath: string = process.cwd()): void {
103
+ if (!shouldPersistWriteGateSnapshot()) return;
104
+ const path = writeGateSnapshotPath(basePath);
105
+ try {
106
+ unlinkSync(path);
107
+ } catch {
108
+ // swallow
109
+ }
110
+ }
111
+
112
+ function normalizeWriteGateSnapshot(value: unknown): WriteGateSnapshot {
113
+ const record = value && typeof value === "object" ? value as Record<string, unknown> : {};
114
+ const verified = Array.isArray(record.verifiedDepthMilestones)
115
+ ? record.verifiedDepthMilestones.filter((item): item is string => typeof item === "string")
116
+ : [];
117
+ return {
118
+ verifiedDepthMilestones: [...new Set(verified)].sort(),
119
+ activeQueuePhase: record.activeQueuePhase === true,
120
+ pendingGateId: typeof record.pendingGateId === "string" ? record.pendingGateId : null,
121
+ };
122
+ }
123
+
124
+ export function loadWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
125
+ const path = writeGateSnapshotPath(basePath);
126
+ if (!existsSync(path)) return currentWriteGateSnapshot();
127
+ try {
128
+ return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
129
+ } catch {
130
+ return currentWriteGateSnapshot();
131
+ }
132
+ }
133
+
68
134
  export function isDepthVerified(): boolean {
69
135
  return verifiedDepthMilestones.size > 0;
70
136
  }
@@ -77,28 +143,40 @@ export function isMilestoneDepthVerified(milestoneId: string | null | undefined)
77
143
  return verifiedDepthMilestones.has(milestoneId);
78
144
  }
79
145
 
146
+ export function isMilestoneDepthVerifiedInSnapshot(
147
+ snapshot: WriteGateSnapshot,
148
+ milestoneId: string | null | undefined,
149
+ ): boolean {
150
+ if (!milestoneId) return false;
151
+ return snapshot.verifiedDepthMilestones.includes(milestoneId);
152
+ }
153
+
80
154
  export function isQueuePhaseActive(): boolean {
81
155
  return activeQueuePhase;
82
156
  }
83
157
 
84
158
  export function setQueuePhaseActive(active: boolean): void {
85
159
  activeQueuePhase = active;
160
+ persistWriteGateSnapshot();
86
161
  }
87
162
 
88
163
  export function resetWriteGateState(): void {
89
164
  verifiedDepthMilestones.clear();
90
165
  pendingGateId = null;
166
+ persistWriteGateSnapshot();
91
167
  }
92
168
 
93
169
  export function clearDiscussionFlowState(): void {
94
170
  verifiedDepthMilestones.clear();
95
171
  activeQueuePhase = false;
96
172
  pendingGateId = null;
173
+ clearPersistedWriteGateSnapshot();
97
174
  }
98
175
 
99
- export function markDepthVerified(milestoneId?: string | null): void {
176
+ export function markDepthVerified(milestoneId?: string | null, basePath: string = process.cwd()): void {
100
177
  if (!milestoneId) return;
101
178
  verifiedDepthMilestones.add(milestoneId);
179
+ persistWriteGateSnapshot(basePath);
102
180
  }
103
181
 
104
182
  /**
@@ -130,6 +208,7 @@ function extractContextMilestoneId(inputPath: string): string | null {
130
208
  */
131
209
  export function setPendingGate(gateId: string): void {
132
210
  pendingGateId = gateId;
211
+ persistWriteGateSnapshot();
133
212
  }
134
213
 
135
214
  /**
@@ -137,6 +216,7 @@ export function setPendingGate(gateId: string): void {
137
216
  */
138
217
  export function clearPendingGate(): void {
139
218
  pendingGateId = null;
219
+ persistWriteGateSnapshot();
140
220
  }
141
221
 
142
222
  /**
@@ -154,11 +234,20 @@ export function getPendingGate(): string | null {
154
234
  * Read-only tools and ask_user_questions itself are always allowed.
155
235
  */
156
236
  export function shouldBlockPendingGate(
237
+ toolName: string,
238
+ milestoneId: string | null,
239
+ queuePhaseActive?: boolean,
240
+ ): { block: boolean; reason?: string } {
241
+ return shouldBlockPendingGateInSnapshot(currentWriteGateSnapshot(), toolName, milestoneId, queuePhaseActive);
242
+ }
243
+
244
+ export function shouldBlockPendingGateInSnapshot(
245
+ snapshot: WriteGateSnapshot,
157
246
  toolName: string,
158
247
  _milestoneId: string | null,
159
248
  _queuePhaseActive?: boolean,
160
249
  ): { block: boolean; reason?: string } {
161
- if (!pendingGateId) return { block: false };
250
+ if (!snapshot.pendingGateId) return { block: false };
162
251
 
163
252
  if (GATE_SAFE_TOOLS.has(toolName)) return { block: false };
164
253
 
@@ -168,7 +257,7 @@ export function shouldBlockPendingGate(
168
257
  return {
169
258
  block: true,
170
259
  reason: [
171
- `HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
260
+ `HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`,
172
261
  `You MUST re-call ask_user_questions with the gate question before making any other tool calls.`,
173
262
  `If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
174
263
  `did not match a provided option, you MUST re-ask — never rationalize past the block.`,
@@ -182,11 +271,20 @@ export function shouldBlockPendingGate(
182
271
  * Read-only bash commands are allowed; mutating commands are blocked.
183
272
  */
184
273
  export function shouldBlockPendingGateBash(
274
+ command: string,
275
+ milestoneId: string | null,
276
+ queuePhaseActive?: boolean,
277
+ ): { block: boolean; reason?: string } {
278
+ return shouldBlockPendingGateBashInSnapshot(currentWriteGateSnapshot(), command, milestoneId, queuePhaseActive);
279
+ }
280
+
281
+ export function shouldBlockPendingGateBashInSnapshot(
282
+ snapshot: WriteGateSnapshot,
185
283
  command: string,
186
284
  _milestoneId: string | null,
187
285
  _queuePhaseActive?: boolean,
188
286
  ): { block: boolean; reason?: string } {
189
- if (!pendingGateId) return { block: false };
287
+ if (!snapshot.pendingGateId) return { block: false };
190
288
 
191
289
  // Allow read-only bash commands
192
290
  if (BASH_READ_ONLY_RE.test(command)) return { block: false };
@@ -194,7 +292,7 @@ export function shouldBlockPendingGateBash(
194
292
  return {
195
293
  block: true,
196
294
  reason: [
197
- `HARD BLOCK: Discussion gate "${pendingGateId}" has not been confirmed by the user.`,
295
+ `HARD BLOCK: Discussion gate "${snapshot.pendingGateId}" has not been confirmed by the user.`,
198
296
  `You MUST re-call ask_user_questions with the gate question before running mutating commands.`,
199
297
  `If the previous ask_user_questions call failed, errored, was cancelled, or the user's response`,
200
298
  `did not match a provided option, you MUST re-ask — never rationalize past the block.`,
@@ -275,6 +373,15 @@ export function shouldBlockContextArtifactSave(
275
373
  artifactType: string,
276
374
  milestoneId: string | null,
277
375
  sliceId?: string | null,
376
+ ): { block: boolean; reason?: string } {
377
+ return shouldBlockContextArtifactSaveInSnapshot(currentWriteGateSnapshot(), artifactType, milestoneId, sliceId);
378
+ }
379
+
380
+ export function shouldBlockContextArtifactSaveInSnapshot(
381
+ snapshot: WriteGateSnapshot,
382
+ artifactType: string,
383
+ milestoneId: string | null,
384
+ sliceId?: string | null,
278
385
  ): { block: boolean; reason?: string } {
279
386
  if (artifactType !== "CONTEXT") return { block: false };
280
387
  if (sliceId) return { block: false };
@@ -287,7 +394,7 @@ export function shouldBlockContextArtifactSave(
287
394
  ].join(" "),
288
395
  };
289
396
  }
290
- if (isMilestoneDepthVerified(milestoneId)) return { block: false };
397
+ if (isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId)) return { block: false };
291
398
 
292
399
  return {
293
400
  block: true,
@@ -317,6 +424,15 @@ export function shouldBlockQueueExecution(
317
424
  toolName: string,
318
425
  input: string,
319
426
  queuePhaseActive: boolean,
427
+ ): { block: boolean; reason?: string } {
428
+ return shouldBlockQueueExecutionInSnapshot(currentWriteGateSnapshot(), toolName, input, queuePhaseActive);
429
+ }
430
+
431
+ export function shouldBlockQueueExecutionInSnapshot(
432
+ snapshot: WriteGateSnapshot,
433
+ toolName: string,
434
+ input: string,
435
+ queuePhaseActive: boolean = snapshot.activeQueuePhase,
320
436
  ): { block: boolean; reason?: string } {
321
437
  if (!queuePhaseActive) return { block: false };
322
438
 
@@ -70,7 +70,7 @@ export const TOP_LEVEL_SUBCOMMANDS: readonly GsdCommandDefinition[] = [
70
70
  { cmd: "templates", desc: "List available workflow templates" },
71
71
  { cmd: "extensions", desc: "Manage extensions (list, enable, disable, info)" },
72
72
  { cmd: "fast", desc: "Toggle OpenAI service tier (on/off/flex/status)" },
73
- { cmd: "mcp", desc: "MCP server status and connectivity check (status, check <server>)" },
73
+ { cmd: "mcp", desc: "MCP server status, connectivity, and local config bootstrap (status, check, init)" },
74
74
  { cmd: "rethink", desc: "Conversational project reorganization — reorder, park, discard, add milestones" },
75
75
  { cmd: "workflow", desc: "Custom workflow lifecycle (new, run, list, validate, pause, resume)" },
76
76
  { cmd: "codebase", desc: "Generate, refresh, and inspect the codebase map cache (.gsd/CODEBASE.md)" },
@@ -201,6 +201,7 @@ const NESTED_COMPLETIONS: CompletionMap = {
201
201
  mcp: [
202
202
  { cmd: "status", desc: "Show all MCP server statuses (default)" },
203
203
  { cmd: "check", desc: "Detailed status for a specific server" },
204
+ { cmd: "init", desc: "Write .mcp.json for the local GSD workflow MCP server" },
204
205
  ],
205
206
  doctor: [
206
207
  { cmd: "fix", desc: "Auto-fix detected issues" },
@@ -60,7 +60,7 @@ export function showHelp(ctx: ExtensionCommandContext): void {
60
60
  " /gsd hooks Show post-unit hook configuration",
61
61
  " /gsd extensions Manage extensions [list|enable|disable|info]",
62
62
  " /gsd fast Toggle OpenAI service tier [on|off|flex|status]",
63
- " /gsd mcp MCP server status and connectivity [status|check <server>]",
63
+ " /gsd mcp MCP server status and connectivity [status|check <server>|init [dir]]",
64
64
  "",
65
65
  "MAINTENANCE",
66
66
  " /gsd doctor Diagnose and repair .gsd/ state [audit|fix|heal] [scope]",
@@ -194,6 +194,56 @@ function sortModelsForSelection(models: Model<any>[], currentModel: Model<any> |
194
194
  });
195
195
  }
196
196
 
197
+ function buildProviderModelGroups(
198
+ models: Model<any>[],
199
+ currentModel: Model<any> | undefined,
200
+ ): Map<string, Model<any>[]> {
201
+ const byProvider = new Map<string, Model<any>[]>();
202
+
203
+ for (const model of sortModelsForSelection(models, currentModel)) {
204
+ let group = byProvider.get(model.provider);
205
+ if (!group) {
206
+ group = [];
207
+ byProvider.set(model.provider, group);
208
+ }
209
+ group.push(model);
210
+ }
211
+ return byProvider;
212
+ }
213
+
214
+ async function selectModelByProvider(
215
+ title: string,
216
+ models: Model<any>[],
217
+ ctx: ExtensionCommandContext,
218
+ currentModel: Model<any> | undefined,
219
+ ): Promise<Model<any> | undefined> {
220
+ const byProvider = buildProviderModelGroups(models, currentModel);
221
+ const providerOptions = Array.from(byProvider.entries()).map(([provider, group]) =>
222
+ `${provider} (${group.length} model${group.length === 1 ? "" : "s"})`,
223
+ );
224
+ providerOptions.push("(cancel)");
225
+
226
+ const providerChoice = await ctx.ui.select(`${title} — choose provider:`, providerOptions);
227
+ if (!providerChoice || typeof providerChoice !== "string" || providerChoice === "(cancel)") return undefined;
228
+
229
+ const providerName = providerChoice.replace(/ \(\d+ models?\)$/, "");
230
+ const providerModels = byProvider.get(providerName);
231
+ if (!providerModels || providerModels.length === 0) return undefined;
232
+
233
+ const optionToModel = new Map<string, Model<any>>();
234
+ const modelOptions = providerModels.map((model) => {
235
+ const isCurrent = currentModel && model.provider === currentModel.provider && model.id === currentModel.id;
236
+ const label = `${isCurrent ? "* " : ""}${model.id}`;
237
+ optionToModel.set(label, model);
238
+ return label;
239
+ });
240
+ modelOptions.push("(cancel)");
241
+
242
+ const modelChoice = await ctx.ui.select(`${title} — ${providerName}:`, modelOptions);
243
+ if (!modelChoice || typeof modelChoice !== "string" || modelChoice === "(cancel)") return undefined;
244
+ return optionToModel.get(modelChoice);
245
+ }
246
+
197
247
  async function resolveRequestedModel(
198
248
  query: string,
199
249
  ctx: ExtensionCommandContext,
@@ -211,19 +261,7 @@ async function resolveRequestedModel(
211
261
 
212
262
  if (partialMatches.length === 1) return partialMatches[0];
213
263
  if (partialMatches.length === 0 || !ctx.hasUI) return undefined;
214
-
215
- const sorted = sortModelsForSelection(partialMatches, ctx.model);
216
- const optionToModel = new Map<string, Model<any>>();
217
- const options = sorted.map((model) => {
218
- const label = `${model.provider}/${model.id}`;
219
- optionToModel.set(label, model);
220
- return label;
221
- });
222
- options.push("(cancel)");
223
-
224
- const choice = await ctx.ui.select(`Multiple models match "${query}" — choose one:`, options);
225
- if (!choice || typeof choice !== "string" || choice === "(cancel)") return undefined;
226
- return optionToModel.get(choice);
264
+ return selectModelByProvider(`Multiple models match "${query}"`, partialMatches, ctx, ctx.model);
227
265
  }
228
266
 
229
267
  async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi: ExtensionAPI | undefined): Promise<void> {
@@ -247,18 +285,7 @@ async function handleModel(trimmedArgs: string, ctx: ExtensionCommandContext, pi
247
285
  return;
248
286
  }
249
287
 
250
- const optionToModel = new Map<string, Model<any>>();
251
- const options = sortModelsForSelection(availableModels, ctx.model).map((model) => {
252
- const isCurrent = ctx.model && model.provider === ctx.model.provider && model.id === ctx.model.id;
253
- const label = `${isCurrent ? "* " : ""}${model.provider}/${model.id}`;
254
- optionToModel.set(label, model);
255
- return label;
256
- });
257
- options.push("(cancel)");
258
-
259
- const choice = await ctx.ui.select("Select session model:", options);
260
- if (!choice || typeof choice !== "string" || choice === "(cancel)") return;
261
- targetModel = optionToModel.get(choice);
288
+ targetModel = await selectModelByProvider("Select session model:", availableModels, ctx, ctx.model);
262
289
  } else {
263
290
  targetModel = await resolveRequestedModel(trimmed, ctx);
264
291
  }
@@ -8,7 +8,13 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
8
8
  getArgumentCompletions: getGsdArgumentCompletions,
9
9
  handler: async (args: string, ctx: ExtensionCommandContext) => {
10
10
  const { handleGSDCommand } = await import("./dispatcher.js");
11
- await handleGSDCommand(args, ctx, pi);
11
+ const { setStderrLoggingEnabled } = await import("../workflow-logger.js");
12
+ const previousStderrSetting = setStderrLoggingEnabled(false);
13
+ try {
14
+ await handleGSDCommand(args, ctx, pi);
15
+ } finally {
16
+ setStderrLoggingEnabled(previousStderrSetting);
17
+ }
12
18
  },
13
19
  });
14
20
  }
@@ -7,12 +7,15 @@
7
7
  * /gsd mcp — Overview of all servers (alias: /gsd mcp status)
8
8
  * /gsd mcp status — Same as bare /gsd mcp
9
9
  * /gsd mcp check <srv> — Detailed status for a specific server
10
+ * /gsd mcp init [dir] — Write project-local GSD workflow MCP config
10
11
  */
11
12
 
12
13
  import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
13
14
 
14
15
  import { existsSync, readFileSync } from "node:fs";
15
- import { join } from "node:path";
16
+ import { join, resolve } from "node:path";
17
+
18
+ import { ensureProjectWorkflowMcpConfig } from "./mcp-project-config.js";
16
19
 
17
20
  // ─── Types ──────────────────────────────────────────────────────────────────
18
21
 
@@ -28,6 +31,28 @@ export interface McpServerDetail extends McpServerStatus {
28
31
  tools: string[];
29
32
  }
30
33
 
34
+ export function formatMcpInitResult(
35
+ status: "created" | "updated" | "unchanged",
36
+ configPath: string,
37
+ targetPath: string,
38
+ ): string {
39
+ const summary =
40
+ status === "created"
41
+ ? "Created project MCP config."
42
+ : status === "updated"
43
+ ? "Updated project MCP config."
44
+ : "Project MCP config is already up to date.";
45
+
46
+ return [
47
+ summary,
48
+ "",
49
+ `Project: ${targetPath}`,
50
+ `Config: ${configPath}`,
51
+ "",
52
+ "Claude Code can now load the GSD workflow MCP server from this folder.",
53
+ ].join("\n");
54
+ }
55
+
31
56
  // ─── Config reader (standalone — does not import mcp-client internals) ──────
32
57
 
33
58
  interface McpServerRawConfig {
@@ -94,6 +119,7 @@ export function formatMcpStatusReport(servers: McpServerStatus[]): string {
94
119
  "No MCP servers configured.",
95
120
  "",
96
121
  "Add servers to .mcp.json or .gsd/mcp.json to enable MCP integrations.",
122
+ "Tip: run /gsd mcp init . to write the local GSD workflow MCP config.",
97
123
  "See: https://modelcontextprotocol.io/quickstart",
98
124
  ].join("\n");
99
125
  }
@@ -153,12 +179,31 @@ export async function handleMcpStatus(
153
179
  args: string,
154
180
  ctx: ExtensionCommandContext,
155
181
  ): Promise<void> {
156
- const trimmed = args.trim().toLowerCase();
182
+ const trimmed = args.trim();
183
+ const lowered = trimmed.toLowerCase();
157
184
  const configs = readMcpConfigs();
158
185
 
186
+ // /gsd mcp init [dir]
187
+ if (!lowered || lowered === "status") {
188
+ // handled below
189
+ } else if (lowered === "init" || lowered.startsWith("init ")) {
190
+ const rawPath = trimmed.slice("init".length).trim();
191
+ const targetPath = resolve(rawPath || ".");
192
+ try {
193
+ const result = ensureProjectWorkflowMcpConfig(targetPath);
194
+ ctx.ui.notify(formatMcpInitResult(result.status, result.configPath, targetPath), "info");
195
+ } catch (err) {
196
+ ctx.ui.notify(
197
+ `Failed to prepare MCP config for ${targetPath}: ${err instanceof Error ? err.message : String(err)}`,
198
+ "error",
199
+ );
200
+ }
201
+ return;
202
+ }
203
+
159
204
  // /gsd mcp check <server>
160
- if (trimmed.startsWith("check ")) {
161
- const serverName = args.trim().slice("check ".length).trim();
205
+ if (lowered.startsWith("check ")) {
206
+ const serverName = trimmed.slice("check ".length).trim();
162
207
  const config = configs.find((c) => c.name === serverName);
163
208
  if (!config) {
164
209
  const available = configs.map((c) => c.name).join(", ") || "(none)";
@@ -202,7 +247,7 @@ export async function handleMcpStatus(
202
247
  }
203
248
 
204
249
  // /gsd mcp or /gsd mcp status
205
- if (!trimmed || trimmed === "status") {
250
+ if (!lowered || lowered === "status") {
206
251
  // Build status for each server
207
252
  const statuses: McpServerStatus[] = [];
208
253
 
@@ -239,9 +284,10 @@ export async function handleMcpStatus(
239
284
 
240
285
  // Unknown subcommand
241
286
  ctx.ui.notify(
242
- "Usage: /gsd mcp [status|check <server>]\n\n" +
287
+ "Usage: /gsd mcp [status|check <server>|init [dir]]\n\n" +
243
288
  " status Show all MCP server statuses (default)\n" +
244
- " check <server> Detailed status for a specific server",
289
+ " check <server> Detailed status for a specific server\n" +
290
+ " init [dir] Write .mcp.json for the local GSD workflow MCP server",
245
291
  "warning",
246
292
  );
247
293
  }
@@ -10,7 +10,7 @@ import { deriveState, isMilestoneComplete } from "./state.js";
10
10
  import { listWorktrees, resolveGitDir, worktreesDir } from "./worktree-manager.js";
11
11
  import { abortAndReset } from "./git-self-heal.js";
12
12
  import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch, writeIntegrationBranch } from "./git-service.js";
13
- import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeWorktreeList, nativeWorktreeRemove, nativeBranchList, nativeBranchDelete, nativeLsFiles, nativeRmCached, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
14
14
  import { getAllWorktreeHealth } from "./worktree-health.js";
15
15
  import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
 
@@ -386,19 +386,19 @@ export async function checkGitHealth(
386
386
  code: "stale_uncommitted_changes",
387
387
  scope: "project",
388
388
  unitId: "project",
389
- message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting uncommitted changes.`,
389
+ message: `Uncommitted changes detected with no commit in ${mins} minute${mins === 1 ? "" : "s"} (threshold: ${thresholdMinutes}m). Snapshotting tracked files.`,
390
390
  fixable: true,
391
391
  });
392
392
 
393
393
  if (shouldFix("stale_uncommitted_changes")) {
394
394
  try {
395
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
395
+ nativeAddTracked(basePath);
396
396
  const commitMsg = `gsd snapshot: uncommitted changes after ${mins}m inactivity`;
397
397
  const result = nativeCommit(basePath, commitMsg);
398
398
  if (result) {
399
399
  fixesApplied.push(`created gsd snapshot after ${mins}m of uncommitted changes`);
400
400
  } else {
401
- fixesApplied.push("gsd snapshot skipped — nothing to commit after staging changes");
401
+ fixesApplied.push("gsd snapshot skipped — nothing to commit after staging tracked files");
402
402
  }
403
403
  } catch {
404
404
  fixesApplied.push("failed to create gsd snapshot commit");
@@ -21,8 +21,8 @@ import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.j
21
21
  import { abortAndReset } from "./git-self-heal.js";
22
22
  import { rebuildState } from "./doctor.js";
23
23
  import { deriveState } from "./state.js";
24
- import { RUNTIME_EXCLUSION_PATHS, resolveMilestoneIntegrationBranch } from "./git-service.js";
25
- import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddAllWithExclusions, nativeCommit } from "./native-git-bridge.js";
24
+ import { resolveMilestoneIntegrationBranch } from "./git-service.js";
25
+ import { nativeIsRepo, nativeHasChanges, nativeLastCommitEpoch, nativeGetCurrentBranch, nativeAddTracked, nativeCommit } from "./native-git-bridge.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { runEnvironmentChecks } from "./doctor-environment.js";
28
28
 
@@ -312,7 +312,7 @@ export async function preDispatchHealthGate(basePath: string): Promise<PreDispat
312
312
  if (minutesSinceCommit >= thresholdMinutes) {
313
313
  const mins = Math.floor(minutesSinceCommit);
314
314
  try {
315
- nativeAddAllWithExclusions(basePath, RUNTIME_EXCLUSION_PATHS);
315
+ nativeAddTracked(basePath);
316
316
  const commitMsg = `gsd snapshot: pre-dispatch, uncommitted changes after ${mins}m inactivity`;
317
317
  const result = nativeCommit(basePath, commitMsg);
318
318
  if (result) {
@@ -8,6 +8,7 @@ import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSl
8
8
  import { deriveState, isMilestoneComplete } from "./state.js";
9
9
  import { invalidateAllCaches } from "./cache.js";
10
10
  import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
11
+ import { isClosedStatus } from "./status-guards.js";
11
12
 
12
13
  import type { DoctorIssue, DoctorIssueCode, DoctorReport } from "./doctor-types.js";
13
14
  import { GLOBAL_STATE_CODES } from "./doctor-types.js";
@@ -474,15 +475,16 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
474
475
  if (!roadmapContent) continue;
475
476
 
476
477
  // Normalize slices: prefer DB, fall back to parser
477
- type NormSlice = RoadmapSliceEntry & { pending?: boolean };
478
+ type NormSlice = RoadmapSliceEntry & { pending?: boolean; skipped?: boolean };
478
479
  let slices: NormSlice[];
479
480
  if (isDbAvailable()) {
480
481
  const dbSlices = getMilestoneSlices(milestoneId);
481
482
  slices = dbSlices.map(s => ({
482
483
  id: s.id,
483
484
  title: s.title,
484
- done: s.status === "complete",
485
+ done: isClosedStatus(s.status),
485
486
  pending: s.status === "pending",
487
+ skipped: s.status === "skipped",
486
488
  risk: (s.risk || "medium") as RoadmapSliceEntry["risk"],
487
489
  depends: s.depends,
488
490
  demo: s.demo,
@@ -578,8 +580,9 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
578
580
  const slicePath = resolveSlicePath(basePath, milestoneId, slice.id);
579
581
  if (!slicePath) {
580
582
  // Pending slices haven't been planned yet — directories are created
581
- // lazily by ensurePreconditions() at dispatch time. Skip them.
582
- if (slice.pending) continue;
583
+ // lazily by ensurePreconditions() at dispatch time. Skipped slices are
584
+ // intentionally allowed to remain summary-less and directory-less.
585
+ if (slice.pending || slice.skipped) continue;
583
586
  const expectedPath = relSlicePath(basePath, milestoneId, slice.id);
584
587
  issues.push({
585
588
  severity: slice.done ? "warning" : "error",
@@ -603,7 +606,8 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
603
606
  const tasksDir = resolveTasksDir(basePath, milestoneId, slice.id);
604
607
  if (!tasksDir) {
605
608
  // Pending slices haven't been planned yet — tasks/ is created on demand.
606
- if (slice.pending) continue;
609
+ // Skipped slices may legitimately never create tasks/.
610
+ if (slice.pending || slice.skipped) continue;
607
611
  issues.push({
608
612
  severity: slice.done ? "warning" : "error",
609
613
  code: "missing_tasks_dir",
@@ -778,6 +778,7 @@ let currentDb: DbAdapter | null = null;
778
778
  let currentPath: string | null = null;
779
779
  let currentPid: number = 0;
780
780
  let _exitHandlerRegistered = false;
781
+ let _dbOpenAttempted = false;
781
782
 
782
783
  export function getDbProvider(): ProviderName | null {
783
784
  loadProvider();
@@ -788,7 +789,18 @@ export function isDbAvailable(): boolean {
788
789
  return currentDb !== null;
789
790
  }
790
791
 
792
+ /**
793
+ * Returns true if openDatabase() has been called at least once this session.
794
+ * Used to distinguish "DB not yet initialized" from "DB genuinely unavailable"
795
+ * so that early callers (e.g. before_agent_start context injection) don't
796
+ * trigger a false degraded-mode warning.
797
+ */
798
+ export function wasDbOpenAttempted(): boolean {
799
+ return _dbOpenAttempted;
800
+ }
801
+
791
802
  export function openDatabase(path: string): boolean {
803
+ _dbOpenAttempted = true;
792
804
  if (currentDb && currentPath !== path) closeDatabase();
793
805
  if (currentDb && currentPath === path) return true;
794
806