gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.2ccf3fb

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 (256) hide show
  1. package/README.md +24 -17
  2. package/dist/cli.js +15 -9
  3. package/dist/resource-loader.js +80 -8
  4. package/dist/resources/extensions/bg-shell/process-manager.ts +13 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
  6. package/dist/resources/extensions/gsd/auto-post-unit.ts +14 -6
  7. package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
  8. package/dist/resources/extensions/gsd/auto-start.ts +25 -10
  9. package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
  10. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  11. package/dist/resources/extensions/gsd/auto.ts +67 -22
  12. package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
  13. package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
  14. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +90 -47
  15. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
  16. package/dist/resources/extensions/gsd/commands.ts +75 -29
  17. package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  18. package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +2 -6
  20. package/dist/resources/extensions/gsd/export.ts +28 -2
  21. package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
  22. package/dist/resources/extensions/gsd/index.ts +2 -1
  23. package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
  24. package/dist/resources/extensions/gsd/metrics.ts +17 -31
  25. package/dist/resources/extensions/gsd/paths.ts +0 -8
  26. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/workflow-start.md +28 -0
  28. package/dist/resources/extensions/gsd/queue-order.ts +10 -11
  29. package/dist/resources/extensions/gsd/routing-history.ts +13 -17
  30. package/dist/resources/extensions/gsd/session-lock.ts +284 -0
  31. package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
  32. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  33. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  34. package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  35. package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
  36. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  37. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  38. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  39. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  40. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  41. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  42. package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
  43. package/dist/resources/extensions/gsd/types.ts +1 -0
  44. package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
  45. package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
  46. package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
  47. package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
  48. package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
  49. package/dist/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
  50. package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
  51. package/dist/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
  52. package/dist/resources/extensions/gsd/workflow-templates/registry.json +85 -0
  53. package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
  54. package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
  55. package/dist/resources/extensions/gsd/workflow-templates/spike.md +69 -0
  56. package/dist/resources/extensions/gsd/workflow-templates.ts +241 -0
  57. package/dist/resources/extensions/mcp-client/index.ts +459 -0
  58. package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  59. package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
  60. package/dist/resources/extensions/remote-questions/notify.ts +1 -2
  61. package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  62. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  63. package/dist/resources/extensions/remote-questions/types.ts +3 -0
  64. package/dist/resources/extensions/shared/mod.ts +3 -0
  65. package/dist/resources/skills/create-gsd-extension/SKILL.md +87 -0
  66. package/dist/resources/skills/create-gsd-extension/references/compaction-session-control.md +77 -0
  67. package/dist/resources/skills/create-gsd-extension/references/custom-commands.md +139 -0
  68. package/dist/resources/skills/create-gsd-extension/references/custom-rendering.md +108 -0
  69. package/dist/resources/skills/create-gsd-extension/references/custom-tools.md +183 -0
  70. package/dist/resources/skills/create-gsd-extension/references/custom-ui.md +490 -0
  71. package/dist/resources/skills/create-gsd-extension/references/events-reference.md +126 -0
  72. package/dist/resources/skills/create-gsd-extension/references/extension-lifecycle.md +64 -0
  73. package/dist/resources/skills/create-gsd-extension/references/extensionapi-reference.md +75 -0
  74. package/dist/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +53 -0
  75. package/dist/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +36 -0
  76. package/dist/resources/skills/create-gsd-extension/references/mode-behavior.md +32 -0
  77. package/dist/resources/skills/create-gsd-extension/references/model-provider-management.md +89 -0
  78. package/dist/resources/skills/create-gsd-extension/references/packaging-distribution.md +55 -0
  79. package/dist/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +90 -0
  80. package/dist/resources/skills/create-gsd-extension/references/state-management.md +70 -0
  81. package/dist/resources/skills/create-gsd-extension/references/system-prompt-modification.md +52 -0
  82. package/dist/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +51 -0
  83. package/dist/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +143 -0
  84. package/dist/resources/skills/create-gsd-extension/workflows/add-capability.md +57 -0
  85. package/dist/resources/skills/create-gsd-extension/workflows/create-extension.md +156 -0
  86. package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +74 -0
  87. package/dist/resources/skills/create-skill/SKILL.md +184 -0
  88. package/dist/resources/skills/create-skill/references/api-security.md +226 -0
  89. package/dist/resources/skills/create-skill/references/be-clear-and-direct.md +531 -0
  90. package/dist/resources/skills/create-skill/references/common-patterns.md +595 -0
  91. package/dist/resources/skills/create-skill/references/core-principles.md +437 -0
  92. package/dist/resources/skills/create-skill/references/executable-code.md +175 -0
  93. package/dist/resources/skills/create-skill/references/gsd-skill-ecosystem.md +68 -0
  94. package/dist/resources/skills/create-skill/references/iteration-and-testing.md +474 -0
  95. package/dist/resources/skills/create-skill/references/recommended-structure.md +168 -0
  96. package/dist/resources/skills/create-skill/references/skill-structure.md +372 -0
  97. package/dist/resources/skills/create-skill/references/use-xml-tags.md +466 -0
  98. package/dist/resources/skills/create-skill/references/using-scripts.md +113 -0
  99. package/dist/resources/skills/create-skill/references/using-templates.md +112 -0
  100. package/dist/resources/skills/create-skill/references/workflows-and-validation.md +510 -0
  101. package/dist/resources/skills/create-skill/templates/router-skill.md +73 -0
  102. package/dist/resources/skills/create-skill/templates/simple-skill.md +33 -0
  103. package/dist/resources/skills/create-skill/workflows/add-reference.md +96 -0
  104. package/dist/resources/skills/create-skill/workflows/add-script.md +93 -0
  105. package/dist/resources/skills/create-skill/workflows/add-template.md +74 -0
  106. package/dist/resources/skills/create-skill/workflows/add-workflow.md +120 -0
  107. package/dist/resources/skills/create-skill/workflows/audit-skill.md +148 -0
  108. package/dist/resources/skills/create-skill/workflows/create-new-skill.md +196 -0
  109. package/dist/resources/skills/create-skill/workflows/get-guidance.md +121 -0
  110. package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +161 -0
  111. package/dist/resources/skills/create-skill/workflows/verify-skill.md +204 -0
  112. package/package.json +6 -3
  113. package/packages/native/dist/native.d.ts +2 -0
  114. package/packages/native/dist/native.js +19 -5
  115. package/packages/native/src/native.ts +23 -9
  116. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/extensions/loader.js +13 -0
  118. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  120. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
  125. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  129. package/packages/pi-coding-agent/package.json +1 -1
  130. package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
  131. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -0
  132. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  133. package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
  135. package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
  136. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  137. package/packages/pi-tui/dist/autocomplete.js +14 -0
  138. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  139. package/packages/pi-tui/src/autocomplete.ts +19 -1
  140. package/pkg/package.json +1 -1
  141. package/src/resources/extensions/bg-shell/process-manager.ts +13 -0
  142. package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
  143. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -6
  144. package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
  145. package/src/resources/extensions/gsd/auto-start.ts +25 -10
  146. package/src/resources/extensions/gsd/auto-verification.ts +41 -7
  147. package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  148. package/src/resources/extensions/gsd/auto.ts +67 -22
  149. package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
  150. package/src/resources/extensions/gsd/commands-logs.ts +536 -0
  151. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +90 -47
  152. package/src/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
  153. package/src/resources/extensions/gsd/commands.ts +75 -29
  154. package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  155. package/src/resources/extensions/gsd/doctor-types.ts +13 -0
  156. package/src/resources/extensions/gsd/doctor.ts +2 -6
  157. package/src/resources/extensions/gsd/export.ts +28 -2
  158. package/src/resources/extensions/gsd/gsd-db.ts +19 -0
  159. package/src/resources/extensions/gsd/index.ts +2 -1
  160. package/src/resources/extensions/gsd/json-persistence.ts +67 -0
  161. package/src/resources/extensions/gsd/metrics.ts +17 -31
  162. package/src/resources/extensions/gsd/paths.ts +0 -8
  163. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/workflow-start.md +28 -0
  165. package/src/resources/extensions/gsd/queue-order.ts +10 -11
  166. package/src/resources/extensions/gsd/routing-history.ts +13 -17
  167. package/src/resources/extensions/gsd/session-lock.ts +284 -0
  168. package/src/resources/extensions/gsd/session-status-io.ts +23 -41
  169. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  170. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  171. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  172. package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
  173. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  174. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  175. package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  176. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  177. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  178. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  179. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
  180. package/src/resources/extensions/gsd/types.ts +1 -0
  181. package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
  182. package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
  183. package/src/resources/extensions/gsd/verification-gate.ts +13 -2
  184. package/src/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
  185. package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
  186. package/src/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
  187. package/src/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
  188. package/src/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
  189. package/src/resources/extensions/gsd/workflow-templates/registry.json +85 -0
  190. package/src/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
  191. package/src/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
  192. package/src/resources/extensions/gsd/workflow-templates/spike.md +69 -0
  193. package/src/resources/extensions/gsd/workflow-templates.ts +241 -0
  194. package/src/resources/extensions/mcp-client/index.ts +459 -0
  195. package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  196. package/src/resources/extensions/remote-questions/http-client.ts +76 -0
  197. package/src/resources/extensions/remote-questions/notify.ts +1 -2
  198. package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  199. package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  200. package/src/resources/extensions/remote-questions/types.ts +3 -0
  201. package/src/resources/extensions/shared/mod.ts +3 -0
  202. package/src/resources/skills/create-gsd-extension/SKILL.md +87 -0
  203. package/src/resources/skills/create-gsd-extension/references/compaction-session-control.md +77 -0
  204. package/src/resources/skills/create-gsd-extension/references/custom-commands.md +139 -0
  205. package/src/resources/skills/create-gsd-extension/references/custom-rendering.md +108 -0
  206. package/src/resources/skills/create-gsd-extension/references/custom-tools.md +183 -0
  207. package/src/resources/skills/create-gsd-extension/references/custom-ui.md +490 -0
  208. package/src/resources/skills/create-gsd-extension/references/events-reference.md +126 -0
  209. package/src/resources/skills/create-gsd-extension/references/extension-lifecycle.md +64 -0
  210. package/src/resources/skills/create-gsd-extension/references/extensionapi-reference.md +75 -0
  211. package/src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +53 -0
  212. package/src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +36 -0
  213. package/src/resources/skills/create-gsd-extension/references/mode-behavior.md +32 -0
  214. package/src/resources/skills/create-gsd-extension/references/model-provider-management.md +89 -0
  215. package/src/resources/skills/create-gsd-extension/references/packaging-distribution.md +55 -0
  216. package/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +90 -0
  217. package/src/resources/skills/create-gsd-extension/references/state-management.md +70 -0
  218. package/src/resources/skills/create-gsd-extension/references/system-prompt-modification.md +52 -0
  219. package/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +51 -0
  220. package/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +143 -0
  221. package/src/resources/skills/create-gsd-extension/workflows/add-capability.md +57 -0
  222. package/src/resources/skills/create-gsd-extension/workflows/create-extension.md +156 -0
  223. package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +74 -0
  224. package/src/resources/skills/create-skill/SKILL.md +184 -0
  225. package/src/resources/skills/create-skill/references/api-security.md +226 -0
  226. package/src/resources/skills/create-skill/references/be-clear-and-direct.md +531 -0
  227. package/src/resources/skills/create-skill/references/common-patterns.md +595 -0
  228. package/src/resources/skills/create-skill/references/core-principles.md +437 -0
  229. package/src/resources/skills/create-skill/references/executable-code.md +175 -0
  230. package/src/resources/skills/create-skill/references/gsd-skill-ecosystem.md +68 -0
  231. package/src/resources/skills/create-skill/references/iteration-and-testing.md +474 -0
  232. package/src/resources/skills/create-skill/references/recommended-structure.md +168 -0
  233. package/src/resources/skills/create-skill/references/skill-structure.md +372 -0
  234. package/src/resources/skills/create-skill/references/use-xml-tags.md +466 -0
  235. package/src/resources/skills/create-skill/references/using-scripts.md +113 -0
  236. package/src/resources/skills/create-skill/references/using-templates.md +112 -0
  237. package/src/resources/skills/create-skill/references/workflows-and-validation.md +510 -0
  238. package/src/resources/skills/create-skill/templates/router-skill.md +73 -0
  239. package/src/resources/skills/create-skill/templates/simple-skill.md +33 -0
  240. package/src/resources/skills/create-skill/workflows/add-reference.md +96 -0
  241. package/src/resources/skills/create-skill/workflows/add-script.md +93 -0
  242. package/src/resources/skills/create-skill/workflows/add-template.md +74 -0
  243. package/src/resources/skills/create-skill/workflows/add-workflow.md +120 -0
  244. package/src/resources/skills/create-skill/workflows/audit-skill.md +148 -0
  245. package/src/resources/skills/create-skill/workflows/create-new-skill.md +196 -0
  246. package/src/resources/skills/create-skill/workflows/get-guidance.md +121 -0
  247. package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +161 -0
  248. package/src/resources/skills/create-skill/workflows/verify-skill.md +204 -0
  249. package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
  250. package/dist/resources/extensions/mcporter/index.ts +0 -525
  251. package/dist/resources/extensions/shared/progress-widget.ts +0 -282
  252. package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
  253. package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
  254. package/src/resources/extensions/mcporter/index.ts +0 -525
  255. package/src/resources/extensions/shared/progress-widget.ts +0 -282
  256. package/src/resources/extensions/shared/thinking-widget.ts +0 -107
@@ -204,6 +204,13 @@ export function estimateTimeRemaining(): string | null {
204
204
 
205
205
  // ─── Slice Progress Cache ─────────────────────────────────────────────────────
206
206
 
207
+ /** Cached task detail for the widget task checklist */
208
+ interface CachedTaskDetail {
209
+ id: string;
210
+ title: string;
211
+ done: boolean;
212
+ }
213
+
207
214
  /** Cached slice progress for the widget — avoid async in render */
208
215
  let cachedSliceProgress: {
209
216
  done: number;
@@ -211,6 +218,8 @@ let cachedSliceProgress: {
211
218
  milestoneId: string;
212
219
  /** Real task progress for the active slice, if its plan file exists */
213
220
  activeSliceTasks: { done: number; total: number } | null;
221
+ /** Full task list for the active slice checklist */
222
+ taskDetails: CachedTaskDetail[] | null;
214
223
  } | null = null;
215
224
 
216
225
  export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
@@ -221,6 +230,7 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
221
230
  const roadmap = parseRoadmap(content);
222
231
 
223
232
  let activeSliceTasks: { done: number; total: number } | null = null;
233
+ let taskDetails: CachedTaskDetail[] | null = null;
224
234
  if (activeSid) {
225
235
  try {
226
236
  const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@@ -231,6 +241,7 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
231
241
  done: plan.tasks.filter(t => t.done).length,
232
242
  total: plan.tasks.length,
233
243
  };
244
+ taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
234
245
  }
235
246
  } catch {
236
247
  // Non-fatal — just omit task count
@@ -242,13 +253,19 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
242
253
  total: roadmap.slices.length,
243
254
  milestoneId: mid,
244
255
  activeSliceTasks,
256
+ taskDetails,
245
257
  };
246
258
  } catch {
247
259
  // Non-fatal — widget just won't show progress bar
248
260
  }
249
261
  }
250
262
 
251
- export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
263
+ export function getRoadmapSlicesSync(): {
264
+ done: number;
265
+ total: number;
266
+ activeSliceTasks: { done: number; total: number } | null;
267
+ taskDetails: CachedTaskDetail[] | null;
268
+ } | null {
252
269
  return cachedSliceProgress;
253
270
  }
254
271
 
@@ -349,87 +366,84 @@ export function updateProgressWidget(
349
366
  const lines: string[] = [];
350
367
  const pad = INDENT.base;
351
368
 
352
- // ── Line 1: Top bar ───────────────────────────────────────────────
369
+ // ── Top bar ─────────────────────────────────────────────────────
353
370
  lines.push(...ui.bar());
354
371
 
372
+ // ── Header: GSD AUTO ... elapsed ────────────────────────────────
355
373
  const dot = pulseBright
356
374
  ? theme.fg("accent", GLYPH.statusActive)
357
375
  : theme.fg("dim", GLYPH.statusPending);
358
376
  const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
359
377
  const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
360
- const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
378
+ const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
361
379
  const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
362
380
  lines.push(rightAlign(headerLeft, headerRight, width));
363
381
 
364
- lines.push("");
365
-
366
- if (mid) {
367
- lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
368
- }
369
-
382
+ // ── Context: project · slice · action (merged into one line) ────
383
+ const contextParts: string[] = [];
384
+ if (mid) contextParts.push(theme.fg("dim", mid.title));
370
385
  if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
371
- lines.push(truncateToWidth(
372
- `${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
373
- width,
374
- ));
386
+ contextParts.push(theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`)));
375
387
  }
376
-
377
- lines.push("");
378
-
379
388
  const isHook = unitType.startsWith("hook/");
380
389
  const target = isHook
381
390
  ? (unitId.split("/").pop() ?? unitId)
382
391
  : (task ? `${task.id}: ${task.title}` : unitId);
383
- const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
392
+ contextParts.push(`${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`);
393
+
384
394
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
385
395
  const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
386
- lines.push(rightAlign(actionLeft, phaseBadge, width));
387
- lines.push("");
388
-
389
- if (mid) {
390
- const roadmapSlices = getRoadmapSlicesSync();
391
- if (roadmapSlices) {
392
- const { done, total, activeSliceTasks } = roadmapSlices;
393
- const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
394
- const pct = total > 0 ? done / total : 0;
395
- const filled = Math.round(pct * barWidth);
396
- const bar = theme.fg("success", "█".repeat(filled))
397
- + theme.fg("dim", "░".repeat(barWidth - filled));
398
-
399
- let meta = theme.fg("dim", `${done}/${total} slices`);
400
-
401
- if (activeSliceTasks && activeSliceTasks.total > 0) {
402
- // For hooks, show the trigger task number (done), not the next task (done + 1)
403
- const taskNum = isHook
404
- ? Math.max(activeSliceTasks.done, 1)
405
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
406
- meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
407
- }
408
-
409
- // ETA estimate
410
- const eta = estimateTimeRemaining();
411
- if (eta) {
412
- meta += theme.fg("dim", ` · ${eta}`);
413
- }
396
+ const contextLine = contextParts.join(theme.fg("dim", " · "));
397
+ lines.push(rightAlign(`${pad}${contextLine}`, phaseBadge, width));
398
+
399
+ // ── Two-column body ─────────────────────────────────────────────
400
+ // Left: progress, ETA, next, stats (fixed) | Right: task checklist (fixed, adjacent)
401
+ // Both columns sit left-to-center; empty space is on the right.
402
+ const divider = theme.fg("dim", "│");
403
+ const minTwoColWidth = 100;
404
+ const rightColFixed = 44;
405
+ const colGap = 5; // breathing room between columns
406
+ // Left column takes remaining space — no truncation on wide terminals
407
+ const useTwoCol = width >= minTwoColWidth;
408
+ const rightColWidth = useTwoCol ? rightColFixed : 0;
409
+ const leftColWidth = useTwoCol ? width - rightColWidth - colGap : width;
410
+
411
+ const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
412
+
413
+ // Build left column: progress bar, ETA, next step, token stats
414
+ const leftLines: string[] = [];
415
+
416
+ if (roadmapSlices) {
417
+ const { done, total, activeSliceTasks } = roadmapSlices;
418
+ const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
419
+ const pct = total > 0 ? done / total : 0;
420
+ const filled = Math.round(pct * barWidth);
421
+ const bar = theme.fg("success", "█".repeat(filled))
422
+ + theme.fg("dim", "░".repeat(barWidth - filled));
423
+
424
+ let meta = theme.fg("dim", `${done}/${total} slices`);
425
+ if (activeSliceTasks && activeSliceTasks.total > 0) {
426
+ const taskNum = isHook
427
+ ? Math.max(activeSliceTasks.done, 1)
428
+ : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
429
+ meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
430
+ }
431
+ leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
414
432
 
415
- lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
433
+ const eta = estimateTimeRemaining();
434
+ if (eta) {
435
+ leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", eta)}`, leftColWidth));
416
436
  }
417
437
  }
418
438
 
419
- lines.push("");
420
-
421
439
  if (next) {
422
- lines.push(truncateToWidth(
440
+ leftLines.push(truncateToWidth(
423
441
  `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
424
- width,
442
+ leftColWidth,
425
443
  ));
426
444
  }
427
445
 
428
- // ── Footer info (pwd, tokens, cost, context, model) ──────────────
429
- lines.push("");
430
- lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
431
-
432
- // Token stats from current unit session + cumulative cost from metrics
446
+ // Token stats
433
447
  {
434
448
  const cmdCtx = accessors.getCmdCtx();
435
449
  let totalInput = 0, totalOutput = 0;
@@ -464,7 +478,6 @@ export function updateProgressWidget(
464
478
  if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
465
479
  if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
466
480
  if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
467
- // Cache hit rate for current unit
468
481
  if (totalCacheRead + totalInput > 0) {
469
482
  const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
470
483
  sp.push(`\u26A1${hitRate}%`);
@@ -483,33 +496,134 @@ export function updateProgressWidget(
483
496
  sp.push(cxDisplay);
484
497
  }
485
498
 
486
- const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
499
+ const tokenLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
487
500
  .join(theme.fg("dim", " "));
501
+ leftLines.push(truncateToWidth(`${pad}${tokenLine}`, leftColWidth));
488
502
 
489
503
  const modelId = cmdCtx?.model?.id ?? "";
490
504
  const modelProvider = cmdCtx?.model?.provider ?? "";
491
- const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
492
505
  const modelDisplay = modelProvider && modelId
493
506
  ? `${modelProvider}/${modelId}`
494
507
  : modelId;
495
- const sRight = modelDisplay
496
- ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
497
- : "";
498
- lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
508
+ if (modelDisplay) {
509
+ leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", modelDisplay)}`, leftColWidth));
510
+ }
499
511
 
500
- // Dynamic routing savings summary
512
+ // Dynamic routing savings
501
513
  if (mLedger && mLedger.units.some(u => u.tier)) {
502
514
  const savings = formatTierSavings(mLedger.units);
503
515
  if (savings) {
504
- lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
516
+ leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", savings)}`, leftColWidth));
517
+ }
518
+ }
519
+ }
520
+
521
+ // Build right column: task checklist (pegged to right edge)
522
+ const rightLines: string[] = [];
523
+ const taskDetails = roadmapSlices?.taskDetails ?? null;
524
+ const maxVisibleTasks = 8;
525
+ const rpad = " ";
526
+
527
+ if (useTwoCol) {
528
+ if (taskDetails && taskDetails.length > 0) {
529
+ const visibleTasks = taskDetails.slice(0, maxVisibleTasks);
530
+ for (const t of visibleTasks) {
531
+ const isCurrent = task && t.id === task.id;
532
+ const glyph = t.done
533
+ ? theme.fg("success", GLYPH.statusDone)
534
+ : isCurrent
535
+ ? theme.fg("accent", "▸")
536
+ : theme.fg("dim", " ");
537
+ const label = isCurrent
538
+ ? theme.fg("text", `${t.id}: ${t.title}`)
539
+ : t.done
540
+ ? theme.fg("dim", `${t.id}: ${t.title}`)
541
+ : theme.fg("text", `${t.id}: ${t.title}`);
542
+ rightLines.push(truncateToWidth(`${rpad}${glyph} ${label}`, rightColWidth));
543
+ }
544
+ if (taskDetails.length > maxVisibleTasks) {
545
+ rightLines.push(truncateToWidth(
546
+ `${rpad}${theme.fg("dim", ` …+${taskDetails.length - maxVisibleTasks} more`)}`,
547
+ rightColWidth,
548
+ ));
549
+ }
550
+ } else if (roadmapSlices?.activeSliceTasks) {
551
+ const { done: tDone, total: tTotal } = roadmapSlices.activeSliceTasks;
552
+ rightLines.push(`${rpad}${theme.fg("dim", `${tDone}/${tTotal} tasks`)}`);
553
+ }
554
+ } else {
555
+ // Narrow single-column: task list goes into left column
556
+ if (taskDetails && taskDetails.length > 0) {
557
+ for (const t of taskDetails.slice(0, maxVisibleTasks)) {
558
+ const isCurrent = task && t.id === task.id;
559
+ const glyph = t.done
560
+ ? theme.fg("success", GLYPH.statusDone)
561
+ : isCurrent
562
+ ? theme.fg("accent", "▸")
563
+ : theme.fg("dim", " ");
564
+ const label = isCurrent
565
+ ? theme.fg("text", `${t.id}: ${t.title}`)
566
+ : t.done
567
+ ? theme.fg("dim", `${t.id}: ${t.title}`)
568
+ : theme.fg("text", `${t.id}: ${t.title}`);
569
+ leftLines.push(truncateToWidth(`${pad}${glyph} ${label}`, leftColWidth));
570
+ }
571
+ }
572
+ // Add progress bar inline
573
+ if (roadmapSlices) {
574
+ const { done, total, activeSliceTasks } = roadmapSlices;
575
+ const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
576
+ const pct = total > 0 ? done / total : 0;
577
+ const filled = Math.round(pct * barWidth);
578
+ const bar = theme.fg("success", "█".repeat(filled))
579
+ + theme.fg("dim", "░".repeat(barWidth - filled));
580
+ let meta = theme.fg("dim", `${done}/${total} slices`);
581
+ if (activeSliceTasks && activeSliceTasks.total > 0) {
582
+ const taskNum = isHook
583
+ ? Math.max(activeSliceTasks.done, 1)
584
+ : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
585
+ meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
505
586
  }
587
+ const eta = estimateTimeRemaining();
588
+ if (eta) meta += theme.fg("dim", ` · ${eta}`);
589
+ leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
590
+ }
591
+ if (next) {
592
+ leftLines.push(truncateToWidth(
593
+ `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
594
+ leftColWidth,
595
+ ));
506
596
  }
507
597
  }
508
598
 
599
+ // Compose columns
600
+ if (useTwoCol) {
601
+ const maxRows = Math.max(leftLines.length, rightLines.length);
602
+ if (maxRows > 0) {
603
+ lines.push(""); // spacer before columns
604
+ for (let i = 0; i < maxRows; i++) {
605
+ const left = padToWidth(leftLines[i] ?? "", leftColWidth);
606
+ const gap = " ".repeat(colGap - 2); // colGap minus divider and its trailing space
607
+ const right = rightLines[i] ?? "";
608
+ lines.push(truncateToWidth(`${left}${gap}${divider} ${right}`, width));
609
+ }
610
+ }
611
+ } else {
612
+ // Narrow single-column: just stack
613
+ if (leftLines.length > 0) {
614
+ lines.push("");
615
+ for (const l of leftLines) lines.push(l);
616
+ }
617
+ }
618
+
619
+ // ── Footer: pwd + hints ─────────────────────────────────────────
620
+ lines.push("");
509
621
  const hintParts: string[] = [];
510
622
  hintParts.push("esc pause");
511
623
  hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
512
- lines.push(...ui.hints(hintParts));
624
+ const hintStr = theme.fg("dim", hintParts.join(" | "));
625
+ const pwdStr = theme.fg("dim", widgetPwd);
626
+ lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
513
627
 
514
628
  lines.push(...ui.bar());
515
629
 
@@ -597,3 +711,10 @@ function rightAlign(left: string, right: string, width: number): string {
597
711
  const gap = Math.max(1, width - leftVis - rightVis);
598
712
  return truncateToWidth(left + " ".repeat(gap) + right, width);
599
713
  }
714
+
715
+ /** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
716
+ function padToWidth(s: string, colWidth: number): string {
717
+ const vis = visibleWidth(s);
718
+ if (vis >= colWidth) return truncateToWidth(s, colWidth);
719
+ return s + " ".repeat(colWidth - vis);
720
+ }
@@ -35,6 +35,7 @@ import {
35
35
  import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.js";
36
36
  import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences } from "./preferences.js";
37
37
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
38
+ import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
38
39
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
39
40
  import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
40
41
  import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
@@ -154,13 +155,17 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
154
155
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
155
156
  }
156
157
 
157
- // Proactive health tracking
158
- const summary = summarizeDoctorIssues(report.issues);
158
+ // Proactive health tracking — exclude completion-transition codes at task level
159
+ // since they are expected after the last task and resolved by complete-slice
160
+ const issuesForHealth = effectiveFixLevel === "task"
161
+ ? report.issues.filter(i => !COMPLETION_TRANSITION_CODES.has(i.code))
162
+ : report.issues;
163
+ const summary = summarizeDoctorIssues(issuesForHealth);
159
164
  recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
160
165
 
161
166
  // Check if we should escalate to LLM-assisted heal
162
167
  if (summary.errors > 0) {
163
- const unresolvedErrors = report.issues
168
+ const unresolvedErrors = issuesForHealth
164
169
  .filter(i => i.severity === "error" && !i.fixable)
165
170
  .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
166
171
  const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
@@ -171,7 +176,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
171
176
  );
172
177
  try {
173
178
  const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
174
- const { dispatchDoctorHeal } = await import("./commands.js");
179
+ const { dispatchDoctorHeal } = await import("./commands-handlers.js");
175
180
  const actionable = report.issues.filter(i => i.severity === "error");
176
181
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
177
182
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
@@ -197,10 +202,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
197
202
  }
198
203
  }
199
204
 
200
- // Prune dead bg-shell processes
205
+ // Prune dead bg-shell processes and kill non-persistent live ones.
206
+ // Without killing live processes between units, dev servers spawned during
207
+ // one task keep ports bound, causing conflicts in subsequent tasks (#1209).
201
208
  try {
202
- const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
209
+ const { pruneDeadProcesses, killSessionProcesses } = await import("../bg-shell/process-manager.js");
203
210
  pruneDeadProcesses();
211
+ killSessionProcesses();
204
212
  } catch {
205
213
  // Non-fatal
206
214
  }
@@ -36,8 +36,10 @@ import {
36
36
  clearPathCache,
37
37
  resolveGsdRootFile,
38
38
  } from "./paths.js";
39
+ import { isValidationTerminal } from "./state.js";
39
40
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
40
41
  import { atomicWriteSync } from "./atomic-write.js";
42
+ import { loadJsonFileOrNull } from "./json-persistence.js";
41
43
  import { dirname, join } from "node:path";
42
44
 
43
45
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
@@ -137,6 +139,21 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
137
139
  if (!absPath) return false;
138
140
  if (!existsSync(absPath)) return false;
139
141
 
142
+ // validate-milestone must have a VALIDATION file with a terminal verdict
143
+ // (pass, needs-attention, or needs-remediation). Without this check, a
144
+ // VALIDATION file with missing/malformed frontmatter or an unrecognized
145
+ // verdict is treated as "complete" by the artifact check but deriveState
146
+ // still returns phase:"validating-milestone" (because isValidationTerminal
147
+ // returns false), creating an infinite skip loop that hits the lifetime cap.
148
+ if (unitType === "validate-milestone") {
149
+ try {
150
+ const validationContent = readFileSync(absPath, "utf-8");
151
+ if (!isValidationTerminal(validationContent)) return false;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
140
157
  // plan-slice must produce a plan with actual task entries, not just a scaffold.
141
158
  // The plan file may exist from a prior discussion/context step with only headings
142
159
  // but no tasks. Without this check the artifact is considered "complete" and the
@@ -211,7 +228,7 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
211
228
  try {
212
229
  const roadmapContent = readFileSync(roadmapFile, "utf-8");
213
230
  const roadmap = parseRoadmap(roadmapContent);
214
- const slice = roadmap.slices.find(s => s.id === sid);
231
+ const slice = (roadmap.slices ?? []).find(s => s.id === sid);
215
232
  if (slice && !slice.done) return false;
216
233
  } catch {
217
234
  // Corrupt/unparseable roadmap — fail verification so the unit
@@ -338,6 +355,10 @@ export function skipExecuteTask(
338
355
 
339
356
  // ─── Disk-backed completed-unit helpers ───────────────────────────────────────
340
357
 
358
+ function isStringArray(data: unknown): data is string[] {
359
+ return Array.isArray(data) && data.every(item => typeof item === "string");
360
+ }
361
+
341
362
  /** Path to the persisted completed-unit keys file. */
342
363
  export function completedKeysPath(base: string): string {
343
364
  return join(base, ".gsd", "completed-units.json");
@@ -346,12 +367,7 @@ export function completedKeysPath(base: string): string {
346
367
  /** Write a completed unit key to disk (read-modify-write append to set). */
347
368
  export function persistCompletedKey(base: string, key: string): void {
348
369
  const file = completedKeysPath(base);
349
- let keys: string[] = [];
350
- try {
351
- if (existsSync(file)) {
352
- keys = JSON.parse(readFileSync(file, "utf-8"));
353
- }
354
- } catch (e) { /* corrupt file — start fresh */ void e; }
370
+ const keys = loadJsonFileOrNull(file, isStringArray) ?? [];
355
371
  const keySet = new Set(keys);
356
372
  if (!keySet.has(key)) {
357
373
  keys.push(key);
@@ -362,27 +378,21 @@ export function persistCompletedKey(base: string, key: string): void {
362
378
  /** Remove a stale completed unit key from disk. */
363
379
  export function removePersistedKey(base: string, key: string): void {
364
380
  const file = completedKeysPath(base);
365
- try {
366
- if (existsSync(file)) {
367
- const keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
368
- const filtered = keys.filter(k => k !== key);
369
- // Only write if the key was actually present
370
- if (filtered.length !== keys.length) {
371
- atomicWriteSync(file, JSON.stringify(filtered));
372
- }
373
- }
374
- } catch (e) { /* non-fatal: removePersistedKey failure */ void e; }
381
+ const keys = loadJsonFileOrNull(file, isStringArray);
382
+ if (!keys) return;
383
+ const filtered = keys.filter(k => k !== key);
384
+ if (filtered.length !== keys.length) {
385
+ atomicWriteSync(file, JSON.stringify(filtered));
386
+ }
375
387
  }
376
388
 
377
389
  /** Load all completed unit keys from disk into the in-memory set. */
378
390
  export function loadPersistedKeys(base: string, target: Set<string>): void {
379
391
  const file = completedKeysPath(base);
380
- try {
381
- if (existsSync(file)) {
382
- const keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
383
- for (const k of keys) target.add(k);
384
- }
385
- } catch (e) { /* non-fatal: loadPersistedKeys failure */ void e; }
392
+ const keys = loadJsonFileOrNull(file, isStringArray);
393
+ if (keys) {
394
+ for (const k of keys) target.add(k);
395
+ }
386
396
  }
387
397
 
388
398
  // ─── Merge State Reconciliation ───────────────────────────────────────────────
@@ -26,6 +26,13 @@ import {
26
26
  import { invalidateAllCaches } from "./cache.js";
27
27
  import { synthesizeCrashRecovery } from "./session-forensics.js";
28
28
  import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
29
+ import {
30
+ acquireSessionLock,
31
+ updateSessionLock,
32
+ releaseSessionLock,
33
+ readSessionLockData,
34
+ isSessionLockProcessAlive,
35
+ } from "./session-lock.js";
29
36
  import { selfHealRuntimeRecords } from "./auto-recovery.js";
30
37
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
31
38
  import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
@@ -81,6 +88,18 @@ export async function bootstrapAutoSession(
81
88
  ): Promise<boolean> {
82
89
  const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase } = deps;
83
90
 
91
+ // ── Session lock: acquire FIRST, before any state mutation ──────────────
92
+ // This is the primary guard against concurrent sessions on the same project.
93
+ // Uses OS-level file locking (proper-lockfile) to prevent TOCTOU races.
94
+ const lockResult = acquireSessionLock(base);
95
+ if (!lockResult.acquired) {
96
+ ctx.ui.notify(
97
+ `${lockResult.reason}\nStop it with \`kill ${lockResult.existingPid ?? "the other process"}\` before starting a new session.`,
98
+ "error",
99
+ );
100
+ return false;
101
+ }
102
+
84
103
  // Ensure git repo exists
85
104
  if (!nativeIsRepo(base)) {
86
105
  const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
@@ -109,16 +128,11 @@ export async function bootstrapAutoSession(
109
128
  // Initialize GitServiceImpl
110
129
  s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
111
130
 
112
- // Check for crash from previous session
131
+ // Check for crash from previous session (use both old and new lock data)
113
132
  const crashLock = readCrashLock(base);
114
133
  if (crashLock) {
115
- if (isLockProcessAlive(crashLock)) {
116
- ctx.ui.notify(
117
- `Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`,
118
- "error",
119
- );
120
- return false;
121
- }
134
+ // We already hold the session lock, so no concurrent session is running.
135
+ // The crash lock is from a dead process — recover context from it.
122
136
  const recoveredMid = crashLock.unitId.split("/")[0];
123
137
  const milestoneAlreadyComplete = recoveredMid
124
138
  ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
@@ -401,13 +415,14 @@ export async function bootstrapAutoSession(
401
415
  ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
402
416
  ctx.ui.setFooter(hideFooter);
403
417
  const modeLabel = s.stepMode ? "Step-mode" : "Auto-mode";
404
- const pendingCount = state.registry.filter(m => m.status !== 'complete' && m.status !== 'parked').length;
418
+ const pendingCount = (state.registry ?? []).filter(m => m.status !== 'complete' && m.status !== 'parked').length;
405
419
  const scopeMsg = pendingCount > 1
406
420
  ? `Will loop through ${pendingCount} milestones.`
407
421
  : "Will loop until milestone complete.";
408
422
  ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
409
423
 
410
- // Write initial lock file
424
+ // Update lock file with milestone info (OS lock already acquired at bootstrap start)
425
+ updateSessionLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
411
426
  writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
412
427
 
413
428
  // Secrets collection gate — pause instead of blocking (#1146)
@@ -105,19 +105,39 @@ export async function runPostUnitVerification(
105
105
  const completionKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
106
106
 
107
107
  if (result.checks.length > 0) {
108
- const passCount = result.checks.filter(c => c.exitCode === 0).length;
109
- const total = result.checks.length;
108
+ const blockingChecks = result.checks.filter(c => c.blocking);
109
+ const advisoryChecks = result.checks.filter(c => !c.blocking);
110
+ const blockingPassCount = blockingChecks.filter(c => c.exitCode === 0).length;
111
+ const advisoryFailCount = advisoryChecks.filter(c => c.exitCode !== 0).length;
112
+
110
113
  if (result.passed) {
111
- ctx.ui.notify(`Verification gate: ${passCount}/${total} checks passed`);
114
+ let msg = blockingChecks.length > 0
115
+ ? `Verification gate: ${blockingPassCount}/${blockingChecks.length} blocking checks passed`
116
+ : `Verification gate: passed (no blocking checks)`;
117
+ if (advisoryFailCount > 0) {
118
+ msg += ` (${advisoryFailCount} advisory warning${advisoryFailCount > 1 ? "s" : ""})`;
119
+ }
120
+ ctx.ui.notify(msg);
121
+ // Log advisory warnings to stderr for visibility
122
+ if (advisoryFailCount > 0) {
123
+ const advisoryFailures = advisoryChecks.filter(c => c.exitCode !== 0);
124
+ process.stderr.write(`verification-gate: ${advisoryFailCount} advisory (non-blocking) failure(s)\n`);
125
+ for (const f of advisoryFailures) {
126
+ process.stderr.write(` [advisory] ${f.command} exited ${f.exitCode}\n`);
127
+ }
128
+ }
112
129
  } else {
113
- const failures = result.checks.filter(c => c.exitCode !== 0);
114
- const failNames = failures.map(f => f.command).join(", ");
130
+ const blockingFailures = blockingChecks.filter(c => c.exitCode !== 0);
131
+ const failNames = blockingFailures.map(f => f.command).join(", ");
115
132
  ctx.ui.notify(`Verification gate: FAILED — ${failNames}`);
116
- process.stderr.write(`verification-gate: ${total - passCount}/${total} checks failed\n`);
117
- for (const f of failures) {
133
+ process.stderr.write(`verification-gate: ${blockingFailures.length}/${blockingChecks.length} blocking checks failed\n`);
134
+ for (const f of blockingFailures) {
118
135
  process.stderr.write(` ${f.command} exited ${f.exitCode}\n`);
119
136
  if (f.stderr) process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
120
137
  }
138
+ if (advisoryFailCount > 0) {
139
+ process.stderr.write(`verification-gate: ${advisoryFailCount} additional advisory (non-blocking) failure(s)\n`);
140
+ }
121
141
  }
122
142
  }
123
143
 
@@ -155,6 +175,20 @@ export async function runPostUnitVerification(
155
175
  s.verificationRetryCount.delete(s.currentUnit.id);
156
176
  s.pendingVerificationRetry = null;
157
177
  return "continue";
178
+ } else if (result.discoverySource === "package-json") {
179
+ // Auto-discovered checks from package.json may fail on pre-existing errors
180
+ // that the current task didn't introduce. Don't trigger the retry loop —
181
+ // log a warning and let the task proceed (#1186).
182
+ process.stderr.write(
183
+ `verification-gate: auto-discovered checks failed (source: package-json) — treating as advisory, not blocking\n`,
184
+ );
185
+ ctx.ui.notify(
186
+ `Verification: auto-discovered checks failed (pre-existing errors likely). Continuing without retry.`,
187
+ "warning",
188
+ );
189
+ s.verificationRetryCount.delete(s.currentUnit.id);
190
+ s.pendingVerificationRetry = null;
191
+ return "continue";
158
192
  } else if (autoFixEnabled && attempt + 1 <= maxRetries) {
159
193
  const nextAttempt = attempt + 1;
160
194
  s.verificationRetryCount.set(s.currentUnit.id, nextAttempt);