@opengsd/gsd-pi 1.4.0-dev.c18009cd → 1.5.0-dev.553dc518

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 (274) hide show
  1. package/README.md +10 -10
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/closeout.js +97 -5
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +4 -3
  5. package/dist/resources/extensions/gsd/auto/pre-dispatch.js +6 -3
  6. package/dist/resources/extensions/gsd/auto/unit-phase.js +5 -9
  7. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +4 -5
  8. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  9. package/dist/resources/extensions/gsd/auto-worktree-branch-lifecycle.js +189 -0
  10. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +133 -0
  11. package/dist/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.js +35 -0
  12. package/dist/resources/extensions/gsd/auto-worktree-creation.js +130 -0
  13. package/dist/resources/extensions/gsd/auto-worktree-entry.js +118 -0
  14. package/dist/resources/extensions/gsd/auto-worktree-merge-already-merged.js +98 -0
  15. package/dist/resources/extensions/gsd/auto-worktree-merge-branch-head.js +52 -0
  16. package/dist/resources/extensions/gsd/auto-worktree-merge-cleanup.js +87 -0
  17. package/dist/resources/extensions/gsd/auto-worktree-merge-code-changes.js +46 -0
  18. package/dist/resources/extensions/gsd/auto-worktree-merge-db-ready.js +68 -0
  19. package/dist/resources/extensions/gsd/auto-worktree-merge-integration-branch.js +51 -0
  20. package/dist/resources/extensions/gsd/auto-worktree-merge-message.js +106 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-merge-pre-teardown.js +64 -0
  22. package/dist/resources/extensions/gsd/auto-worktree-merge-stash.js +168 -0
  23. package/dist/resources/extensions/gsd/auto-worktree-merge.js +302 -0
  24. package/dist/resources/extensions/gsd/auto-worktree-milestone-shelter.js +119 -0
  25. package/dist/resources/extensions/gsd/auto-worktree-path-resolution.js +5 -0
  26. package/dist/resources/extensions/gsd/auto-worktree-path-utils.js +5 -0
  27. package/dist/resources/extensions/gsd/auto-worktree-resource-version.js +41 -0
  28. package/dist/resources/extensions/gsd/auto-worktree-runtime-cleanup.js +99 -0
  29. package/dist/resources/extensions/gsd/auto-worktree-session-registry.js +57 -0
  30. package/dist/resources/extensions/gsd/auto-worktree-sync.js +249 -0
  31. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +120 -0
  32. package/dist/resources/extensions/gsd/auto-worktree.js +20 -2073
  33. package/dist/resources/extensions/gsd/auto.js +9 -3
  34. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -1
  35. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -1
  36. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  37. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +118 -21
  38. package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
  39. package/dist/resources/extensions/gsd/commands-workflow-templates.js +3 -3
  40. package/dist/resources/extensions/gsd/db/queries.js +9 -9
  41. package/dist/resources/extensions/gsd/doctor-engine-checks.js +93 -11
  42. package/dist/resources/extensions/gsd/flat-phase-migration.js +14 -3
  43. package/dist/resources/extensions/gsd/forensics.js +1 -1
  44. package/dist/resources/extensions/gsd/git-conflict-resolve.js +2 -2
  45. package/dist/resources/extensions/gsd/git-service.js +45 -0
  46. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  47. package/dist/resources/extensions/gsd/guided-flow.js +6 -10
  48. package/dist/resources/extensions/gsd/markdown-renderer.js +15 -3
  49. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +11 -0
  50. package/dist/resources/extensions/gsd/milestone-settlement.js +1 -1
  51. package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
  52. package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -1
  53. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  54. package/dist/resources/extensions/gsd/preferences-validation.js +82 -0
  55. package/dist/resources/extensions/gsd/prompts/system.md +6 -6
  56. package/dist/resources/extensions/gsd/quick.js +12 -3
  57. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +2 -1
  58. package/dist/resources/extensions/gsd/state/derive/from-db.js +88 -153
  59. package/dist/resources/extensions/gsd/tool-contract.js +14 -0
  60. package/dist/resources/extensions/gsd/unit-registry.js +12 -2
  61. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +1 -1
  62. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -1
  63. package/dist/resources/extensions/gsd/workspace-git-preflight.js +2 -2
  64. package/dist/resources/extensions/gsd/worktree-command.js +1 -1
  65. package/dist/resources/extensions/gsd/worktree-lifecycle.js +14 -9
  66. package/dist/resources/extensions/gsd/worktree-reentry.js +2 -1
  67. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  68. package/dist/web/standalone/.next/BUILD_ID +1 -1
  69. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  70. package/dist/web/standalone/.next/build-manifest.json +2 -2
  71. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  72. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.html +1 -1
  89. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  96. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  98. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  99. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  100. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  101. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  102. package/integrations/hermes/open_gsd_hermes/gsd_client.py +24 -35
  103. package/integrations/hermes/tests/test_gsd_client_timeout.py +4 -4
  104. package/package.json +6 -6
  105. package/packages/cloud-mcp-gateway/package.json +2 -2
  106. package/packages/contracts/package.json +1 -1
  107. package/packages/daemon/package.json +4 -4
  108. package/packages/gsd-agent-core/dist/export-html/template.js +53 -4
  109. package/packages/gsd-agent-core/package.json +5 -5
  110. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +3 -2
  112. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  113. package/packages/gsd-agent-modes/package.json +7 -7
  114. package/packages/mcp-server/package.json +4 -4
  115. package/packages/native/package.json +1 -1
  116. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  117. package/packages/pi-agent-core/dist/agent-loop.js +14 -9
  118. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  119. package/packages/pi-agent-core/package.json +1 -1
  120. package/packages/pi-ai/dist/index.d.ts +1 -0
  121. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  122. package/packages/pi-ai/dist/index.js +1 -0
  123. package/packages/pi-ai/dist/index.js.map +1 -1
  124. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  125. package/packages/pi-ai/dist/providers/transform-messages.js +7 -3
  126. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  127. package/packages/pi-ai/dist/tool-result-content.d.ts +3 -0
  128. package/packages/pi-ai/dist/tool-result-content.d.ts.map +1 -0
  129. package/packages/pi-ai/dist/tool-result-content.js +62 -0
  130. package/packages/pi-ai/dist/tool-result-content.js.map +1 -0
  131. package/packages/pi-ai/dist/types.d.ts +8 -20
  132. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  133. package/packages/pi-ai/dist/types.js.map +1 -1
  134. package/packages/pi-ai/package.json +1 -1
  135. package/packages/pi-coding-agent/package.json +7 -7
  136. package/packages/pi-tui/package.json +2 -2
  137. package/packages/rpc-client/package.json +2 -2
  138. package/pkg/dist/core/export-html/template.js +53 -4
  139. package/pkg/package.json +1 -1
  140. package/src/resources/extensions/gsd/auto/closeout.ts +114 -7
  141. package/src/resources/extensions/gsd/auto/orchestrator.ts +4 -3
  142. package/src/resources/extensions/gsd/auto/pre-dispatch.ts +6 -3
  143. package/src/resources/extensions/gsd/auto/unit-phase.ts +5 -9
  144. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +4 -5
  145. package/src/resources/extensions/gsd/auto-start.ts +4 -2
  146. package/src/resources/extensions/gsd/auto-worktree-branch-lifecycle.ts +237 -0
  147. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +166 -0
  148. package/src/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.ts +37 -0
  149. package/src/resources/extensions/gsd/auto-worktree-creation.ts +171 -0
  150. package/src/resources/extensions/gsd/auto-worktree-entry.ts +154 -0
  151. package/src/resources/extensions/gsd/auto-worktree-merge-already-merged.ts +161 -0
  152. package/src/resources/extensions/gsd/auto-worktree-merge-branch-head.ts +83 -0
  153. package/src/resources/extensions/gsd/auto-worktree-merge-cleanup.ts +132 -0
  154. package/src/resources/extensions/gsd/auto-worktree-merge-code-changes.ts +71 -0
  155. package/src/resources/extensions/gsd/auto-worktree-merge-db-ready.ts +112 -0
  156. package/src/resources/extensions/gsd/auto-worktree-merge-integration-branch.ts +99 -0
  157. package/src/resources/extensions/gsd/auto-worktree-merge-message.ts +166 -0
  158. package/src/resources/extensions/gsd/auto-worktree-merge-pre-teardown.ts +93 -0
  159. package/src/resources/extensions/gsd/auto-worktree-merge-stash.ts +213 -0
  160. package/src/resources/extensions/gsd/auto-worktree-merge.ts +391 -0
  161. package/src/resources/extensions/gsd/auto-worktree-milestone-shelter.ts +159 -0
  162. package/src/resources/extensions/gsd/auto-worktree-path-resolution.ts +6 -0
  163. package/src/resources/extensions/gsd/auto-worktree-path-utils.ts +10 -0
  164. package/src/resources/extensions/gsd/auto-worktree-resource-version.ts +44 -0
  165. package/src/resources/extensions/gsd/auto-worktree-runtime-cleanup.ts +119 -0
  166. package/src/resources/extensions/gsd/auto-worktree-session-registry.ts +71 -0
  167. package/src/resources/extensions/gsd/auto-worktree-sync.ts +370 -0
  168. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +161 -0
  169. package/src/resources/extensions/gsd/auto-worktree.ts +69 -2429
  170. package/src/resources/extensions/gsd/auto.ts +11 -17
  171. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -1
  172. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +19 -1
  173. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  174. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +152 -21
  175. package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
  176. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -3
  177. package/src/resources/extensions/gsd/db/queries.ts +9 -9
  178. package/src/resources/extensions/gsd/doctor-engine-checks.ts +109 -10
  179. package/src/resources/extensions/gsd/flat-phase-migration.ts +14 -3
  180. package/src/resources/extensions/gsd/forensics.ts +1 -1
  181. package/src/resources/extensions/gsd/git-conflict-resolve.ts +2 -2
  182. package/src/resources/extensions/gsd/git-service.ts +53 -0
  183. package/src/resources/extensions/gsd/gsd-db.ts +13 -0
  184. package/src/resources/extensions/gsd/guided-flow.ts +7 -11
  185. package/src/resources/extensions/gsd/markdown-renderer.ts +15 -3
  186. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +15 -0
  187. package/src/resources/extensions/gsd/milestone-settlement.ts +1 -1
  188. package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
  189. package/src/resources/extensions/gsd/parallel-orchestrator.ts +3 -1
  190. package/src/resources/extensions/gsd/preferences-types.ts +30 -0
  191. package/src/resources/extensions/gsd/preferences-validation.ts +69 -0
  192. package/src/resources/extensions/gsd/prompts/system.md +6 -6
  193. package/src/resources/extensions/gsd/quick.ts +11 -3
  194. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +3 -1
  195. package/src/resources/extensions/gsd/state/derive/from-db.ts +201 -157
  196. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +2 -1
  197. package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +1 -1
  198. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +2 -1
  199. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +1 -1
  200. package/src/resources/extensions/gsd/tests/auto-worktree-merge-already-merged.test.ts +177 -0
  201. package/src/resources/extensions/gsd/tests/auto-worktree-merge-branch-head.test.ts +118 -0
  202. package/src/resources/extensions/gsd/tests/auto-worktree-merge-cleanup.test.ts +115 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-merge-code-changes.test.ts +76 -0
  204. package/src/resources/extensions/gsd/tests/auto-worktree-merge-db-ready.test.ts +140 -0
  205. package/src/resources/extensions/gsd/tests/auto-worktree-merge-integration-branch.test.ts +123 -0
  206. package/src/resources/extensions/gsd/tests/auto-worktree-merge-message.test.ts +114 -0
  207. package/src/resources/extensions/gsd/tests/auto-worktree-merge-pre-teardown.test.ts +84 -0
  208. package/src/resources/extensions/gsd/tests/auto-worktree-merge-stash.test.ts +70 -0
  209. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-shelter.test.ts +52 -0
  210. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +6 -6
  211. package/src/resources/extensions/gsd/tests/auto-worktree-resource-version.test.ts +59 -0
  212. package/src/resources/extensions/gsd/tests/auto-worktree-untracked-content.test.ts +3 -1
  213. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +1 -1
  214. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +4 -0
  215. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +174 -0
  216. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +1 -1
  217. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +44 -1
  218. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +25 -1
  219. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +40 -0
  220. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +4 -6
  221. package/src/resources/extensions/gsd/tests/integration/auto-stash-merge.test.ts +2 -1
  222. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +3 -3
  223. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +7 -8
  224. package/src/resources/extensions/gsd/tests/integration/feature-branch-lifecycle-integration.test.ts +2 -5
  225. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +76 -0
  226. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +1 -1
  227. package/src/resources/extensions/gsd/tests/integration/merge-preserve-worktree-on-preteardown-error.test.ts +1 -1
  228. package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +2 -1
  229. package/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts +5 -7
  230. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +2 -4
  231. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +1 -1
  232. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +1 -1
  233. package/src/resources/extensions/gsd/tests/merge-self-branch-guard.test.ts +1 -1
  234. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +439 -0
  235. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +24 -0
  236. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +1 -1
  237. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -3
  238. package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +1 -1
  239. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +1 -4
  240. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +5 -0
  241. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
  242. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -5
  243. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +2 -1
  244. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +2 -1
  245. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +1 -1
  246. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +1 -1
  247. package/src/resources/extensions/gsd/tests/system-flat-phase-layout.test.ts +33 -0
  248. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +4 -4
  249. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +2 -1
  250. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +4 -4
  251. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +103 -0
  252. package/src/resources/extensions/gsd/tests/uat-stuck-loop-orphaned-worktree.test.ts +1 -1
  253. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +26 -1
  254. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -1
  255. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +1 -1
  256. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +1 -3
  257. package/src/resources/extensions/gsd/tests/worktree-expected-warnings.test.ts +1 -1
  258. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +1 -1
  259. package/src/resources/extensions/gsd/tests/worktree-main-branch.test.ts +1 -1
  260. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +1 -1
  261. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +2 -1
  262. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +5 -2
  263. package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +1 -1
  264. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +1 -1
  265. package/src/resources/extensions/gsd/tool-contract.ts +29 -0
  266. package/src/resources/extensions/gsd/unit-registry.ts +16 -2
  267. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +1 -1
  268. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -1
  269. package/src/resources/extensions/gsd/workspace-git-preflight.ts +2 -2
  270. package/src/resources/extensions/gsd/worktree-command.ts +1 -1
  271. package/src/resources/extensions/gsd/worktree-lifecycle.ts +19 -15
  272. package/src/resources/extensions/gsd/worktree-reentry.ts +2 -1
  273. /package/dist/web/standalone/.next/static/{L2UMqXELiDH3oz-7yguXp → ZpFDazP-38PXBPiI_LdZf}/_buildManifest.js +0 -0
  274. /package/dist/web/standalone/.next/static/{L2UMqXELiDH3oz-7yguXp → ZpFDazP-38PXBPiI_LdZf}/_ssgManifest.js +0 -0
@@ -177,24 +177,18 @@ import {
177
177
  import { GitServiceImpl } from "./git-service.js";
178
178
  import { nativeCheckoutBranch } from "./native-git-bridge.js";
179
179
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
180
- import {
181
- createAutoWorktree,
182
- enterAutoWorktree,
183
- enterBranchModeForMilestone,
184
- teardownAutoWorktree,
185
- isInAutoWorktree,
186
- getAutoWorktreePath,
187
- getAutoWorktreeOriginalBase,
188
- mergeMilestoneToMain,
189
- autoWorktreeBranch,
190
- syncWorktreeStateBack,
191
- readResourceVersion,
192
- checkResourcesStale,
193
- escapeStaleWorktree,
194
- } from "./auto-worktree.js";
180
+ import { autoWorktreeBranch, enterBranchModeForMilestone } from "./auto-worktree-branch-lifecycle.js";
181
+ import { createAutoWorktree } from "./auto-worktree-creation.js";
182
+ import { enterAutoWorktree, isInAutoWorktree } from "./auto-worktree-entry.js";
183
+ import { getAutoWorktreePath } from "./auto-worktree-path-resolution.js";
184
+ import { checkResourcesStale, readResourceVersion } from "./auto-worktree-resource-version.js";
185
+ import { escapeStaleWorktree } from "./auto-worktree-runtime-cleanup.js";
186
+ import { getAutoWorktreeOriginalBase } from "./auto-worktree-session-registry.js";
187
+ import { syncWorktreeStateBack } from "./auto-worktree-sync.js";
188
+ import { teardownAutoWorktree } from "./auto-worktree-teardown.js";
195
189
  import { pruneQueueOrder } from "./queue-order.js";
196
190
  import { startCommandPolling as _startCommandPolling, isRemoteConfigured } from "../remote-questions/manager.js";
197
- import { createMilestoneMergeTransaction } from "./milestone-merge-transaction.js";
191
+ import { createDefaultMilestoneMergeTransaction } from "./milestone-merge-transaction.js";
198
192
 
199
193
  import { debugLog, isDebugEnabled, writeDebugSummary } from "./debug-logger.js";
200
194
  import {
@@ -2326,7 +2320,7 @@ export function buildWorktreeLifecycleDeps(): WorktreeLifecycleDeps {
2326
2320
  return new GitServiceImpl(basePath, gitConfig);
2327
2321
  },
2328
2322
  worktreeProjection: new WorktreeStateProjection(),
2329
- mergeMilestone: createMilestoneMergeTransaction(mergeMilestoneToMain),
2323
+ mergeMilestone: createDefaultMilestoneMergeTransaction(),
2330
2324
  };
2331
2325
  }
2332
2326
 
@@ -14,7 +14,7 @@ import {
14
14
  type WorkflowDatabaseOpenResult,
15
15
  type WorkflowDatabaseStatus,
16
16
  } from "../db-workspace.js";
17
- import { getAutoWorktreePath } from "../auto-worktree.js";
17
+ import { getAutoWorktreePath } from "../auto-worktree-path-resolution.js";
18
18
  import { resolveWorktreeProjectRoot } from "../worktree-root.js";
19
19
  import { worktreesDirs } from "../worktree-placement.js";
20
20
 
@@ -40,7 +40,7 @@ import {
40
40
  } from "../auto-tool-tracking.js";
41
41
  import { applyProviderPayloadPolicy } from "../provider-payload-policy.js";
42
42
 
43
- import { checkToolCallLoop, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
43
+ import { checkToolCallLoop, configureToolCallLoopGuard, recordToolCallLoopMutation, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
44
44
  import { MINIMAL_AUTO_BASE_TOOL_NAMES } from "./core-session-tools.js";
45
45
  import { maybePauseAutoForApprovalGate, resetPendingGatePauseGuard } from "./pending-gate-pause.js";
46
46
  import { saveActivityLog } from "../activity-log.js";
@@ -594,6 +594,22 @@ async function applyCompactionThresholdOverride(ctx: ExtensionContext): Promise<
594
594
  }
595
595
  }
596
596
 
597
+ /**
598
+ * Apply user-tunable tool-call loop guard thresholds (#1198) from GSD
599
+ * preferences plus GSD_TOOL_LOOP_* env overrides. Runs at session boundaries
600
+ * so both interactive sessions and `/gsd auto` pick up the configuration.
601
+ * Non-fatal: falls back to built-in defaults when preferences cannot be loaded.
602
+ */
603
+ async function applyToolCallLoopGuardConfig(basePath: string): Promise<void> {
604
+ try {
605
+ const { loadEffectiveGSDPreferences } = await import("../preferences.js");
606
+ const prefs = loadEffectiveGSDPreferences(basePath);
607
+ configureToolCallLoopGuard(prefs?.preferences.tool_call_loop_guard);
608
+ } catch {
609
+ configureToolCallLoopGuard(null);
610
+ }
611
+ }
612
+
597
613
  function clearDeferredApprovalGate(basePath?: string): void {
598
614
  if (!basePath) {
599
615
  deferredApprovalGates.clear();
@@ -996,6 +1012,7 @@ export function registerHooks(
996
1012
  }
997
1013
  resetWriteGateState(basePath);
998
1014
  resetToolCallLoopGuard();
1015
+ await applyToolCallLoopGuardConfig(basePath);
999
1016
  approvalQuestionAbortInFlight = false;
1000
1017
  clearDeferredApprovalGate();
1001
1018
  await resetAskUserQuestionsTurnCache();
@@ -1087,6 +1104,7 @@ export function registerHooks(
1087
1104
  initSessionNotifications(ctx);
1088
1105
  resetWriteGateState(basePath);
1089
1106
  resetToolCallLoopGuard();
1107
+ await applyToolCallLoopGuardConfig(basePath);
1090
1108
  clearDeferredApprovalGate();
1091
1109
  await resetAskUserQuestionsTurnCache();
1092
1110
  clearDiscussionFlowState(basePath);
@@ -14,7 +14,7 @@ import { resolveModelWithFallbacksForUnit } from "../preferences-models.js";
14
14
  import { resolveGsdRootFile, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, relSliceFile, relSlicePath, relTaskFile } from "../paths.js";
15
15
  import { extractIntroAndRules } from "../knowledge-parser.js";
16
16
  import { ensureCodebaseMapFresh, readCodebaseMap } from "../codebase-generator.js";
17
- import { getActiveAutoWorktreeContext } from "../auto-worktree.js";
17
+ import { getActiveAutoWorktreeContext } from "../auto-worktree-session-registry.js";
18
18
  import { getActiveWorktreeName, getWorktreeOriginalCwd } from "../worktree-session-state.js";
19
19
  import { deriveState } from "../state.js";
20
20
  import { formatOverridesSection, formatShortcut, loadActiveOverrides, loadFile, parseContinue, parseSummary } from "../files.js";
@@ -21,6 +21,11 @@
21
21
  * signature, so the identical-args streak never trips, while allowing
22
22
  * tool-heavy turns that are making file-mutation progress. Whichever guard
23
23
  * trips first blocks.
24
+ *
25
+ * Thresholds, exempt tools, and enable flags are user-tunable (#1198) via the
26
+ * `tool_call_loop_guard` key in `.gsd/PREFERENCES.md` and `GSD_TOOL_LOOP_*`
27
+ * environment variables, applied through {@link configureToolCallLoopGuard}.
28
+ * Defaults preserve the original hardcoded behavior.
24
29
  */
25
30
 
26
31
  import { createHash } from "node:crypto";
@@ -28,26 +33,143 @@ import { INHERENTLY_REPEATABLE_TOOL_SET } from "./core-session-tools.js";
28
33
  import { hasBrowserContractPrefix } from "../../shared/browser-contract.js";
29
34
  import { canonicalToolName } from "../engine-hook-contract.js";
30
35
 
31
- const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
36
+ /** Built-in defaults. Preserved when preferences/env do not override them. */
37
+ const DEFAULT_MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
38
+ const DEFAULT_PER_TOOL_DEFAULT_CAP = 6;
39
+ const DEFAULT_PER_TOOL_REPEATABLE_CAP = 15;
40
+ const DEFAULT_PER_TOOL_CAP_EXEMPT_TOOLS = ["find", "glob", "grep", "ls", "read", "search_and_read"] as const;
32
41
 
33
42
  /** Interactive/user-facing tools where even 1 duplicate is confusing. */
34
43
  const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
35
44
  const MAX_CONSECUTIVE_STRICT = 1;
36
45
 
46
+ const STATE_MUTATING_TOOL_SET = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
47
+
37
48
  /**
38
- * Per-turn cap on calls to the SAME tool name, regardless of args (#783).
49
+ * User-tunable configuration shape for the loop guard (#1198).
39
50
  *
40
- * General-purpose execution tools are routinely called many times per turn
41
- * (touching multiple files, running several commands), so they get a higher
42
- * ceiling. Everything else workflow one-shot tools (e.g. gsd_complete_milestone)
43
- * and any non-allowlisted tool — gets the default cap. The default is generous
44
- * enough to absorb legitimate retries but catches the reported improvisation
45
- * loop (~51 calls) well before a cost spike.
51
+ * Mirrors the `tool_call_loop_guard` key in `.gsd/PREFERENCES.md`. All fields
52
+ * are optional; anything omitted falls back to the built-in defaults so that
53
+ * existing installs keep their current behavior.
46
54
  */
47
- const PER_TOOL_DEFAULT_CAP = 6;
48
- const PER_TOOL_REPEATABLE_CAP = 15;
49
- const PER_TOOL_CAP_EXEMPT_TOOLS = new Set(["find", "glob", "grep", "ls", "read", "search_and_read"]);
50
- const STATE_MUTATING_TOOL_SET = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
55
+ export interface ToolCallLoopGuardConfig {
56
+ enabled?: boolean;
57
+ identical_args?: {
58
+ enabled?: boolean;
59
+ max_consecutive_calls?: number;
60
+ };
61
+ repeated_tool?: {
62
+ enabled?: boolean;
63
+ default_cap?: number;
64
+ repeatable_cap?: number;
65
+ exempt_tools?: string[];
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Active, resolved configuration. Set by {@link configureToolCallLoopGuard}
71
+ * from preferences + environment overrides, and deliberately NOT touched by
72
+ * {@link resetToolCallLoopGuard} so a session's tuning survives turn
73
+ * boundaries.
74
+ */
75
+ interface ResolvedGuardConfig {
76
+ /** Master switch for both guards. */
77
+ guardEnabled: boolean;
78
+ identicalEnabled: boolean;
79
+ maxConsecutiveIdentical: number;
80
+ repeatedEnabled: boolean;
81
+ perToolDefaultCap: number;
82
+ perToolRepeatableCap: number;
83
+ /** Tool names exempt from Guard 2 (per-tool-name cap). */
84
+ perToolExempt: Set<string>;
85
+ }
86
+
87
+ function defaultGuardConfig(): ResolvedGuardConfig {
88
+ return {
89
+ guardEnabled: true,
90
+ identicalEnabled: true,
91
+ maxConsecutiveIdentical: DEFAULT_MAX_CONSECUTIVE_IDENTICAL_CALLS,
92
+ repeatedEnabled: true,
93
+ perToolDefaultCap: DEFAULT_PER_TOOL_DEFAULT_CAP,
94
+ perToolRepeatableCap: DEFAULT_PER_TOOL_REPEATABLE_CAP,
95
+ perToolExempt: new Set<string>(DEFAULT_PER_TOOL_CAP_EXEMPT_TOOLS),
96
+ };
97
+ }
98
+
99
+ let config: ResolvedGuardConfig = defaultGuardConfig();
100
+
101
+ /** Parse a positive-integer env var, returning undefined when unset/invalid. */
102
+ function envPositiveInt(name: string): number | undefined {
103
+ const raw = process.env[name];
104
+ if (raw === undefined || raw.trim() === "") return undefined;
105
+ const n = Number(raw);
106
+ return Number.isFinite(n) && n >= 1 ? Math.floor(n) : undefined;
107
+ }
108
+
109
+ /** Parse a boolean env var (`true`/`1` vs `false`/`0`), undefined when unset. */
110
+ function envBool(name: string): boolean | undefined {
111
+ const raw = process.env[name];
112
+ if (raw === undefined || raw.trim() === "") return undefined;
113
+ const v = raw.trim().toLowerCase();
114
+ if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
115
+ if (v === "false" || v === "0" || v === "no" || v === "off") return false;
116
+ return undefined;
117
+ }
118
+
119
+ /** Parse a comma-separated tool-name list env var. */
120
+ function envToolList(name: string): string[] {
121
+ const raw = process.env[name];
122
+ if (!raw) return [];
123
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
124
+ }
125
+
126
+ /**
127
+ * Resolve preferences + environment overrides into the active configuration.
128
+ * Environment variables win over preferences; preferences win over built-in
129
+ * defaults. User-supplied exempt tools are additive to the built-in exempt
130
+ * set so defaults are always preserved.
131
+ *
132
+ * Applied to both interactive sessions and `/gsd auto` (called from the
133
+ * session_start / session_switch hooks).
134
+ */
135
+ export function configureToolCallLoopGuard(prefs?: ToolCallLoopGuardConfig | null): void {
136
+ const next = defaultGuardConfig();
137
+
138
+ if (prefs) {
139
+ if (typeof prefs.enabled === "boolean") next.guardEnabled = prefs.enabled;
140
+ if (prefs.identical_args) {
141
+ if (typeof prefs.identical_args.enabled === "boolean") next.identicalEnabled = prefs.identical_args.enabled;
142
+ if (typeof prefs.identical_args.max_consecutive_calls === "number" && prefs.identical_args.max_consecutive_calls >= 1) {
143
+ next.maxConsecutiveIdentical = Math.floor(prefs.identical_args.max_consecutive_calls);
144
+ }
145
+ }
146
+ if (prefs.repeated_tool) {
147
+ if (typeof prefs.repeated_tool.enabled === "boolean") next.repeatedEnabled = prefs.repeated_tool.enabled;
148
+ if (typeof prefs.repeated_tool.default_cap === "number" && prefs.repeated_tool.default_cap >= 1) {
149
+ next.perToolDefaultCap = Math.floor(prefs.repeated_tool.default_cap);
150
+ }
151
+ if (typeof prefs.repeated_tool.repeatable_cap === "number" && prefs.repeated_tool.repeatable_cap >= 1) {
152
+ next.perToolRepeatableCap = Math.floor(prefs.repeated_tool.repeatable_cap);
153
+ }
154
+ for (const tool of prefs.repeated_tool.exempt_tools ?? []) {
155
+ if (typeof tool === "string" && tool.trim()) next.perToolExempt.add(tool.trim());
156
+ }
157
+ }
158
+ }
159
+
160
+ // Environment overrides (win over preferences).
161
+ const envEnabled = envBool("GSD_TOOL_LOOP_GUARD_ENABLED");
162
+ if (envEnabled !== undefined) next.guardEnabled = envEnabled;
163
+ const envIdenticalMax = envPositiveInt("GSD_TOOL_LOOP_IDENTICAL_MAX");
164
+ if (envIdenticalMax !== undefined) next.maxConsecutiveIdentical = envIdenticalMax;
165
+ const envDefaultCap = envPositiveInt("GSD_TOOL_LOOP_REPEATED_DEFAULT_CAP");
166
+ if (envDefaultCap !== undefined) next.perToolDefaultCap = envDefaultCap;
167
+ const envRepeatableCap = envPositiveInt("GSD_TOOL_LOOP_REPEATED_REPEATABLE_CAP");
168
+ if (envRepeatableCap !== undefined) next.perToolRepeatableCap = envRepeatableCap;
169
+ for (const tool of envToolList("GSD_TOOL_LOOP_EXEMPT_TOOLS")) next.perToolExempt.add(tool);
170
+
171
+ config = next;
172
+ }
51
173
 
52
174
  let consecutiveCount = 0;
53
175
  let lastSignature = "";
@@ -82,17 +204,20 @@ function hashToolCall(toolName: string, args: Record<string, unknown>): string {
82
204
  * Returns `{ block: true, reason }` when the loop threshold is exceeded.
83
205
  *
84
206
  * Two independent guards run; whichever trips first blocks:
85
- * 1. Identical-signature streak (MAX_CONSECUTIVE_IDENTICAL_CALLS, strict for
207
+ * 1. Identical-signature streak (config.maxConsecutiveIdentical, strict for
86
208
  * ask_user_questions).
87
- * 2. Per-tool-name cap (PER_TOOL_DEFAULT_CAP / PER_TOOL_REPEATABLE_CAP),
209
+ * 2. Per-tool-name cap (config.perToolDefaultCap / config.perToolRepeatableCap),
88
210
  * independent of args, reset after file-mutation progress — catches
89
211
  * improvisation loops (#783).
212
+ *
213
+ * Both guards, their thresholds, and the per-tool exempt set are user-tunable
214
+ * via {@link configureToolCallLoopGuard} (#1198).
90
215
  */
91
216
  export function checkToolCallLoop(
92
217
  toolName: string,
93
218
  args: Record<string, unknown>,
94
219
  ): { block: boolean; reason?: string; count?: number } {
95
- if (!enabled) return { block: false, count: 0 };
220
+ if (!enabled || !config.guardEnabled) return { block: false, count: 0 };
96
221
 
97
222
  const sig = hashToolCall(toolName, args);
98
223
 
@@ -107,14 +232,16 @@ export function checkToolCallLoop(
107
232
  // ── Guard 1: identical-signature streak ──
108
233
  const threshold = STRICT_LOOP_TOOLS.has(toolName)
109
234
  ? MAX_CONSECUTIVE_STRICT
110
- : MAX_CONSECUTIVE_IDENTICAL_CALLS;
235
+ : config.maxConsecutiveIdentical;
111
236
 
112
- if (consecutiveCount > threshold) {
237
+ if (config.identicalEnabled && consecutiveCount > threshold) {
113
238
  return {
114
239
  block: true,
115
240
  reason:
116
241
  `Tool loop detected (identical args): ${toolName} called ${consecutiveCount} times ` +
117
- `with identical arguments. Blocking to prevent infinite loop. ` +
242
+ `with identical arguments (max ${threshold}). Blocking to prevent infinite loop. ` +
243
+ `Raise tool_call_loop_guard.identical_args.max_consecutive_calls in .gsd/PREFERENCES.md ` +
244
+ `(or GSD_TOOL_LOOP_IDENTICAL_MAX) if this is expected. ` +
118
245
  `Do not retry this tool or call other tools this turn — stop and respond to the user in text.`,
119
246
  count: consecutiveCount,
120
247
  };
@@ -139,15 +266,16 @@ export function checkToolCallLoop(
139
266
  // Guard 1's identical-signature streak still catches a genuinely stuck
140
267
  // browser loop.
141
268
  if (
142
- PER_TOOL_CAP_EXEMPT_TOOLS.has(toolName) ||
269
+ !config.repeatedEnabled ||
270
+ config.perToolExempt.has(toolName) ||
143
271
  hasBrowserContractPrefix(canonicalToolName(toolName))
144
272
  ) {
145
273
  return { block: false, count: consecutiveCount };
146
274
  }
147
275
 
148
276
  const perToolCap = INHERENTLY_REPEATABLE_TOOL_SET.has(toolName)
149
- ? PER_TOOL_REPEATABLE_CAP
150
- : PER_TOOL_DEFAULT_CAP;
277
+ ? config.perToolRepeatableCap
278
+ : config.perToolDefaultCap;
151
279
 
152
280
  if (perToolCount > perToolCap) {
153
281
  return {
@@ -156,6 +284,9 @@ export function checkToolCallLoop(
156
284
  `Tool loop detected (repeated tool): ${toolName} called ${perToolCount} times ` +
157
285
  `this turn (cap ${perToolCap}). Blocking to prevent infinite loop. ` +
158
286
  `The tool may be unavailable or failing repeatedly. ` +
287
+ `Raise tool_call_loop_guard.repeated_tool.default_cap/repeatable_cap or add "${toolName}" ` +
288
+ `to tool_call_loop_guard.repeated_tool.exempt_tools in .gsd/PREFERENCES.md ` +
289
+ `(or GSD_TOOL_LOOP_REPEATED_* / GSD_TOOL_LOOP_EXEMPT_TOOLS) if this is expected. ` +
159
290
  `Do not retry this tool or pivot to other tools this turn — stop and respond to the user in text.`,
160
291
  count: perToolCount,
161
292
  };
@@ -25,7 +25,7 @@ import {
25
25
  filterDoctorIssues,
26
26
  } from "./doctor.js";
27
27
  import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
28
- import { getAutoWorktreePath } from "./auto-worktree.js";
28
+ import { getAutoWorktreePath } from "./auto-worktree-path-resolution.js";
29
29
  import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
30
30
  import { loadPrompt } from "./prompt-loader.js";
31
31
  import { buildClaudeRuntimeFloorAdvisory } from "../../shared/claude-runtime-floor.js";
@@ -23,7 +23,7 @@ import {
23
23
  } from "./workflow-templates.js";
24
24
  import { loadPrompt } from "./prompt-loader.js";
25
25
  import { gsdRoot } from "./paths.js";
26
- import { createGitService, runGit } from "./git-service.js";
26
+ import { createGitService, runGit, taskBranchArgs } from "./git-service.js";
27
27
  import { isAutoActive, isAutoPaused } from "./auto.js";
28
28
  import { getErrorMessage } from "./error-utils.js";
29
29
  import { resolvePlugin, type WorkflowPlugin } from "./workflow-plugins.js";
@@ -464,7 +464,7 @@ export async function handleStart(
464
464
  try {
465
465
  git.autoCommit("workflow-template", templateId, []);
466
466
  } catch { /* nothing to commit */ }
467
- runGit(basePath, ["checkout", "-b", branchName]);
467
+ runGit(basePath, taskBranchArgs(basePath, branchName, current, git.getMainBranch()));
468
468
  branchCreated = true;
469
469
  }
470
470
  } catch (err) {
@@ -636,7 +636,7 @@ export function dispatchMarkdownPhasePlugin(
636
636
  const current = git.getCurrentBranch();
637
637
  if (current !== branchName) {
638
638
  try { git.autoCommit("workflow-template", templateId, []); } catch { /* nothing to commit */ }
639
- runGit(basePath, ["checkout", "-b", branchName]);
639
+ runGit(basePath, taskBranchArgs(basePath, branchName, current, git.getMainBranch()));
640
640
  branchCreated = true;
641
641
  }
642
642
  } catch (err) {
@@ -281,7 +281,7 @@ export function getMilestone(id: string): MilestoneRow | null {
281
281
  export function getActiveMilestoneFromDb(): MilestoneRow | null {
282
282
  if (!getDbOrNull()!) return null;
283
283
  const row = getDbOrNull()!.prepare(
284
- "SELECT * FROM milestones WHERE status NOT IN ('complete', 'done', 'skipped', 'closed', 'parked') ORDER BY id LIMIT 1",
284
+ `SELECT * FROM milestones WHERE status NOT IN (${TERMINAL_STATUS_SQL}, 'parked') ORDER BY id LIMIT 1`,
285
285
  ).get();
286
286
  if (!row) return null;
287
287
  return rowToMilestone(row);
@@ -295,11 +295,11 @@ export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
295
295
  const row = getDbOrNull()!.prepare(
296
296
  `SELECT s.* FROM slices s
297
297
  WHERE s.milestone_id = :mid
298
- AND s.status NOT IN ('complete', 'done', 'skipped')
298
+ AND s.status NOT IN (${TERMINAL_STATUS_SQL})
299
299
  AND NOT EXISTS (
300
300
  SELECT 1 FROM json_each(s.depends) AS dep
301
301
  WHERE dep.value NOT IN (
302
- SELECT id FROM slices WHERE milestone_id = :mid AND status IN ('complete', 'done', 'skipped')
302
+ SELECT id FROM slices WHERE milestone_id = :mid AND status IN (${TERMINAL_STATUS_SQL})
303
303
  )
304
304
  )
305
305
  ORDER BY s.sequence, s.id
@@ -312,7 +312,7 @@ export function getActiveSliceFromDb(milestoneId: string): SliceRow | null {
312
312
  export function getActiveTaskFromDb(milestoneId: string, sliceId: string): TaskRow | null {
313
313
  if (!getDbOrNull()!) return null;
314
314
  const row = getDbOrNull()!.prepare(
315
- "SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
315
+ `SELECT * FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN (${TERMINAL_STATUS_SQL}) ORDER BY sequence, id LIMIT 1`,
316
316
  ).get({ ":mid": milestoneId, ":sid": sliceId });
317
317
  if (!row) return null;
318
318
  return rowToTask(row);
@@ -339,7 +339,7 @@ export function getParallelMonitorSliceProgress(milestoneId: string): ParallelMo
339
339
  s.id AS id,
340
340
  s.status AS status,
341
341
  COUNT(t.id) AS total,
342
- COALESCE(SUM(CASE WHEN t.status='complete' THEN 1 ELSE 0 END), 0) AS done
342
+ COALESCE(SUM(CASE WHEN t.status IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END), 0) AS done
343
343
  FROM slices s
344
344
  LEFT JOIN tasks t ON s.milestone_id=t.milestone_id AND s.id=t.slice_id
345
345
  WHERE s.milestone_id=:mid
@@ -518,7 +518,7 @@ export function getSliceScopedArtifacts(milestoneId: string, sliceId: string): A
518
518
  export function getActiveMilestoneIdFromDb(): IdStatusSummary | null {
519
519
  if (!getDbOrNull()!) return null;
520
520
  const row = getDbOrNull()!.prepare(
521
- "SELECT id, status FROM milestones WHERE status NOT IN ('complete', 'done', 'skipped', 'closed', 'parked') ORDER BY id LIMIT 1",
521
+ `SELECT id, status FROM milestones WHERE status NOT IN (${TERMINAL_STATUS_SQL}, 'parked') ORDER BY id LIMIT 1`,
522
522
  ).get();
523
523
  if (!row) return null;
524
524
  return rowToIdStatusSummary(row);
@@ -536,7 +536,7 @@ export function getSliceStatusSummary(milestoneId: string): IdStatusSummary[] {
536
536
  export function getActiveTaskIdFromDb(milestoneId: string, sliceId: string): ActiveTaskSummary | null {
537
537
  if (!getDbOrNull()!) return null;
538
538
  const row = getDbOrNull()!.prepare(
539
- "SELECT id, status, title FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN ('complete', 'done') ORDER BY sequence, id LIMIT 1",
539
+ `SELECT id, status, title FROM tasks WHERE milestone_id = :mid AND slice_id = :sid AND status NOT IN (${TERMINAL_STATUS_SQL}) ORDER BY sequence, id LIMIT 1`,
540
540
  ).get({ ":mid": milestoneId, ":sid": sliceId });
541
541
  if (!row) return null;
542
542
  return rowToActiveTaskSummary(row);
@@ -548,8 +548,8 @@ export function getSliceTaskCounts(milestoneId: string, sliceId: string): TaskSt
548
548
  const row = getDbOrNull()!.prepare(
549
549
  `SELECT
550
550
  COUNT(*) as total,
551
- SUM(CASE WHEN status IN ('complete', 'done') THEN 1 ELSE 0 END) as done,
552
- SUM(CASE WHEN status NOT IN ('complete', 'done') THEN 1 ELSE 0 END) as pending
551
+ SUM(CASE WHEN status IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END) as done,
552
+ SUM(CASE WHEN status NOT IN (${TERMINAL_STATUS_SQL}) THEN 1 ELSE 0 END) as pending
553
553
  FROM tasks WHERE milestone_id = :mid AND slice_id = :sid`,
554
554
  ).get({ ":mid": milestoneId, ":sid": sliceId });
555
555
  return rowToTaskStatusCounts(row);
@@ -14,7 +14,15 @@ import {
14
14
  } from "./gsd-db.js";
15
15
  import { MEMORIES_FTS_REBUILT_KEY } from "./db-memory-fts-schema.js";
16
16
  import { isAfter, latestExplicitReopenAt } from "./milestone-reopen-events.js";
17
- import { gsdProjectionRoot, gsdRoot, resolveGsdPathContract, resolveMilestoneFile, resolveSliceFile } from "./paths.js";
17
+ import {
18
+ buildTaskFileName,
19
+ gsdProjectionRoot,
20
+ gsdRoot,
21
+ resolveGsdPathContract,
22
+ resolveMilestoneFile,
23
+ resolveMilestonePath,
24
+ resolveSliceFile,
25
+ } from "./paths.js";
18
26
  import { deriveState } from "./state.js";
19
27
  import { isClosedStatus } from "./status-guards.js";
20
28
  import { workflowEventLogPath } from "./workflow-event-ledger.js";
@@ -23,6 +31,7 @@ import { flushWorkflowProjections } from "./projection-flush.js";
23
31
  import { parseRoadmapSlices } from "./roadmap-slices.js";
24
32
  import { parsePlan } from "./parsers-legacy.js";
25
33
  import { parseSummary } from "./files.js";
34
+ import { LAYOUT_SEGMENTS } from "./layout-policy.js";
26
35
 
27
36
  const USER_AUTHORED_ARTIFACT_TYPES = new Set(["CONTEXT", "RESEARCH"]);
28
37
 
@@ -160,11 +169,11 @@ function isClearedByMilestoneShellProjectionFlush(
160
169
  return issue.file === relativeFile(basePath, roadmapPath);
161
170
  }
162
171
 
163
- function artifactExistsOnDisk(basePath: string, artifactPath: string): boolean {
164
- return resolveArtifactDiskPath(basePath, artifactPath) !== null;
172
+ function artifactExistsOnDisk(basePath: string, artifactPath: string, row?: ArtifactRow): boolean {
173
+ return resolveArtifactDiskPath(basePath, artifactPath, row) !== null;
165
174
  }
166
175
 
167
- function resolveArtifactDiskPath(basePath: string, artifactPath: string): string | null {
176
+ function resolveLiteralArtifactDiskPath(basePath: string, artifactPath: string): string | null {
168
177
  const relativeArtifactPath = artifactPathRelativeToGsd(artifactPath);
169
178
  if (isAbsolute(relativeArtifactPath)) {
170
179
  return existsSync(relativeArtifactPath) ? relativeArtifactPath : null;
@@ -176,6 +185,50 @@ function resolveArtifactDiskPath(basePath: string, artifactPath: string): string
176
185
  return null;
177
186
  }
178
187
 
188
+ function relativeToGsdRoot(basePath: string, diskPath: string): string | null {
189
+ for (const root of [gsdProjectionRoot(basePath), gsdRoot(basePath)]) {
190
+ if (!isPathInside(root, diskPath)) continue;
191
+ return relative(root, diskPath).split("\\").join("/");
192
+ }
193
+ return null;
194
+ }
195
+
196
+ function isFlatPhaseDiskPath(basePath: string, diskPath: string): boolean {
197
+ return relativeToGsdRoot(basePath, diskPath)?.startsWith(`${LAYOUT_SEGMENTS.level1}/`) ?? false;
198
+ }
199
+
200
+ function hasFlatPhaseLayout(basePath: string): boolean {
201
+ return existsSync(join(gsdProjectionRoot(basePath), LAYOUT_SEGMENTS.level1));
202
+ }
203
+
204
+ function resolveFlatPhaseArtifactDiskPath(basePath: string, row: ArtifactRow): string | null {
205
+ if (!hasFlatPhaseLayout(basePath)) return null;
206
+ if (!row.milestone_id) return null;
207
+ const artifactType = normalizedArtifactType(row.artifact_type);
208
+ if (!artifactType) return null;
209
+
210
+ if (row.slice_id && row.task_id) {
211
+ const phaseDir = resolveMilestonePath(basePath, row.milestone_id);
212
+ if (!phaseDir || !isFlatPhaseDiskPath(basePath, phaseDir)) return null;
213
+ const candidate = join(phaseDir, buildTaskFileName(row.task_id, artifactType));
214
+ return existsSync(candidate) ? candidate : null;
215
+ }
216
+
217
+ const candidate = row.slice_id
218
+ ? resolveSliceFile(basePath, row.milestone_id, row.slice_id, artifactType)
219
+ : resolveMilestoneFile(basePath, row.milestone_id, artifactType);
220
+
221
+ return candidate && isFlatPhaseDiskPath(basePath, candidate) ? candidate : null;
222
+ }
223
+
224
+ function resolveArtifactDiskPath(basePath: string, artifactPath: string, row?: ArtifactRow): string | null {
225
+ if (row && isMilestonesArtifactPath(row.path)) {
226
+ const flatPath = resolveFlatPhaseArtifactDiskPath(basePath, row);
227
+ if (flatPath) return flatPath;
228
+ }
229
+ return resolveLiteralArtifactDiskPath(basePath, artifactPath);
230
+ }
231
+
179
232
  function taskExistsInPlan(basePath: string, milestoneId: string, sliceId: string, taskId: string): boolean {
180
233
  const planPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
181
234
  if (!planPath || !existsSync(planPath)) return false;
@@ -228,7 +281,7 @@ function readTaskCompletionEvidenceFromSummary(
228
281
  if (!row.task_status && Number(row.task_count) > 0 && !taskExistsInPlan(basePath, row.milestone_id, row.slice_id, row.task_id)) {
229
282
  return null;
230
283
  }
231
- const diskPath = resolveArtifactDiskPath(basePath, row.path);
284
+ const diskPath = resolveArtifactDiskPath(basePath, row.path, { ...row, artifact_type: "SUMMARY" });
232
285
  if (!diskPath) return null;
233
286
  try {
234
287
  const fullSummaryMd = readFileSync(diskPath, "utf-8");
@@ -262,6 +315,7 @@ function repairTaskArtifactDbStatusDivergence(
262
315
  basePath: string,
263
316
  row: {
264
317
  path: string;
318
+ artifact_type?: string;
265
319
  milestone_id: string;
266
320
  slice_id: string | null;
267
321
  task_id: string | null;
@@ -353,6 +407,49 @@ function hasPresentMilestonesReplacement(basePath: string, row: ArtifactRow, art
353
407
  );
354
408
  }
355
409
 
410
+ function hasPresentFlatPhaseReplacement(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
411
+ if (resolveFlatPhaseArtifactDiskPath(basePath, row)) return true;
412
+
413
+ return artifactRows.some(
414
+ (other) =>
415
+ other.path !== row.path &&
416
+ !isMilestonesArtifactPath(other.path) &&
417
+ sameArtifactIdentity(row, other) &&
418
+ artifactExistsOnDisk(basePath, other.path, other),
419
+ );
420
+ }
421
+
422
+ function hasNoFlatPhaseFileEquivalent(row: ArtifactRow): boolean {
423
+ const artifactType = normalizedArtifactType(row.artifact_type);
424
+ if (row.slice_id && row.task_id) {
425
+ return artifactType === "PLAN" || artifactType === "SUMMARY";
426
+ }
427
+ return Boolean(row.slice_id && !row.task_id && artifactType === "SUMMARY");
428
+ }
429
+
430
+ function staleMilestonesArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
431
+ if (!isMilestonesArtifactPath(row.path)) return false;
432
+ if (!hasFlatPhaseLayout(basePath)) return false;
433
+ if (hasPresentFlatPhaseReplacement(basePath, row, artifactRows)) return true;
434
+ return hasNoFlatPhaseFileEquivalent(row) && !resolveLiteralArtifactDiskPath(basePath, row.path);
435
+ }
436
+
437
+ function stalePhasesArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
438
+ const issuePath = artifactPathRelativeToGsd(row.path);
439
+ return issuePath.startsWith(`${LAYOUT_SEGMENTS.level1}/`) && hasPresentMilestonesReplacement(basePath, row, artifactRows);
440
+ }
441
+
442
+ function staleArtifactRowFixable(basePath: string, row: ArtifactRow, artifactRows: ArtifactRow[]): boolean {
443
+ return staleMilestonesArtifactRowFixable(basePath, row, artifactRows) ||
444
+ stalePhasesArtifactRowFixable(basePath, row, artifactRows);
445
+ }
446
+
447
+ function staleArtifactPruneMessage(row: ArtifactRow): string {
448
+ return isMilestonesArtifactPath(row.path)
449
+ ? `pruned stale legacy artifact row ${row.path}`
450
+ : `pruned stale flat-phase artifact row ${row.path}`;
451
+ }
452
+
356
453
  export async function checkEngineHealth(
357
454
  basePath: string,
358
455
  issues: DoctorIssue[],
@@ -579,17 +676,18 @@ export async function checkEngineHealth(
579
676
  .all() as ArtifactRow[];
580
677
 
581
678
  for (const row of artifactRows) {
582
- if (artifactExistsOnDisk(basePath, row.path)) continue;
583
679
  const unitId = artifactUnitId(row);
584
680
  const issuePath = artifactPathRelativeToGsd(row.path);
585
- if (options?.repair && issuePath.startsWith("phases/") && hasPresentMilestonesReplacement(basePath, row, artifactRows)) {
681
+ if (artifactExistsOnDisk(basePath, row.path)) continue;
682
+ if (options?.repair && staleArtifactRowFixable(basePath, row, artifactRows)) {
586
683
  // Route the write through the Single Writer owner (gsd-db.ts) instead
587
684
  // of issuing raw DELETE SQL here — doctor is a read-only consumer and
588
685
  // the single-writer invariant forbids write SQL outside the allowlist.
589
686
  deleteArtifactByPath(row.path);
590
- fixesApplied.push(`pruned stale flat-phase artifact row ${row.path}`);
687
+ fixesApplied.push(staleArtifactPruneMessage(row));
591
688
  continue;
592
689
  }
690
+ if (artifactExistsOnDisk(basePath, row.path, row)) continue;
593
691
  if (isUserAuthoredArtifactType(row.artifact_type)) {
594
692
  const artifactType = normalizedArtifactType(row.artifact_type);
595
693
  missingUserContentArtifacts.push({ path: issuePath, artifactType });
@@ -611,7 +709,7 @@ export async function checkEngineHealth(
611
709
  unitId,
612
710
  message: `Artifact ${issuePath} is recorded in the database as ${row.artifact_type || "UNKNOWN"} but no matching file exists on disk`,
613
711
  file: issuePath,
614
- fixable: issuePath.startsWith("phases/") && hasPresentMilestonesReplacement(basePath, row, artifactRows),
712
+ fixable: staleArtifactRowFixable(basePath, row, artifactRows),
615
713
  });
616
714
  }
617
715
  } catch {
@@ -643,6 +741,7 @@ export async function checkEngineHealth(
643
741
  )
644
742
  .all() as Array<{
645
743
  path: string;
744
+ artifact_type: string;
646
745
  milestone_id: string;
647
746
  slice_id: string | null;
648
747
  task_id: string | null;
@@ -654,7 +753,7 @@ export async function checkEngineHealth(
654
753
 
655
754
  const seen = new Set<string>();
656
755
  for (const row of rows) {
657
- if (!artifactExistsOnDisk(basePath, row.path)) continue;
756
+ if (!artifactExistsOnDisk(basePath, row.path, row)) continue;
658
757
  const reopenAt = latestExplicitReopenAt(basePath, row.milestone_id);
659
758
  if (!isAfter(row.imported_at, reopenAt)) continue;
660
759
  const isSliceSummary = row.slice_id && !row.task_id && row.slice_status && !["complete", "done", "skipped", "closed"].includes(row.slice_status);