gsd-pi 2.38.0-dev.4d4d14a → 2.38.0-dev.5492881
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.
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +538 -469
- package/dist/resources/extensions/gsd/auto-post-unit.js +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +18 -14
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/files.js +4 -0
- package/dist/resources/extensions/gsd/git-service.js +22 -11
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +342 -304
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -14
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/commands.ts +2 -2
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/files.ts +3 -1
- package/src/resources/extensions/gsd/git-service.ts +31 -9
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +106 -31
- package/src/resources/extensions/mcp-client/index.ts +17 -1
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* (per-unit one-shot resolver) and `_sessionSwitchInFlight` (guard for
|
|
10
10
|
* session rotation). No queue — stale agent_end events are dropped.
|
|
11
11
|
*/
|
|
12
|
+
import { importExtensionModule } from "@gsd/pi-coding-agent";
|
|
12
13
|
import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
|
|
13
14
|
import { debugLog } from "./debug-logger.js";
|
|
15
|
+
import { gsdRoot } from "./paths.js";
|
|
16
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
17
|
+
import { join } from "node:path";
|
|
14
18
|
/**
|
|
15
19
|
* Maximum total loop iterations before forced stop. Prevents runaway loops
|
|
16
20
|
* when units alternate IDs (bypassing the same-unit stuck detector).
|
|
@@ -18,6 +22,8 @@ import { debugLog } from "./debug-logger.js";
|
|
|
18
22
|
* generous headroom including retries and sidecar work.
|
|
19
23
|
*/
|
|
20
24
|
const MAX_LOOP_ITERATIONS = 500;
|
|
25
|
+
/** Maximum characters of failure/crash context included in recovery prompts. */
|
|
26
|
+
const MAX_RECOVERY_CHARS = 50_000;
|
|
21
27
|
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
22
28
|
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
23
29
|
* a simple notification. */
|
|
@@ -81,6 +87,50 @@ export function _resetPendingResolve() {
|
|
|
81
87
|
export function _setActiveSession(_session) {
|
|
82
88
|
// No-op — kept for test backward compatibility
|
|
83
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Analyze a sliding window of recent unit dispatches for stuck patterns.
|
|
92
|
+
* Returns a signal with reason if stuck, null otherwise.
|
|
93
|
+
*
|
|
94
|
+
* Rule 1: Same error string twice in a row → stuck immediately.
|
|
95
|
+
* Rule 2: Same unit key 3+ consecutive times → stuck (preserves prior behavior).
|
|
96
|
+
* Rule 3: Oscillation A→B→A→B in last 4 entries → stuck.
|
|
97
|
+
*/
|
|
98
|
+
export function detectStuck(window) {
|
|
99
|
+
if (window.length < 2)
|
|
100
|
+
return null;
|
|
101
|
+
const last = window[window.length - 1];
|
|
102
|
+
const prev = window[window.length - 2];
|
|
103
|
+
// Rule 1: Same error repeated consecutively
|
|
104
|
+
if (last.error && prev.error && last.error === prev.error) {
|
|
105
|
+
return {
|
|
106
|
+
stuck: true,
|
|
107
|
+
reason: `Same error repeated: ${last.error.slice(0, 200)}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Rule 2: Same unit 3+ consecutive times
|
|
111
|
+
if (window.length >= 3) {
|
|
112
|
+
const lastThree = window.slice(-3);
|
|
113
|
+
if (lastThree.every((u) => u.key === last.key)) {
|
|
114
|
+
return {
|
|
115
|
+
stuck: true,
|
|
116
|
+
reason: `${last.key} derived 3 consecutive times without progress`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Rule 3: Oscillation (A→B→A→B in last 4)
|
|
121
|
+
if (window.length >= 4) {
|
|
122
|
+
const w = window.slice(-4);
|
|
123
|
+
if (w[0].key === w[2].key &&
|
|
124
|
+
w[1].key === w[3].key &&
|
|
125
|
+
w[0].key !== w[1].key) {
|
|
126
|
+
return {
|
|
127
|
+
stuck: true,
|
|
128
|
+
reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
84
134
|
// ─── runUnit ─────────────────────────────────────────────────────────────────
|
|
85
135
|
/**
|
|
86
136
|
* Execute a single unit: create a new session, send the prompt, and await
|
|
@@ -163,9 +213,9 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
163
213
|
* Extracted from the milestone-transition block in autoLoop.
|
|
164
214
|
*/
|
|
165
215
|
async function generateMilestoneReport(s, ctx, milestoneId) {
|
|
166
|
-
const { loadVisualizerData } = await import
|
|
167
|
-
const { generateHtmlReport } = await import
|
|
168
|
-
const { writeReportSnapshot } = await import
|
|
216
|
+
const { loadVisualizerData } = await importExtensionModule(import.meta.url, "./visualizer-data.js");
|
|
217
|
+
const { generateHtmlReport } = await importExtensionModule(import.meta.url, "./export-html.js");
|
|
218
|
+
const { writeReportSnapshot } = await importExtensionModule(import.meta.url, "./reports.js");
|
|
169
219
|
const { basename } = await import("node:path");
|
|
170
220
|
const snapData = await loadVisualizerData(s.basePath);
|
|
171
221
|
const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
|
|
@@ -223,8 +273,10 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
|
223
273
|
export async function autoLoop(ctx, pi, s, deps) {
|
|
224
274
|
debugLog("autoLoop", { phase: "enter" });
|
|
225
275
|
let iteration = 0;
|
|
226
|
-
|
|
227
|
-
|
|
276
|
+
// ── Sliding-window stuck detection ──
|
|
277
|
+
const recentUnits = [];
|
|
278
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
279
|
+
let stuckRecoveryAttempts = 0;
|
|
228
280
|
let consecutiveErrors = 0;
|
|
229
281
|
while (s.active) {
|
|
230
282
|
iteration++;
|
|
@@ -244,6 +296,18 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
244
296
|
}
|
|
245
297
|
try {
|
|
246
298
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
299
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
300
|
+
// ── Check sidecar queue before deriveState ──
|
|
301
|
+
let sidecarItem;
|
|
302
|
+
if (s.sidecarQueue.length > 0) {
|
|
303
|
+
sidecarItem = s.sidecarQueue.shift();
|
|
304
|
+
debugLog("autoLoop", {
|
|
305
|
+
phase: "sidecar-dequeue",
|
|
306
|
+
kind: sidecarItem.kind,
|
|
307
|
+
unitType: sidecarItem.unitType,
|
|
308
|
+
unitId: sidecarItem.unitId,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
247
311
|
const sessionLockBase = deps.lockBase();
|
|
248
312
|
if (sessionLockBase) {
|
|
249
313
|
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
@@ -263,369 +327,436 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
263
327
|
break;
|
|
264
328
|
}
|
|
265
329
|
}
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
if (!healthGate.proceed) {
|
|
284
|
-
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
285
|
-
await deps.pauseAuto(ctx, pi);
|
|
286
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
330
|
+
// Variables shared between the sidecar and normal paths
|
|
331
|
+
let unitType;
|
|
332
|
+
let unitId;
|
|
333
|
+
let prompt;
|
|
334
|
+
let pauseAfterUatDispatch = false;
|
|
335
|
+
let state;
|
|
336
|
+
let mid;
|
|
337
|
+
let midTitle;
|
|
338
|
+
let observabilityIssues = [];
|
|
339
|
+
if (!sidecarItem) {
|
|
340
|
+
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
341
|
+
// Resource version guard
|
|
342
|
+
const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
|
|
343
|
+
if (staleMsg) {
|
|
344
|
+
await deps.stopAuto(ctx, pi, staleMsg);
|
|
345
|
+
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
287
346
|
break;
|
|
288
347
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
deps.syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
298
|
-
}
|
|
299
|
-
// Derive state
|
|
300
|
-
let state = await deps.deriveState(s.basePath);
|
|
301
|
-
deps.syncCmuxSidebar(deps.loadEffectiveGSDPreferences()?.preferences, state);
|
|
302
|
-
let mid = state.activeMilestone?.id;
|
|
303
|
-
let midTitle = state.activeMilestone?.title;
|
|
304
|
-
debugLog("autoLoop", {
|
|
305
|
-
phase: "state-derived",
|
|
306
|
-
iteration,
|
|
307
|
-
mid,
|
|
308
|
-
statePhase: state.phase,
|
|
309
|
-
});
|
|
310
|
-
// ── Milestone transition ────────────────────────────────────────────
|
|
311
|
-
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
312
|
-
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
313
|
-
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
314
|
-
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
315
|
-
const vizPrefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
316
|
-
if (vizPrefs?.auto_visualize) {
|
|
317
|
-
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
318
|
-
}
|
|
319
|
-
if (vizPrefs?.auto_report !== false) {
|
|
320
|
-
try {
|
|
321
|
-
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
348
|
+
deps.invalidateAllCaches();
|
|
349
|
+
s.lastPromptCharCount = undefined;
|
|
350
|
+
s.lastBaselineCharCount = undefined;
|
|
351
|
+
// Pre-dispatch health gate
|
|
352
|
+
try {
|
|
353
|
+
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
354
|
+
if (healthGate.fixesApplied.length > 0) {
|
|
355
|
+
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
322
356
|
}
|
|
323
|
-
|
|
324
|
-
ctx.ui.notify(
|
|
357
|
+
if (!healthGate.proceed) {
|
|
358
|
+
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
359
|
+
await deps.pauseAuto(ctx, pi);
|
|
360
|
+
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
361
|
+
break;
|
|
325
362
|
}
|
|
326
363
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
364
|
+
catch {
|
|
365
|
+
// Non-fatal
|
|
366
|
+
}
|
|
367
|
+
// Sync project root artifacts into worktree
|
|
368
|
+
if (s.originalBasePath &&
|
|
369
|
+
s.basePath !== s.originalBasePath &&
|
|
370
|
+
s.currentMilestoneId) {
|
|
371
|
+
deps.syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
372
|
+
}
|
|
373
|
+
// Derive state
|
|
336
374
|
state = await deps.deriveState(s.basePath);
|
|
375
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
337
376
|
mid = state.activeMilestone?.id;
|
|
338
377
|
midTitle = state.activeMilestone?.title;
|
|
378
|
+
debugLog("autoLoop", {
|
|
379
|
+
phase: "state-derived",
|
|
380
|
+
iteration,
|
|
381
|
+
mid,
|
|
382
|
+
statePhase: state.phase,
|
|
383
|
+
});
|
|
384
|
+
// ── Milestone transition ────────────────────────────────────────────
|
|
385
|
+
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
386
|
+
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
387
|
+
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
388
|
+
deps.logCmuxEvent(prefs, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
389
|
+
const vizPrefs = prefs;
|
|
390
|
+
if (vizPrefs?.auto_visualize) {
|
|
391
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
392
|
+
}
|
|
393
|
+
if (vizPrefs?.auto_report !== false) {
|
|
394
|
+
try {
|
|
395
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Reset dispatch counters for new milestone
|
|
402
|
+
s.unitDispatchCount.clear();
|
|
403
|
+
s.unitRecoveryCount.clear();
|
|
404
|
+
s.unitLifetimeDispatches.clear();
|
|
405
|
+
recentUnits.length = 0;
|
|
406
|
+
stuckRecoveryAttempts = 0;
|
|
407
|
+
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
408
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
409
|
+
// Opt-in: create draft PR on milestone completion
|
|
410
|
+
if (prefs?.git?.auto_pr) {
|
|
411
|
+
try {
|
|
412
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
413
|
+
const prUrl = createDraftPR(s.basePath, s.currentMilestoneId, `[GSD] ${s.currentMilestoneId} complete`, `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`);
|
|
414
|
+
if (prUrl) {
|
|
415
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
// Non-fatal — PR creation is best-effort
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
deps.invalidateAllCaches();
|
|
423
|
+
state = await deps.deriveState(s.basePath);
|
|
424
|
+
mid = state.activeMilestone?.id;
|
|
425
|
+
midTitle = state.activeMilestone?.title;
|
|
426
|
+
if (mid) {
|
|
427
|
+
if (deps.getIsolationMode() !== "none") {
|
|
428
|
+
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
429
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
// mid is undefined — no milestone to capture integration branch for
|
|
436
|
+
}
|
|
437
|
+
const pendingIds = state.registry
|
|
438
|
+
.filter((m) => m.status !== "complete" && m.status !== "parked")
|
|
439
|
+
.map((m) => m.id);
|
|
440
|
+
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
441
|
+
}
|
|
339
442
|
if (mid) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
443
|
+
s.currentMilestoneId = mid;
|
|
444
|
+
deps.setActiveMilestoneId(s.basePath, mid);
|
|
445
|
+
}
|
|
446
|
+
// ── Terminal conditions ──────────────────────────────────────────────
|
|
447
|
+
if (!mid) {
|
|
448
|
+
if (s.currentUnit) {
|
|
449
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
345
450
|
}
|
|
346
|
-
|
|
451
|
+
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
452
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
453
|
+
// All milestones complete — merge milestone branch before stopping
|
|
454
|
+
if (s.currentMilestoneId) {
|
|
455
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
456
|
+
// Opt-in: create draft PR on milestone completion
|
|
457
|
+
if (prefs?.git?.auto_pr) {
|
|
458
|
+
try {
|
|
459
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
460
|
+
const prUrl = createDraftPR(s.basePath, s.currentMilestoneId, `[GSD] ${s.currentMilestoneId} complete`, `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`);
|
|
461
|
+
if (prUrl) {
|
|
462
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Non-fatal — PR creation is best-effort
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
471
|
+
deps.logCmuxEvent(prefs, "All milestones complete.", "success");
|
|
472
|
+
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
473
|
+
}
|
|
474
|
+
else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
475
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
476
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
477
|
+
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
478
|
+
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
479
|
+
}
|
|
480
|
+
else if (state.phase === "blocked") {
|
|
481
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
482
|
+
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
483
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
484
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
485
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const ids = incomplete.map((m) => m.id).join(", ");
|
|
489
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
490
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
491
|
+
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
492
|
+
}
|
|
493
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
494
|
+
break;
|
|
347
495
|
}
|
|
348
|
-
|
|
349
|
-
|
|
496
|
+
if (!midTitle) {
|
|
497
|
+
midTitle = mid;
|
|
498
|
+
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
350
499
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
500
|
+
// Mid-merge safety check
|
|
501
|
+
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
502
|
+
deps.invalidateAllCaches();
|
|
503
|
+
state = await deps.deriveState(s.basePath);
|
|
504
|
+
mid = state.activeMilestone?.id;
|
|
505
|
+
midTitle = state.activeMilestone?.title;
|
|
506
|
+
}
|
|
507
|
+
if (!mid || !midTitle) {
|
|
508
|
+
const noMilestoneReason = !mid
|
|
509
|
+
? "No active milestone after merge reconciliation"
|
|
510
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
511
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
512
|
+
debugLog("autoLoop", {
|
|
513
|
+
phase: "exit",
|
|
514
|
+
reason: "no-milestone-after-reconciliation",
|
|
515
|
+
});
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
// Terminal: complete
|
|
519
|
+
if (state.phase === "complete") {
|
|
520
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
368
521
|
if (s.currentMilestoneId) {
|
|
369
522
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
523
|
+
// Opt-in: create draft PR on milestone completion
|
|
524
|
+
if (prefs?.git?.auto_pr) {
|
|
525
|
+
try {
|
|
526
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
527
|
+
const prUrl = createDraftPR(s.basePath, s.currentMilestoneId, `[GSD] ${s.currentMilestoneId} complete`, `Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`);
|
|
528
|
+
if (prUrl) {
|
|
529
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
// Non-fatal — PR creation is best-effort
|
|
534
|
+
}
|
|
535
|
+
}
|
|
370
536
|
}
|
|
371
|
-
deps.sendDesktopNotification("GSD",
|
|
372
|
-
deps.logCmuxEvent(
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// Empty registry — no milestones visible, likely a path resolution bug
|
|
377
|
-
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
378
|
-
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
379
|
-
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
537
|
+
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
538
|
+
deps.logCmuxEvent(prefs, `Milestone ${mid} complete.`, "success");
|
|
539
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
540
|
+
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
541
|
+
break;
|
|
380
542
|
}
|
|
381
|
-
|
|
543
|
+
// Terminal: blocked
|
|
544
|
+
if (state.phase === "blocked") {
|
|
382
545
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
383
|
-
await
|
|
546
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
384
547
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
385
548
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
386
|
-
deps.logCmuxEvent(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const ids = incomplete.map((m) => m.id).join(", ");
|
|
390
|
-
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
391
|
-
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
392
|
-
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
393
|
-
}
|
|
394
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
if (!midTitle) {
|
|
398
|
-
midTitle = mid;
|
|
399
|
-
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
400
|
-
}
|
|
401
|
-
// Mid-merge safety check
|
|
402
|
-
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
403
|
-
deps.invalidateAllCaches();
|
|
404
|
-
state = await deps.deriveState(s.basePath);
|
|
405
|
-
mid = state.activeMilestone?.id;
|
|
406
|
-
midTitle = state.activeMilestone?.title;
|
|
407
|
-
}
|
|
408
|
-
if (!mid || !midTitle) {
|
|
409
|
-
const noMilestoneReason = !mid
|
|
410
|
-
? "No active milestone after merge reconciliation"
|
|
411
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
412
|
-
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
413
|
-
debugLog("autoLoop", {
|
|
414
|
-
phase: "exit",
|
|
415
|
-
reason: "no-milestone-after-reconciliation",
|
|
416
|
-
});
|
|
417
|
-
break;
|
|
418
|
-
}
|
|
419
|
-
// Terminal: complete
|
|
420
|
-
if (state.phase === "complete") {
|
|
421
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
422
|
-
if (s.currentMilestoneId) {
|
|
423
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
549
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
550
|
+
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
551
|
+
break;
|
|
424
552
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// 100% — special enforcement logic (halt/pause/warn)
|
|
462
|
-
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
463
|
-
if (budgetEnforcementAction === "halt") {
|
|
464
|
-
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
465
|
-
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
466
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
if (budgetEnforcementAction === "pause") {
|
|
470
|
-
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
553
|
+
// ── Phase 2: Guards ─────────────────────────────────────────────────
|
|
554
|
+
// Budget ceiling guard
|
|
555
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
556
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
557
|
+
const currentLedger = deps.getLedger();
|
|
558
|
+
const totalCost = currentLedger
|
|
559
|
+
? deps.getProjectTotals(currentLedger.units).cost
|
|
560
|
+
: 0;
|
|
561
|
+
const budgetPct = totalCost / budgetCeiling;
|
|
562
|
+
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
563
|
+
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
564
|
+
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
565
|
+
const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
|
|
566
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
567
|
+
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel >= t.pct);
|
|
568
|
+
if (threshold) {
|
|
569
|
+
s.lastBudgetAlertLevel =
|
|
570
|
+
newBudgetAlertLevel;
|
|
571
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
572
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
573
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
574
|
+
if (budgetEnforcementAction === "halt") {
|
|
575
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
576
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
577
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
if (budgetEnforcementAction === "pause") {
|
|
581
|
+
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
582
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
583
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
584
|
+
await deps.pauseAuto(ctx, pi);
|
|
585
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
471
589
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
472
590
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
473
|
-
await deps.pauseAuto(ctx, pi);
|
|
474
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
475
|
-
break;
|
|
476
591
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
592
|
+
else if (threshold.pct < 100) {
|
|
593
|
+
// Sub-100% — simple notification
|
|
594
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
595
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
596
|
+
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
597
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
598
|
+
}
|
|
480
599
|
}
|
|
481
|
-
else if (
|
|
482
|
-
|
|
483
|
-
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
484
|
-
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
485
|
-
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
486
|
-
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
600
|
+
else if (budgetAlertLevel === 0) {
|
|
601
|
+
s.lastBudgetAlertLevel = 0;
|
|
487
602
|
}
|
|
488
603
|
}
|
|
489
|
-
else
|
|
604
|
+
else {
|
|
490
605
|
s.lastBudgetAlertLevel = 0;
|
|
491
606
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
s.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
deps.sendDesktopNotification("GSD", `Context ${contextUsage.percent}% — paused`, "warning", "attention");
|
|
506
|
-
await deps.pauseAuto(ctx, pi);
|
|
507
|
-
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
// Secrets re-check gate
|
|
512
|
-
try {
|
|
513
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
514
|
-
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
515
|
-
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
516
|
-
if (result &&
|
|
517
|
-
result.applied &&
|
|
518
|
-
result.skipped &&
|
|
519
|
-
result.existingSkipped) {
|
|
520
|
-
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
607
|
+
// Context window guard
|
|
608
|
+
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
609
|
+
if (contextThreshold > 0 && s.cmdCtx) {
|
|
610
|
+
const contextUsage = s.cmdCtx.getContextUsage();
|
|
611
|
+
if (contextUsage &&
|
|
612
|
+
contextUsage.percent !== null &&
|
|
613
|
+
contextUsage.percent >= contextThreshold) {
|
|
614
|
+
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
615
|
+
ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
|
|
616
|
+
deps.sendDesktopNotification("GSD", `Context ${contextUsage.percent}% — paused`, "warning", "attention");
|
|
617
|
+
await deps.pauseAuto(ctx, pi);
|
|
618
|
+
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
619
|
+
break;
|
|
521
620
|
}
|
|
522
|
-
|
|
523
|
-
|
|
621
|
+
}
|
|
622
|
+
// Secrets re-check gate
|
|
623
|
+
try {
|
|
624
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
625
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
626
|
+
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
627
|
+
if (result &&
|
|
628
|
+
result.applied &&
|
|
629
|
+
result.skipped &&
|
|
630
|
+
result.existingSkipped) {
|
|
631
|
+
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
635
|
+
}
|
|
524
636
|
}
|
|
525
637
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
session: s,
|
|
539
|
-
});
|
|
540
|
-
if (dispatchResult.action === "stop") {
|
|
541
|
-
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
542
|
-
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
if (dispatchResult.action !== "dispatch") {
|
|
546
|
-
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
547
|
-
await new Promise((r) => setImmediate(r));
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
let unitType = dispatchResult.unitType;
|
|
551
|
-
let unitId = dispatchResult.unitId;
|
|
552
|
-
let prompt = dispatchResult.prompt;
|
|
553
|
-
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
554
|
-
// ── Same-unit stuck counter with graduated recovery ──
|
|
555
|
-
const derivedKey = `${unitType}/${unitId}`;
|
|
556
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
557
|
-
sameUnitCount++;
|
|
558
|
-
debugLog("autoLoop", {
|
|
559
|
-
phase: "stuck-check",
|
|
560
|
-
unitType,
|
|
561
|
-
unitId,
|
|
562
|
-
sameUnitCount,
|
|
638
|
+
catch (err) {
|
|
639
|
+
ctx.ui.notify(`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, "warning");
|
|
640
|
+
}
|
|
641
|
+
// ── Phase 3: Dispatch resolution ────────────────────────────────────
|
|
642
|
+
debugLog("autoLoop", { phase: "dispatch-resolve", iteration });
|
|
643
|
+
const dispatchResult = await deps.resolveDispatch({
|
|
644
|
+
basePath: s.basePath,
|
|
645
|
+
mid,
|
|
646
|
+
midTitle: midTitle,
|
|
647
|
+
state,
|
|
648
|
+
prefs,
|
|
649
|
+
session: s,
|
|
563
650
|
});
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
651
|
+
if (dispatchResult.action === "stop") {
|
|
652
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
653
|
+
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
if (dispatchResult.action !== "dispatch") {
|
|
657
|
+
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
658
|
+
await new Promise((r) => setImmediate(r));
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
unitType = dispatchResult.unitType;
|
|
662
|
+
unitId = dispatchResult.unitId;
|
|
663
|
+
prompt = dispatchResult.prompt;
|
|
664
|
+
pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
665
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
666
|
+
const derivedKey = `${unitType}/${unitId}`;
|
|
667
|
+
if (!s.pendingVerificationRetry) {
|
|
668
|
+
recentUnits.push({ key: derivedKey });
|
|
669
|
+
if (recentUnits.length > STUCK_WINDOW_SIZE)
|
|
670
|
+
recentUnits.shift();
|
|
671
|
+
const stuckSignal = detectStuck(recentUnits);
|
|
672
|
+
if (stuckSignal) {
|
|
568
673
|
debugLog("autoLoop", {
|
|
569
|
-
phase: "stuck-
|
|
570
|
-
|
|
571
|
-
|
|
674
|
+
phase: "stuck-check",
|
|
675
|
+
unitType,
|
|
676
|
+
unitId,
|
|
677
|
+
reason: stuckSignal.reason,
|
|
678
|
+
recoveryAttempts: stuckRecoveryAttempts,
|
|
572
679
|
});
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
680
|
+
if (stuckRecoveryAttempts === 0) {
|
|
681
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
682
|
+
stuckRecoveryAttempts++;
|
|
683
|
+
const artifactExists = deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
684
|
+
if (artifactExists) {
|
|
685
|
+
debugLog("autoLoop", {
|
|
686
|
+
phase: "stuck-recovery",
|
|
687
|
+
level: 1,
|
|
688
|
+
action: "artifact-found",
|
|
689
|
+
});
|
|
690
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
691
|
+
deps.invalidateAllCaches();
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
|
|
695
|
+
deps.invalidateAllCaches();
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
// Level 2: hard stop — genuinely stuck
|
|
699
|
+
debugLog("autoLoop", {
|
|
700
|
+
phase: "stuck-detected",
|
|
701
|
+
unitType,
|
|
702
|
+
unitId,
|
|
703
|
+
reason: stuckSignal.reason,
|
|
704
|
+
});
|
|
705
|
+
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
706
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`, "error");
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
// Progress detected — reset recovery counter
|
|
712
|
+
if (stuckRecoveryAttempts > 0) {
|
|
713
|
+
debugLog("autoLoop", {
|
|
714
|
+
phase: "stuck-counter-reset",
|
|
715
|
+
from: recentUnits[recentUnits.length - 2]?.key ?? "",
|
|
716
|
+
to: derivedKey,
|
|
717
|
+
});
|
|
718
|
+
stuckRecoveryAttempts = 0;
|
|
719
|
+
}
|
|
576
720
|
}
|
|
577
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`, "warning");
|
|
578
|
-
deps.invalidateAllCaches();
|
|
579
721
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
722
|
+
// Pre-dispatch hooks
|
|
723
|
+
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
724
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
725
|
+
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
726
|
+
}
|
|
727
|
+
if (preDispatchResult.action === "skip") {
|
|
728
|
+
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
729
|
+
await new Promise((r) => setImmediate(r));
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (preDispatchResult.action === "replace") {
|
|
733
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
734
|
+
if (preDispatchResult.unitType)
|
|
735
|
+
unitType = preDispatchResult.unitType;
|
|
736
|
+
}
|
|
737
|
+
else if (preDispatchResult.prompt) {
|
|
738
|
+
prompt = preDispatchResult.prompt;
|
|
739
|
+
}
|
|
740
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(s.basePath, deps.getMainBranch(s.basePath), unitType, unitId);
|
|
741
|
+
if (priorSliceBlocker) {
|
|
742
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
743
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
590
744
|
break;
|
|
591
745
|
}
|
|
746
|
+
observabilityIssues = await deps.collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
747
|
+
// Derive state for shared use in execution phase
|
|
748
|
+
// (state, mid, midTitle already set above)
|
|
592
749
|
}
|
|
593
750
|
else {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
sameUnitCount = 0;
|
|
603
|
-
}
|
|
604
|
-
// Pre-dispatch hooks
|
|
605
|
-
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
606
|
-
if (preDispatchResult.firedHooks.length > 0) {
|
|
607
|
-
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
608
|
-
}
|
|
609
|
-
if (preDispatchResult.action === "skip") {
|
|
610
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
611
|
-
await new Promise((r) => setImmediate(r));
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
if (preDispatchResult.action === "replace") {
|
|
615
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
616
|
-
if (preDispatchResult.unitType)
|
|
617
|
-
unitType = preDispatchResult.unitType;
|
|
618
|
-
}
|
|
619
|
-
else if (preDispatchResult.prompt) {
|
|
620
|
-
prompt = preDispatchResult.prompt;
|
|
621
|
-
}
|
|
622
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(s.basePath, deps.getMainBranch(s.basePath), unitType, unitId);
|
|
623
|
-
if (priorSliceBlocker) {
|
|
624
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
625
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
626
|
-
break;
|
|
751
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
752
|
+
unitType = sidecarItem.unitType;
|
|
753
|
+
unitId = sidecarItem.unitId;
|
|
754
|
+
prompt = sidecarItem.prompt;
|
|
755
|
+
// Derive minimal state for progress widget / execution context
|
|
756
|
+
state = await deps.deriveState(s.basePath);
|
|
757
|
+
mid = state.activeMilestone?.id;
|
|
758
|
+
midTitle = state.activeMilestone?.title;
|
|
627
759
|
}
|
|
628
|
-
const observabilityIssues = await deps.collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
629
760
|
// ── Phase 4: Unit execution ─────────────────────────────────────────
|
|
630
761
|
debugLog("autoLoop", {
|
|
631
762
|
phase: "unit-execution",
|
|
@@ -638,33 +769,6 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
638
769
|
s.currentUnit.type === unitType &&
|
|
639
770
|
s.currentUnit.id === unitId);
|
|
640
771
|
const previousTier = s.currentUnitRouting?.tier;
|
|
641
|
-
// Closeout previous unit
|
|
642
|
-
if (s.currentUnit) {
|
|
643
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
644
|
-
if (s.currentUnitRouting) {
|
|
645
|
-
const isRetryForOutcome = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
646
|
-
deps.recordOutcome(s.currentUnit.type, s.currentUnitRouting.tier, !isRetryForOutcome);
|
|
647
|
-
}
|
|
648
|
-
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
649
|
-
const incomingKey = `${unitType}/${unitId}`;
|
|
650
|
-
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
651
|
-
const artifactVerified = isHookUnit ||
|
|
652
|
-
deps.verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
|
|
653
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
654
|
-
s.completedUnits.push({
|
|
655
|
-
type: s.currentUnit.type,
|
|
656
|
-
id: s.currentUnit.id,
|
|
657
|
-
startedAt: s.currentUnit.startedAt,
|
|
658
|
-
finishedAt: Date.now(),
|
|
659
|
-
});
|
|
660
|
-
if (s.completedUnits.length > 200) {
|
|
661
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
662
|
-
}
|
|
663
|
-
deps.clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
|
|
664
|
-
s.unitDispatchCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
|
|
665
|
-
s.unitRecoveryCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
772
|
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
669
773
|
deps.captureAvailableSkills();
|
|
670
774
|
deps.writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
@@ -682,7 +786,6 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
682
786
|
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
683
787
|
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
684
788
|
// Prompt injection
|
|
685
|
-
const MAX_RECOVERY_CHARS = 50_000;
|
|
686
789
|
let finalPrompt = prompt;
|
|
687
790
|
if (s.pendingVerificationRetry) {
|
|
688
791
|
const retryCtx = s.pendingVerificationRetry;
|
|
@@ -720,7 +823,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
720
823
|
s.lastBaselineCharCount = undefined;
|
|
721
824
|
if (deps.isDbAvailable()) {
|
|
722
825
|
try {
|
|
723
|
-
const { inlineGsdRootFile } = await import
|
|
826
|
+
const { inlineGsdRootFile } = await importExtensionModule(import.meta.url, "./auto-prompts.js");
|
|
724
827
|
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
725
828
|
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
726
829
|
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
@@ -743,8 +846,8 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
743
846
|
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
744
847
|
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
745
848
|
}
|
|
746
|
-
// Select and apply model (with tier escalation on retry)
|
|
747
|
-
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, { isRetry, previousTier });
|
|
849
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
850
|
+
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
|
|
748
851
|
s.currentUnitRouting =
|
|
749
852
|
modelResult.routing;
|
|
750
853
|
// Start unit supervision
|
|
@@ -778,12 +881,60 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
778
881
|
unitId,
|
|
779
882
|
status: unitResult.status,
|
|
780
883
|
});
|
|
884
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
885
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
886
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
887
|
+
if (lastEntry) {
|
|
888
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
else if (unitResult.event?.messages?.length) {
|
|
892
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
893
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
894
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
895
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
896
|
+
if (lastEntry) {
|
|
897
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
781
901
|
if (unitResult.status === "cancelled") {
|
|
782
902
|
ctx.ui.notify(`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`, "warning");
|
|
783
903
|
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
784
904
|
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
785
905
|
break;
|
|
786
906
|
}
|
|
907
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
908
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
909
|
+
// crash between iterations.
|
|
910
|
+
await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
|
|
911
|
+
if (s.currentUnitRouting) {
|
|
912
|
+
deps.recordOutcome(unitType, s.currentUnitRouting.tier, true);
|
|
913
|
+
}
|
|
914
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
915
|
+
const artifactVerified = isHookUnit ||
|
|
916
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
917
|
+
if (artifactVerified) {
|
|
918
|
+
s.completedUnits.push({
|
|
919
|
+
type: unitType,
|
|
920
|
+
id: unitId,
|
|
921
|
+
startedAt: s.currentUnit.startedAt,
|
|
922
|
+
finishedAt: Date.now(),
|
|
923
|
+
});
|
|
924
|
+
if (s.completedUnits.length > 200) {
|
|
925
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
926
|
+
}
|
|
927
|
+
// Flush completed-units to disk so the record survives crashes
|
|
928
|
+
try {
|
|
929
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
930
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
931
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
932
|
+
}
|
|
933
|
+
catch { /* non-fatal: disk flush failure */ }
|
|
934
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
935
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
936
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
937
|
+
}
|
|
787
938
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
788
939
|
debugLog("autoLoop", { phase: "finalize", iteration });
|
|
789
940
|
// Clear unit timeout (unit completed)
|
|
@@ -800,7 +951,13 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
800
951
|
updateProgressWidget: deps.updateProgressWidget,
|
|
801
952
|
};
|
|
802
953
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
803
|
-
|
|
954
|
+
// Sidecar items use lightweight pre-verification opts
|
|
955
|
+
const preVerificationOpts = sidecarItem
|
|
956
|
+
? sidecarItem.kind === "hook"
|
|
957
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
958
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
959
|
+
: undefined;
|
|
960
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
804
961
|
if (preResult === "dispatched") {
|
|
805
962
|
debugLog("autoLoop", {
|
|
806
963
|
phase: "exit",
|
|
@@ -814,17 +971,28 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
814
971
|
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
815
972
|
break;
|
|
816
973
|
}
|
|
817
|
-
// Verification gate
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
974
|
+
// Verification gate
|
|
975
|
+
// Hook sidecar items skip verification entirely.
|
|
976
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
977
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
978
|
+
if (!skipVerification) {
|
|
979
|
+
const verificationResult = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
980
|
+
if (verificationResult === "pause") {
|
|
981
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
if (verificationResult === "retry") {
|
|
985
|
+
if (sidecarItem) {
|
|
986
|
+
// Sidecar verification retries are skipped — just continue
|
|
987
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration });
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
991
|
+
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
992
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
993
|
+
continue;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
828
996
|
}
|
|
829
997
|
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
830
998
|
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
@@ -840,105 +1008,6 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
840
1008
|
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
841
1009
|
break;
|
|
842
1010
|
}
|
|
843
|
-
// ── Sidecar drain: dispatch enqueued hooks/triage/quick-tasks ──
|
|
844
|
-
let sidecarBroke = false;
|
|
845
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
846
|
-
const item = s.sidecarQueue.shift();
|
|
847
|
-
debugLog("autoLoop", {
|
|
848
|
-
phase: "sidecar-dequeue",
|
|
849
|
-
kind: item.kind,
|
|
850
|
-
unitType: item.unitType,
|
|
851
|
-
unitId: item.unitId,
|
|
852
|
-
});
|
|
853
|
-
// Set up as current unit
|
|
854
|
-
const sidecarStartedAt = Date.now();
|
|
855
|
-
s.currentUnit = {
|
|
856
|
-
type: item.unitType,
|
|
857
|
-
id: item.unitId,
|
|
858
|
-
startedAt: sidecarStartedAt,
|
|
859
|
-
};
|
|
860
|
-
deps.writeUnitRuntimeRecord(s.basePath, item.unitType, item.unitId, sidecarStartedAt, {
|
|
861
|
-
phase: "dispatched",
|
|
862
|
-
wrapupWarningSent: false,
|
|
863
|
-
timeoutAt: null,
|
|
864
|
-
lastProgressAt: sidecarStartedAt,
|
|
865
|
-
progressCount: 0,
|
|
866
|
-
lastProgressKind: "dispatch",
|
|
867
|
-
});
|
|
868
|
-
// Model selection (handles hook model override)
|
|
869
|
-
await deps.selectAndApplyModel(ctx, pi, item.unitType, item.unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel);
|
|
870
|
-
// Supervision
|
|
871
|
-
deps.clearUnitTimeout();
|
|
872
|
-
deps.startUnitSupervision({
|
|
873
|
-
s,
|
|
874
|
-
ctx,
|
|
875
|
-
pi,
|
|
876
|
-
unitType: item.unitType,
|
|
877
|
-
unitId: item.unitId,
|
|
878
|
-
prefs,
|
|
879
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
880
|
-
buildRecoveryContext: () => ({}),
|
|
881
|
-
pauseAuto: deps.pauseAuto,
|
|
882
|
-
});
|
|
883
|
-
// Write lock
|
|
884
|
-
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
885
|
-
deps.writeLock(deps.lockBase(), item.unitType, item.unitId, s.completedUnits.length, sidecarSessionFile);
|
|
886
|
-
// Execute via standard runUnit
|
|
887
|
-
const sidecarResult = await runUnit(ctx, pi, s, item.unitType, item.unitId, item.prompt);
|
|
888
|
-
deps.clearUnitTimeout();
|
|
889
|
-
if (sidecarResult.status === "cancelled") {
|
|
890
|
-
ctx.ui.notify(`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`, "warning");
|
|
891
|
-
await deps.stopAuto(ctx, pi, "Sidecar session creation failed");
|
|
892
|
-
sidecarBroke = true;
|
|
893
|
-
break;
|
|
894
|
-
}
|
|
895
|
-
// Run pre-verification for the sidecar unit (lightweight path)
|
|
896
|
-
const sidecarPreOpts = item.kind === "hook"
|
|
897
|
-
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
898
|
-
: { skipSettleDelay: true, skipStateRebuild: true };
|
|
899
|
-
const sidecarPreResult = await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
|
|
900
|
-
if (sidecarPreResult === "dispatched") {
|
|
901
|
-
// Pre-verification caused stop/pause
|
|
902
|
-
debugLog("autoLoop", {
|
|
903
|
-
phase: "exit",
|
|
904
|
-
reason: "sidecar-pre-verification-stop",
|
|
905
|
-
});
|
|
906
|
-
sidecarBroke = true;
|
|
907
|
-
break;
|
|
908
|
-
}
|
|
909
|
-
// Verification gate for non-hook sidecar units (triage, quick-tasks)
|
|
910
|
-
// Hook units are lightweight and don't need verification.
|
|
911
|
-
if (item.kind !== "hook") {
|
|
912
|
-
const sidecarVerification = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
913
|
-
if (sidecarVerification === "pause") {
|
|
914
|
-
debugLog("autoLoop", {
|
|
915
|
-
phase: "exit",
|
|
916
|
-
reason: "sidecar-verification-pause",
|
|
917
|
-
});
|
|
918
|
-
sidecarBroke = true;
|
|
919
|
-
break;
|
|
920
|
-
}
|
|
921
|
-
// "retry" for sidecars — skip retry, just continue (sidecar retries are not worth the complexity)
|
|
922
|
-
}
|
|
923
|
-
// Post-verification (may enqueue more sidecar items)
|
|
924
|
-
const sidecarPostResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
925
|
-
if (sidecarPostResult === "stopped") {
|
|
926
|
-
debugLog("autoLoop", { phase: "exit", reason: "sidecar-stopped" });
|
|
927
|
-
sidecarBroke = true;
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
if (sidecarPostResult === "step-wizard") {
|
|
931
|
-
debugLog("autoLoop", {
|
|
932
|
-
phase: "exit",
|
|
933
|
-
reason: "sidecar-step-wizard",
|
|
934
|
-
});
|
|
935
|
-
sidecarBroke = true;
|
|
936
|
-
break;
|
|
937
|
-
}
|
|
938
|
-
// "continue" — loop checks sidecarQueue again
|
|
939
|
-
}
|
|
940
|
-
if (sidecarBroke)
|
|
941
|
-
break;
|
|
942
1011
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
943
1012
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
944
1013
|
}
|