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
@@ -55,19 +55,31 @@ const QUEUE_SAFE_TOOLS = new Set([
55
55
  * true / false — shell no-ops / test exit codes
56
56
  */
57
57
  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)/;
58
- const verifiedDepthMilestones = new Set();
59
- const verifiedApprovalGates = new Set();
60
- let activeQueuePhase = false;
58
+ function createEmptyWriteGateState() {
59
+ return {
60
+ verifiedDepthMilestones: new Set(),
61
+ verifiedApprovalGates: new Set(),
62
+ activeQueuePhase: false,
63
+ pendingGateId: null,
64
+ };
65
+ }
66
+ const writeGateStatesByBasePath = new Map();
67
+ function writeGateStateKey(basePath) {
68
+ return resolve(basePath);
69
+ }
70
+ function getWriteGateState(basePath = process.cwd()) {
71
+ const key = writeGateStateKey(basePath);
72
+ let state = writeGateStatesByBasePath.get(key);
73
+ if (!state) {
74
+ state = createEmptyWriteGateState();
75
+ writeGateStatesByBasePath.set(key, state);
76
+ }
77
+ return state;
78
+ }
61
79
  /**
62
- * Discussion gate enforcement state.
63
- *
64
- * When ask_user_questions is called with a recognized gate question ID,
65
- * we track the pending gate. Until the gate is confirmed (user selects the
66
- * first/recommended option), all non-read-only tool calls are blocked.
67
- * This mechanically prevents the model from rationalizing past failed or
68
- * cancelled gate questions.
80
+ * Discussion gate enforcement state is scoped per basePath so multiple
81
+ * workspaces can coexist in the same process without sharing gate state.
69
82
  */
70
- let pendingGateId = null;
71
83
  /**
72
84
  * Recognized gate question ID patterns.
73
85
  * These appear in discuss.md (depth/requirements/roadmap).
@@ -99,24 +111,25 @@ function shouldPersistWriteGateSnapshot(env = process.env) {
99
111
  const v = env.GSD_PERSIST_WRITE_GATE_STATE;
100
112
  return v !== "0" && v !== "false";
101
113
  }
102
- function writeGateSnapshotPath(basePath = process.cwd()) {
114
+ function writeGateSnapshotPath(basePath) {
103
115
  return join(basePath, ".gsd", "runtime", "write-gate-state.json");
104
116
  }
105
- function currentWriteGateSnapshot() {
117
+ function currentWriteGateSnapshot(basePath = process.cwd()) {
118
+ const state = getWriteGateState(basePath);
106
119
  return {
107
- verifiedDepthMilestones: [...verifiedDepthMilestones].sort(),
108
- verifiedApprovalGates: [...verifiedApprovalGates].sort(),
109
- activeQueuePhase,
110
- pendingGateId,
120
+ verifiedDepthMilestones: [...state.verifiedDepthMilestones].sort(),
121
+ verifiedApprovalGates: [...state.verifiedApprovalGates].sort(),
122
+ activeQueuePhase: state.activeQueuePhase,
123
+ pendingGateId: state.pendingGateId,
111
124
  };
112
125
  }
113
- function persistWriteGateSnapshot(basePath = process.cwd()) {
126
+ function persistWriteGateSnapshot(basePath) {
114
127
  if (!shouldPersistWriteGateSnapshot())
115
128
  return;
116
129
  const path = writeGateSnapshotPath(basePath);
117
130
  mkdirSync(join(basePath, ".gsd", "runtime"), { recursive: true });
118
131
  const tempPath = `${path}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
119
- writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(), null, 2), "utf-8");
132
+ writeFileSync(tempPath, JSON.stringify(currentWriteGateSnapshot(basePath), null, 2), "utf-8");
120
133
  try {
121
134
  renameSync(tempPath, path);
122
135
  }
@@ -132,7 +145,7 @@ function persistWriteGateSnapshot(basePath = process.cwd()) {
132
145
  }
133
146
  }
134
147
  }
135
- function clearPersistedWriteGateSnapshot(basePath = process.cwd()) {
148
+ function clearPersistedWriteGateSnapshot(basePath) {
136
149
  if (!shouldPersistWriteGateSnapshot())
137
150
  return;
138
151
  const path = writeGateSnapshotPath(basePath);
@@ -164,7 +177,7 @@ const EMPTY_SNAPSHOT = {
164
177
  activeQueuePhase: false,
165
178
  pendingGateId: null,
166
179
  };
167
- export function loadWriteGateSnapshot(basePath = process.cwd()) {
180
+ export function loadWriteGateSnapshot(basePath) {
168
181
  const path = writeGateSnapshotPath(basePath);
169
182
  if (!existsSync(path)) {
170
183
  // When persist mode is active and the file has been deleted, treat it as a
@@ -172,61 +185,59 @@ export function loadWriteGateSnapshot(basePath = process.cwd()) {
172
185
  // In non-persist mode the file is never written, so fall back to in-memory.
173
186
  if (shouldPersistWriteGateSnapshot())
174
187
  return EMPTY_SNAPSHOT;
175
- return currentWriteGateSnapshot();
188
+ return currentWriteGateSnapshot(basePath);
176
189
  }
177
190
  try {
178
191
  return normalizeWriteGateSnapshot(JSON.parse(readFileSync(path, "utf-8")));
179
192
  }
180
193
  catch {
181
- return currentWriteGateSnapshot();
194
+ return currentWriteGateSnapshot(basePath);
182
195
  }
183
196
  }
184
- export function isDepthVerified() {
185
- return verifiedDepthMilestones.size > 0;
197
+ export function isDepthVerified(basePath = process.cwd()) {
198
+ return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
186
199
  }
187
200
  /**
188
201
  * Check whether a specific milestone has passed depth verification.
189
202
  */
190
- export function isMilestoneDepthVerified(milestoneId) {
203
+ export function isMilestoneDepthVerified(milestoneId, basePath = process.cwd()) {
191
204
  if (!milestoneId)
192
205
  return false;
193
- return verifiedDepthMilestones.has(milestoneId);
206
+ return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
194
207
  }
195
208
  export function isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId) {
196
209
  if (!milestoneId)
197
210
  return false;
198
211
  return snapshot.verifiedDepthMilestones.includes(milestoneId);
199
212
  }
200
- export function isQueuePhaseActive() {
201
- return activeQueuePhase;
213
+ export function isQueuePhaseActive(basePath = process.cwd()) {
214
+ return getWriteGateState(basePath).activeQueuePhase;
202
215
  }
203
- export function setQueuePhaseActive(active) {
204
- activeQueuePhase = active;
205
- persistWriteGateSnapshot();
216
+ export function setQueuePhaseActive(active, basePath) {
217
+ getWriteGateState(basePath).activeQueuePhase = active;
218
+ persistWriteGateSnapshot(basePath);
206
219
  }
207
- export function resetWriteGateState() {
208
- verifiedDepthMilestones.clear();
209
- verifiedApprovalGates.clear();
210
- pendingGateId = null;
211
- persistWriteGateSnapshot();
220
+ export function resetWriteGateState(basePath) {
221
+ const state = getWriteGateState(basePath);
222
+ state.verifiedDepthMilestones.clear();
223
+ state.verifiedApprovalGates.clear();
224
+ state.pendingGateId = null;
225
+ persistWriteGateSnapshot(basePath);
212
226
  }
213
- export function clearDiscussionFlowState() {
214
- verifiedDepthMilestones.clear();
215
- verifiedApprovalGates.clear();
216
- activeQueuePhase = false;
217
- pendingGateId = null;
218
- clearPersistedWriteGateSnapshot();
227
+ export function clearDiscussionFlowState(basePath) {
228
+ writeGateStatesByBasePath.delete(writeGateStateKey(basePath));
229
+ clearPersistedWriteGateSnapshot(basePath);
219
230
  }
220
231
  export function markDepthVerified(milestoneId, basePath = process.cwd()) {
221
232
  if (!milestoneId)
222
233
  return;
223
- verifiedDepthMilestones.add(milestoneId);
234
+ getWriteGateState(basePath).verifiedDepthMilestones.add(milestoneId);
224
235
  persistWriteGateSnapshot(basePath);
225
236
  }
226
237
  export function markApprovalGateVerified(gateId, basePath = process.cwd()) {
227
238
  if (!gateId)
228
239
  return;
229
- verifiedApprovalGates.add(gateId);
240
+ getWriteGateState(basePath).verifiedApprovalGates.add(gateId);
230
241
  persistWriteGateSnapshot(basePath);
231
242
  }
232
243
  export function isApprovalGateVerifiedInSnapshot(snapshot, gateId) {
@@ -258,26 +269,27 @@ function extractContextMilestoneId(inputPath) {
258
269
  /**
259
270
  * Mark a gate as pending (called when ask_user_questions is invoked with a gate ID).
260
271
  */
261
- export function setPendingGate(gateId) {
262
- pendingGateId = gateId;
263
- verifiedApprovalGates.delete(gateId);
272
+ export function setPendingGate(gateId, basePath) {
273
+ const state = getWriteGateState(basePath);
274
+ state.pendingGateId = gateId;
275
+ state.verifiedApprovalGates.delete(gateId);
264
276
  const milestoneId = extractDepthVerificationMilestoneId(gateId);
265
277
  if (milestoneId)
266
- verifiedDepthMilestones.delete(milestoneId);
267
- persistWriteGateSnapshot();
278
+ state.verifiedDepthMilestones.delete(milestoneId);
279
+ persistWriteGateSnapshot(basePath);
268
280
  }
269
281
  /**
270
282
  * Clear the pending gate (called when the user confirms).
271
283
  */
272
- export function clearPendingGate() {
273
- pendingGateId = null;
274
- persistWriteGateSnapshot();
284
+ export function clearPendingGate(basePath) {
285
+ getWriteGateState(basePath).pendingGateId = null;
286
+ persistWriteGateSnapshot(basePath);
275
287
  }
276
288
  /**
277
289
  * Get the currently pending gate, if any.
278
290
  */
279
- export function getPendingGate() {
280
- return pendingGateId;
291
+ export function getPendingGate(basePath = process.cwd()) {
292
+ return getWriteGateState(basePath).pendingGateId;
281
293
  }
282
294
  /**
283
295
  * Check whether a tool call should be blocked because a discussion gate
@@ -1,24 +1,106 @@
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
  */
23
+ import { emitJournalEvent, queryJournal, } from "./journal.js";
12
24
  import { readFileSync, unlinkSync, existsSync } from "node:fs";
13
25
  import { join } from "node:path";
14
- import { gsdRoot } from "./paths.js";
26
+ import { findStaleWorkerForProject, getAllAutoWorkers, } from "./db/auto-workers.js";
27
+ import { getRuntimeKv, setRuntimeKv, deleteRuntimeKv } from "./db/runtime-kv.js";
28
+ import { _getAdapter, isDbAvailable } from "./gsd-db.js";
29
+ import { gsdRoot, normalizeRealPath } from "./paths.js";
15
30
  import { atomicWriteSync } from "./atomic-write.js";
16
31
  import { effectiveLockFile } from "./session-lock.js";
17
- import { emitJournalEvent, queryJournal } from "./journal.js";
32
+ const SESSION_FILE_KV_KEY = "session_file";
18
33
  function lockPath(basePath) {
19
34
  return join(gsdRoot(basePath), effectiveLockFile());
20
35
  }
21
- /** Write or update the lock file with current auto-mode state. */
36
+ function readLegacyLock(basePath) {
37
+ try {
38
+ const p = lockPath(basePath);
39
+ if (!existsSync(p))
40
+ return null;
41
+ return JSON.parse(readFileSync(p, "utf-8"));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ function findActiveWorkerForCurrentProcess(projectRootRealpath) {
48
+ if (!isDbAvailable())
49
+ return null;
50
+ const workers = getAllAutoWorkers();
51
+ for (const worker of workers) {
52
+ if (worker.pid === process.pid
53
+ && worker.project_root_realpath === projectRootRealpath) {
54
+ return worker;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Look up the most recent dispatch row for a worker, regardless of status.
61
+ * Returns null if the worker has no dispatch history yet (e.g. crashed
62
+ * during bootstrap before claiming the first unit).
63
+ */
64
+ function getLatestDispatchForWorker(workerId) {
65
+ if (!isDbAvailable())
66
+ return null;
67
+ const db = _getAdapter();
68
+ const row = db.prepare(`SELECT unit_type, unit_id, started_at, status
69
+ FROM unit_dispatches
70
+ WHERE worker_id = :worker_id
71
+ ORDER BY id DESC
72
+ LIMIT 1`).get({ ":worker_id": workerId });
73
+ return row ?? null;
74
+ }
75
+ function workerToLockData(worker) {
76
+ const dispatch = getLatestDispatchForWorker(worker.worker_id);
77
+ const sessionFile = getRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY) ?? undefined;
78
+ return {
79
+ pid: worker.pid,
80
+ startedAt: worker.started_at,
81
+ // Pre-Phase-C-pt-2 default: when no dispatch row exists yet (bootstrap
82
+ // crash), report unitType="starting", unitId="bootstrap" — same shape
83
+ // the file-based writer used to produce.
84
+ unitType: dispatch?.unit_type ?? "starting",
85
+ unitId: dispatch?.unit_id ?? "bootstrap",
86
+ unitStartedAt: dispatch?.started_at ?? worker.started_at,
87
+ sessionFile,
88
+ };
89
+ }
90
+ /**
91
+ * Write or update the lock state for the current auto-mode session.
92
+ *
93
+ * Phase C pt 2: the only persistent state this function adds beyond what
94
+ * the workers + unit_dispatches tables already track is the pi session
95
+ * JSONL path, which lands in runtime_kv (worker scope, key
96
+ * "session_file"). The pid/startedAt/unitType/unitId/unitStartedAt are
97
+ * recorded by registerAutoWorker / heartbeatAutoWorker / recordDispatchClaim
98
+ * already.
99
+ *
100
+ * basePath is unused by the new implementation (kept as a parameter for
101
+ * back-compat with the 15+ call sites) — the worker is identified by
102
+ * pid + project_root_realpath in the workers table.
103
+ */
22
104
  export function writeLock(basePath, unitType, unitId, sessionFile) {
23
105
  try {
24
106
  const data = {
@@ -29,51 +111,86 @@ export function writeLock(basePath, unitType, unitId, sessionFile) {
29
111
  unitStartedAt: new Date().toISOString(),
30
112
  sessionFile,
31
113
  };
32
- const lp = lockPath(basePath);
33
- atomicWriteSync(lp, JSON.stringify(data, null, 2));
114
+ atomicWriteSync(lockPath(basePath), JSON.stringify(data, null, 2));
115
+ }
116
+ catch {
117
+ // Best-effort — never throw from the lock writer.
118
+ }
119
+ if (!isDbAvailable() || !sessionFile)
120
+ return;
121
+ try {
122
+ const projectRoot = normalizeRealPath(basePath);
123
+ const worker = findActiveWorkerForCurrentProcess(projectRoot);
124
+ if (!worker)
125
+ return;
126
+ setRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY, sessionFile);
34
127
  }
35
- catch (e) { /* non-fatal: lock write failure */
36
- void e;
128
+ catch {
129
+ // Best-effort — never throw from the lock writer.
37
130
  }
38
131
  }
39
- /** Remove the lock file on clean stop. */
132
+ /**
133
+ * Phase C pt 2: clearLock no longer deletes a file. The cleanup path
134
+ * (markWorkerStopping in stopAuto) flips the workers row to 'stopping'.
135
+ * This function additionally drops the session_file runtime_kv row for
136
+ * the current worker so a follow-up crash detection doesn't pick up a
137
+ * stale session-file pointer.
138
+ */
40
139
  export function clearLock(basePath) {
41
140
  try {
42
141
  const p = lockPath(basePath);
43
142
  if (existsSync(p))
44
143
  unlinkSync(p);
45
144
  }
46
- catch (e) { /* non-fatal: lock clear failure */
47
- void e;
145
+ catch {
146
+ // Best-effort.
48
147
  }
49
- }
50
- /** Check if a crash lock exists and return its data. */
51
- export function readCrashLock(basePath) {
148
+ if (!isDbAvailable())
149
+ return;
52
150
  try {
53
- const p = lockPath(basePath);
54
- if (!existsSync(p))
55
- return null;
56
- const raw = readFileSync(p, "utf-8");
57
- return JSON.parse(raw);
151
+ const projectRoot = normalizeRealPath(basePath);
152
+ const worker = findActiveWorkerForCurrentProcess(projectRoot);
153
+ if (!worker)
154
+ return;
155
+ deleteRuntimeKv("worker", worker.worker_id, SESSION_FILE_KV_KEY);
58
156
  }
59
- catch (e) {
60
- /* non-fatal: corrupt or unreadable lock file */ void e;
61
- return null;
157
+ catch {
158
+ // Best-effort.
159
+ }
160
+ }
161
+ /**
162
+ * Detect a previous crashed auto-mode session.
163
+ *
164
+ * Phase C pt 2: synthesized from workers (status='active' + lapsed
165
+ * heartbeat) + unit_dispatches (most recent for that worker) +
166
+ * runtime_kv (session_file). Returns null when no stale worker exists
167
+ * or the DB is unavailable.
168
+ */
169
+ export function readCrashLock(basePath) {
170
+ if (isDbAvailable()) {
171
+ try {
172
+ const projectRoot = normalizeRealPath(basePath);
173
+ const stale = findStaleWorkerForProject(projectRoot);
174
+ if (stale)
175
+ return workerToLockData(stale);
176
+ }
177
+ catch {
178
+ // Fall through to the legacy lock-file compatibility path.
179
+ }
62
180
  }
181
+ return readLegacyLock(basePath);
63
182
  }
64
183
  /**
65
184
  * Check whether the process that wrote the lock is still running.
66
185
  * Uses `process.kill(pid, 0)` which sends no signal but checks liveness.
67
186
  * Returns true if the PID matches our own — we are the lock holder (#2470).
187
+ *
188
+ * Unchanged from the file-based era — pure stateless OS check.
68
189
  */
69
190
  export function isLockProcessAlive(lock) {
70
191
  const pid = lock.pid;
71
192
  if (!Number.isInteger(pid) || pid <= 0)
72
193
  return false;
73
- // Our own PID means WE hold this lock — we are alive. (#2470)
74
- // Callers that need to distinguish "our lock" from "someone else's lock"
75
- // (e.g. startAuto checking for a prior crashed session with a recycled PID)
76
- // already guard with `crashLock.pid !== process.pid` before calling us.
77
194
  if (pid === process.pid)
78
195
  return true;
79
196
  try {
@@ -81,8 +198,6 @@ export function isLockProcessAlive(lock) {
81
198
  return true;
82
199
  }
83
200
  catch (err) {
84
- // EPERM means the process exists but we lack permission — treat as alive.
85
- // ESRCH means the process does not exist — treat as dead (stale lock).
86
201
  if (err.code === "EPERM")
87
202
  return true;
88
203
  return false;
@@ -96,7 +211,6 @@ export function formatCrashInfo(lock) {
96
211
  ` Started at: ${lock.unitStartedAt}`,
97
212
  ` PID: ${lock.pid}`,
98
213
  ];
99
- // Add recovery guidance based on what was happening when it crashed
100
214
  if (lock.unitType === "starting" && lock.unitId === "bootstrap") {
101
215
  lines.push(`No work was lost. Run /gsd auto to restart.`);
102
216
  }
@@ -113,33 +227,23 @@ export function formatCrashInfo(lock) {
113
227
  }
114
228
  /**
115
229
  * Emit a synthetic unit-end event for a unit that crashed without emitting its own.
116
- *
117
- * Queries the journal to find the most recent unit-start for the crashed unit.
118
- * If a matching unit-end already exists (e.g. the hard timeout fired), this is a
119
- * no-op. Called during crash recovery, before clearing the stale lock.
120
- *
121
- * Addresses the gap reported in #3348 where `unit-start` was emitted but no
122
- * `unit-end` followed — side effects landed but the worker died before closeout.
230
+ * Unchanged from the file era — operates on the journal, not the lock.
123
231
  */
124
232
  export function emitCrashRecoveredUnitEnd(basePath, lock) {
125
- // Skip bootstrap / starting pseudo-units — they have no meaningful unit-start event.
126
233
  if (!lock.unitType || !lock.unitId || lock.unitType === "starting")
127
234
  return;
128
235
  try {
129
236
  const all = queryJournal(basePath);
130
- // Find the most recent unit-start for this unitId
131
237
  const starts = all.filter((e) => e.eventType === "unit-start" && e.data?.unitId === lock.unitId);
132
238
  if (starts.length === 0)
133
239
  return;
134
240
  const lastStart = starts[starts.length - 1];
135
- // Check if a unit-end was already emitted (e.g. hard timeout fired after the crash)
136
241
  const alreadyClosed = all.some((e) => e.eventType === "unit-end" &&
137
242
  e.data?.unitId === lock.unitId &&
138
243
  e.causedBy?.flowId === lastStart.flowId &&
139
244
  e.causedBy?.seq === lastStart.seq);
140
245
  if (alreadyClosed)
141
246
  return;
142
- // Find the highest seq in this flow for monotonic ordering
143
247
  const maxSeq = all
144
248
  .filter((e) => e.flowId === lastStart.flowId)
145
249
  .reduce((max, e) => Math.max(max, e.seq), lastStart.seq);
@@ -158,6 +262,15 @@ export function emitCrashRecoveredUnitEnd(basePath, lock) {
158
262
  });
159
263
  }
160
264
  catch {
161
- // Never throw from crash recovery path — journal failure must not block recovery
265
+ // Never throw from crash recovery path.
162
266
  }
163
267
  }
268
+ /**
269
+ * Used by the doctor checks (doctor-runtime-checks.ts, doctor-proactive.ts)
270
+ * to enumerate stale workers across all projects this DB knows about.
271
+ * Phase C pt 2 export — surface for the same diagnostics that previously
272
+ * iterated `auto.lock` files.
273
+ */
274
+ export function findStaleAutoWorker(basePath) {
275
+ return readCrashLock(basePath);
276
+ }