gsd-pi 2.77.0-dev.c9c8932c6 → 2.77.0-dev.eaa4973bc

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 (245) hide show
  1. package/dist/cli-web-branch.d.ts +1 -0
  2. package/dist/cli-web-branch.js +3 -0
  3. package/dist/cli.js +24 -2
  4. package/dist/provider-migrations.d.ts +18 -0
  5. package/dist/provider-migrations.js +14 -0
  6. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +14 -10
  7. package/dist/resources/extensions/gsd/abandon-detect.js +44 -0
  8. package/dist/resources/extensions/gsd/auto/resolve.js +24 -0
  9. package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
  10. package/dist/resources/extensions/gsd/auto/turn-epoch.js +95 -0
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +24 -0
  12. package/dist/resources/extensions/gsd/auto-loop.js +1 -1
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +31 -0
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +11 -5
  15. package/dist/resources/extensions/gsd/auto-unit-closeout.js +11 -2
  16. package/dist/resources/extensions/gsd/auto-worktree.js +58 -8
  17. package/dist/resources/extensions/gsd/auto.js +4 -0
  18. package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +4 -2
  19. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +31 -4
  20. package/dist/resources/extensions/gsd/file-lock.js +49 -9
  21. package/dist/resources/extensions/gsd/git-service.js +1 -0
  22. package/dist/resources/extensions/gsd/gitignore.js +1 -0
  23. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -1
  24. package/dist/resources/extensions/gsd/journal.js +17 -2
  25. package/dist/resources/extensions/gsd/milestone-actions.js +15 -0
  26. package/dist/resources/extensions/gsd/reports.js +5 -4
  27. package/dist/resources/extensions/gsd/tools/complete-slice.js +21 -0
  28. package/dist/resources/extensions/gsd/tools/complete-task.js +31 -0
  29. package/dist/resources/extensions/gsd/uok/audit.js +18 -2
  30. package/dist/resources/extensions/gsd/workflow-logger.js +10 -2
  31. package/dist/resources/extensions/gsd/worktree-manager.js +1 -0
  32. package/dist/resources/extensions/mcp-client/auth.js +10 -1
  33. package/dist/resources/extensions/mcp-client/index.js +118 -9
  34. package/dist/resources/skills/create-skill/SKILL.md +2 -2
  35. package/dist/resources/skills/create-skill/references/gsd-skill-ecosystem.md +4 -4
  36. package/dist/resources/skills/create-skill/workflows/audit-skill.md +4 -4
  37. package/dist/resources/skills/create-skill/workflows/create-new-skill.md +5 -5
  38. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  39. package/dist/web/standalone/.next/BUILD_ID +1 -1
  40. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  41. package/dist/web/standalone/.next/build-manifest.json +3 -3
  42. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  43. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  62. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/notifications/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/events/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  110. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  120. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  122. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  124. package/dist/web/standalone/.next/server/app/index.html +1 -1
  125. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  128. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  130. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  132. package/dist/web/standalone/.next/server/chunks/1926.js +1 -0
  133. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  134. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  136. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  138. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  139. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  140. package/dist/web/standalone/.next/static/chunks/2826.e9f5195e91f9cad2.js +11 -0
  141. package/dist/web/standalone/.next/static/chunks/{webpack-1832629448831fdc.js → webpack-2e68521d7c82f7c2.js} +1 -1
  142. package/package.json +3 -3
  143. package/packages/mcp-server/README.md +3 -3
  144. package/packages/mcp-server/dist/env-writer.d.ts +1 -0
  145. package/packages/mcp-server/dist/env-writer.d.ts.map +1 -1
  146. package/packages/mcp-server/dist/env-writer.js +74 -6
  147. package/packages/mcp-server/dist/env-writer.js.map +1 -1
  148. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  149. package/packages/mcp-server/dist/server.js +25 -2
  150. package/packages/mcp-server/dist/server.js.map +1 -1
  151. package/packages/mcp-server/src/env-writer.test.ts +79 -1
  152. package/packages/mcp-server/src/env-writer.ts +76 -6
  153. package/packages/mcp-server/src/server.ts +29 -2
  154. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  155. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts +1 -0
  156. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +21 -16
  158. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.d.ts +2 -0
  160. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.d.ts.map +1 -0
  161. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.js +130 -0
  162. package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.js.map +1 -0
  163. package/packages/pi-coding-agent/dist/core/sdk.d.ts +1 -0
  164. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  165. package/packages/pi-coding-agent/dist/core/sdk.js +4 -1
  166. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  167. package/packages/pi-coding-agent/dist/core/sdk.test.js +19 -1
  168. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
  169. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +15 -6
  170. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -1
  171. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -5
  173. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +7 -1
  175. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +31 -9
  177. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  178. package/packages/pi-coding-agent/src/core/compaction-orchestrator.test.ts +154 -0
  179. package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +24 -17
  180. package/packages/pi-coding-agent/src/core/sdk.test.ts +25 -1
  181. package/packages/pi-coding-agent/src/core/sdk.ts +10 -3
  182. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +17 -7
  183. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +14 -5
  184. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +45 -11
  185. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  186. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +19 -0
  187. package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -1
  188. package/packages/pi-tui/dist/stdin-buffer.d.ts +7 -0
  189. package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
  190. package/packages/pi-tui/dist/stdin-buffer.js +20 -0
  191. package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
  192. package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +25 -0
  193. package/packages/pi-tui/src/stdin-buffer.ts +26 -0
  194. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  195. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +14 -10
  196. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +22 -16
  197. package/src/resources/extensions/gsd/abandon-detect.ts +62 -0
  198. package/src/resources/extensions/gsd/auto/resolve.ts +29 -0
  199. package/src/resources/extensions/gsd/auto/run-unit.ts +16 -2
  200. package/src/resources/extensions/gsd/auto/turn-epoch.ts +108 -0
  201. package/src/resources/extensions/gsd/auto-dispatch.ts +20 -0
  202. package/src/resources/extensions/gsd/auto-loop.ts +1 -1
  203. package/src/resources/extensions/gsd/auto-post-unit.ts +30 -0
  204. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -5
  205. package/src/resources/extensions/gsd/auto-unit-closeout.ts +14 -3
  206. package/src/resources/extensions/gsd/auto-worktree.ts +60 -5
  207. package/src/resources/extensions/gsd/auto.ts +5 -0
  208. package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +6 -2
  209. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +27 -8
  210. package/src/resources/extensions/gsd/file-lock.ts +84 -11
  211. package/src/resources/extensions/gsd/git-service.ts +1 -0
  212. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  213. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -1
  214. package/src/resources/extensions/gsd/journal.ts +27 -2
  215. package/src/resources/extensions/gsd/milestone-actions.ts +18 -0
  216. package/src/resources/extensions/gsd/reports.ts +5 -4
  217. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -2
  218. package/src/resources/extensions/gsd/tests/auto-mode-guards.test.ts +79 -0
  219. package/src/resources/extensions/gsd/tests/file-lock.test.ts +86 -12
  220. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +30 -0
  221. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
  222. package/src/resources/extensions/gsd/tests/mcp-client-security.test.ts +76 -0
  223. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +39 -25
  224. package/src/resources/extensions/gsd/tests/queue-auto-guard.test.ts +181 -0
  225. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +13 -7
  226. package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +170 -0
  227. package/src/resources/extensions/gsd/tests/rewrite-docs-abandon-detect.test.ts +195 -0
  228. package/src/resources/extensions/gsd/tests/stuck-detection-coverage.test.ts +2 -2
  229. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +50 -2
  230. package/src/resources/extensions/gsd/tests/turn-epoch.test.ts +162 -0
  231. package/src/resources/extensions/gsd/tools/complete-slice.ts +38 -0
  232. package/src/resources/extensions/gsd/tools/complete-task.ts +49 -0
  233. package/src/resources/extensions/gsd/uok/audit.ts +20 -2
  234. package/src/resources/extensions/gsd/workflow-logger.ts +22 -3
  235. package/src/resources/extensions/gsd/worktree-manager.ts +1 -0
  236. package/src/resources/extensions/mcp-client/auth.ts +12 -1
  237. package/src/resources/extensions/mcp-client/index.ts +129 -10
  238. package/src/resources/skills/create-skill/SKILL.md +2 -2
  239. package/src/resources/skills/create-skill/references/gsd-skill-ecosystem.md +4 -4
  240. package/src/resources/skills/create-skill/workflows/audit-skill.md +4 -4
  241. package/src/resources/skills/create-skill/workflows/create-new-skill.md +5 -5
  242. package/dist/web/standalone/.next/server/chunks/7461.js +0 -1
  243. package/dist/web/standalone/.next/static/chunks/2826.d445fb428ef41fa1.js +0 -9
  244. /package/dist/web/standalone/.next/static/{H51SzzS_EEgdBsDVhrLBv → 5wbu35_C2_MQ3Jj1lEVDx}/_buildManifest.js +0 -0
  245. /package/dist/web/standalone/.next/static/{H51SzzS_EEgdBsDVhrLBv → 5wbu35_C2_MQ3Jj1lEVDx}/_ssgManifest.js +0 -0
@@ -9,7 +9,7 @@ import { existsSync, cpSync, readFileSync, readdirSync, mkdirSync, realpathSync,
9
9
  import { isAbsolute, join, sep as pathSep } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
12
- import { reconcileWorktreeDb, isDbAvailable, getMilestone, getMilestoneSlices, } from "./gsd-db.js";
12
+ import { reconcileWorktreeDb, isDbAvailable, getMilestone, getMilestoneSlices, closeDatabase, openDatabase, getDbPath, } from "./gsd-db.js";
13
13
  import { atomicWriteSync } from "./atomic-write.js";
14
14
  import { execFileSync } from "node:child_process";
15
15
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
@@ -1433,26 +1433,53 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1433
1433
  const milestonesDir = join(gsdRoot(originalBasePath_), "milestones");
1434
1434
  const shelterDir = join(gsdRoot(originalBasePath_), ".milestone-shelter");
1435
1435
  const shelteredDirs = [];
1436
+ let shelterRestored = false;
1436
1437
  // Helper: restore sheltered milestone directories (#2505).
1437
1438
  // Called on both success and error paths to ensure queued CONTEXT files
1438
- // are never permanently lost.
1439
+ // are never permanently lost. Idempotent — the error path may fire after
1440
+ // the success path has already restored and removed the shelter dir; a
1441
+ // second call is a no-op instead of logging a misleading "shelter restore
1442
+ // failed: ENOENT" error for shelter sources that were cleaned up legitimately.
1439
1443
  const restoreShelter = () => {
1444
+ if (shelterRestored)
1445
+ return;
1446
+ shelterRestored = true;
1440
1447
  if (shelteredDirs.length === 0)
1441
1448
  return;
1449
+ let restoreFailed = false;
1442
1450
  for (const dirName of shelteredDirs) {
1451
+ const src = join(shelterDir, dirName);
1452
+ // If the shelter source is missing the restore cannot proceed for this
1453
+ // entry. Distinguish "legitimately missing" (shelter dir removed by a
1454
+ // prior successful restore or never copied) from a surprising ENOENT
1455
+ // inside an otherwise-populated shelter.
1456
+ if (!existsSync(src)) {
1457
+ logWarning("worktree", `shelter source missing for ${dirName}; skipping restore (shelter already cleaned or entry never staged)`);
1458
+ continue;
1459
+ }
1443
1460
  try {
1444
1461
  mkdirSync(milestonesDir, { recursive: true });
1445
- cpSync(join(shelterDir, dirName), join(milestonesDir, dirName), { recursive: true, force: true });
1462
+ cpSync(src, join(milestonesDir, dirName), { recursive: true, force: true });
1446
1463
  }
1447
1464
  catch (err) { /* best-effort */
1448
- logError("worktree", `shelter restore failed: ${err instanceof Error ? err.message : String(err)}`);
1465
+ restoreFailed = true;
1466
+ logError("worktree", `shelter restore failed (${dirName}): ${err instanceof Error ? err.message : String(err)}`);
1449
1467
  }
1450
1468
  }
1451
- try {
1452
- rmSync(shelterDir, { recursive: true, force: true });
1469
+ // Preserve the shelter if any per-entry restore failed — it is the only
1470
+ // surviving copy of the queued milestone dirs (sources were deleted during
1471
+ // shelter). Deleting it here would permanently lose those files (#2505).
1472
+ if (restoreFailed) {
1473
+ logWarning("worktree", `shelter retained at ${shelterDir} — manual recovery required for unrestored entries`);
1474
+ return;
1453
1475
  }
1454
- catch (err) { /* best-effort */
1455
- logWarning("worktree", `shelter cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1476
+ if (existsSync(shelterDir)) {
1477
+ try {
1478
+ rmSync(shelterDir, { recursive: true, force: true });
1479
+ }
1480
+ catch (err) { /* best-effort */
1481
+ logWarning("worktree", `shelter cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1482
+ }
1456
1483
  }
1457
1484
  };
1458
1485
  try {
@@ -1491,6 +1518,21 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1491
1518
  // entries (e.g. a gitignored `.gsd` symlink under ADR-002) (#4573).
1492
1519
  // Queued CONTEXT files under `.gsd/milestones/*` are already sheltered
1493
1520
  // in step 7 above, so they won't be swept into the stash.
1521
+ // On Windows, SQLite holds mandatory file locks on the gsd.db WAL/SHM
1522
+ // sidecars while the connection is open. `git stash --include-untracked`
1523
+ // walks those files and fails with EBUSY (#4704). Close the DB before
1524
+ // stashing so Windows releases the handles; reopen after. No-op on
1525
+ // POSIX, where advisory locks don't block git.
1526
+ const needsDbCycle = process.platform === "win32" && isDbAvailable();
1527
+ const dbPathToReopen = needsDbCycle ? getDbPath() : null;
1528
+ if (needsDbCycle) {
1529
+ try {
1530
+ closeDatabase();
1531
+ }
1532
+ catch (err) {
1533
+ logWarning("worktree", `pre-stash db close failed: ${err instanceof Error ? err.message : String(err)}`);
1534
+ }
1535
+ }
1494
1536
  let stashed = false;
1495
1537
  try {
1496
1538
  const status = execFileSync("git", ["status", "--porcelain"], {
@@ -1508,6 +1550,14 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1508
1550
  // report the dirty tree if it fails.
1509
1551
  logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
1510
1552
  }
1553
+ if (needsDbCycle && dbPathToReopen) {
1554
+ try {
1555
+ openDatabase(dbPathToReopen);
1556
+ }
1557
+ catch (err) {
1558
+ logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
1559
+ }
1560
+ }
1511
1561
  // 7b. Clean up stale merge state before attempting squash merge (#2912).
1512
1562
  // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
1513
1563
  // or interrupted operation) causes `git merge --squash` to refuse with
@@ -274,6 +274,10 @@ export function getAutoDashboardData() {
274
274
  export function isAutoActive() {
275
275
  return s.active;
276
276
  }
277
+ /** Test-only seam for validating auto-mode guards (#4704). Do not use in production code. */
278
+ export function _setAutoActiveForTest(active) {
279
+ s.active = active;
280
+ }
277
281
  export function isAutoPaused() {
278
282
  return s.paused;
279
283
  }
@@ -3,6 +3,8 @@ import { resetTransientRetryState } from "./agent-end-recovery.js";
3
3
  import { resetSessionTimeoutState } from "../auto/phases.js";
4
4
  const defaultDeps = {
5
5
  getSnapshot: () => getAutoDashboardData(),
6
+ resetTransientRetryState,
7
+ resetSessionTimeoutState,
6
8
  startAuto,
7
9
  };
8
10
  export async function resumeAutoAfterProviderDelay(pi, ctx, deps = defaultDeps) {
@@ -18,8 +20,8 @@ export async function resumeAutoAfterProviderDelay(pi, ctx, deps = defaultDeps)
18
20
  // Reset retry counters before restarting — without this, counters
19
21
  // accumulate across pause/resume cycles and permanently lock out
20
22
  // auto-resume after their respective MAX thresholds.
21
- resetTransientRetryState();
22
- resetSessionTimeoutState();
23
+ deps.resetTransientRetryState();
24
+ deps.resetSessionTimeoutState();
23
25
  await deps.startAuto(ctx, pi, snapshot.basePath, false, { step: snapshot.stepMode });
24
26
  return "resumed";
25
27
  }
@@ -17,6 +17,22 @@ import { validateDefinition } from "../../definition-loader.js";
17
17
  import { formatPluginInfo, listPluginsFormatted, resolvePlugin, } from "../../workflow-plugins.js";
18
18
  import { dispatchOneshot } from "../../workflow-dispatch.js";
19
19
  import { fetchWorkflowSource, globalInstallDir, inferPluginName, installPlugin, previewContent, projectInstallDir, resolveSourceUrl, uninstallPlugin, validateFetchedContent, } from "../../workflow-install.js";
20
+ /**
21
+ * Refuses interactive commands that mutate durable .gsd/ planning state while
22
+ * auto-mode holds the worktree. Returns true if the command was blocked and
23
+ * the caller should return immediately; false if it is safe to proceed.
24
+ *
25
+ * Auto-mode's squash merge performs a pre-merge dirty-tree check; concurrent
26
+ * writes by interactive commands between that check and the merge itself
27
+ * cause __dirty_working_tree__ failures (#4704).
28
+ */
29
+ function requireNotAutoActive(commandName, ctx) {
30
+ if (!isAutoActive())
31
+ return false;
32
+ ctx.ui.notify(`${commandName} cannot run while auto-mode is active.\n` +
33
+ `Stop auto-mode first with /gsd stop, then run ${commandName}.`, "error");
34
+ return true;
35
+ }
20
36
  // ─── Custom Workflow Subcommands ─────────────────────────────────────────
21
37
  const RESERVED_SUBCOMMANDS = new Set([
22
38
  "new", "run", "list", "validate", "pause", "resume",
@@ -394,12 +410,16 @@ async function handleCustomWorkflow(sub, ctx, pi) {
394
410
  export async function handleWorkflowCommand(trimmed, ctx, pi) {
395
411
  // ── /gsd do — natural language routing (must be early to route to other commands) ──
396
412
  if (trimmed === "do" || trimmed.startsWith("do ")) {
413
+ if (requireNotAutoActive("/gsd do", ctx))
414
+ return true;
397
415
  const { handleDo } = await import("../../commands-do.js");
398
416
  await handleDo(trimmed.replace(/^do\s*/, "").trim(), ctx, pi);
399
417
  return true;
400
418
  }
401
419
  // ── Backlog management ──
402
420
  if (trimmed === "backlog" || trimmed.startsWith("backlog ")) {
421
+ if (requireNotAutoActive("/gsd backlog", ctx))
422
+ return true;
403
423
  const { handleBacklog } = await import("../../commands-backlog.js");
404
424
  await handleBacklog(trimmed.replace(/^backlog\s*/, "").trim(), ctx, pi);
405
425
  return true;
@@ -410,23 +430,26 @@ export async function handleWorkflowCommand(trimmed, ctx, pi) {
410
430
  return handleCustomWorkflow(sub, ctx, pi);
411
431
  }
412
432
  if (trimmed === "queue") {
433
+ if (requireNotAutoActive("/gsd queue", ctx))
434
+ return true;
413
435
  await showQueue(ctx, pi, projectRoot());
414
436
  return true;
415
437
  }
416
438
  if (trimmed === "discuss") {
439
+ if (requireNotAutoActive("/gsd discuss", ctx))
440
+ return true;
417
441
  await showDiscuss(ctx, pi, projectRoot());
418
442
  return true;
419
443
  }
420
444
  if (trimmed === "quick" || trimmed.startsWith("quick ")) {
421
- if (isAutoActive()) {
422
- ctx.ui.notify("/gsd quick cannot run while auto-mode is active.\n" +
423
- "Stop auto-mode first with /gsd stop, then run /gsd quick.", "error");
445
+ if (requireNotAutoActive("/gsd quick", ctx))
424
446
  return true;
425
- }
426
447
  await handleQuick(trimmed.replace(/^quick\s*/, "").trim(), ctx, pi);
427
448
  return true;
428
449
  }
429
450
  if (trimmed === "new-milestone") {
451
+ if (requireNotAutoActive("/gsd new-milestone", ctx))
452
+ return true;
430
453
  const basePath = projectRoot();
431
454
  const headlessContextPath = join(gsdRoot(basePath), "runtime", "headless-context.md");
432
455
  if (existsSync(headlessContextPath)) {
@@ -452,6 +475,8 @@ export async function handleWorkflowCommand(trimmed, ctx, pi) {
452
475
  return true;
453
476
  }
454
477
  if (trimmed === "park" || trimmed.startsWith("park ")) {
478
+ if (requireNotAutoActive("/gsd park", ctx))
479
+ return true;
455
480
  const basePath = projectRoot();
456
481
  const arg = trimmed.replace(/^park\s*/, "").trim();
457
482
  let targetId = arg;
@@ -474,6 +499,8 @@ export async function handleWorkflowCommand(trimmed, ctx, pi) {
474
499
  return true;
475
500
  }
476
501
  if (trimmed === "unpark" || trimmed.startsWith("unpark ")) {
502
+ if (requireNotAutoActive("/gsd unpark", ctx))
503
+ return true;
477
504
  const basePath = projectRoot();
478
505
  const arg = trimmed.replace(/^unpark\s*/, "").trim();
479
506
  let targetId = arg;
@@ -1,11 +1,19 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { join } from "node:path";
4
+ // The file-lock module is loaded in both CJS builds and ESM sources. Under ESM
5
+ // the bare `require` identifier is not defined, so we always go through
6
+ // createRequire. We try the current module's resolution context first and fall
7
+ // back to the installed gsd-pi package if we are running from a consumer
8
+ // project that does not hoist proper-lockfile.
9
+ const localRequire = createRequire(import.meta.url);
2
10
  function _require(name) {
3
11
  try {
4
- return require(name);
12
+ return localRequire(name);
5
13
  }
6
14
  catch {
7
15
  try {
8
- const gsdPiRequire = require("module").createRequire(require("path").join(process.cwd(), "node_modules", "gsd-pi", "index.js"));
16
+ const gsdPiRequire = createRequire(join(process.cwd(), "node_modules", "gsd-pi", "index.js"));
9
17
  return gsdPiRequire(name);
10
18
  }
11
19
  catch {
@@ -13,14 +21,44 @@ function _require(name) {
13
21
  }
14
22
  }
15
23
  }
16
- export function withFileLockSync(filePath, fn) {
24
+ const DEFAULT_RETRIES = 5;
25
+ const DEFAULT_STALE_MS = 10000;
26
+ const SYNC_RETRY_DELAY_MS = 50;
27
+ // Block the thread for `ms` milliseconds without spinning the CPU.
28
+ // Used by the sync lock retry loop, since proper-lockfile's lockSync does not
29
+ // accept a `retries` option (only the async `lock` does).
30
+ function sleepSync(ms) {
31
+ if (ms <= 0)
32
+ return;
33
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
34
+ }
35
+ function acquireLockSyncWithRetry(lockfile, filePath, retries, stale) {
36
+ let lastErr;
37
+ for (let attempt = 0; attempt <= retries; attempt++) {
38
+ try {
39
+ return lockfile.lockSync(filePath, { stale });
40
+ }
41
+ catch (err) {
42
+ lastErr = err;
43
+ if (err?.code !== "ELOCKED")
44
+ throw err;
45
+ if (attempt < retries)
46
+ sleepSync(SYNC_RETRY_DELAY_MS);
47
+ }
48
+ }
49
+ throw lastErr;
50
+ }
51
+ export function withFileLockSync(filePath, fn, opts = {}) {
17
52
  const lockfile = _require("proper-lockfile");
18
53
  if (!lockfile)
19
54
  return fn();
20
55
  if (!existsSync(filePath))
21
56
  return fn();
57
+ const retries = opts.retries ?? DEFAULT_RETRIES;
58
+ const stale = opts.stale ?? DEFAULT_STALE_MS;
59
+ const onLocked = opts.onLocked ?? "fail";
22
60
  try {
23
- const release = lockfile.lockSync(filePath, { retries: 5, stale: 10000 });
61
+ const release = acquireLockSyncWithRetry(lockfile, filePath, retries, stale);
24
62
  try {
25
63
  return fn();
26
64
  }
@@ -29,21 +67,23 @@ export function withFileLockSync(filePath, fn) {
29
67
  }
30
68
  }
31
69
  catch (err) {
32
- if (err.code === "ELOCKED") {
33
- // Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
70
+ if (err?.code === "ELOCKED" && onLocked === "skip") {
34
71
  return fn();
35
72
  }
36
73
  throw err;
37
74
  }
38
75
  }
39
- export async function withFileLock(filePath, fn) {
76
+ export async function withFileLock(filePath, fn, opts = {}) {
40
77
  const lockfile = _require("proper-lockfile");
41
78
  if (!lockfile)
42
79
  return await fn();
43
80
  if (!existsSync(filePath))
44
81
  return await fn();
82
+ const retries = opts.retries ?? DEFAULT_RETRIES;
83
+ const stale = opts.stale ?? DEFAULT_STALE_MS;
84
+ const onLocked = opts.onLocked ?? "fail";
45
85
  try {
46
- const release = await lockfile.lock(filePath, { retries: 5, stale: 10000 });
86
+ const release = await lockfile.lock(filePath, { retries, stale });
47
87
  try {
48
88
  return await fn();
49
89
  }
@@ -52,7 +92,7 @@ export async function withFileLock(filePath, fn) {
52
92
  }
53
93
  }
54
94
  catch (err) {
55
- if (err.code === "ELOCKED") {
95
+ if (err?.code === "ELOCKED" && onLocked === "skip") {
56
96
  return await fn();
57
97
  }
58
98
  throw err;
@@ -84,6 +84,7 @@ export class MergeConflictError extends GSDError {
84
84
  */
85
85
  export const RUNTIME_EXCLUSION_PATHS = [
86
86
  ".gsd/activity/",
87
+ ".gsd/audit/",
87
88
  ".gsd/forensics/",
88
89
  ".gsd/runtime/",
89
90
  ".gsd/worktrees/",
@@ -24,6 +24,7 @@ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
24
24
  */
25
25
  const GSD_RUNTIME_PATTERNS = [
26
26
  ".gsd/activity/",
27
+ ".gsd/audit/",
27
28
  ".gsd/forensics/",
28
29
  ".gsd/runtime/",
29
30
  ".gsd/worktrees/",
@@ -13,6 +13,7 @@ import { deriveState } from "./state.js";
13
13
  import { invalidateAllCaches } from "./cache.js";
14
14
  import { gsdRoot, resolveMilestoneFile, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
15
15
  import { readFileSync, writeFileSync, existsSync } from "node:fs";
16
+ import { atomicWriteSync } from "./atomic-write.js";
16
17
  import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
17
18
  import { loadEffectiveGSDPreferences } from "./preferences.js";
18
19
  import { saveQueueOrder } from "./queue-order.js";
@@ -361,5 +362,7 @@ function syncProjectMdSequence(basePath, registry, newOrder) {
361
362
  const separatorLine = lines[tableStart + 1];
362
363
  const newTable = [headerLine, separatorLine, ...newRows];
363
364
  lines.splice(tableStart, tableEnd - tableStart, ...newTable);
364
- writeFileSync(projectPath, lines.join("\n"), "utf-8");
365
+ // Atomic write: tmp+rename avoids a torn PROJECT.md appearing dirty in
366
+ // another worktree's working tree during a concurrent /gsd auto merge.
367
+ atomicWriteSync(projectPath, lines.join("\n"), "utf-8");
365
368
  }
@@ -11,8 +11,10 @@
11
11
  * - queryJournal() enables programmatic filtering by flowId, eventType, unitId, time range
12
12
  * - Silent failure: journal writes never throw — absence of events is the failure signal
13
13
  */
14
- import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
14
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, } from "node:fs";
15
15
  import { join } from "node:path";
16
+ import { isStaleWrite } from "./auto/turn-epoch.js";
17
+ import { withFileLockSync } from "./file-lock.js";
16
18
  import { gsdRoot } from "./paths.js";
17
19
  import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
18
20
  import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
@@ -26,12 +28,25 @@ import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
26
28
  * Never throws — all errors are silently caught.
27
29
  */
28
30
  export function emitJournalEvent(basePath, entry) {
31
+ // Drop writes from a turn superseded by timeout recovery / cancellation.
32
+ // See auto/turn-epoch.ts for the full rationale.
33
+ if (isStaleWrite("journal"))
34
+ return;
29
35
  try {
30
36
  const journalDir = join(gsdRoot(basePath), "journal");
31
37
  mkdirSync(journalDir, { recursive: true });
32
38
  const dateStr = entry.ts.slice(0, 10);
33
39
  const filePath = join(journalDir, `${dateStr}.jsonl`);
34
- appendFileSync(filePath, JSON.stringify(entry) + "\n");
40
+ // Ensure file exists so proper-lockfile can acquire a lock against it.
41
+ if (!existsSync(filePath))
42
+ closeSync(openSync(filePath, "a"));
43
+ // onLocked: "skip" — journal writes are best-effort. POSIX O_APPEND
44
+ // atomicity still protects small entries; the lock mainly serializes
45
+ // larger writes and gives cross-process exclusivity on platforms where
46
+ // O_APPEND semantics are weaker (Windows).
47
+ withFileLockSync(filePath, () => {
48
+ appendFileSync(filePath, JSON.stringify(entry) + "\n");
49
+ }, { onLocked: "skip" });
35
50
  }
36
51
  catch {
37
52
  // Silent failure — journal must never break auto-mode
@@ -18,6 +18,18 @@ import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
18
18
  import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
19
19
  import { removeWorktree } from "./worktree-manager.js";
20
20
  import { logWarning } from "./workflow-logger.js";
21
+ import { isAutoActive } from "./auto.js";
22
+ /**
23
+ * Writer-side assert for mutations that race with auto-mode's squash merge (#4704).
24
+ * Auto-mode is confirmed not to call parkMilestone/discardMilestone/unparkMilestone
25
+ * internally — these throws only surface invariant violations from new or forgotten
26
+ * call sites, which is the correct failure mode to catch loudly.
27
+ */
28
+ function assertNotAutoActive(action) {
29
+ if (isAutoActive()) {
30
+ throw new Error(`${action} cannot run while auto-mode is active. Stop auto-mode first with /gsd stop.`);
31
+ }
32
+ }
21
33
  // ─── Park ──────────────────────────────────────────────────────────────────
22
34
  /**
23
35
  * Park a milestone — creates a PARKED.md marker file with reason and timestamp.
@@ -25,6 +37,7 @@ import { logWarning } from "./workflow-logger.js";
25
37
  * Returns true if successfully parked, false if milestone not found or already parked.
26
38
  */
27
39
  export function parkMilestone(basePath, milestoneId, reason) {
40
+ assertNotAutoActive("park milestone");
28
41
  const mDir = resolveMilestonePath(basePath, milestoneId);
29
42
  if (!mDir || !existsSync(mDir))
30
43
  return false;
@@ -65,6 +78,7 @@ export function parkMilestone(basePath, milestoneId, reason) {
65
78
  * Returns true if successfully unparked, false if milestone not found or not parked.
66
79
  */
67
80
  export function unparkMilestone(basePath, milestoneId) {
81
+ assertNotAutoActive("unpark milestone");
68
82
  const mDir = resolveMilestonePath(basePath, milestoneId);
69
83
  if (!mDir || !existsSync(mDir))
70
84
  return false;
@@ -97,6 +111,7 @@ export function unparkMilestone(basePath, milestoneId) {
97
111
  * Returns true if successfully discarded, false if milestone not found.
98
112
  */
99
113
  export function discardMilestone(basePath, milestoneId) {
114
+ assertNotAutoActive("discard milestone");
100
115
  const mDir = resolveMilestonePath(basePath, milestoneId);
101
116
  if (!mDir || !existsSync(mDir))
102
117
  return false;
@@ -13,8 +13,9 @@
13
13
  * Auto-triggered: after each milestone completion (when auto_report: true).
14
14
  * Manual: /gsd export --html
15
15
  */
16
- import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
16
+ import { readFileSync, mkdirSync, existsSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
+ import { atomicWriteSync } from './atomic-write.js';
18
19
  import { gsdRoot } from './paths.js';
19
20
  import { formatCost, formatTokenCount } from './metrics.js';
20
21
  import { formatDateShort, formatDuration } from '../shared/format-utils.js';
@@ -43,7 +44,7 @@ export function loadReportsIndex(basePath) {
43
44
  function saveReportsIndex(basePath, index) {
44
45
  const dir = reportsDir(basePath);
45
46
  mkdirSync(dir, { recursive: true });
46
- writeFileSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
47
+ atomicWriteSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
47
48
  }
48
49
  /**
49
50
  * Write a report snapshot to .gsd/reports/, update reports.json, regenerate index.html.
@@ -56,7 +57,7 @@ export function writeReportSnapshot(args) {
56
57
  const prefix = args.milestoneId === 'final' ? 'final' : args.milestoneId;
57
58
  const filename = `${prefix}-${timestamp}.html`;
58
59
  const filePath = join(dir, filename);
59
- writeFileSync(filePath, args.html, 'utf-8');
60
+ atomicWriteSync(filePath, args.html, 'utf-8');
60
61
  // Load or init registry
61
62
  const existing = loadReportsIndex(args.basePath);
62
63
  const index = existing ?? {
@@ -97,7 +98,7 @@ export function writeReportSnapshot(args) {
97
98
  // ─── HTML Index Generator ─────────────────────────────────────────────────────
98
99
  export function regenerateHtmlIndex(basePath, index) {
99
100
  const html = buildIndexHtml(index);
100
- writeFileSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
101
+ atomicWriteSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
101
102
  }
102
103
  function buildIndexHtml(index) {
103
104
  const { projectName, projectPath, gsdVersion, entries } = index;
@@ -16,6 +16,7 @@ import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
16
16
  import { saveFile, clearParseCache } from "../files.js";
17
17
  import { invalidateStateCache } from "../state.js";
18
18
  import { renderRoadmapCheckboxes } from "../markdown-renderer.js";
19
+ import { isStaleWrite } from "../auto/turn-epoch.js";
19
20
  import { renderAllProjections } from "../workflow-projections.js";
20
21
  import { writeManifest } from "../workflow-manifest.js";
21
22
  import { appendEvent } from "../workflow-events.js";
@@ -228,6 +229,10 @@ export async function handleCompleteSlice(params, basePath) {
228
229
  }
229
230
  const slice = getSlice(params.milestoneId, params.sliceId);
230
231
  if (slice && isClosedStatus(slice.status)) {
232
+ if (isStaleWrite("complete-slice")) {
233
+ guardError = "__stale_duplicate__";
234
+ return;
235
+ }
231
236
  guardError = `slice ${params.sliceId} is already complete — use gsd_slice_reopen first if you need to redo it`;
232
237
  return;
233
238
  }
@@ -248,6 +253,22 @@ export async function handleCompleteSlice(params, basePath) {
248
253
  insertSlice({ id: params.sliceId, milestoneId: params.milestoneId, title: params.sliceId });
249
254
  updateSliceStatus(params.milestoneId, params.sliceId, "complete", completedAt);
250
255
  });
256
+ if (guardError === "__stale_duplicate__") {
257
+ // Stale duplicate from a turn superseded by timeout recovery. Return a
258
+ // non-mutating success so the orphaned LLM tool call unwinds quietly.
259
+ const sliceDir = resolveSlicePath(basePath, params.milestoneId, params.sliceId);
260
+ const staleSummaryPath = sliceDir
261
+ ? join(sliceDir, `${params.sliceId}-SUMMARY.md`)
262
+ : join(basePath, ".gsd", "milestones", params.milestoneId, "slices", params.sliceId, `${params.sliceId}-SUMMARY.md`);
263
+ return {
264
+ sliceId: params.sliceId,
265
+ milestoneId: params.milestoneId,
266
+ summaryPath: staleSummaryPath,
267
+ uatPath: staleSummaryPath.replace(/-SUMMARY\.md$/, "-UAT.md"),
268
+ duplicate: true,
269
+ stale: true,
270
+ };
271
+ }
251
272
  if (guardError) {
252
273
  return { error: guardError };
253
274
  }
@@ -21,6 +21,7 @@ import { writeManifest } from "../workflow-manifest.js";
21
21
  import { appendEvent } from "../workflow-events.js";
22
22
  import { logWarning, logError } from "../workflow-logger.js";
23
23
  import { loadEffectiveGSDPreferences } from "../preferences.js";
24
+ import { isStaleWrite } from "../auto/turn-epoch.js";
24
25
  import { buildEscalationArtifact, writeEscalationArtifact } from "../escalation.js";
25
26
  /**
26
27
  * Map an execute-task-owned gate id to the CompleteTaskParams field whose
@@ -167,6 +168,18 @@ export async function handleCompleteTask(params, basePath) {
167
168
  }
168
169
  const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
169
170
  if (existingTask && isClosedStatus(existingTask.status)) {
171
+ // Stale-turn path: a timed-out turn that was superseded by recovery
172
+ // can still reach this code when its LLM call eventually returns and
173
+ // invokes gsd_complete_task. Returning an error would produce noisy
174
+ // "already complete — use reopen first" logs in the orphaned turn.
175
+ // Instead, signal the duplicate via a non-mutating success shape that
176
+ // callers can detect via `duplicate: true` / `stale: true`.
177
+ if (isStaleWrite("complete-task")) {
178
+ // Sentinel handled below — outside the transaction — so we don't
179
+ // render SUMMARY.md or flip plan checkboxes for a stale duplicate.
180
+ guardError = "__stale_duplicate__";
181
+ return;
182
+ }
170
183
  guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
171
184
  return;
172
185
  }
@@ -201,6 +214,24 @@ export async function handleCompleteTask(params, basePath) {
201
214
  });
202
215
  }
203
216
  });
217
+ if (guardError === "__stale_duplicate__") {
218
+ // Orphaned-turn duplicate: the task is already complete from the
219
+ // superseded turn's earlier (real) call. Return a non-mutating success
220
+ // so the stale LLM tool call unwinds cleanly. summaryPath is synthesized
221
+ // from the existing on-disk layout; no file is written.
222
+ const tasksDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
223
+ const staleSummaryPath = tasksDir
224
+ ? join(tasksDir, `${params.taskId}-SUMMARY.md`)
225
+ : join(basePath, ".gsd", "milestones", params.milestoneId, "slices", params.sliceId, "tasks", `${params.taskId}-SUMMARY.md`);
226
+ return {
227
+ taskId: params.taskId,
228
+ sliceId: params.sliceId,
229
+ milestoneId: params.milestoneId,
230
+ summaryPath: staleSummaryPath,
231
+ duplicate: true,
232
+ stale: true,
233
+ };
234
+ }
204
235
  if (guardError) {
205
236
  return { error: guardError };
206
237
  }
@@ -1,6 +1,8 @@
1
- import { appendFileSync, mkdirSync } from "node:fs";
1
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { isStaleWrite } from "../auto/turn-epoch.js";
5
+ import { withFileLockSync } from "../file-lock.js";
4
6
  import { gsdRoot } from "../paths.js";
5
7
  import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
6
8
  function auditLogPath(basePath) {
@@ -22,9 +24,23 @@ export function buildAuditEnvelope(args) {
22
24
  };
23
25
  }
24
26
  export function emitUokAuditEvent(basePath, event) {
27
+ // Drop writes from a turn superseded by timeout recovery / cancellation.
28
+ if (isStaleWrite("uok-audit"))
29
+ return;
25
30
  try {
26
31
  ensureAuditDir(basePath);
27
- appendFileSync(auditLogPath(basePath), `${JSON.stringify(event)}\n`, "utf-8");
32
+ const path = auditLogPath(basePath);
33
+ // proper-lockfile requires the target file to exist before locking.
34
+ // Touch it via open(O_APPEND|O_CREAT) so the first writer wins the race
35
+ // atomically at the kernel level.
36
+ if (!existsSync(path))
37
+ closeSync(openSync(path, "a"));
38
+ // onLocked: "skip" — audit writes are best-effort; under heavy contention
39
+ // POSIX O_APPEND atomicity still protects small line writes, so skipping
40
+ // the lock rather than stalling orchestration is the correct tradeoff.
41
+ withFileLockSync(path, () => {
42
+ appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
43
+ }, { onLocked: "skip" });
28
44
  }
29
45
  catch {
30
46
  // Best-effort: audit writes must never break orchestration.
@@ -15,8 +15,9 @@
15
15
  // a process. The auto-loop must call _resetLogs() (or drainAndSummarize()) at
16
16
  // the start of each unit to prevent log bleed between units running in the same
17
17
  // Node process.
18
- import { appendFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
18
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, } from "node:fs";
19
19
  import { join } from "node:path";
20
+ import { withFileLockSync } from "./file-lock.js";
20
21
  import { appendNotification } from "./notification-store.js";
21
22
  import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
22
23
  import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
@@ -231,8 +232,15 @@ function _push(severity, component, message, context) {
231
232
  try {
232
233
  const auditDir = join(_auditBasePath, ".gsd");
233
234
  mkdirSync(auditDir, { recursive: true });
235
+ const auditPath = join(auditDir, "audit-log.jsonl");
234
236
  const sanitized = _sanitizeForAudit(entry);
235
- appendFileSync(join(auditDir, "audit-log.jsonl"), JSON.stringify(sanitized) + "\n", "utf-8");
237
+ // Ensure file exists so proper-lockfile can acquire a lock against it.
238
+ if (!existsSync(auditPath))
239
+ closeSync(openSync(auditPath, "a"));
240
+ // onLocked: "skip" — never block error logging on lock contention.
241
+ withFileLockSync(auditPath, () => {
242
+ appendFileSync(auditPath, JSON.stringify(sanitized) + "\n", "utf-8");
243
+ }, { onLocked: "skip" });
236
244
  }
237
245
  catch (auditErr) {
238
246
  // Best-effort — never let audit write failures bubble up