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
@@ -12,7 +12,6 @@ import {
12
12
  formatValidationIssues,
13
13
  } from "./observability-validator.js";
14
14
  import type { ValidationIssue } from "./observability-validator.js";
15
- import { parseUnitId } from "./unit-id.js";
16
15
 
17
16
  export async function collectObservabilityWarnings(
18
17
  ctx: ExtensionContext,
@@ -23,7 +22,10 @@ export async function collectObservabilityWarnings(
23
22
  // Hook units have custom artifacts — skip standard observability checks
24
23
  if (unitType.startsWith("hook/")) return [];
25
24
 
26
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
25
+ const parts = unitId.split("/");
26
+ const mid = parts[0];
27
+ const sid = parts[1];
28
+ const tid = parts[2];
27
29
 
28
30
  if (!mid || !sid) return [];
29
31
 
@@ -11,7 +11,7 @@
11
11
  * Extracted from handleAgentEnd() in auto.ts.
12
12
  */
13
13
 
14
- import type { ExtensionContext, ExtensionCommandContext, ExtensionAPI } from "@gsd/pi-coding-agent";
14
+ import type { ExtensionContext, ExtensionAPI } from "@gsd/pi-coding-agent";
15
15
  import { deriveState } from "./state.js";
16
16
  import { loadFile, parseSummary, resolveAllOverrides } from "./files.js";
17
17
  import { loadPrompt } from "./prompt-loader.js";
@@ -19,7 +19,6 @@ import {
19
19
  resolveSliceFile,
20
20
  resolveTaskFile,
21
21
  resolveMilestoneFile,
22
- gsdRoot,
23
22
  } from "./paths.js";
24
23
  import { invalidateAllCaches } from "./cache.js";
25
24
  import { closeoutUnit, type CloseoutOptions } from "./auto-unit-closeout.js";
@@ -29,30 +28,23 @@ import {
29
28
  } from "./worktree.js";
30
29
  import {
31
30
  verifyExpectedArtifact,
32
- persistCompletedKey,
33
- removePersistedKey,
34
31
  } from "./auto-recovery.js";
35
32
  import { writeUnitRuntimeRecord, clearUnitRuntimeRecord } from "./unit-runtime.js";
36
- import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences } from "./preferences.js";
37
33
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
38
- import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
39
34
  import { recordHealthSnapshot, checkHealEscalation } from "./doctor-proactive.js";
35
+ import { syncStateToProjectRoot } from "./auto-worktree-sync.js";
40
36
  import { resetRewriteCircuitBreaker } from "./auto-dispatch.js";
41
37
  import { isDbAvailable } from "./gsd-db.js";
42
38
  import { consumeSignal } from "./session-status-io.js";
43
39
  import {
44
40
  checkPostUnitHooks,
45
- getActiveHook,
46
- resetHookState,
47
41
  isRetryPending,
48
42
  consumeRetryTrigger,
49
43
  persistHookState,
50
44
  } from "./post-unit-hooks.js";
51
- import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
52
- import { writeLock } from "./crash-recovery.js";
45
+ import { hasPendingCaptures, loadPendingCaptures } from "./captures.js";
53
46
  import { debugLog } from "./debug-logger.js";
54
47
  import type { AutoSession } from "./auto/session.js";
55
- import type { WidgetStateAccessors, AutoDashboardData } from "./auto-dashboard.js";
56
48
  import {
57
49
  updateProgressWidget as _updateProgressWidget,
58
50
  updateSliceProgressCache,
@@ -60,32 +52,9 @@ import {
60
52
  hideFooter,
61
53
  } from "./auto-dashboard.js";
62
54
  import { join } from "node:path";
63
- import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
64
- import { parseUnitId } from "./unit-id.js";
65
55
 
66
- /**
67
- * Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
68
- * and persist the initial runtime record. Returns `startedAt` for callers
69
- * that need the timestamp.
70
- */
71
- function dispatchUnit(
72
- s: AutoSession,
73
- basePath: string,
74
- unitType: string,
75
- unitId: string,
76
- ): number {
77
- const startedAt = Date.now();
78
- s.currentUnit = { type: unitType, id: unitId, startedAt };
79
- writeUnitRuntimeRecord(basePath, unitType, unitId, startedAt, {
80
- phase: "dispatched",
81
- wrapupWarningSent: false,
82
- timeoutAt: null,
83
- lastProgressAt: startedAt,
84
- progressCount: 0,
85
- lastProgressKind: "dispatch",
86
- });
87
- return startedAt;
88
- }
56
+ /** Throttle STATE.md rebuilds — at most once per 30 seconds */
57
+ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
89
58
 
90
59
  export interface PostUnitContext {
91
60
  s: AutoSession;
@@ -135,7 +104,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
135
104
  let taskContext: TaskCommitContext | undefined;
136
105
 
137
106
  if (s.currentUnit.type === "execute-task") {
138
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
107
+ const parts = s.currentUnit.id.split("/");
108
+ const [mid, sid, tid] = parts;
139
109
  if (mid && sid && tid) {
140
110
  const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY");
141
111
  if (summaryPath) {
@@ -167,8 +137,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
167
137
 
168
138
  // Doctor: fix mechanical bookkeeping
169
139
  try {
170
- const { milestone, slice } = parseUnitId(s.currentUnit.id);
171
- const doctorScope = slice ? `${milestone}/${slice}` : milestone;
140
+ const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
141
+ const doctorScope = scopeParts.join("/");
172
142
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
173
143
  const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" as const : "task" as const;
174
144
  const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
@@ -176,17 +146,13 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
176
146
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
177
147
  }
178
148
 
179
- // Proactive health tracking — exclude completion-transition codes at task level
180
- // since they are expected after the last task and resolved by complete-slice
181
- const issuesForHealth = effectiveFixLevel === "task"
182
- ? report.issues.filter(i => !COMPLETION_TRANSITION_CODES.has(i.code))
183
- : report.issues;
184
- const summary = summarizeDoctorIssues(issuesForHealth);
149
+ // Proactive health tracking
150
+ const summary = summarizeDoctorIssues(report.issues);
185
151
  recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
186
152
 
187
153
  // Check if we should escalate to LLM-assisted heal
188
154
  if (summary.errors > 0) {
189
- const unresolvedErrors = issuesForHealth
155
+ const unresolvedErrors = report.issues
190
156
  .filter(i => i.severity === "error" && !i.fixable)
191
157
  .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
192
158
  const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
@@ -223,17 +189,23 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
223
189
  }
224
190
  }
225
191
 
226
- // Prune dead bg-shell processes and kill non-persistent live ones.
227
- // Without killing live processes between units, dev servers spawned during
228
- // one task keep ports bound, causing conflicts in subsequent tasks (#1209).
192
+ // Prune dead bg-shell processes
229
193
  try {
230
- const { pruneDeadProcesses, killSessionProcesses } = await import("../bg-shell/process-manager.js");
194
+ const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
231
195
  pruneDeadProcesses();
232
- killSessionProcesses();
233
196
  } catch {
234
197
  // Non-fatal
235
198
  }
236
199
 
200
+ // Sync worktree state back to project root
201
+ if (s.originalBasePath && s.originalBasePath !== s.basePath) {
202
+ try {
203
+ syncStateToProjectRoot(s.basePath, s.originalBasePath, s.currentMilestoneId);
204
+ } catch {
205
+ // Non-fatal
206
+ }
207
+ }
208
+
237
209
  // Rewrite-docs completion
238
210
  if (s.currentUnit.type === "rewrite-docs") {
239
211
  try {
@@ -286,17 +258,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
286
258
  }
287
259
  }
288
260
 
289
- // Artifact verification and completion persistence
261
+ // Artifact verification
290
262
  let triggerArtifactVerified = false;
291
263
  if (!s.currentUnit.type.startsWith("hook/")) {
292
264
  try {
293
265
  triggerArtifactVerified = verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
294
266
  if (triggerArtifactVerified) {
295
- const completionKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
296
- if (!s.completedKeySet.has(completionKey)) {
297
- persistCompletedKey(s.basePath, completionKey);
298
- s.completedKeySet.add(completionKey);
299
- }
300
267
  invalidateAllCaches();
301
268
  }
302
269
  } catch {
@@ -324,13 +291,15 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
324
291
  * Post-verification processing: DB dual-write, post-unit hooks, triage
325
292
  * capture dispatch, quick-task dispatch.
326
293
  *
294
+ * Sidecar work (hooks, triage, quick-tasks) is enqueued on `s.sidecarQueue`
295
+ * for the main loop to drain via `runUnit()`.
296
+ *
327
297
  * Returns:
328
- * - "dispatched" — a hook/triage/quick-task was dispatched (sendMessage sent)
329
- * - "continue" — proceed to normal dispatchNextUnit
298
+ * - "continue" — proceed to sidecar drain / normal dispatch
330
299
  * - "step-wizard" — step mode, show wizard instead
331
300
  * - "stopped" — stopAuto was called
332
301
  */
333
- export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"dispatched" | "continue" | "step-wizard" | "stopped"> {
302
+ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"continue" | "step-wizard" | "stopped"> {
334
303
  const { s, ctx, pi, buildSnapshotOpts, lockBase, stopAuto, pauseAuto, updateProgressWidget } = pctx;
335
304
 
336
305
  // ── DB dual-write ──
@@ -343,45 +312,6 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
343
312
  }
344
313
  }
345
314
 
346
- // ── Mechanical completion (ADR-003) ──
347
- // After task execution, attempt mechanical slice and milestone completion
348
- // instead of dispatching LLM sessions for complete-slice / validate-milestone.
349
- if (s.currentUnit?.type === "execute-task" && !s.stepMode) {
350
- try {
351
- const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id);
352
- if (mid && sid) {
353
- const state = await deriveState(s.basePath);
354
- if (state.phase === "summarizing" && state.activeSlice?.id === sid) {
355
- const { mechanicalSliceCompletion } = await import("./mechanical-completion.js");
356
- const ok = await mechanicalSliceCompletion(s.basePath, mid, sid);
357
- if (ok) {
358
- invalidateAllCaches();
359
- autoCommitCurrentBranch(s.basePath, "mechanical-completion", `${mid}/${sid}`);
360
- ctx.ui.notify(`Mechanical completion: ${sid} summary + roadmap updated.`, "info");
361
-
362
- // Re-derive state — check if milestone is now ready for validation
363
- invalidateAllCaches();
364
- const postSliceState = await deriveState(s.basePath);
365
- if (postSliceState.phase === "validating-milestone" || postSliceState.phase === "completing-milestone") {
366
- const { aggregateMilestoneVerification, generateMilestoneSummary } = await import("./mechanical-completion.js");
367
- const validation = await aggregateMilestoneVerification(s.basePath, mid);
368
- if (validation.verdict !== "failed") {
369
- await generateMilestoneSummary(s.basePath, mid);
370
- invalidateAllCaches();
371
- autoCommitCurrentBranch(s.basePath, "mechanical-milestone-completion", mid);
372
- ctx.ui.notify(`Mechanical completion: ${mid} validation + summary written.`, "info");
373
- }
374
- }
375
- }
376
- // If !ok, summarizing phase persists → dispatch rule fires as LLM fallback
377
- }
378
- }
379
- } catch (err) {
380
- process.stderr.write(`gsd-mechanical: completion failed: ${(err as Error).message}\n`);
381
- // Non-fatal — fall through to normal dispatch
382
- }
383
- }
384
-
385
315
  // ── Post-unit hooks ──
386
316
  if (s.currentUnit && !s.stepMode) {
387
317
  const hookUnit = checkPostUnitHooks(s.currentUnit.type, s.currentUnit.id, s.basePath);
@@ -389,79 +319,36 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
389
319
  if (s.currentUnit) {
390
320
  await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
391
321
  }
392
- dispatchUnit(s, s.basePath, hookUnit.unitType, hookUnit.unitId);
393
-
394
- const state = await deriveState(s.basePath);
395
- updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
396
- const hookState = getActiveHook();
397
- ctx.ui.notify(
398
- `Running post-unit hook: ${hookUnit.hookName} (cycle ${hookState?.cycle ?? 1})`,
399
- "info",
400
- );
401
-
402
- // Switch model if the hook specifies one
403
- if (hookUnit.model) {
404
- const availableModels = ctx.modelRegistry.getAvailable();
405
- const match = availableModels.find(m =>
406
- m.id === hookUnit.model || `${m.provider}/${m.id}` === hookUnit.model,
407
- );
408
- if (match) {
409
- try {
410
- await pi.setModel(match);
411
- } catch { /* non-fatal */ }
412
- }
413
- }
414
-
415
- const result = await s.cmdCtx!.newSession();
416
- if (result.cancelled) {
417
- resetHookState();
418
- await stopAuto(ctx, pi, "Hook session cancelled");
419
- return "stopped";
420
- }
421
- const sessionFile = ctx.sessionManager.getSessionFile();
422
- writeLock(lockBase(), hookUnit.unitType, hookUnit.unitId, s.completedUnits.length, sessionFile);
423
322
  persistHookState(s.basePath);
424
323
 
425
- // Start supervision timers for hook units
426
- const supervisor = resolveAutoSupervisorConfig();
427
- const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
428
- s.unitTimeoutHandle = setTimeout(async () => {
429
- s.unitTimeoutHandle = null;
430
- if (!s.active) return;
431
- if (s.currentUnit) {
432
- writeUnitRuntimeRecord(s.basePath, hookUnit.unitType, hookUnit.unitId, s.currentUnit.startedAt, {
433
- phase: "timeout",
434
- timeoutAt: Date.now(),
435
- });
436
- }
437
- ctx.ui.notify(
438
- `Hook ${hookUnit.hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`,
439
- "warning",
440
- );
441
- resetHookState();
442
- await pauseAuto(ctx, pi);
443
- }, hookHardTimeoutMs);
444
-
445
- if (!s.active) return "stopped";
446
- pi.sendMessage(
447
- { customType: "gsd-auto", content: hookUnit.prompt, display: s.verbose },
448
- { triggerTurn: true },
449
- );
450
- return "dispatched";
324
+ s.sidecarQueue.push({
325
+ kind: "hook",
326
+ unitType: hookUnit.unitType,
327
+ unitId: hookUnit.unitId,
328
+ prompt: hookUnit.prompt,
329
+ model: hookUnit.model,
330
+ });
331
+
332
+ debugLog("postUnitPostVerification", {
333
+ phase: "sidecar-enqueue",
334
+ kind: "hook",
335
+ unitType: hookUnit.unitType,
336
+ unitId: hookUnit.unitId,
337
+ hookName: hookUnit.hookName,
338
+ });
339
+
340
+ return "continue";
451
341
  }
452
342
 
453
343
  // Check if a hook requested a retry of the trigger unit
454
344
  if (isRetryPending()) {
455
345
  const trigger = consumeRetryTrigger();
456
346
  if (trigger) {
457
- const triggerKey = `${trigger.unitType}/${trigger.unitId}`;
458
- s.completedKeySet.delete(triggerKey);
459
- removePersistedKey(s.basePath, triggerKey);
460
347
  ctx.ui.notify(
461
348
  `Hook requested retry of ${trigger.unitType} ${trigger.unitId}.`,
462
349
  "info",
463
350
  );
464
- // Fall through to normal dispatch
351
+ // Fall through to normal dispatch — deriveState will re-derive the unit
465
352
  }
466
353
  }
467
354
  }
@@ -500,46 +387,31 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
500
387
  roadmapContext: roadmapContext || "(no active roadmap)",
501
388
  });
502
389
 
503
- ctx.ui.notify(
504
- `Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
505
- "info",
506
- );
507
-
508
390
  if (s.currentUnit) {
509
391
  await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt);
510
392
  }
511
393
 
512
- const triageUnitType = "triage-captures";
513
394
  const triageUnitId = `${mid}/${sid}/triage`;
514
- dispatchUnit(s, s.basePath, triageUnitType, triageUnitId);
515
- updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
395
+ s.sidecarQueue.push({
396
+ kind: "triage",
397
+ unitType: "triage-captures",
398
+ unitId: triageUnitId,
399
+ prompt,
400
+ });
516
401
 
517
- const result = await s.cmdCtx!.newSession();
518
- if (result.cancelled) {
519
- await stopAuto(ctx, pi);
520
- return "stopped";
521
- }
522
- const sessionFile = ctx.sessionManager.getSessionFile();
523
- writeLock(lockBase(), triageUnitType, triageUnitId, s.completedUnits.length, sessionFile);
524
-
525
- const supervisor = resolveAutoSupervisorConfig();
526
- const triageTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
527
- s.unitTimeoutHandle = setTimeout(async () => {
528
- s.unitTimeoutHandle = null;
529
- if (!s.active) return;
530
- ctx.ui.notify(
531
- `Triage unit exceeded timeout. Pausing auto-mode.`,
532
- "warning",
533
- );
534
- await pauseAuto(ctx, pi);
535
- }, triageTimeoutMs);
536
-
537
- if (!s.active) return "stopped";
538
- pi.sendMessage(
539
- { customType: "gsd-auto", content: prompt, display: s.verbose },
540
- { triggerTurn: true },
402
+ debugLog("postUnitPostVerification", {
403
+ phase: "sidecar-enqueue",
404
+ kind: "triage",
405
+ unitId: triageUnitId,
406
+ pendingCount: pending.length,
407
+ });
408
+
409
+ ctx.ui.notify(
410
+ `Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
411
+ "info",
541
412
  );
542
- return "dispatched";
413
+
414
+ return "continue";
543
415
  }
544
416
  }
545
417
  }
@@ -561,49 +433,34 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
561
433
  const { markCaptureExecuted } = await import("./captures.js");
562
434
  const prompt = buildQuickTaskPrompt(capture);
563
435
 
564
- ctx.ui.notify(
565
- `Executing quick-task: ${capture.id} — "${capture.text}"`,
566
- "info",
567
- );
568
-
569
436
  if (s.currentUnit) {
570
437
  await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt);
571
438
  }
572
439
 
573
- const qtUnitType = "quick-task";
574
- const qtUnitId = `${s.currentMilestoneId}/${capture.id}`;
575
- dispatchUnit(s, s.basePath, qtUnitType, qtUnitId);
576
- const state = await deriveState(s.basePath);
577
- updateProgressWidget(ctx, qtUnitType, qtUnitId, state);
578
-
579
- const result = await s.cmdCtx!.newSession();
580
- if (result.cancelled) {
581
- await stopAuto(ctx, pi);
582
- return "stopped";
583
- }
584
- const sessionFile = ctx.sessionManager.getSessionFile();
585
- writeLock(lockBase(), qtUnitType, qtUnitId, s.completedUnits.length, sessionFile);
586
-
587
440
  markCaptureExecuted(s.basePath, capture.id);
588
441
 
589
- const supervisor = resolveAutoSupervisorConfig();
590
- const qtTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
591
- s.unitTimeoutHandle = setTimeout(async () => {
592
- s.unitTimeoutHandle = null;
593
- if (!s.active) return;
594
- ctx.ui.notify(
595
- `Quick-task ${capture.id} exceeded timeout. Pausing auto-mode.`,
596
- "warning",
597
- );
598
- await pauseAuto(ctx, pi);
599
- }, qtTimeoutMs);
442
+ const qtUnitId = `${s.currentMilestoneId}/${capture.id}`;
443
+ s.sidecarQueue.push({
444
+ kind: "quick-task",
445
+ unitType: "quick-task",
446
+ unitId: qtUnitId,
447
+ prompt,
448
+ captureId: capture.id,
449
+ });
450
+
451
+ debugLog("postUnitPostVerification", {
452
+ phase: "sidecar-enqueue",
453
+ kind: "quick-task",
454
+ unitId: qtUnitId,
455
+ captureId: capture.id,
456
+ });
600
457
 
601
- if (!s.active) return "stopped";
602
- pi.sendMessage(
603
- { customType: "gsd-auto", content: prompt, display: s.verbose },
604
- { triggerTurn: true },
458
+ ctx.ui.notify(
459
+ `Executing quick-task: ${capture.id} — "${capture.text}"`,
460
+ "info",
605
461
  );
606
- return "dispatched";
462
+
463
+ return "continue";
607
464
  } catch {
608
465
  // Non-fatal — proceed to normal dispatch
609
466
  }