gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "2.33.1",
3
+ "version": "2.34.0",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "piConfig": {
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.33.1",
3
+ "version": "2.34.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -52,16 +52,28 @@ export interface PendingVerificationRetry {
52
52
  attempt: number;
53
53
  }
54
54
 
55
+ /**
56
+ * A typed item enqueued by postUnitPostVerification for the main loop to
57
+ * drain via the standard runUnit path. Replaces inline dispatch
58
+ * (pi.sendMessage / s.cmdCtx.newSession()) for hooks, triage, and quick-tasks.
59
+ */
60
+ export interface SidecarItem {
61
+ kind: "hook" | "triage" | "quick-task";
62
+ unitType: string;
63
+ unitId: string;
64
+ prompt: string;
65
+ /** Model override for hook units (e.g. "anthropic/claude-3-5-sonnet"). */
66
+ model?: string;
67
+ /** Capture ID for quick-task items (already marked executed at enqueue time). */
68
+ captureId?: string;
69
+ }
70
+
55
71
  // ─── Constants ───────────────────────────────────────────────────────────────
56
72
 
57
73
  export const MAX_UNIT_DISPATCHES = 3;
58
74
  export const STUB_RECOVERY_THRESHOLD = 2;
59
75
  export const MAX_LIFETIME_DISPATCHES = 6;
60
- export const MAX_CONSECUTIVE_SKIPS = 3;
61
- export const DISPATCH_GAP_TIMEOUT_MS = 5_000;
62
- export const MAX_SKIP_DEPTH = 20;
63
76
  export const NEW_SESSION_TIMEOUT_MS = 30_000;
64
- export const DISPATCH_HANG_TIMEOUT_MS = 60_000;
65
77
 
66
78
  // ─── AutoSession ─────────────────────────────────────────────────────────────
67
79
 
@@ -69,7 +81,6 @@ export class AutoSession {
69
81
  // ── Lifecycle ────────────────────────────────────────────────────────────
70
82
  active = false;
71
83
  paused = false;
72
- pausedForSecrets = false;
73
84
  stepMode = false;
74
85
  verbose = false;
75
86
  cmdCtx: ExtensionCommandContext | null = null;
@@ -83,15 +94,12 @@ export class AutoSession {
83
94
  readonly unitDispatchCount = new Map<string, number>();
84
95
  readonly unitLifetimeDispatches = new Map<string, number>();
85
96
  readonly unitRecoveryCount = new Map<string, number>();
86
- readonly unitConsecutiveSkips = new Map<string, number>();
87
- readonly completedKeySet = new Set<string>();
88
97
 
89
98
  // ── Timers ───────────────────────────────────────────────────────────────
90
99
  unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
91
100
  wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
92
101
  idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
93
102
  continueHereHandle: ReturnType<typeof setInterval> | null = null;
94
- dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
95
103
 
96
104
  // ── Current unit ─────────────────────────────────────────────────────────
97
105
  currentUnit: CurrentUnit | null = null;
@@ -113,12 +121,8 @@ export class AutoSession {
113
121
  resourceVersionOnStart: string | null = null;
114
122
  lastStateRebuildAt = 0;
115
123
 
116
- // ── Guards ───────────────────────────────────────────────────────────────
117
- handlingAgentEnd = false;
118
- pendingAgentEndRetry = false;
119
- dispatching = false;
120
- skipDepth = 0;
121
- readonly recentlyEvictedKeys = new Set<string>();
124
+ // ── Sidecar queue ─────────────────────────────────────────────────────
125
+ sidecarQueue: SidecarItem[] = [];
122
126
 
123
127
  // ── Metrics ──────────────────────────────────────────────────────────────
124
128
  autoStartTime = 0;
@@ -129,6 +133,29 @@ export class AutoSession {
129
133
  // ── Signal handler ───────────────────────────────────────────────────────
130
134
  sigtermHandler: (() => void) | null = null;
131
135
 
136
+ // ── Loop promise state ──────────────────────────────────────────────────
137
+ /**
138
+ * True only while runUnit is rotating into a fresh session. agent_end events
139
+ * emitted from the previous session's abort during this window must be
140
+ * ignored; they do not belong to the new unit.
141
+ */
142
+ sessionSwitchInFlight = false;
143
+
144
+ /**
145
+ * One-shot resolver for the current unit's agent_end promise.
146
+ * Non-null only while a unit is in-flight (between sendMessage and agent_end).
147
+ * Scoped to the session to prevent concurrent session corruption.
148
+ */
149
+ pendingResolve: ((result: { status: "completed" | "cancelled" | "error"; event?: { messages: unknown[] } }) => void) | null = null;
150
+
151
+ /**
152
+ * Queue for agent_end events that arrive when no pendingResolve exists.
153
+ * This happens when error-recovery sendMessage retries produce agent_end
154
+ * events between loop iterations. The next runUnit drains this queue
155
+ * instead of waiting for a new event.
156
+ */
157
+ pendingAgentEndQueue: Array<{ messages: unknown[] }> = [];
158
+
132
159
  // ── Methods ──────────────────────────────────────────────────────────────
133
160
 
134
161
  clearTimers(): void {
@@ -136,13 +163,11 @@ export class AutoSession {
136
163
  if (this.wrapupWarningHandle) { clearTimeout(this.wrapupWarningHandle); this.wrapupWarningHandle = null; }
137
164
  if (this.idleWatchdogHandle) { clearInterval(this.idleWatchdogHandle); this.idleWatchdogHandle = null; }
138
165
  if (this.continueHereHandle) { clearInterval(this.continueHereHandle); this.continueHereHandle = null; }
139
- if (this.dispatchGapHandle) { clearTimeout(this.dispatchGapHandle); this.dispatchGapHandle = null; }
140
166
  }
141
167
 
142
168
  resetDispatchCounters(): void {
143
169
  this.unitDispatchCount.clear();
144
170
  this.unitLifetimeDispatches.clear();
145
- this.unitConsecutiveSkips.clear();
146
171
  }
147
172
 
148
173
  get lockBasePath(): string {
@@ -163,7 +188,6 @@ export class AutoSession {
163
188
  // Lifecycle
164
189
  this.active = false;
165
190
  this.paused = false;
166
- this.pausedForSecrets = false;
167
191
  this.stepMode = false;
168
192
  this.verbose = false;
169
193
  this.cmdCtx = null;
@@ -177,9 +201,6 @@ export class AutoSession {
177
201
  this.unitDispatchCount.clear();
178
202
  this.unitLifetimeDispatches.clear();
179
203
  this.unitRecoveryCount.clear();
180
- this.unitConsecutiveSkips.clear();
181
- // Note: completedKeySet is intentionally NOT cleared — it persists
182
- // across restarts to prevent re-dispatching completed units.
183
204
 
184
205
  // Unit
185
206
  this.currentUnit = null;
@@ -201,21 +222,20 @@ export class AutoSession {
201
222
  this.resourceVersionOnStart = null;
202
223
  this.lastStateRebuildAt = 0;
203
224
 
204
- // Guards
205
- this.handlingAgentEnd = false;
206
- this.pendingAgentEndRetry = false;
207
- this.dispatching = false;
208
- this.skipDepth = 0;
209
- this.recentlyEvictedKeys.clear();
210
-
211
225
  // Metrics
212
226
  this.autoStartTime = 0;
213
227
  this.lastPromptCharCount = undefined;
214
228
  this.lastBaselineCharCount = undefined;
215
229
  this.pendingQuickTasks = [];
230
+ this.sidecarQueue = [];
216
231
 
217
232
  // Signal handler
218
233
  this.sigtermHandler = null;
234
+
235
+ // Loop promise state
236
+ this.sessionSwitchInFlight = false;
237
+ this.pendingResolve = null;
238
+ this.pendingAgentEndQueue = [];
219
239
  }
220
240
 
221
241
  toJSON(): Record<string, unknown> {
@@ -227,10 +247,7 @@ export class AutoSession {
227
247
  currentMilestoneId: this.currentMilestoneId,
228
248
  currentUnit: this.currentUnit,
229
249
  completedUnits: this.completedUnits.length,
230
- completedKeySet: this.completedKeySet.size,
231
250
  unitDispatchCount: Object.fromEntries(this.unitDispatchCount),
232
- dispatching: this.dispatching,
233
- skipDepth: this.skipDepth,
234
251
  };
235
252
  }
236
253
  }
@@ -11,7 +11,6 @@ import type { GSDState } from "./types.js";
11
11
  import { getCurrentBranch } from "./worktree.js";
12
12
  import { getActiveHook } from "./post-unit-hooks.js";
13
13
  import { getLedger, getProjectTotals, formatCost, formatTokenCount, formatTierSavings } from "./metrics.js";
14
- import { getHealthTrend, getConsecutiveErrorUnits } from "./doctor-proactive.js";
15
14
  import {
16
15
  resolveMilestoneFile,
17
16
  resolveSliceFile,
@@ -20,7 +19,6 @@ import { parseRoadmap, parsePlan } from "./files.js";
20
19
  import { readFileSync, existsSync } from "node:fs";
21
20
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
22
21
  import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
23
- import { parseUnitId } from "./unit-id.js";
24
22
 
25
23
  // ─── Dashboard Data ───────────────────────────────────────────────────────────
26
24
 
@@ -49,34 +47,40 @@ export interface AutoDashboardData {
49
47
 
50
48
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
51
49
 
52
- /** Canonical verb and phase label for each known unit type. */
53
- const UNIT_TYPE_INFO: Record<string, { verb: string; phaseLabel: string }> = {
54
- "research-milestone": { verb: "researching", phaseLabel: "RESEARCH" },
55
- "research-slice": { verb: "researching", phaseLabel: "RESEARCH" },
56
- "plan-milestone": { verb: "planning", phaseLabel: "PLAN" },
57
- "plan-slice": { verb: "planning", phaseLabel: "PLAN" },
58
- "execute-task": { verb: "executing", phaseLabel: "EXECUTE" },
59
- "complete-slice": { verb: "completing", phaseLabel: "COMPLETE" },
60
- "replan-slice": { verb: "replanning", phaseLabel: "REPLAN" },
61
- "rewrite-docs": { verb: "rewriting", phaseLabel: "REWRITE" },
62
- "reassess-roadmap": { verb: "reassessing", phaseLabel: "REASSESS" },
63
- "run-uat": { verb: "running UAT", phaseLabel: "UAT" },
64
- };
65
-
66
50
  export function unitVerb(unitType: string): string {
67
51
  if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
68
- return UNIT_TYPE_INFO[unitType]?.verb ?? unitType;
52
+ switch (unitType) {
53
+ case "research-milestone":
54
+ case "research-slice": return "researching";
55
+ case "plan-milestone":
56
+ case "plan-slice": return "planning";
57
+ case "execute-task": return "executing";
58
+ case "complete-slice": return "completing";
59
+ case "replan-slice": return "replanning";
60
+ case "rewrite-docs": return "rewriting";
61
+ case "reassess-roadmap": return "reassessing";
62
+ case "run-uat": return "running UAT";
63
+ default: return unitType;
64
+ }
69
65
  }
70
66
 
71
67
  export function unitPhaseLabel(unitType: string): string {
72
68
  if (unitType.startsWith("hook/")) return "HOOK";
73
- return UNIT_TYPE_INFO[unitType]?.phaseLabel ?? unitType.toUpperCase();
69
+ switch (unitType) {
70
+ case "research-milestone": return "RESEARCH";
71
+ case "research-slice": return "RESEARCH";
72
+ case "plan-milestone": return "PLAN";
73
+ case "plan-slice": return "PLAN";
74
+ case "execute-task": return "EXECUTE";
75
+ case "complete-slice": return "COMPLETE";
76
+ case "replan-slice": return "REPLAN";
77
+ case "rewrite-docs": return "REWRITE";
78
+ case "reassess-roadmap": return "REASSESS";
79
+ case "run-uat": return "UAT";
80
+ default: return unitType.toUpperCase();
81
+ }
74
82
  }
75
83
 
76
- /**
77
- * Describe the expected next step after the current unit completes.
78
- * Unit types here mirror the keys in UNIT_TYPE_INFO above.
79
- */
80
84
  function peekNext(unitType: string, state: GSDState): string {
81
85
  // Show active hook info in progress display
82
86
  const activeHookState = getActiveHook();
@@ -305,16 +309,6 @@ export function updateProgressWidget(
305
309
  }
306
310
  if (cachedBranch) widgetPwd = `${widgetPwd} (${cachedBranch})`;
307
311
 
308
- // Set a string-array fallback first — this is the only version RPC mode will
309
- // see, since the factory widget set below is not supported in RPC mode.
310
- const progressText = buildProgressTextLines(
311
- verb, phaseLabel, unitId, mid, slice, task, next,
312
- accessors, tierBadge, widgetPwd,
313
- );
314
- ctx.ui.setWidget("gsd-progress", progressText);
315
-
316
- // Set the factory-based widget — in TUI mode this replaces the string-array
317
- // version with a dynamic, animated widget. In RPC mode this call is a no-op.
318
312
  ctx.ui.setWidget("gsd-progress", (tui, theme) => {
319
313
  let pulseBright = true;
320
314
  let cachedLines: string[] | undefined;
@@ -372,11 +366,7 @@ export function updateProgressWidget(
372
366
 
373
367
  lines.push("");
374
368
 
375
- const isHook = unitType.startsWith("hook/");
376
- const hookParsed = isHook ? parseUnitId(unitId) : undefined;
377
- const target = isHook
378
- ? (hookParsed!.task ?? hookParsed!.slice ?? unitId)
379
- : (task ? `${task.id}: ${task.title}` : unitId);
369
+ const target = task ? `${task.id}: ${task.title}` : unitId;
380
370
  const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
381
371
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
382
372
  const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
@@ -396,10 +386,7 @@ export function updateProgressWidget(
396
386
  let meta = theme.fg("dim", `${done}/${total} slices`);
397
387
 
398
388
  if (activeSliceTasks && activeSliceTasks.total > 0) {
399
- // For hooks, show the trigger task number (done), not the next task (done + 1)
400
- const taskNum = isHook
401
- ? Math.max(activeSliceTasks.done, 1)
402
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
389
+ const taskNum = Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
403
390
  meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
404
391
  }
405
392
 
@@ -467,7 +454,6 @@ export function updateProgressWidget(
467
454
  sp.push(`\u26A1${hitRate}%`);
468
455
  }
469
456
  if (cumulativeCost) sp.push(`$${cumulativeCost.toFixed(3)}`);
470
- else if (autoTotals?.apiRequests) sp.push(`${autoTotals.apiRequests} reqs`);
471
457
 
472
458
  const cxDisplay = cxPct === "?"
473
459
  ? `?/${formatWidgetTokens(cxWindow)}`
@@ -526,95 +512,6 @@ export function updateProgressWidget(
526
512
  });
527
513
  }
528
514
 
529
- // ─── Text Fallback for RPC Mode ───────────────────────────────────────────
530
-
531
- /**
532
- * Build a compact string-array representation of the progress widget.
533
- * Used as a fallback when the factory-based widget cannot render (RPC mode).
534
- */
535
- // ─── Model Health Indicator ───────────────────────────────────────────────────
536
-
537
- /**
538
- * Compute a traffic-light health indicator from observable signals.
539
- * 🟢 progressing well — no errors, trend stable/improving
540
- * 🟡 struggling — some errors or degrading trend
541
- * 🔴 stuck — consecutive errors, likely needs attention
542
- */
543
- export function getModelHealthIndicator(): { emoji: string; label: string } {
544
- const trend = getHealthTrend();
545
- const consecutiveErrors = getConsecutiveErrorUnits();
546
-
547
- if (consecutiveErrors >= 3) {
548
- return { emoji: "🔴", label: "stuck" };
549
- }
550
- if (consecutiveErrors >= 1 || trend === "degrading") {
551
- return { emoji: "🟡", label: "struggling" };
552
- }
553
- if (trend === "improving") {
554
- return { emoji: "🟢", label: "progressing well" };
555
- }
556
- // stable or unknown
557
- return { emoji: "🟢", label: "progressing" };
558
- }
559
-
560
- function buildProgressTextLines(
561
- verb: string,
562
- phaseLabel: string,
563
- unitId: string,
564
- mid: { id: string; title: string } | null,
565
- slice: { id: string; title: string } | null,
566
- task: { id: string; title: string } | null,
567
- next: string,
568
- accessors: WidgetStateAccessors,
569
- tierBadge: string | undefined,
570
- widgetPwd: string,
571
- ): string[] {
572
- const mode = accessors.isStepMode() ? "step" : "auto";
573
- const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
574
- const tierStr = tierBadge ? ` [${tierBadge}]` : "";
575
-
576
- const lines: string[] = [];
577
- lines.push(`[GSD ${mode}] ${verb} ${unitId}${tierStr}${elapsed ? ` — ${elapsed}` : ""}`);
578
-
579
- if (mid) lines.push(` Milestone: ${mid.id} — ${mid.title}`);
580
- if (slice) lines.push(` Slice: ${slice.id} — ${slice.title}`);
581
- if (task) lines.push(` Task: ${task.id} — ${task.title}`);
582
-
583
- // Progress bar
584
- const sp = cachedSliceProgress;
585
- if (sp && sp.total > 0) {
586
- const pct = Math.round((sp.done / sp.total) * 100);
587
- const taskInfo = sp.activeSliceTasks
588
- ? ` (tasks: ${sp.activeSliceTasks.done}/${sp.activeSliceTasks.total})`
589
- : "";
590
- lines.push(` Progress: ${sp.done}/${sp.total} slices (${pct}%)${taskInfo}`);
591
- }
592
-
593
- // Cost / tokens
594
- const ledger = getLedger();
595
- const totals = ledger ? getProjectTotals(ledger.units) : null;
596
- if (totals) {
597
- const parts: string[] = [];
598
- if (totals.tokens.input || totals.tokens.output) {
599
- parts.push(`tokens: ${formatWidgetTokens(totals.tokens.input)}↑ ${formatWidgetTokens(totals.tokens.output)}↓`);
600
- }
601
- if (totals.cost > 0) {
602
- parts.push(`cost: ${formatCost(totals.cost)}`);
603
- }
604
- if (parts.length > 0) lines.push(` ${parts.join(" — ")}`);
605
- }
606
-
607
- if (next) lines.push(` Next: ${next}`);
608
-
609
- // Model health indicator
610
- const health = getModelHealthIndicator();
611
- lines.push(` Health: ${health.emoji} ${health.label}`);
612
-
613
- lines.push(` ${widgetPwd}`);
614
-
615
- return lines;
616
- }
617
-
618
515
  // ─── Right-align Helper ───────────────────────────────────────────────────────
619
516
 
620
517
  /** Right-align helper: build a line with left content and right content. */
@@ -182,10 +182,15 @@ export async function dispatchDirectPhase(
182
182
  ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
183
183
  return;
184
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
+ }
185
190
  const uatPath = relSliceFile(base, mid, sid, "UAT");
186
191
  unitType = "run-uat";
187
192
  unitId = `${mid}/${sid}`;
188
- prompt = await buildRunUatPrompt(mid, sid, uatPath, base);
193
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
189
194
  break;
190
195
  }
191
196