gsd-pi 2.23.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +12 -3
  3. package/dist/headless.d.ts +4 -0
  4. package/dist/headless.js +118 -10
  5. package/dist/help-text.js +22 -7
  6. package/dist/models-resolver.d.ts +0 -11
  7. package/dist/models-resolver.js +0 -15
  8. package/dist/resource-loader.d.ts +0 -1
  9. package/dist/resource-loader.js +64 -18
  10. package/dist/resources/GSD-WORKFLOW.md +12 -9
  11. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  12. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  13. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  15. package/dist/resources/extensions/gsd/auto-prompts.ts +87 -0
  16. package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
  17. package/dist/resources/extensions/gsd/auto-worktree.ts +134 -4
  18. package/dist/resources/extensions/gsd/auto.ts +307 -77
  19. package/dist/resources/extensions/gsd/cache.ts +3 -1
  20. package/dist/resources/extensions/gsd/commands.ts +176 -10
  21. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  22. package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  23. package/dist/resources/extensions/gsd/doctor.ts +58 -11
  24. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  25. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  26. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  27. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  28. package/dist/resources/extensions/gsd/guided-flow.ts +109 -12
  29. package/dist/resources/extensions/gsd/index.ts +48 -2
  30. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  31. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  32. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  33. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  34. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  35. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  36. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  39. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -4
  40. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  43. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  45. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  47. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  48. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  49. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  50. package/dist/resources/extensions/gsd/state.ts +72 -30
  51. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  52. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  53. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  54. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
  55. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  56. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  57. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  58. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  59. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  60. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  62. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  63. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  64. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  65. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  66. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  67. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  68. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  69. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  70. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  71. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  72. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  73. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  74. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  75. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  76. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  77. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  78. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  79. package/dist/resources/extensions/gsd/types.ts +15 -1
  80. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  81. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  82. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  83. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  84. package/dist/resources/extensions/search-the-web/native-search.ts +15 -5
  85. package/dist/resources/extensions/subagent/index.ts +5 -0
  86. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  87. package/dist/update-check.d.ts +9 -0
  88. package/dist/update-check.js +97 -0
  89. package/package.json +6 -1
  90. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  91. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  92. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  93. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/anthropic.js +55 -7
  95. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  96. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  97. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  98. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  99. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  100. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  101. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  102. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  103. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  106. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  110. package/packages/pi-ai/dist/types.d.ts +23 -1
  111. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  112. package/packages/pi-ai/dist/types.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +59 -9
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  117. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  118. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  119. package/packages/pi-ai/src/types.ts +19 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +72 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  131. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  132. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  133. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +84 -0
  135. package/scripts/postinstall.js +7 -109
  136. package/src/resources/GSD-WORKFLOW.md +12 -9
  137. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  138. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  139. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  140. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  141. package/src/resources/extensions/gsd/auto-prompts.ts +87 -0
  142. package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
  143. package/src/resources/extensions/gsd/auto-worktree.ts +134 -4
  144. package/src/resources/extensions/gsd/auto.ts +307 -77
  145. package/src/resources/extensions/gsd/cache.ts +3 -1
  146. package/src/resources/extensions/gsd/commands.ts +176 -10
  147. package/src/resources/extensions/gsd/complexity.ts +1 -0
  148. package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
  149. package/src/resources/extensions/gsd/doctor.ts +58 -11
  150. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  151. package/src/resources/extensions/gsd/git-service.ts +74 -14
  152. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  153. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  154. package/src/resources/extensions/gsd/guided-flow.ts +109 -12
  155. package/src/resources/extensions/gsd/index.ts +48 -2
  156. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  157. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  158. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  159. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  160. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  161. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  162. package/src/resources/extensions/gsd/preferences.ts +65 -1
  163. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  165. package/src/resources/extensions/gsd/prompts/discuss.md +4 -4
  166. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  170. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
  174. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  175. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  176. package/src/resources/extensions/gsd/state.ts +72 -30
  177. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  178. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  179. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  180. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +256 -2
  181. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  182. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  183. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  184. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  185. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  186. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  187. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  188. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  189. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  190. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  191. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  192. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  193. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  194. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  195. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  196. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  197. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  198. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  199. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  200. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  201. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  202. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  203. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  204. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  205. package/src/resources/extensions/gsd/types.ts +15 -1
  206. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  207. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  208. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  209. package/src/resources/extensions/gsd/worktree.ts +9 -2
  210. package/src/resources/extensions/search-the-web/native-search.ts +15 -5
  211. package/src/resources/extensions/subagent/index.ts +5 -0
  212. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * GSD Session Status I/O
3
+ *
4
+ * File-based IPC protocol for coordinator-worker communication in
5
+ * parallel milestone orchestration. Each worker writes its status to a
6
+ * file; the coordinator reads all status files to monitor progress.
7
+ *
8
+ * Atomic writes (write to .tmp, then rename) prevent partial reads.
9
+ * Signal files let the coordinator send pause/resume/stop/rebase to workers.
10
+ * Stale detection combines PID liveness checks with heartbeat timeouts.
11
+ */
12
+
13
+ import {
14
+ writeFileSync,
15
+ readFileSync,
16
+ renameSync,
17
+ unlinkSync,
18
+ readdirSync,
19
+ mkdirSync,
20
+ existsSync,
21
+ } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { gsdRoot } from "./paths.js";
24
+
25
+ // ─── Types ─────────────────────────────────────────────────────────────────
26
+
27
+ export interface SessionStatus {
28
+ milestoneId: string;
29
+ pid: number;
30
+ state: "running" | "paused" | "stopped" | "error";
31
+ currentUnit: { type: string; id: string; startedAt: number } | null;
32
+ completedUnits: number;
33
+ cost: number;
34
+ lastHeartbeat: number;
35
+ startedAt: number;
36
+ worktreePath: string;
37
+ }
38
+
39
+ export type SessionSignal = "pause" | "resume" | "stop" | "rebase";
40
+
41
+ export interface SignalMessage {
42
+ signal: SessionSignal;
43
+ sentAt: number;
44
+ from: "coordinator";
45
+ }
46
+
47
+ // ─── Constants ─────────────────────────────────────────────────────────────
48
+
49
+ const PARALLEL_DIR = "parallel";
50
+ const STATUS_SUFFIX = ".status.json";
51
+ const SIGNAL_SUFFIX = ".signal.json";
52
+ const TMP_SUFFIX = ".tmp";
53
+ const DEFAULT_STALE_TIMEOUT_MS = 30_000;
54
+
55
+ // ─── Helpers ───────────────────────────────────────────────────────────────
56
+
57
+ function parallelDir(basePath: string): string {
58
+ return join(gsdRoot(basePath), PARALLEL_DIR);
59
+ }
60
+
61
+ function statusPath(basePath: string, milestoneId: string): string {
62
+ return join(parallelDir(basePath), `${milestoneId}${STATUS_SUFFIX}`);
63
+ }
64
+
65
+ function signalPath(basePath: string, milestoneId: string): string {
66
+ return join(parallelDir(basePath), `${milestoneId}${SIGNAL_SUFFIX}`);
67
+ }
68
+
69
+ function ensureParallelDir(basePath: string): void {
70
+ const dir = parallelDir(basePath);
71
+ if (!existsSync(dir)) {
72
+ mkdirSync(dir, { recursive: true });
73
+ }
74
+ }
75
+
76
+ function isPidAlive(pid: number): boolean {
77
+ try {
78
+ process.kill(pid, 0);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ // ─── Status I/O ────────────────────────────────────────────────────────────
86
+
87
+ /** Write session status atomically (write to .tmp, then rename). */
88
+ export function writeSessionStatus(basePath: string, status: SessionStatus): void {
89
+ try {
90
+ ensureParallelDir(basePath);
91
+ const dest = statusPath(basePath, status.milestoneId);
92
+ const tmp = dest + TMP_SUFFIX;
93
+ writeFileSync(tmp, JSON.stringify(status, null, 2), "utf-8");
94
+ renameSync(tmp, dest);
95
+ } catch { /* non-fatal */ }
96
+ }
97
+
98
+ /** Read a specific milestone's session status. */
99
+ export function readSessionStatus(basePath: string, milestoneId: string): SessionStatus | null {
100
+ try {
101
+ const p = statusPath(basePath, milestoneId);
102
+ if (!existsSync(p)) return null;
103
+ const raw = readFileSync(p, "utf-8");
104
+ return JSON.parse(raw) as SessionStatus;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /** Read all session status files from .gsd/parallel/. */
111
+ export function readAllSessionStatuses(basePath: string): SessionStatus[] {
112
+ const dir = parallelDir(basePath);
113
+ if (!existsSync(dir)) return [];
114
+
115
+ const results: SessionStatus[] = [];
116
+ try {
117
+ const entries = readdirSync(dir);
118
+ for (const entry of entries) {
119
+ if (!entry.endsWith(STATUS_SUFFIX)) continue;
120
+ try {
121
+ const raw = readFileSync(join(dir, entry), "utf-8");
122
+ results.push(JSON.parse(raw) as SessionStatus);
123
+ } catch { /* skip corrupt files */ }
124
+ }
125
+ } catch { /* non-fatal */ }
126
+ return results;
127
+ }
128
+
129
+ /** Remove a milestone's session status file. */
130
+ export function removeSessionStatus(basePath: string, milestoneId: string): void {
131
+ try {
132
+ const p = statusPath(basePath, milestoneId);
133
+ if (existsSync(p)) unlinkSync(p);
134
+ } catch { /* non-fatal */ }
135
+ }
136
+
137
+ // ─── Signal I/O ────────────────────────────────────────────────────────────
138
+
139
+ /** Write a signal file for a worker to consume. */
140
+ export function sendSignal(basePath: string, milestoneId: string, signal: SessionSignal): void {
141
+ try {
142
+ ensureParallelDir(basePath);
143
+ const dest = signalPath(basePath, milestoneId);
144
+ const tmp = dest + TMP_SUFFIX;
145
+ const msg: SignalMessage = { signal, sentAt: Date.now(), from: "coordinator" };
146
+ writeFileSync(tmp, JSON.stringify(msg, null, 2), "utf-8");
147
+ renameSync(tmp, dest);
148
+ } catch { /* non-fatal */ }
149
+ }
150
+
151
+ /** Read and delete a signal file (atomic consume). Returns null if no signal pending. */
152
+ export function consumeSignal(basePath: string, milestoneId: string): SignalMessage | null {
153
+ try {
154
+ const p = signalPath(basePath, milestoneId);
155
+ if (!existsSync(p)) return null;
156
+ const raw = readFileSync(p, "utf-8");
157
+ unlinkSync(p);
158
+ return JSON.parse(raw) as SignalMessage;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ // ─── Stale Detection ───────────────────────────────────────────────────────
165
+
166
+ /** Check whether a session is stale (PID dead or heartbeat timed out). */
167
+ export function isSessionStale(
168
+ status: SessionStatus,
169
+ timeoutMs: number = DEFAULT_STALE_TIMEOUT_MS,
170
+ ): boolean {
171
+ if (!isPidAlive(status.pid)) return true;
172
+ const elapsed = Date.now() - status.lastHeartbeat;
173
+ return elapsed > timeoutMs;
174
+ }
175
+
176
+ /** Find and remove stale sessions. Returns the milestone IDs that were cleaned up. */
177
+ export function cleanupStaleSessions(
178
+ basePath: string,
179
+ timeoutMs: number = DEFAULT_STALE_TIMEOUT_MS,
180
+ ): string[] {
181
+ const removed: string[] = [];
182
+ const statuses = readAllSessionStatuses(basePath);
183
+
184
+ for (const status of statuses) {
185
+ if (isSessionStale(status, timeoutMs)) {
186
+ removeSessionStatus(basePath, status.milestoneId);
187
+ // Also clean up any lingering signal file
188
+ try {
189
+ const sig = signalPath(basePath, status.milestoneId);
190
+ if (existsSync(sig)) unlinkSync(sig);
191
+ } catch { /* non-fatal */ }
192
+ removed.push(status.milestoneId);
193
+ }
194
+ }
195
+
196
+ return removed;
197
+ }
@@ -32,7 +32,6 @@ import {
32
32
 
33
33
  import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
34
34
  import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
35
- import { isDbAvailable, _getAdapter } from './gsd-db.js';
36
35
 
37
36
  import { join, resolve } from 'path';
38
37
  import { debugCount, debugTime } from './debug-logger.js';
@@ -53,6 +52,19 @@ export function isMilestoneComplete(roadmap: Roadmap): boolean {
53
52
  return roadmap.slices.length > 0 && roadmap.slices.every(s => s.done);
54
53
  }
55
54
 
55
+ /**
56
+ * Check whether a VALIDATION file's verdict is terminal (pass or needs-attention).
57
+ * A non-terminal verdict (needs-remediation) means validation must re-run
58
+ * after remediation slices are executed.
59
+ */
60
+ export function isValidationTerminal(validationContent: string): boolean {
61
+ const match = validationContent.match(/^---\n([\s\S]*?)\n---/);
62
+ if (!match) return false;
63
+ const verdict = match[1].match(/verdict:\s*(\S+)/);
64
+ if (!verdict) return false;
65
+ return verdict[1] === 'pass' || verdict[1] === 'needs-attention';
66
+ }
67
+
56
68
  // ─── State Derivation ──────────────────────────────────────────────────────
57
69
 
58
70
  // ── deriveState memoization ─────────────────────────────────────────────────
@@ -82,6 +94,11 @@ export function invalidateStateCache(): void {
82
94
  */
83
95
  export async function getActiveMilestoneId(basePath: string): Promise<string | null> {
84
96
  const milestoneIds = findMilestoneIds(basePath);
97
+ // Parallel worker isolation
98
+ const milestoneLock = process.env.GSD_MILESTONE_LOCK;
99
+ if (milestoneLock) {
100
+ return milestoneIds.includes(milestoneLock) ? milestoneLock : null;
101
+ }
85
102
  for (const mid of milestoneIds) {
86
103
  const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
87
104
  const content = roadmapFile ? await loadFile(roadmapFile) : null;
@@ -129,6 +146,18 @@ export async function deriveState(basePath: string): Promise<GSDState> {
129
146
  async function _deriveStateImpl(basePath: string): Promise<GSDState> {
130
147
  const milestoneIds = findMilestoneIds(basePath);
131
148
 
149
+ // ── Parallel worker isolation ──────────────────────────────────────────
150
+ // When GSD_MILESTONE_LOCK is set, this process is a parallel worker
151
+ // scoped to a single milestone. Filter the milestone list so this worker
152
+ // only sees its assigned milestone (all others are treated as if they
153
+ // don't exist). This gives each worker complete isolation without
154
+ // modifying any other state derivation logic.
155
+ const milestoneLock = process.env.GSD_MILESTONE_LOCK;
156
+ if (milestoneLock && milestoneIds.includes(milestoneLock)) {
157
+ milestoneIds.length = 0;
158
+ milestoneIds.push(milestoneLock);
159
+ }
160
+
132
161
  // ── Batch-parse file cache ──────────────────────────────────────────────
133
162
  // When the native Rust parser is available, read every .md file under .gsd/
134
163
  // in one call and build an in-memory content map keyed by absolute path.
@@ -136,30 +165,12 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
136
165
  const fileContentCache = new Map<string, string>();
137
166
  const gsdDir = gsdRoot(basePath);
138
167
 
139
- // ── DB-first content loading ──
140
- // When the DB is available, load artifact content from the artifacts table
141
- // (indexed SELECT instead of O(N) file I/O). Falls back to native Rust batch
142
- // parser, which in turn falls back to sequential JS reads via cachedLoadFile.
143
- let dbContentLoaded = false;
144
- if (isDbAvailable()) {
145
- const adapter = _getAdapter();
146
- if (adapter) {
147
- try {
148
- const rows = adapter.prepare('SELECT path, full_content FROM artifacts').all();
149
- for (const row of rows) {
150
- const relPath = (row as Record<string, unknown>)['path'] as string;
151
- const content = (row as Record<string, unknown>)['full_content'] as string;
152
- const absPath = resolve(gsdDir, relPath);
153
- fileContentCache.set(absPath, content);
154
- }
155
- dbContentLoaded = rows.length > 0;
156
- } catch {
157
- // DB query failed — fall through to native batch parse
158
- }
159
- }
160
- }
161
-
162
- if (!dbContentLoaded) {
168
+ // NOTE: We intentionally do NOT load from the SQLite DB here (#759).
169
+ // The DB's artifacts table is populated once during migrateFromMarkdown
170
+ // and is never updated when files change on disk (e.g. roadmap [x] updates,
171
+ // plan checkbox changes). Using stale DB content causes deriveState to
172
+ // return incorrect phase/slice state, leading to infinite skip loops.
173
+ // The native Rust batch parser is fast enough for state derivation.
163
174
  const batchFiles = nativeBatchParseGsdFiles(gsdDir);
164
175
  if (batchFiles) {
165
176
  for (const f of batchFiles) {
@@ -167,7 +178,6 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
167
178
  fileContentCache.set(absPath, f.rawContent);
168
179
  }
169
180
  }
170
- }
171
181
 
172
182
  /**
173
183
  * Load file content from batch cache first, falling back to disk read.
@@ -279,10 +289,20 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
279
289
  const complete = isMilestoneComplete(roadmap);
280
290
 
281
291
  if (complete) {
282
- // All slices done — check if milestone summary exists
292
+ // All slices done — check validation and summary state
293
+ const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
294
+ const validationContent = validationFile ? await cachedLoadFile(validationFile) : null;
295
+ const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
283
296
  const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
284
- if (!summaryFile && !activeMilestoneFound) {
285
- // All slices complete but no summary written yet → completing-milestone
297
+
298
+ if (!validationTerminal && !activeMilestoneFound) {
299
+ // No terminal validation yet → validating-milestone
300
+ activeMilestone = { id: mid, title };
301
+ activeRoadmap = roadmap;
302
+ activeMilestoneFound = true;
303
+ registry.push({ id: mid, title, status: 'active' });
304
+ } else if (!summaryFile && !activeMilestoneFound) {
305
+ // Validated but no summary written yet → completing-milestone
286
306
  activeMilestone = { id: mid, title };
287
307
  activeRoadmap = roadmap;
288
308
  activeMilestoneFound = true;
@@ -385,12 +405,34 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
385
405
  };
386
406
  }
387
407
 
388
- // Check if active milestone needs completion (all slices done, no summary)
408
+ // Check if active milestone needs validation or completion (all slices done)
389
409
  if (isMilestoneComplete(activeRoadmap)) {
410
+ const validationFile = resolveMilestoneFile(basePath, activeMilestone.id, "VALIDATION");
411
+ const validationContent = validationFile ? await cachedLoadFile(validationFile) : null;
412
+ const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
390
413
  const sliceProgress = {
391
414
  done: activeRoadmap.slices.length,
392
415
  total: activeRoadmap.slices.length,
393
416
  };
417
+
418
+ if (!validationTerminal) {
419
+ return {
420
+ activeMilestone,
421
+ activeSlice: null,
422
+ activeTask: null,
423
+ phase: 'validating-milestone',
424
+ recentDecisions: [],
425
+ blockers: [],
426
+ nextAction: `Validate milestone ${activeMilestone.id} before completion.`,
427
+ registry,
428
+ requirements,
429
+ progress: {
430
+ milestones: milestoneProgress,
431
+ slices: sliceProgress,
432
+ },
433
+ };
434
+ }
435
+
394
436
  return {
395
437
  activeMilestone,
396
438
  activeSlice: null,
@@ -27,3 +27,84 @@ test("pauseAutoForProviderError warns and pauses without requiring ctx.log", asy
27
27
  },
28
28
  ]);
29
29
  });
30
+
31
+ test("pauseAutoForProviderError schedules auto-resume for rate limit errors", async () => {
32
+ const notifications: Array<{ message: string; level: string }> = [];
33
+ let pauseCalls = 0;
34
+ let resumeCalled = false;
35
+
36
+ // Use fake timer
37
+ const originalSetTimeout = globalThis.setTimeout;
38
+ const timers: Array<{ fn: () => void; delay: number }> = [];
39
+ globalThis.setTimeout = ((fn: () => void, delay: number) => {
40
+ timers.push({ fn, delay });
41
+ return 0 as unknown as ReturnType<typeof setTimeout>;
42
+ }) as typeof setTimeout;
43
+
44
+ try {
45
+ await pauseAutoForProviderError(
46
+ {
47
+ notify(message, level?) {
48
+ notifications.push({ message, level: level ?? "info" });
49
+ },
50
+ },
51
+ ": rate limit exceeded",
52
+ async () => {
53
+ pauseCalls += 1;
54
+ },
55
+ {
56
+ isRateLimit: true,
57
+ retryAfterMs: 90000,
58
+ resume: () => {
59
+ resumeCalled = true;
60
+ },
61
+ },
62
+ );
63
+
64
+ assert.equal(pauseCalls, 1, "should pause auto-mode");
65
+ assert.equal(timers.length, 1, "should schedule one timer");
66
+ assert.equal(timers[0].delay, 90000, "timer should match retryAfterMs");
67
+ assert.deepEqual(notifications[0], {
68
+ message: "Rate limited: rate limit exceeded. Auto-resuming in 90s...",
69
+ level: "warning",
70
+ });
71
+
72
+ // Fire the timer
73
+ timers[0].fn();
74
+ assert.equal(resumeCalled, true, "should call resume after timer fires");
75
+ assert.deepEqual(notifications[1], {
76
+ message: "Rate limit window elapsed. Resuming auto-mode.",
77
+ level: "info",
78
+ });
79
+ } finally {
80
+ globalThis.setTimeout = originalSetTimeout;
81
+ }
82
+ });
83
+
84
+ test("pauseAutoForProviderError falls back to indefinite pause when not rate limit", async () => {
85
+ const notifications: Array<{ message: string; level: string }> = [];
86
+ let pauseCalls = 0;
87
+
88
+ await pauseAutoForProviderError(
89
+ {
90
+ notify(message, level?) {
91
+ notifications.push({ message, level: level ?? "info" });
92
+ },
93
+ },
94
+ ": connection refused",
95
+ async () => {
96
+ pauseCalls += 1;
97
+ },
98
+ {
99
+ isRateLimit: false,
100
+ },
101
+ );
102
+
103
+ assert.equal(pauseCalls, 1);
104
+ assert.deepEqual(notifications, [
105
+ {
106
+ message: "Auto-mode paused due to provider error: connection refused",
107
+ level: "warning",
108
+ },
109
+ ]);
110
+ });
@@ -9,8 +9,12 @@ import {
9
9
 
10
10
  test("getBudgetAlertLevel returns the expected threshold bucket", () => {
11
11
  assert.equal(getBudgetAlertLevel(0.10), 0);
12
+ assert.equal(getBudgetAlertLevel(0.74), 0);
12
13
  assert.equal(getBudgetAlertLevel(0.75), 75);
13
- assert.equal(getBudgetAlertLevel(0.89), 75);
14
+ assert.equal(getBudgetAlertLevel(0.79), 75);
15
+ assert.equal(getBudgetAlertLevel(0.80), 80);
16
+ assert.equal(getBudgetAlertLevel(0.85), 80);
17
+ assert.equal(getBudgetAlertLevel(0.89), 80);
14
18
  assert.equal(getBudgetAlertLevel(0.90), 90);
15
19
  assert.equal(getBudgetAlertLevel(1.00), 100);
16
20
  });
@@ -18,14 +22,27 @@ test("getBudgetAlertLevel returns the expected threshold bucket", () => {
18
22
  test("getNewBudgetAlertLevel only emits once per threshold", () => {
19
23
  assert.equal(getNewBudgetAlertLevel(0, 0.74), null);
20
24
  assert.equal(getNewBudgetAlertLevel(0, 0.75), 75);
21
- assert.equal(getNewBudgetAlertLevel(75, 0.80), null);
22
- assert.equal(getNewBudgetAlertLevel(75, 0.90), 90);
25
+ assert.equal(getNewBudgetAlertLevel(75, 0.79), null);
26
+ assert.equal(getNewBudgetAlertLevel(75, 0.80), 80);
27
+ assert.equal(getNewBudgetAlertLevel(80, 0.85), null);
28
+ assert.equal(getNewBudgetAlertLevel(80, 0.90), 90);
23
29
  assert.equal(getNewBudgetAlertLevel(90, 0.95), null);
24
30
  assert.equal(getNewBudgetAlertLevel(90, 1.0), 100);
25
31
  assert.equal(getNewBudgetAlertLevel(100, 1.2), null);
26
32
  });
27
33
 
34
+ test("80% alert fires exactly once between 75% and 90%", () => {
35
+ // Transition from 75 → 80 emits 80
36
+ assert.equal(getNewBudgetAlertLevel(75, 0.80), 80);
37
+ // Already at 80 — no re-emission
38
+ assert.equal(getNewBudgetAlertLevel(80, 0.82), null);
39
+ assert.equal(getNewBudgetAlertLevel(80, 0.89), null);
40
+ // Transition from 80 → 90 emits 90
41
+ assert.equal(getNewBudgetAlertLevel(80, 0.90), 90);
42
+ });
43
+
28
44
  test("getBudgetEnforcementAction maps the configured ceiling behavior", () => {
45
+ assert.equal(getBudgetEnforcementAction("warn", 0.80), "none");
29
46
  assert.equal(getBudgetEnforcementAction("warn", 0.99), "none");
30
47
  assert.equal(getBudgetEnforcementAction("warn", 1.0), "warn");
31
48
  assert.equal(getBudgetEnforcementAction("pause", 1.0), "pause");
@@ -17,6 +17,7 @@ writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "S01-PLAN.md"), `
17
17
  writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"), `---\nid: T01\nparent: S01\nmilestone: M001\nprovides: []\nrequires: []\naffects: []\nkey_files: []\nkey_decisions: []\npatterns_established: []\nobservability_surfaces: []\ndrill_down_paths: []\nduration: 5m\nverification_result: passed\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# T01: Old Task\n\n**Done**\n\n## What Happened\nDone.\n\n## Diagnostics\n- log\n`);
18
18
  writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"), `---\nid: S01\nparent: M001\nmilestone: M001\nprovides: []\nrequires: []\naffects: []\nkey_files: []\nkey_decisions: []\npatterns_established: []\nobservability_surfaces: []\ndrill_down_paths: []\nduration: 5m\nverification_result: passed\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# S01: Old Slice\n\n**Done**\n\n## What Happened\nDone.\n\n## Verification\nDone.\n\n## Deviations\nNone\n\n## Known Limitations\nNone\n\n## Follow-ups\nNone\n\n## Files Created/Modified\n- \`x\` — x\n\n## Forward Intelligence\n\n### What the next slice should know\n- x\n\n### What's fragile\n- x\n\n### Authoritative diagnostics\n- x\n\n### What assumptions changed\n- x\n`);
19
19
 
20
+ writeFileSync(join(gsd, "milestones", "M001", "M001-VALIDATION.md"), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.\n`);
20
21
  writeFileSync(join(gsd, "milestones", "M001", "M001-SUMMARY.md"), `---\nid: M001\nstatus: complete\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# M001: Historical\n\nComplete.\n`);
21
22
 
22
23
  writeFileSync(join(gsd, "milestones", "M009", "M009-ROADMAP.md"), `# M009: Active\n\n## Slices\n- [ ] **S01: Active Slice** \`risk:low\` \`depends:[]\`\n > After this: active works\n`);