gsd-pi 2.26.0 → 2.27.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 (171) hide show
  1. package/README.md +43 -6
  2. package/dist/cli.js +4 -2
  3. package/dist/headless.d.ts +3 -0
  4. package/dist/headless.js +136 -8
  5. package/dist/help-text.js +3 -0
  6. package/dist/loader.js +33 -4
  7. package/dist/resources/extensions/bg-shell/index.ts +19 -2
  8. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  9. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  10. package/dist/resources/extensions/gsd/auto/session.ts +224 -0
  11. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  12. package/dist/resources/extensions/gsd/auto-dashboard.ts +63 -10
  13. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  14. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  15. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  16. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  17. package/dist/resources/extensions/gsd/auto-prompts.ts +0 -1
  18. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  19. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  20. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  22. package/dist/resources/extensions/gsd/auto.ts +977 -1551
  23. package/dist/resources/extensions/gsd/commands.ts +3 -3
  24. package/dist/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  25. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  26. package/dist/resources/extensions/gsd/export-html.ts +1001 -0
  27. package/dist/resources/extensions/gsd/export.ts +49 -1
  28. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  29. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  30. package/dist/resources/extensions/gsd/guided-flow.ts +24 -5
  31. package/dist/resources/extensions/gsd/index.ts +54 -1
  32. package/dist/resources/extensions/gsd/native-git-bridge.ts +30 -2
  33. package/dist/resources/extensions/gsd/observability-validator.ts +21 -0
  34. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  35. package/dist/resources/extensions/gsd/preferences.ts +62 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -3
  37. package/dist/resources/extensions/gsd/prompts/system.md +1 -1
  38. package/dist/resources/extensions/gsd/reports.ts +510 -0
  39. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  40. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  41. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  42. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  43. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  44. package/dist/resources/extensions/gsd/state.ts +30 -0
  45. package/dist/resources/extensions/gsd/templates/task-summary.md +9 -0
  46. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  47. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  48. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  49. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  51. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  52. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  54. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  55. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  57. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  58. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  59. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  60. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  61. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  62. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  63. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  64. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  65. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  66. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  67. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  68. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  69. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  70. package/dist/resources/extensions/gsd/types.ts +38 -0
  71. package/dist/resources/extensions/gsd/verification-evidence.ts +183 -0
  72. package/dist/resources/extensions/gsd/verification-gate.ts +567 -0
  73. package/dist/resources/extensions/gsd/visualizer-data.ts +25 -3
  74. package/dist/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  75. package/dist/resources/extensions/gsd/visualizer-views.ts +15 -66
  76. package/dist/resources/extensions/search-the-web/tool-search.ts +26 -0
  77. package/dist/resources/extensions/shared/format-utils.ts +85 -0
  78. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  79. package/dist/resources/extensions/subagent/index.ts +46 -1
  80. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  81. package/package.json +1 -1
  82. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  83. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  84. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  85. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/lsp/client.js +7 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  90. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  91. package/packages/pi-coding-agent/src/core/lsp/client.ts +8 -0
  92. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  93. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  94. package/packages/pi-tui/dist/components/editor.js +1 -1
  95. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  96. package/packages/pi-tui/src/components/editor.ts +3 -1
  97. package/scripts/link-workspace-packages.cjs +22 -6
  98. package/src/resources/extensions/bg-shell/index.ts +19 -2
  99. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  100. package/src/resources/extensions/bg-shell/types.ts +21 -1
  101. package/src/resources/extensions/gsd/auto/session.ts +224 -0
  102. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  103. package/src/resources/extensions/gsd/auto-dashboard.ts +63 -10
  104. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  105. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  106. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  107. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  108. package/src/resources/extensions/gsd/auto-prompts.ts +0 -1
  109. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  110. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  111. package/src/resources/extensions/gsd/auto-unit-closeout.ts +46 -0
  112. package/src/resources/extensions/gsd/auto-worktree-sync.ts +207 -0
  113. package/src/resources/extensions/gsd/auto.ts +977 -1551
  114. package/src/resources/extensions/gsd/commands.ts +3 -3
  115. package/src/resources/extensions/gsd/dashboard-overlay.ts +47 -72
  116. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  117. package/src/resources/extensions/gsd/export-html.ts +1001 -0
  118. package/src/resources/extensions/gsd/export.ts +49 -1
  119. package/src/resources/extensions/gsd/git-service.ts +6 -0
  120. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  121. package/src/resources/extensions/gsd/guided-flow.ts +24 -5
  122. package/src/resources/extensions/gsd/index.ts +54 -1
  123. package/src/resources/extensions/gsd/native-git-bridge.ts +30 -2
  124. package/src/resources/extensions/gsd/observability-validator.ts +21 -0
  125. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  126. package/src/resources/extensions/gsd/preferences.ts +62 -1
  127. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -3
  128. package/src/resources/extensions/gsd/prompts/system.md +1 -1
  129. package/src/resources/extensions/gsd/reports.ts +510 -0
  130. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  131. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +178 -0
  132. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +54 -0
  133. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +59 -0
  134. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +185 -0
  135. package/src/resources/extensions/gsd/state.ts +30 -0
  136. package/src/resources/extensions/gsd/templates/task-summary.md +9 -0
  137. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  138. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  139. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  140. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  141. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  142. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  143. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  144. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  146. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +1 -1
  147. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  148. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +51 -1
  149. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  150. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  151. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +465 -0
  152. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  153. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +71 -0
  154. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  155. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +9 -9
  156. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +743 -0
  157. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +965 -0
  158. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  159. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  161. package/src/resources/extensions/gsd/types.ts +38 -0
  162. package/src/resources/extensions/gsd/verification-evidence.ts +183 -0
  163. package/src/resources/extensions/gsd/verification-gate.ts +567 -0
  164. package/src/resources/extensions/gsd/visualizer-data.ts +25 -3
  165. package/src/resources/extensions/gsd/visualizer-overlay.ts +31 -21
  166. package/src/resources/extensions/gsd/visualizer-views.ts +15 -66
  167. package/src/resources/extensions/search-the-web/tool-search.ts +26 -0
  168. package/src/resources/extensions/shared/format-utils.ts +85 -0
  169. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  170. package/src/resources/extensions/subagent/index.ts +46 -1
  171. package/src/resources/extensions/subagent/isolation.ts +9 -6
@@ -53,6 +53,10 @@ export interface BgProcess {
53
53
  label: string;
54
54
  command: string;
55
55
  cwd: string;
56
+ /** Session file that created this process (used for per-session cleanup) */
57
+ ownerSessionFile: string | null;
58
+ /** Whether this process should survive a new-session boundary */
59
+ persistAcrossSessions: boolean;
56
60
  startedAt: number;
57
61
  proc: import("node:child_process").ChildProcess;
58
62
  /** Unified chronologically-interleaved output buffer */
@@ -103,7 +107,17 @@ export interface BgProcess {
103
107
  /** Restart count */
104
108
  restartCount: number;
105
109
  /** Original start config for restart */
106
- startConfig: { command: string; cwd: string; label: string; processType: ProcessType; readyPattern: string | null; readyPort: number | null; group: string | null };
110
+ startConfig: {
111
+ command: string;
112
+ cwd: string;
113
+ label: string;
114
+ processType: ProcessType;
115
+ ownerSessionFile: string | null;
116
+ persistAcrossSessions: boolean;
117
+ readyPattern: string | null;
118
+ readyPort: number | null;
119
+ group: string | null;
120
+ };
107
121
  }
108
122
 
109
123
  export interface BgProcessInfo {
@@ -111,6 +125,8 @@ export interface BgProcessInfo {
111
125
  label: string;
112
126
  command: string;
113
127
  cwd: string;
128
+ ownerSessionFile: string | null;
129
+ persistAcrossSessions: boolean;
114
130
  startedAt: number;
115
131
  alive: boolean;
116
132
  exitCode: number | null;
@@ -133,6 +149,8 @@ export interface BgProcessInfo {
133
149
  export interface StartOptions {
134
150
  command: string;
135
151
  cwd: string;
152
+ ownerSessionFile?: string | null;
153
+ persistAcrossSessions?: boolean;
136
154
  label?: string;
137
155
  type?: ProcessType;
138
156
  readyPattern?: string;
@@ -154,6 +172,8 @@ export interface ProcessManifest {
154
172
  label: string;
155
173
  command: string;
156
174
  cwd: string;
175
+ ownerSessionFile: string | null;
176
+ persistAcrossSessions: boolean;
157
177
  startedAt: number;
158
178
  processType: ProcessType;
159
179
  group: string | null;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * AutoSession — encapsulates all mutable auto-mode state into a single instance.
3
+ *
4
+ * Replaces ~40 module-level variables scattered across auto.ts with typed
5
+ * properties on a class instance. Benefits:
6
+ *
7
+ * - reset() clears everything in one call (was 25+ manual resets in stopAuto)
8
+ * - toJSON() provides diagnostic snapshots
9
+ * - grep `s.` shows every state access
10
+ * - Constructable for testing
11
+ */
12
+
13
+ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
14
+ import type { GitServiceImpl } from "../git-service.js";
15
+ import type { CaptureEntry } from "../captures.js";
16
+ import type { BudgetAlertLevel } from "../auto-budget.js";
17
+
18
+ // ─── Exported Types ──────────────────────────────────────────────────────────
19
+
20
+ export interface CompletedUnit {
21
+ type: string;
22
+ id: string;
23
+ startedAt: number;
24
+ finishedAt: number;
25
+ }
26
+
27
+ export interface CurrentUnit {
28
+ type: string;
29
+ id: string;
30
+ startedAt: number;
31
+ }
32
+
33
+ export interface UnitRouting {
34
+ tier: string;
35
+ modelDowngraded: boolean;
36
+ }
37
+
38
+ export interface StartModel {
39
+ provider: string;
40
+ id: string;
41
+ }
42
+
43
+ export interface PendingVerificationRetry {
44
+ unitId: string;
45
+ failureContext: string;
46
+ attempt: number;
47
+ }
48
+
49
+ // ─── Constants ───────────────────────────────────────────────────────────────
50
+
51
+ export const MAX_UNIT_DISPATCHES = 3;
52
+ export const STUB_RECOVERY_THRESHOLD = 2;
53
+ export const MAX_LIFETIME_DISPATCHES = 6;
54
+ export const MAX_CONSECUTIVE_SKIPS = 3;
55
+ export const DISPATCH_GAP_TIMEOUT_MS = 5_000;
56
+ export const MAX_SKIP_DEPTH = 20;
57
+
58
+ // ─── AutoSession ─────────────────────────────────────────────────────────────
59
+
60
+ export class AutoSession {
61
+ // ── Lifecycle ────────────────────────────────────────────────────────────
62
+ active = false;
63
+ paused = false;
64
+ stepMode = false;
65
+ verbose = false;
66
+ cmdCtx: ExtensionCommandContext | null = null;
67
+
68
+ // ── Paths ────────────────────────────────────────────────────────────────
69
+ basePath = "";
70
+ originalBasePath = "";
71
+ gitService: GitServiceImpl | null = null;
72
+
73
+ // ── Dispatch counters ────────────────────────────────────────────────────
74
+ readonly unitDispatchCount = new Map<string, number>();
75
+ readonly unitLifetimeDispatches = new Map<string, number>();
76
+ readonly unitRecoveryCount = new Map<string, number>();
77
+ readonly unitConsecutiveSkips = new Map<string, number>();
78
+ readonly completedKeySet = new Set<string>();
79
+
80
+ // ── Timers ───────────────────────────────────────────────────────────────
81
+ unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
82
+ wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
83
+ idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
84
+ continueHereHandle: ReturnType<typeof setInterval> | null = null;
85
+ dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
86
+
87
+ // ── Current unit ─────────────────────────────────────────────────────────
88
+ currentUnit: CurrentUnit | null = null;
89
+ currentUnitRouting: UnitRouting | null = null;
90
+ completedUnits: CompletedUnit[] = [];
91
+ currentMilestoneId: string | null = null;
92
+
93
+ // ── Model state ──────────────────────────────────────────────────────────
94
+ autoModeStartModel: StartModel | null = null;
95
+ originalModelId: string | null = null;
96
+ originalModelProvider: string | null = null;
97
+ lastBudgetAlertLevel: BudgetAlertLevel = 0;
98
+
99
+ // ── Recovery ─────────────────────────────────────────────────────────────
100
+ pendingCrashRecovery: string | null = null;
101
+ pendingVerificationRetry: PendingVerificationRetry | null = null;
102
+ readonly verificationRetryCount = new Map<string, number>();
103
+ pausedSessionFile: string | null = null;
104
+ resourceVersionOnStart: string | null = null;
105
+ lastStateRebuildAt = 0;
106
+
107
+ // ── Guards ───────────────────────────────────────────────────────────────
108
+ handlingAgentEnd = false;
109
+ dispatching = false;
110
+ skipDepth = 0;
111
+ readonly recentlyEvictedKeys = new Set<string>();
112
+
113
+ // ── Metrics ──────────────────────────────────────────────────────────────
114
+ autoStartTime = 0;
115
+ lastPromptCharCount: number | undefined;
116
+ lastBaselineCharCount: number | undefined;
117
+ pendingQuickTasks: CaptureEntry[] = [];
118
+
119
+ // ── Signal handler ───────────────────────────────────────────────────────
120
+ sigtermHandler: (() => void) | null = null;
121
+
122
+ // ── Methods ──────────────────────────────────────────────────────────────
123
+
124
+ clearTimers(): void {
125
+ if (this.unitTimeoutHandle) { clearTimeout(this.unitTimeoutHandle); this.unitTimeoutHandle = null; }
126
+ if (this.wrapupWarningHandle) { clearTimeout(this.wrapupWarningHandle); this.wrapupWarningHandle = null; }
127
+ if (this.idleWatchdogHandle) { clearInterval(this.idleWatchdogHandle); this.idleWatchdogHandle = null; }
128
+ if (this.continueHereHandle) { clearInterval(this.continueHereHandle); this.continueHereHandle = null; }
129
+ if (this.dispatchGapHandle) { clearTimeout(this.dispatchGapHandle); this.dispatchGapHandle = null; }
130
+ }
131
+
132
+ resetDispatchCounters(): void {
133
+ this.unitDispatchCount.clear();
134
+ this.unitLifetimeDispatches.clear();
135
+ this.unitConsecutiveSkips.clear();
136
+ }
137
+
138
+ get lockBasePath(): string {
139
+ return this.originalBasePath || this.basePath;
140
+ }
141
+
142
+ completeCurrentUnit(): CompletedUnit | null {
143
+ if (!this.currentUnit) return null;
144
+ const done: CompletedUnit = { ...this.currentUnit, finishedAt: Date.now() };
145
+ this.completedUnits.push(done);
146
+ this.currentUnit = null;
147
+ return done;
148
+ }
149
+
150
+ reset(): void {
151
+ this.clearTimers();
152
+
153
+ // Lifecycle
154
+ this.active = false;
155
+ this.paused = false;
156
+ this.stepMode = false;
157
+ this.verbose = false;
158
+ this.cmdCtx = null;
159
+
160
+ // Paths
161
+ this.basePath = "";
162
+ this.originalBasePath = "";
163
+ this.gitService = null;
164
+
165
+ // Dispatch
166
+ this.unitDispatchCount.clear();
167
+ this.unitLifetimeDispatches.clear();
168
+ this.unitRecoveryCount.clear();
169
+ this.unitConsecutiveSkips.clear();
170
+ // Note: completedKeySet is intentionally NOT cleared — it persists
171
+ // across restarts to prevent re-dispatching completed units.
172
+
173
+ // Unit
174
+ this.currentUnit = null;
175
+ this.currentUnitRouting = null;
176
+ this.completedUnits = [];
177
+ this.currentMilestoneId = null;
178
+
179
+ // Model
180
+ this.autoModeStartModel = null;
181
+ this.originalModelId = null;
182
+ this.originalModelProvider = null;
183
+ this.lastBudgetAlertLevel = 0;
184
+
185
+ // Recovery
186
+ this.pendingCrashRecovery = null;
187
+ this.pendingVerificationRetry = null;
188
+ this.verificationRetryCount.clear();
189
+ this.pausedSessionFile = null;
190
+ this.resourceVersionOnStart = null;
191
+ this.lastStateRebuildAt = 0;
192
+
193
+ // Guards
194
+ this.handlingAgentEnd = false;
195
+ this.dispatching = false;
196
+ this.skipDepth = 0;
197
+ this.recentlyEvictedKeys.clear();
198
+
199
+ // Metrics
200
+ this.autoStartTime = 0;
201
+ this.lastPromptCharCount = undefined;
202
+ this.lastBaselineCharCount = undefined;
203
+ this.pendingQuickTasks = [];
204
+
205
+ // Signal handler
206
+ this.sigtermHandler = null;
207
+ }
208
+
209
+ toJSON(): Record<string, unknown> {
210
+ return {
211
+ active: this.active,
212
+ paused: this.paused,
213
+ stepMode: this.stepMode,
214
+ basePath: this.basePath,
215
+ currentMilestoneId: this.currentMilestoneId,
216
+ currentUnit: this.currentUnit,
217
+ completedUnits: this.completedUnits.length,
218
+ completedKeySet: this.completedKeySet.size,
219
+ unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
220
+ dispatching: this.dispatching,
221
+ skipDepth: this.skipDepth,
222
+ };
223
+ }
224
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Budget alert level tracking and enforcement for auto-mode.
3
+ * Pure functions — no module state or side effects.
4
+ */
5
+
6
+ import type { BudgetEnforcementMode } from "./types.js";
7
+
8
+ export type BudgetAlertLevel = 0 | 75 | 80 | 90 | 100;
9
+
10
+ export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
11
+ if (budgetPct >= 1.0) return 100;
12
+ if (budgetPct >= 0.90) return 90;
13
+ if (budgetPct >= 0.80) return 80;
14
+ if (budgetPct >= 0.75) return 75;
15
+ return 0;
16
+ }
17
+
18
+ export function getNewBudgetAlertLevel(previousLevel: BudgetAlertLevel, budgetPct: number): BudgetAlertLevel | null {
19
+ const currentLevel = getBudgetAlertLevel(budgetPct);
20
+ if (currentLevel === 0 || currentLevel <= previousLevel) return null;
21
+ return currentLevel;
22
+ }
23
+
24
+ export function getBudgetEnforcementAction(
25
+ enforcement: BudgetEnforcementMode,
26
+ budgetPct: number,
27
+ ): "none" | "warn" | "pause" | "halt" {
28
+ if (budgetPct < 1.0) return "none";
29
+ if (enforcement === "halt") return "halt";
30
+ if (enforcement === "pause") return "pause";
31
+ return "warn";
32
+ }
@@ -6,7 +6,7 @@
6
6
  * or AutoContext dependency. State accessors are passed as callbacks.
7
7
  */
8
8
 
9
- import type { ExtensionContext, ExtensionCommandContext } from "@gsd/pi-coding-agent";
9
+ import type { ExtensionContext, ExtensionCommandContext, SessionMessageEntry } from "@gsd/pi-coding-agent";
10
10
  import type { GSDState } from "./types.js";
11
11
  import { getCurrentBranch } from "./worktree.js";
12
12
  import { getActiveHook } from "./post-unit-hooks.js";
@@ -159,6 +159,49 @@ export function formatWidgetTokens(count: number): string {
159
159
  return `${Math.round(count / 1000000)}M`;
160
160
  }
161
161
 
162
+ // ─── ETA Estimation ──────────────────────────────────────────────────────────
163
+
164
+ /**
165
+ * Estimate remaining time based on average unit duration from the metrics ledger.
166
+ * Returns a formatted string like "~12m remaining" or null if insufficient data.
167
+ */
168
+ export function estimateTimeRemaining(): string | null {
169
+ const ledger = getLedger();
170
+ if (!ledger || ledger.units.length < 2) return null;
171
+
172
+ const sliceProgress = getRoadmapSlicesSync();
173
+ if (!sliceProgress || sliceProgress.total === 0) return null;
174
+
175
+ const remainingSlices = sliceProgress.total - sliceProgress.done;
176
+ if (remainingSlices <= 0) return null;
177
+
178
+ // Compute average duration per completed slice from the ledger
179
+ const completedSliceUnits = ledger.units.filter(
180
+ u => u.finishedAt > 0 && u.startedAt > 0,
181
+ );
182
+ if (completedSliceUnits.length < 2) return null;
183
+
184
+ const totalDuration = completedSliceUnits.reduce(
185
+ (sum, u) => sum + (u.finishedAt - u.startedAt), 0,
186
+ );
187
+ const avgDuration = totalDuration / completedSliceUnits.length;
188
+
189
+ // Rough estimate: remaining slices × average units per slice × avg duration
190
+ const completedSlices = sliceProgress.done || 1;
191
+ const unitsPerSlice = completedSliceUnits.length / completedSlices;
192
+ const estimatedMs = remainingSlices * unitsPerSlice * avgDuration;
193
+
194
+ if (estimatedMs < 5_000) return null; // Too small to display
195
+
196
+ const s = Math.floor(estimatedMs / 1000);
197
+ if (s < 60) return `~${s}s remaining`;
198
+ const m = Math.floor(s / 60);
199
+ if (m < 60) return `~${m}m remaining`;
200
+ const h = Math.floor(m / 60);
201
+ const rm = m % 60;
202
+ return rm > 0 ? `~${h}h ${rm}m remaining` : `~${h}h remaining`;
203
+ }
204
+
162
205
  // ─── Slice Progress Cache ─────────────────────────────────────────────────────
163
206
 
164
207
  /** Cached slice progress for the widget — avoid async in render */
@@ -277,15 +320,16 @@ export function updateProgressWidget(
277
320
  tui.requestRender();
278
321
  }, 800);
279
322
 
280
- // Refresh progress cache from disk every 5s so the widget reflects
323
+ // Refresh progress cache from disk every 15s so the widget reflects
281
324
  // task/slice completion mid-unit. Without this, the progress bar only
282
325
  // updates at dispatch time, appearing frozen during long-running units.
326
+ // 15s (vs 5s) reduces synchronous file I/O on the hot path.
283
327
  const progressRefreshTimer = mid ? setInterval(() => {
284
328
  try {
285
329
  updateSliceProgressCache(accessors.getBasePath(), mid.id, slice?.id);
286
330
  cachedLines = undefined;
287
331
  } catch { /* non-fatal */ }
288
- }, 5_000) : null;
332
+ }, 15_000) : null;
289
333
 
290
334
  return {
291
335
  render(width: number): string[] {
@@ -346,6 +390,12 @@ export function updateProgressWidget(
346
390
  meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
347
391
  }
348
392
 
393
+ // ETA estimate
394
+ const eta = estimateTimeRemaining();
395
+ if (eta) {
396
+ meta += theme.fg("dim", ` · ${eta}`);
397
+ }
398
+
349
399
  lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
350
400
  }
351
401
  }
@@ -370,13 +420,16 @@ export function updateProgressWidget(
370
420
  let totalCacheRead = 0, totalCacheWrite = 0;
371
421
  if (cmdCtx) {
372
422
  for (const entry of cmdCtx.sessionManager.getEntries()) {
373
- if (entry.type === "message" && (entry as any).message?.role === "assistant") {
374
- const u = (entry as any).message.usage;
375
- if (u) {
376
- totalInput += u.input || 0;
377
- totalOutput += u.output || 0;
378
- totalCacheRead += u.cacheRead || 0;
379
- totalCacheWrite += u.cacheWrite || 0;
423
+ if (entry.type === "message") {
424
+ const msgEntry = entry as SessionMessageEntry;
425
+ if (msgEntry.message?.role === "assistant") {
426
+ const u = (msgEntry.message as any).usage;
427
+ if (u) {
428
+ totalInput += u.input || 0;
429
+ totalOutput += u.output || 0;
430
+ totalCacheRead += u.cacheRead || 0;
431
+ totalCacheWrite += u.cacheWrite || 0;
432
+ }
380
433
  }
381
434
  }
382
435
  }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Direct phase dispatch — handles manual /gsd dispatch commands.
3
+ * Resolves phase name → unit type + prompt, creates a session, and sends the message.
4
+ */
5
+
6
+ import type {
7
+ ExtensionAPI,
8
+ ExtensionCommandContext,
9
+ } from "@gsd/pi-coding-agent";
10
+
11
+ import { deriveState } from "./state.js";
12
+ import { loadFile, parseRoadmap } from "./files.js";
13
+ import {
14
+ resolveMilestoneFile, resolveSliceFile, relSliceFile,
15
+ } from "./paths.js";
16
+ import {
17
+ buildResearchSlicePrompt,
18
+ buildResearchMilestonePrompt,
19
+ buildPlanSlicePrompt,
20
+ buildPlanMilestonePrompt,
21
+ buildExecuteTaskPrompt,
22
+ buildCompleteSlicePrompt,
23
+ buildCompleteMilestonePrompt,
24
+ buildReassessRoadmapPrompt,
25
+ buildRunUatPrompt,
26
+ buildReplanSlicePrompt,
27
+ } from "./auto-prompts.js";
28
+ import { loadEffectiveGSDPreferences } from "./preferences.js";
29
+ import { pauseAuto } from "./auto.js";
30
+
31
+ export async function dispatchDirectPhase(
32
+ ctx: ExtensionCommandContext,
33
+ pi: ExtensionAPI,
34
+ phase: string,
35
+ base: string,
36
+ ): Promise<void> {
37
+ const state = await deriveState(base);
38
+ const mid = state.activeMilestone?.id;
39
+ const midTitle = state.activeMilestone?.title ?? "";
40
+
41
+ if (!mid) {
42
+ ctx.ui.notify("Cannot dispatch: no active milestone.", "warning");
43
+ return;
44
+ }
45
+
46
+ const normalized = phase.toLowerCase();
47
+ let unitType: string;
48
+ let unitId: string;
49
+ let prompt: string;
50
+
51
+ switch (normalized) {
52
+ case "research":
53
+ case "research-milestone":
54
+ case "research-slice": {
55
+ const isSlice = normalized === "research-slice" || (normalized === "research" && state.phase !== "pre-planning");
56
+ if (isSlice) {
57
+ const sid = state.activeSlice?.id;
58
+ const sTitle = state.activeSlice?.title ?? "";
59
+ if (!sid) {
60
+ ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
61
+ return;
62
+ }
63
+
64
+ // When require_slice_discussion is enabled, pause auto-mode before
65
+ // each new slice so the user can discuss requirements first (#789).
66
+ const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
67
+ const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
68
+ if (requireDiscussion && !sliceContextFile) {
69
+ ctx.ui.notify(
70
+ `Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
71
+ "info",
72
+ );
73
+ await pauseAuto(ctx, pi);
74
+ return;
75
+ }
76
+
77
+ unitType = "research-slice";
78
+ unitId = `${mid}/${sid}`;
79
+ prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
80
+ } else {
81
+ unitType = "research-milestone";
82
+ unitId = mid;
83
+ prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
84
+ }
85
+ break;
86
+ }
87
+
88
+ case "plan":
89
+ case "plan-milestone":
90
+ case "plan-slice": {
91
+ const isSlice = normalized === "plan-slice" || (normalized === "plan" && state.phase !== "pre-planning");
92
+ if (isSlice) {
93
+ const sid = state.activeSlice?.id;
94
+ const sTitle = state.activeSlice?.title ?? "";
95
+ if (!sid) {
96
+ ctx.ui.notify("Cannot dispatch plan-slice: no active slice.", "warning");
97
+ return;
98
+ }
99
+ unitType = "plan-slice";
100
+ unitId = `${mid}/${sid}`;
101
+ prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base);
102
+ } else {
103
+ unitType = "plan-milestone";
104
+ unitId = mid;
105
+ prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
106
+ }
107
+ break;
108
+ }
109
+
110
+ case "execute":
111
+ case "execute-task": {
112
+ const sid = state.activeSlice?.id;
113
+ const sTitle = state.activeSlice?.title ?? "";
114
+ const tid = state.activeTask?.id;
115
+ const tTitle = state.activeTask?.title ?? "";
116
+ if (!sid) {
117
+ ctx.ui.notify("Cannot dispatch execute-task: no active slice.", "warning");
118
+ return;
119
+ }
120
+ if (!tid) {
121
+ ctx.ui.notify("Cannot dispatch execute-task: no active task.", "warning");
122
+ return;
123
+ }
124
+ unitType = "execute-task";
125
+ unitId = `${mid}/${sid}/${tid}`;
126
+ prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
127
+ break;
128
+ }
129
+
130
+ case "complete":
131
+ case "complete-slice":
132
+ case "complete-milestone": {
133
+ const isSlice = normalized === "complete-slice" || (normalized === "complete" && state.phase === "summarizing");
134
+ if (isSlice) {
135
+ const sid = state.activeSlice?.id;
136
+ const sTitle = state.activeSlice?.title ?? "";
137
+ if (!sid) {
138
+ ctx.ui.notify("Cannot dispatch complete-slice: no active slice.", "warning");
139
+ return;
140
+ }
141
+ unitType = "complete-slice";
142
+ unitId = `${mid}/${sid}`;
143
+ prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
144
+ } else {
145
+ unitType = "complete-milestone";
146
+ unitId = mid;
147
+ prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
148
+ }
149
+ break;
150
+ }
151
+
152
+ case "reassess":
153
+ case "reassess-roadmap": {
154
+ const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
155
+ const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
156
+ if (!roadmapContent) {
157
+ ctx.ui.notify("Cannot dispatch reassess-roadmap: no roadmap found.", "warning");
158
+ return;
159
+ }
160
+ const roadmap = parseRoadmap(roadmapContent);
161
+ const completedSlices = roadmap.slices.filter(s => s.done);
162
+ if (completedSlices.length === 0) {
163
+ ctx.ui.notify("Cannot dispatch reassess-roadmap: no completed slices.", "warning");
164
+ return;
165
+ }
166
+ const completedSliceId = completedSlices[completedSlices.length - 1].id;
167
+ unitType = "reassess-roadmap";
168
+ unitId = `${mid}/${completedSliceId}`;
169
+ prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
170
+ break;
171
+ }
172
+
173
+ case "uat":
174
+ case "run-uat": {
175
+ const sid = state.activeSlice?.id;
176
+ if (!sid) {
177
+ ctx.ui.notify("Cannot dispatch run-uat: no active slice.", "warning");
178
+ return;
179
+ }
180
+ const uatFile = resolveSliceFile(base, mid, sid, "UAT");
181
+ if (!uatFile) {
182
+ ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
183
+ return;
184
+ }
185
+ const uatContent = await loadFile(uatFile);
186
+ if (!uatContent) {
187
+ ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
188
+ return;
189
+ }
190
+ const uatPath = relSliceFile(base, mid, sid, "UAT");
191
+ unitType = "run-uat";
192
+ unitId = `${mid}/${sid}`;
193
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
194
+ break;
195
+ }
196
+
197
+ case "replan":
198
+ case "replan-slice": {
199
+ const sid = state.activeSlice?.id;
200
+ const sTitle = state.activeSlice?.title ?? "";
201
+ if (!sid) {
202
+ ctx.ui.notify("Cannot dispatch replan-slice: no active slice.", "warning");
203
+ return;
204
+ }
205
+ unitType = "replan-slice";
206
+ unitId = `${mid}/${sid}`;
207
+ prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
208
+ break;
209
+ }
210
+
211
+ default:
212
+ ctx.ui.notify(
213
+ `Unknown phase "${phase}". Valid phases: research, plan, execute, complete, reassess, uat, replan.`,
214
+ "warning",
215
+ );
216
+ return;
217
+ }
218
+
219
+ ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
220
+ const result = await ctx.newSession();
221
+ if (result.cancelled) {
222
+ ctx.ui.notify("Session creation cancelled.", "warning");
223
+ return;
224
+ }
225
+ pi.sendMessage(
226
+ { customType: "gsd-dispatch", content: prompt, display: false },
227
+ { triggerTurn: true },
228
+ );
229
+ }