gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27

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 (212) hide show
  1. package/README.md +5 -7
  2. package/dist/help-text.js +1 -1
  3. package/dist/resource-loader.js +6 -1
  4. package/dist/resources/.managed-resources-content-hash +1 -1
  5. package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
  6. package/dist/resources/extensions/gsd/auto/loop.js +235 -36
  7. package/dist/resources/extensions/gsd/auto/phases.js +14 -7
  8. package/dist/resources/extensions/gsd/auto/session.js +36 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
  11. package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
  12. package/dist/resources/extensions/gsd/auto.js +139 -49
  13. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
  15. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  16. package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
  17. package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
  18. package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
  19. package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
  20. package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
  21. package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
  22. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  23. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  24. package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  25. package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
  26. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
  27. package/dist/resources/extensions/gsd/doctor.js +12 -2
  28. package/dist/resources/extensions/gsd/gsd-db.js +355 -3
  29. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  30. package/dist/resources/extensions/gsd/guided-flow.js +116 -26
  31. package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
  32. package/dist/resources/extensions/gsd/metrics.js +287 -1
  33. package/dist/resources/extensions/gsd/paths.js +79 -8
  34. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  35. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  38. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  40. package/dist/resources/extensions/gsd/state.js +21 -6
  41. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  42. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  43. package/dist/resources/extensions/gsd/workspace.js +59 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
  45. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  46. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  47. package/dist/web/standalone/.next/BUILD_ID +1 -1
  48. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  49. package/dist/web/standalone/.next/build-manifest.json +2 -2
  50. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  51. package/dist/web/standalone/.next/required-server-files.json +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.html +1 -1
  69. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  76. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  78. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  79. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  80. package/dist/web/standalone/server.js +1 -1
  81. package/package.json +1 -1
  82. package/packages/mcp-server/README.md +2 -11
  83. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  84. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  85. package/packages/mcp-server/dist/remote-questions.js +28 -0
  86. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  87. package/packages/mcp-server/dist/server.d.ts +28 -0
  88. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  89. package/packages/mcp-server/dist/server.js +94 -4
  90. package/packages/mcp-server/dist/server.js.map +1 -1
  91. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  92. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  93. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  94. package/packages/mcp-server/src/remote-questions.ts +35 -0
  95. package/packages/mcp-server/src/server.ts +129 -6
  96. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  97. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  98. package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
  99. package/src/resources/extensions/gsd/auto/loop.ts +263 -41
  100. package/src/resources/extensions/gsd/auto/phases.ts +15 -7
  101. package/src/resources/extensions/gsd/auto/session.ts +40 -0
  102. package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
  103. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
  104. package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
  105. package/src/resources/extensions/gsd/auto.ts +166 -43
  106. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  107. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
  108. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  109. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  110. package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
  111. package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
  112. package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
  113. package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
  114. package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
  115. package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
  116. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  117. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  118. package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
  119. package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
  120. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
  121. package/src/resources/extensions/gsd/doctor.ts +10 -2
  122. package/src/resources/extensions/gsd/gsd-db.ts +354 -3
  123. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  124. package/src/resources/extensions/gsd/guided-flow.ts +152 -26
  125. package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
  126. package/src/resources/extensions/gsd/metrics.ts +321 -1
  127. package/src/resources/extensions/gsd/paths.ts +67 -8
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  129. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  130. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  131. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  134. package/src/resources/extensions/gsd/state.ts +44 -6
  135. package/src/resources/extensions/gsd/templates/project.md +10 -0
  136. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  137. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
  138. package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
  139. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  140. package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
  141. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  142. package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
  143. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
  144. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
  145. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  146. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  147. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  148. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  149. package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
  150. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  151. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  152. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  153. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  154. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  155. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  156. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  157. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  158. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  159. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  160. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
  161. package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
  162. package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
  163. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
  164. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
  165. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
  166. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
  167. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  168. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  169. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  170. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  171. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  172. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  173. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
  174. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  175. package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
  176. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  177. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  178. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  179. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
  180. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  181. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
  182. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
  183. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  184. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
  185. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  186. package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
  187. package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
  188. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
  189. package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
  190. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
  191. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  192. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
  193. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  194. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
  196. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
  197. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  198. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  199. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  200. package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
  201. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  202. package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
  203. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  204. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  205. package/src/resources/extensions/gsd/workspace.ts +95 -0
  206. package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
  207. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  208. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
  209. package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
  210. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
  211. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
  212. /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
@@ -63,20 +63,42 @@ const QUEUE_SAFE_TOOLS = new Set([
63
63
  */
64
64
  const BASH_READ_ONLY_RE = /^\s*(cat|head|tail|less|more|wc|file|stat|du|df|which|type|echo|printf|ls|find|grep|rg|awk|sed\b(?!.*-i)|sort|uniq|diff|comm|tr|cut|tee\s+-a\s+\/dev\/null|git\s+(log|show|diff|status|branch|tag|remote|rev-parse|ls-files|blame|shortlog|describe|stash\s+list|config\s+--get|cat-file)|gh\s+(issue|pr|api|repo|release)\s+(view|list|diff|status|checks)|mkdir\s+-p\s+\.gsd|rtk\s|npm\s+run\s+(test|test:\w+|lint|lint:\w+|typecheck|type-check|type-check:\w+|check|verify|audit|outdated|format:check|ci|validate)\b|npm\s+(ls|list|info|view|show|outdated|audit|explain|doctor|ping|--version|-v)\b|npx\s|tsx\s|node\s+(--print|--version|-v\b)|python[23]?\s+(-c\s+'[^']*'|--version|-V\b|-m\s+(pip\s+show|pip\s+list|site))|pip[23]?\s+(show|list|freeze|check|index\s+versions)\b|jq\s|yq\s|curl\s+(-s\b|--silent\b)(?!\s+[^|>]*\s-[oO]\b)(?!\s+[^|>]*\s--output\b)[^|>]*$|openssl\s+(version|x509|s_client)|env\b|printenv\b|true\b|false\b)/;
65
65
 
66
- const verifiedDepthMilestones = new Set<string>();
67
- const verifiedApprovalGates = new Set<string>();
68
- let activeQueuePhase = false;
66
+ interface InMemoryWriteGateState {
67
+ verifiedDepthMilestones: Set<string>;
68
+ verifiedApprovalGates: Set<string>;
69
+ activeQueuePhase: boolean;
70
+ pendingGateId: string | null;
71
+ }
72
+
73
+ function createEmptyWriteGateState(): InMemoryWriteGateState {
74
+ return {
75
+ verifiedDepthMilestones: new Set<string>(),
76
+ verifiedApprovalGates: new Set<string>(),
77
+ activeQueuePhase: false,
78
+ pendingGateId: null,
79
+ };
80
+ }
81
+
82
+ const writeGateStatesByBasePath = new Map<string, InMemoryWriteGateState>();
83
+
84
+ function writeGateStateKey(basePath: string): string {
85
+ return resolve(basePath);
86
+ }
87
+
88
+ function getWriteGateState(basePath: string = process.cwd()): InMemoryWriteGateState {
89
+ const key = writeGateStateKey(basePath);
90
+ let state = writeGateStatesByBasePath.get(key);
91
+ if (!state) {
92
+ state = createEmptyWriteGateState();
93
+ writeGateStatesByBasePath.set(key, state);
94
+ }
95
+ return state;
96
+ }
69
97
 
70
98
  /**
71
- * Discussion gate enforcement state.
72
- *
73
- * When ask_user_questions is called with a recognized gate question ID,
74
- * we track the pending gate. Until the gate is confirmed (user selects the
75
- * first/recommended option), all non-read-only tool calls are blocked.
76
- * This mechanically prevents the model from rationalizing past failed or
77
- * cancelled gate questions.
99
+ * Discussion gate enforcement state is scoped per basePath so multiple
100
+ * workspaces can coexist in the same process without sharing gate state.
78
101
  */
79
- let pendingGateId: string | null = null;
80
102
 
81
103
  /**
82
104
  * Recognized gate question ID patterns.
@@ -119,25 +141,26 @@ function shouldPersistWriteGateSnapshot(env: NodeJS.ProcessEnv = process.env): b
119
141
  return v !== "0" && v !== "false";
120
142
  }
121
143
 
122
- function writeGateSnapshotPath(basePath: string = process.cwd()): string {
144
+ function writeGateSnapshotPath(basePath: string): string {
123
145
  return join(basePath, ".gsd", "runtime", "write-gate-state.json");
124
146
  }
125
147
 
126
- function currentWriteGateSnapshot(): WriteGateSnapshot {
148
+ function currentWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
149
+ const state = getWriteGateState(basePath);
127
150
  return {
128
- verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
129
- verifiedApprovalGates: [...verifiedApprovalGates].sort(),
130
- activeQueuePhase,
131
- pendingGateId,
151
+ verifiedDepthMilestones: [...state.verifiedDepthMilestones].sort(),
152
+ verifiedApprovalGates: [...state.verifiedApprovalGates].sort(),
153
+ activeQueuePhase: state.activeQueuePhase,
154
+ pendingGateId: state.pendingGateId,
132
155
  };
133
156
  }
134
157
 
135
- function persistWriteGateSnapshot(basePath: string = process.cwd()): void {
158
+ function persistWriteGateSnapshot(basePath: string): void {
136
159
  if (!shouldPersistWriteGateSnapshot()) return;
137
160
  const path = writeGateSnapshotPath(basePath);
138
161
  mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
139
162
  const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
140
- writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
163
+ writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(basePath), null, 2), "utf-8");
141
164
  try {
142
165
  renameSync(tempPath, path);
143
166
  } catch (err: unknown) {
@@ -152,7 +175,7 @@ function persistWriteGateSnapshot(basePath: string = process.cwd()): void {
152
175
  }
153
176
  }
154
177
 
155
- function clearPersistedWriteGateSnapshot(basePath: string = process.cwd()): void {
178
+ function clearPersistedWriteGateSnapshot(basePath: string): void {
156
179
  if (!shouldPersistWriteGateSnapshot()) return;
157
180
  const path = writeGateSnapshotPath(basePath);
158
181
  try {
@@ -185,32 +208,35 @@ const EMPTY_SNAPSHOT: WriteGateSnapshot = {
185
208
  pendingGateId: null,
186
209
  };
187
210
 
188
- export function loadWriteGateSnapshot(basePath: string = process.cwd()): WriteGateSnapshot {
211
+ export function loadWriteGateSnapshot(basePath: string): WriteGateSnapshot {
189
212
  const path = writeGateSnapshotPath(basePath);
190
213
  if (!existsSync(path)) {
191
214
  // When persist mode is active and the file has been deleted, treat it as a
192
215
  // full state reset so deleting the file clears the HARD BLOCK gate.
193
216
  // In non-persist mode the file is never written, so fall back to in-memory.
194
217
  if (shouldPersistWriteGateSnapshot()) return EMPTY_SNAPSHOT;
195
- return currentWriteGateSnapshot();
218
+ return currentWriteGateSnapshot(basePath);
196
219
  }
197
220
  try {
198
221
  return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
199
222
  } catch {
200
- return currentWriteGateSnapshot();
223
+ return currentWriteGateSnapshot(basePath);
201
224
  }
202
225
  }
203
226
 
204
- export function isDepthVerified(): boolean {
205
- return verifiedDepthMilestones.size > 0;
227
+ export function isDepthVerified(basePath: string = process.cwd()): boolean {
228
+ return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
206
229
  }
207
230
 
208
231
  /**
209
232
  * Check whether a specific milestone has passed depth verification.
210
233
  */
211
- export function isMilestoneDepthVerified(milestoneId: string | null | undefined): boolean {
234
+ export function isMilestoneDepthVerified(
235
+ milestoneId: string | null | undefined,
236
+ basePath: string = process.cwd(),
237
+ ): boolean {
212
238
  if (!milestoneId) return false;
213
- return verifiedDepthMilestones.has(milestoneId);
239
+ return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
214
240
  }
215
241
 
216
242
  export function isMilestoneDepthVerifiedInSnapshot(
@@ -221,39 +247,37 @@ export function isMilestoneDepthVerifiedInSnapshot(
221
247
  return snapshot.verifiedDepthMilestones.includes(milestoneId);
222
248
  }
223
249
 
224
- export function isQueuePhaseActive(): boolean {
225
- return activeQueuePhase;
250
+ export function isQueuePhaseActive(basePath: string = process.cwd()): boolean {
251
+ return getWriteGateState(basePath).activeQueuePhase;
226
252
  }
227
253
 
228
- export function setQueuePhaseActive(active: boolean): void {
229
- activeQueuePhase = active;
230
- persistWriteGateSnapshot();
254
+ export function setQueuePhaseActive(active: boolean, basePath: string): void {
255
+ getWriteGateState(basePath).activeQueuePhase = active;
256
+ persistWriteGateSnapshot(basePath);
231
257
  }
232
258
 
233
- export function resetWriteGateState(): void {
234
- verifiedDepthMilestones.clear();
235
- verifiedApprovalGates.clear();
236
- pendingGateId = null;
237
- persistWriteGateSnapshot();
259
+ export function resetWriteGateState(basePath: string): void {
260
+ const state = getWriteGateState(basePath);
261
+ state.verifiedDepthMilestones.clear();
262
+ state.verifiedApprovalGates.clear();
263
+ state.pendingGateId = null;
264
+ persistWriteGateSnapshot(basePath);
238
265
  }
239
266
 
240
- export function clearDiscussionFlowState(): void {
241
- verifiedDepthMilestones.clear();
242
- verifiedApprovalGates.clear();
243
- activeQueuePhase = false;
244
- pendingGateId = null;
245
- clearPersistedWriteGateSnapshot();
267
+ export function clearDiscussionFlowState(basePath: string): void {
268
+ writeGateStatesByBasePath.delete(writeGateStateKey(basePath));
269
+ clearPersistedWriteGateSnapshot(basePath);
246
270
  }
247
271
 
248
272
  export function markDepthVerified(milestoneId?: string | null, basePath: string = process.cwd()): void {
249
273
  if (!milestoneId) return;
250
- verifiedDepthMilestones.add(milestoneId);
274
+ getWriteGateState(basePath).verifiedDepthMilestones.add(milestoneId);
251
275
  persistWriteGateSnapshot(basePath);
252
276
  }
253
277
 
254
278
  export function markApprovalGateVerified(gateId?: string | null, basePath: string = process.cwd()): void {
255
279
  if (!gateId) return;
256
- verifiedApprovalGates.add(gateId);
280
+ getWriteGateState(basePath).verifiedApprovalGates.add(gateId);
257
281
  persistWriteGateSnapshot(basePath);
258
282
  }
259
283
 
@@ -292,27 +316,28 @@ function extractContextMilestoneId(inputPath: string): string | null {
292
316
  /**
293
317
  * Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
294
318
  */
295
- export function setPendingGate(gateId: string): void {
296
- pendingGateId = gateId;
297
- verifiedApprovalGates.delete(gateId);
319
+ export function setPendingGate(gateId: string, basePath: string): void {
320
+ const state = getWriteGateState(basePath);
321
+ state.pendingGateId = gateId;
322
+ state.verifiedApprovalGates.delete(gateId);
298
323
  const milestoneId = extractDepthVerificationMilestoneId(gateId);
299
- if (milestoneId) verifiedDepthMilestones.delete(milestoneId);
300
- persistWriteGateSnapshot();
324
+ if (milestoneId) state.verifiedDepthMilestones.delete(milestoneId);
325
+ persistWriteGateSnapshot(basePath);
301
326
  }
302
327
 
303
328
  /**
304
329
  * Clear the pending gate (called when the user confirms).
305
330
  */
306
- export function clearPendingGate(): void {
307
- pendingGateId = null;
308
- persistWriteGateSnapshot();
331
+ export function clearPendingGate(basePath: string): void {
332
+ getWriteGateState(basePath).pendingGateId = null;
333
+ persistWriteGateSnapshot(basePath);
309
334
  }
310
335
 
311
336
  /**
312
337
  * Get the currently pending gate, if any.
313
338
  */
314
- export function getPendingGate(): string | null {
315
- return pendingGateId;
339
+ export function getPendingGate(basePath: string = process.cwd()): string | null {
340
+ return getWriteGateState(basePath).pendingGateId;
316
341
  }
317
342
 
318
343
  /**
@@ -1,21 +1,43 @@
1
1
  /**
2
- * GSD Crash Recovery
2
+ * GSD Crash Recovery (Phase C pt 2 — DB-backed)
3
3
  *
4
- * Detects interrupted auto-mode sessions via a lock file.
5
- * Written on auto-start, updated on each unit dispatch, deleted on clean stop.
6
- * If the lock file exists on next startup, the previous session crashed.
4
+ * Detects interrupted auto-mode sessions via the DB-backed workers +
5
+ * unit_dispatches + runtime_kv tables. The auto.lock file is gone; the
6
+ * `LockData` shape is preserved for backward compatibility with callers
7
+ * (auto.ts, doctor checks, interrupted-session.ts), but the contents are
8
+ * now synthesized from:
7
9
  *
8
- * The lock records the pi session file path so crash recovery can read the
9
- * surviving JSONL (pi appends entries incrementally via appendFileSync,
10
- * so the file on disk reflects every tool call up to the crash point).
10
+ * - workers.pid / .started_at / .last_heartbeat_at → liveness + age
11
+ * - unit_dispatches.unit_type / .unit_id / .started_at → what was running
12
+ * - runtime_kv("worker", workerId, "session_file") → pi session JSONL path
13
+ *
14
+ * "Crashed" is detected via workers.status='active' + heartbeat past TTL,
15
+ * cross-checked with the OS PID via isLockProcessAlive(). When the DB is
16
+ * unavailable (fresh project before init), all readers return null and
17
+ * writers no-op — preserving the historical "no lock means no prior
18
+ * crash" semantics.
19
+ *
20
+ * The journal-based emitCrashRecoveredUnitEnd is unchanged from the file
21
+ * era — it queries the journal independently of the lock mechanism.
11
22
  */
12
23
 
24
+ import {
25
+ emitJournalEvent,
26
+ queryJournal,
27
+ } from "./journal.js";
13
28
  import { readFileSync, unlinkSync, existsSync } from "node:fs";
14
29
  import { join } from "node:path";
15
- import { gsdRoot } from "./paths.js";
30
+ import {
31
+ findStaleWorkerForProject,
32
+ getAllAutoWorkers,
33
+ type AutoWorkerRow,
34
+ } from "./db/auto-workers.js";
35
+ import { getLatestForUnit, type DispatchStatus } from "./db/unit-dispatches.js";
36
+ import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
37
+ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
38
+ import { gsdRoot, normalizeRealPath } from "./paths.js";
16
39
  import { atomicWriteSync } from "./atomic-write.js";
17
40
  import { effectiveLockFile } from "./session-lock.js";
18
- import { emitJournalEvent, queryJournal } from "./journal.js";
19
41
 
20
42
  export interface LockData {
21
43
  pid: number;
@@ -27,11 +49,91 @@ export interface LockData {
27
49
  sessionFile?: string;
28
50
  }
29
51
 
52
+ const SESSION_FILE_KV_KEY = "session_file";
53
+
30
54
  function lockPath(basePath: string): string {
31
55
  return join(gsdRoot(basePath), effectiveLockFile());
32
56
  }
33
57
 
34
- /** Write or update the lock file with current auto-mode state. */
58
+ function readLegacyLock(basePath: string): LockData | null {
59
+ try {
60
+ const p = lockPath(basePath);
61
+ if (!existsSync(p)) return null;
62
+ return JSON.parse(readFileSync(p, "utf-8")) as LockData;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function findActiveWorkerForCurrentProcess(
69
+ projectRootRealpath: string,
70
+ ): AutoWorkerRow | null {
71
+ if (!isDbAvailable()) return null;
72
+ const workers = getAllAutoWorkers();
73
+ for (const worker of workers) {
74
+ if (
75
+ worker.pid === process.pid
76
+ && worker.project_root_realpath === projectRootRealpath
77
+ ) {
78
+ return worker;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Look up the most recent dispatch row for a worker, regardless of status.
86
+ * Returns null if the worker has no dispatch history yet (e.g. crashed
87
+ * during bootstrap before claiming the first unit).
88
+ */
89
+ function getLatestDispatchForWorker(workerId: string):
90
+ | { unit_type: string; unit_id: string; started_at: string; status: DispatchStatus }
91
+ | null {
92
+ if (!isDbAvailable()) return null;
93
+ const db = _getAdapter()!;
94
+ const row = db.prepare(
95
+ `SELECT unit_type, unit_id, started_at, status
96
+ FROM unit_dispatches
97
+ WHERE worker_id = :worker_id
98
+ ORDER BY id DESC
99
+ LIMIT 1`,
100
+ ).get({ ":worker_id": workerId }) as
101
+ | { unit_type: string; unit_id: string; started_at: string; status: DispatchStatus }
102
+ | undefined;
103
+ return row ?? null;
104
+ }
105
+
106
+ function workerToLockData(worker: AutoWorkerRow): LockData {
107
+ const dispatch = getLatestDispatchForWorker(worker.worker_id);
108
+ const sessionFile =
109
+ getRuntimeKv<string>("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
110
+ return {
111
+ pid: worker.pid,
112
+ startedAt: worker.started_at,
113
+ // Pre-Phase-C-pt-2 default: when no dispatch row exists yet (bootstrap
114
+ // crash), report unitType="starting", unitId="bootstrap" — same shape
115
+ // the file-based writer used to produce.
116
+ unitType: dispatch?.unit_type ?? "starting",
117
+ unitId: dispatch?.unit_id ?? "bootstrap",
118
+ unitStartedAt: dispatch?.started_at ?? worker.started_at,
119
+ sessionFile,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Write or update the lock state for the current auto-mode session.
125
+ *
126
+ * Phase C pt 2: the only persistent state this function adds beyond what
127
+ * the workers + unit_dispatches tables already track is the pi session
128
+ * JSONL path, which lands in runtime_kv (worker scope, key
129
+ * "session_file"). The pid/startedAt/unitType/unitId/unitStartedAt are
130
+ * recorded by registerAutoWorker / heartbeatAutoWorker / recordDispatchClaim
131
+ * already.
132
+ *
133
+ * basePath is unused by the new implementation (kept as a parameter for
134
+ * back-compat with the 15+ call sites) — the worker is identified by
135
+ * pid + project_root_realpath in the workers table.
136
+ */
35
137
  export function writeLock(
36
138
  basePath: string,
37
139
  unitType: string,
@@ -47,51 +149,84 @@ export function writeLock(
47
149
  unitStartedAt: new Date().toISOString(),
48
150
  sessionFile,
49
151
  };
50
- const lp = lockPath(basePath);
51
- atomicWriteSync(lp, JSON.stringify(data, null, 2));
52
- } catch (e) { /* non-fatal: lock write failure */ void e; }
152
+ atomicWriteSync(lockPath(basePath), JSON.stringify(data, null, 2));
153
+ } catch {
154
+ // Best-effort never throw from the lock writer.
155
+ }
156
+
157
+ if (!isDbAvailable() || !sessionFile) return;
158
+ try {
159
+ const projectRoot = normalizeRealPath(basePath);
160
+ const worker = findActiveWorkerForCurrentProcess(projectRoot);
161
+ if (!worker) return;
162
+ setRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY, sessionFile);
163
+ } catch {
164
+ // Best-effort — never throw from the lock writer.
165
+ }
53
166
  }
54
167
 
55
- /** Remove the lock file on clean stop. */
168
+ /**
169
+ * Phase C pt 2: clearLock no longer deletes a file. The cleanup path
170
+ * (markWorkerStopping in stopAuto) flips the workers row to 'stopping'.
171
+ * This function additionally drops the session_file runtime_kv row for
172
+ * the current worker so a follow-up crash detection doesn't pick up a
173
+ * stale session-file pointer.
174
+ */
56
175
  export function clearLock(basePath: string): void {
57
176
  try {
58
177
  const p = lockPath(basePath);
59
178
  if (existsSync(p)) unlinkSync(p);
60
- } catch (e) { /* non-fatal: lock clear failure */ void e; }
179
+ } catch {
180
+ // Best-effort.
181
+ }
182
+
183
+ if (!isDbAvailable()) return;
184
+ try {
185
+ const projectRoot = normalizeRealPath(basePath);
186
+ const worker = findActiveWorkerForCurrentProcess(projectRoot);
187
+ if (!worker) return;
188
+ deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
189
+ } catch {
190
+ // Best-effort.
191
+ }
61
192
  }
62
193
 
63
- /** Check if a crash lock exists and return its data. */
194
+ /**
195
+ * Detect a previous crashed auto-mode session.
196
+ *
197
+ * Phase C pt 2: synthesized from workers (status='active' + lapsed
198
+ * heartbeat) + unit_dispatches (most recent for that worker) +
199
+ * runtime_kv (session_file). Returns null when no stale worker exists
200
+ * or the DB is unavailable.
201
+ */
64
202
  export function readCrashLock(basePath: string): LockData | null {
65
- try {
66
- const p = lockPath(basePath);
67
- if (!existsSync(p)) return null;
68
- const raw = readFileSync(p, "utf-8");
69
- return JSON.parse(raw) as LockData;
70
- } catch (e) {
71
- /* non-fatal: corrupt or unreadable lock file */ void e;
72
- return null;
203
+ if (isDbAvailable()) {
204
+ try {
205
+ const projectRoot = normalizeRealPath(basePath);
206
+ const stale = findStaleWorkerForProject(projectRoot);
207
+ if (stale) return workerToLockData(stale);
208
+ } catch {
209
+ // Fall through to the legacy lock-file compatibility path.
210
+ }
73
211
  }
212
+ return readLegacyLock(basePath);
74
213
  }
75
214
 
76
215
  /**
77
216
  * Check whether the process that wrote the lock is still running.
78
217
  * Uses `process.kill(pid, 0)` which sends no signal but checks liveness.
79
218
  * Returns true if the PID matches our own — we are the lock holder (#2470).
219
+ *
220
+ * Unchanged from the file-based era — pure stateless OS check.
80
221
  */
81
222
  export function isLockProcessAlive(lock: LockData): boolean {
82
223
  const pid = lock.pid;
83
224
  if (!Number.isInteger(pid) || pid <= 0) return false;
84
- // Our own PID means WE hold this lock — we are alive. (#2470)
85
- // Callers that need to distinguish "our lock" from "someone else's lock"
86
- // (e.g. startAuto checking for a prior crashed session with a recycled PID)
87
- // already guard with `crashLock.pid !== process.pid` before calling us.
88
225
  if (pid === process.pid) return true;
89
226
  try {
90
227
  process.kill(pid, 0);
91
228
  return true;
92
229
  } catch (err) {
93
- // EPERM means the process exists but we lack permission — treat as alive.
94
- // ESRCH means the process does not exist — treat as dead (stale lock).
95
230
  if ((err as NodeJS.ErrnoException).code === "EPERM") return true;
96
231
  return false;
97
232
  }
@@ -106,7 +241,6 @@ export function formatCrashInfo(lock: LockData): string {
106
241
  ` PID: ${lock.pid}`,
107
242
  ];
108
243
 
109
- // Add recovery guidance based on what was happening when it crashed
110
244
  if (lock.unitType === "starting" && lock.unitId === "bootstrap") {
111
245
  lines.push(`No work was lost. Run /gsd auto to restart.`);
112
246
  } else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
@@ -122,22 +256,14 @@ export function formatCrashInfo(lock: LockData): string {
122
256
 
123
257
  /**
124
258
  * Emit a synthetic unit-end event for a unit that crashed without emitting its own.
125
- *
126
- * Queries the journal to find the most recent unit-start for the crashed unit.
127
- * If a matching unit-end already exists (e.g. the hard timeout fired), this is a
128
- * no-op. Called during crash recovery, before clearing the stale lock.
129
- *
130
- * Addresses the gap reported in #3348 where `unit-start` was emitted but no
131
- * `unit-end` followed — side effects landed but the worker died before closeout.
259
+ * Unchanged from the file era — operates on the journal, not the lock.
132
260
  */
133
261
  export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): void {
134
- // Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
135
262
  if (!lock.unitType || !lock.unitId || lock.unitType === "starting") return;
136
263
 
137
264
  try {
138
265
  const all = queryJournal(basePath);
139
266
 
140
- // Find the most recent unit-start for this unitId
141
267
  const starts = all.filter(
142
268
  (e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId,
143
269
  );
@@ -145,7 +271,6 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
145
271
 
146
272
  const lastStart = starts[starts.length - 1];
147
273
 
148
- // Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
149
274
  const alreadyClosed = all.some(
150
275
  (e) =>
151
276
  e.eventType === "unit-end" &&
@@ -155,7 +280,6 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
155
280
  );
156
281
  if (alreadyClosed) return;
157
282
 
158
- // Find the highest seq in this flow for monotonic ordering
159
283
  const maxSeq = all
160
284
  .filter((e) => e.flowId === lastStart.flowId)
161
285
  .reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
@@ -174,6 +298,16 @@ export function emitCrashRecoveredUnitEnd(basePath: string, lock: LockData): voi
174
298
  causedBy: { flowId: lastStart.flowId, seq: lastStart.seq },
175
299
  });
176
300
  } catch {
177
- // Never throw from crash recovery path — journal failure must not block recovery
301
+ // Never throw from crash recovery path.
178
302
  }
179
303
  }
304
+
305
+ /**
306
+ * Used by the doctor checks (doctor-runtime-checks.ts, doctor-proactive.ts)
307
+ * to enumerate stale workers across all projects this DB knows about.
308
+ * Phase C pt 2 export — surface for the same diagnostics that previously
309
+ * iterated `auto.lock` files.
310
+ */
311
+ export function findStaleAutoWorker(basePath: string): LockData | null {
312
+ return readCrashLock(basePath);
313
+ }