gsd-pi 2.39.0-dev.d6a1625 → 2.40.0-dev.4a93031
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 +66 -2
- package/dist/resources/extensions/get-secrets-from-user.js +1 -1
- package/dist/resources/extensions/gsd/auto-dashboard.js +7 -0
- package/dist/resources/extensions/gsd/auto-loop.js +747 -673
- package/dist/resources/extensions/gsd/auto-post-unit.js +10 -2
- package/dist/resources/extensions/gsd/auto-start.js +6 -1
- package/dist/resources/extensions/gsd/auto.js +6 -4
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +126 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +233 -0
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +59 -0
- package/dist/resources/extensions/gsd/bootstrap/register-extension.js +38 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +156 -0
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +46 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +300 -0
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +38 -0
- package/dist/resources/extensions/gsd/commands/catalog.js +278 -0
- package/dist/resources/extensions/gsd/commands/context.js +84 -0
- package/dist/resources/extensions/gsd/commands/dispatcher.js +21 -0
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +72 -0
- package/dist/resources/extensions/gsd/commands/handlers/core.js +246 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +166 -0
- package/dist/resources/extensions/gsd/commands/handlers/parallel.js +94 -0
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +102 -0
- package/dist/resources/extensions/gsd/commands/index.js +11 -0
- package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
- package/dist/resources/extensions/gsd/commands.js +8 -1190
- package/dist/resources/extensions/gsd/dashboard-overlay.js +9 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +80 -10
- package/dist/resources/extensions/gsd/doctor.js +32 -2
- package/dist/resources/extensions/gsd/export-html.js +46 -0
- package/dist/resources/extensions/gsd/files.js +1 -1
- package/dist/resources/extensions/gsd/health-widget.js +1 -1
- package/dist/resources/extensions/gsd/index.js +4 -1115
- package/dist/resources/extensions/gsd/progress-score.js +20 -1
- package/dist/resources/extensions/gsd/prompts/forensics.md +121 -46
- package/dist/resources/extensions/gsd/visualizer-data.js +26 -1
- package/dist/resources/extensions/gsd/visualizer-views.js +52 -0
- package/dist/welcome-screen.d.ts +3 -2
- package/dist/welcome-screen.js +66 -22
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +107 -24
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +70 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +2 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +244 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts +3 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +58 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +12 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +54 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts +6 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +63 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +38 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -457
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +122 -23
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +89 -0
- package/packages/pi-coding-agent/src/core/skills.ts +2 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +302 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +59 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +68 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +71 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +37 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +18 -510
- package/pkg/package.json +1 -1
- package/src/resources/extensions/get-secrets-from-user.ts +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +10 -0
- package/src/resources/extensions/gsd/auto-loop.ts +1060 -920
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -2
- package/src/resources/extensions/gsd/auto-start.ts +6 -1
- package/src/resources/extensions/gsd/auto.ts +13 -10
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +142 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +238 -0
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +90 -0
- package/src/resources/extensions/gsd/bootstrap/register-extension.ts +46 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +167 -0
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +55 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +340 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +51 -0
- package/src/resources/extensions/gsd/commands/catalog.ts +301 -0
- package/src/resources/extensions/gsd/commands/context.ts +101 -0
- package/src/resources/extensions/gsd/commands/dispatcher.ts +32 -0
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +74 -0
- package/src/resources/extensions/gsd/commands/handlers/core.ts +274 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +169 -0
- package/src/resources/extensions/gsd/commands/handlers/parallel.ts +118 -0
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +109 -0
- package/src/resources/extensions/gsd/commands/index.ts +14 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
- package/src/resources/extensions/gsd/commands.ts +10 -1329
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +106 -10
- package/src/resources/extensions/gsd/doctor.ts +47 -3
- package/src/resources/extensions/gsd/export-html.ts +51 -0
- package/src/resources/extensions/gsd/files.ts +1 -1
- package/src/resources/extensions/gsd/health-widget.ts +2 -1
- package/src/resources/extensions/gsd/index.ts +12 -1314
- package/src/resources/extensions/gsd/progress-score.ts +23 -0
- package/src/resources/extensions/gsd/prompts/forensics.md +121 -46
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +13 -9
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +16 -16
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +10 -10
- package/src/resources/extensions/gsd/visualizer-data.ts +51 -1
- package/src/resources/extensions/gsd/visualizer-views.ts +58 -0
- /package/dist/resources/extensions/{env-utils.js → gsd/env-utils.js} +0 -0
- /package/src/resources/extensions/{env-utils.ts → gsd/env-utils.ts} +0 -0
|
@@ -275,6 +275,717 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
|
275
275
|
}
|
|
276
276
|
await deps.stopAuto(ctx, pi, reason);
|
|
277
277
|
}
|
|
278
|
+
// ─── runPreDispatch ───────────────────────────────────────────────────────────
|
|
279
|
+
/**
|
|
280
|
+
* Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
|
|
281
|
+
* milestone transition, terminal conditions.
|
|
282
|
+
* Returns break to exit the loop, or next with PreDispatchData on success.
|
|
283
|
+
*/
|
|
284
|
+
async function runPreDispatch(ic, loopState) {
|
|
285
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
286
|
+
// Resource version guard
|
|
287
|
+
const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
|
|
288
|
+
if (staleMsg) {
|
|
289
|
+
await deps.stopAuto(ctx, pi, staleMsg);
|
|
290
|
+
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
291
|
+
return { action: "break", reason: "resources-stale" };
|
|
292
|
+
}
|
|
293
|
+
deps.invalidateAllCaches();
|
|
294
|
+
s.lastPromptCharCount = undefined;
|
|
295
|
+
s.lastBaselineCharCount = undefined;
|
|
296
|
+
// Pre-dispatch health gate
|
|
297
|
+
try {
|
|
298
|
+
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
299
|
+
if (healthGate.fixesApplied.length > 0) {
|
|
300
|
+
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
301
|
+
}
|
|
302
|
+
if (!healthGate.proceed) {
|
|
303
|
+
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
304
|
+
await deps.pauseAuto(ctx, pi);
|
|
305
|
+
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
306
|
+
return { action: "break", reason: "health-gate-failed" };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Non-fatal
|
|
311
|
+
}
|
|
312
|
+
// Sync project root artifacts into worktree
|
|
313
|
+
if (s.originalBasePath &&
|
|
314
|
+
s.basePath !== s.originalBasePath &&
|
|
315
|
+
s.currentMilestoneId) {
|
|
316
|
+
deps.syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
317
|
+
}
|
|
318
|
+
// Derive state
|
|
319
|
+
let state = await deps.deriveState(s.basePath);
|
|
320
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
321
|
+
let mid = state.activeMilestone?.id;
|
|
322
|
+
let midTitle = state.activeMilestone?.title;
|
|
323
|
+
debugLog("autoLoop", {
|
|
324
|
+
phase: "state-derived",
|
|
325
|
+
iteration: ic.iteration,
|
|
326
|
+
mid,
|
|
327
|
+
statePhase: state.phase,
|
|
328
|
+
});
|
|
329
|
+
// ── Milestone transition ────────────────────────────────────────────
|
|
330
|
+
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
331
|
+
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
332
|
+
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
333
|
+
deps.logCmuxEvent(prefs, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
334
|
+
const vizPrefs = prefs;
|
|
335
|
+
if (vizPrefs?.auto_visualize) {
|
|
336
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
337
|
+
}
|
|
338
|
+
if (vizPrefs?.auto_report !== false) {
|
|
339
|
+
try {
|
|
340
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Reset dispatch counters for new milestone
|
|
347
|
+
s.unitDispatchCount.clear();
|
|
348
|
+
s.unitRecoveryCount.clear();
|
|
349
|
+
s.unitLifetimeDispatches.clear();
|
|
350
|
+
loopState.recentUnits.length = 0;
|
|
351
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
352
|
+
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
353
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
354
|
+
// Opt-in: create draft PR on milestone completion
|
|
355
|
+
if (prefs?.git?.auto_pr) {
|
|
356
|
+
try {
|
|
357
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
358
|
+
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.`);
|
|
359
|
+
if (prUrl) {
|
|
360
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// Non-fatal — PR creation is best-effort
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
deps.invalidateAllCaches();
|
|
368
|
+
state = await deps.deriveState(s.basePath);
|
|
369
|
+
mid = state.activeMilestone?.id;
|
|
370
|
+
midTitle = state.activeMilestone?.title;
|
|
371
|
+
if (mid) {
|
|
372
|
+
if (deps.getIsolationMode() !== "none") {
|
|
373
|
+
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
374
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// mid is undefined — no milestone to capture integration branch for
|
|
381
|
+
}
|
|
382
|
+
const pendingIds = state.registry
|
|
383
|
+
.filter((m) => m.status !== "complete" && m.status !== "parked")
|
|
384
|
+
.map((m) => m.id);
|
|
385
|
+
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
386
|
+
}
|
|
387
|
+
if (mid) {
|
|
388
|
+
s.currentMilestoneId = mid;
|
|
389
|
+
deps.setActiveMilestoneId(s.basePath, mid);
|
|
390
|
+
}
|
|
391
|
+
// ── Terminal conditions ──────────────────────────────────────────────
|
|
392
|
+
if (!mid) {
|
|
393
|
+
if (s.currentUnit) {
|
|
394
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
395
|
+
}
|
|
396
|
+
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
397
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
398
|
+
// All milestones complete — merge milestone branch before stopping
|
|
399
|
+
if (s.currentMilestoneId) {
|
|
400
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
401
|
+
// Opt-in: create draft PR on milestone completion
|
|
402
|
+
if (prefs?.git?.auto_pr) {
|
|
403
|
+
try {
|
|
404
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
405
|
+
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.`);
|
|
406
|
+
if (prUrl) {
|
|
407
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Non-fatal — PR creation is best-effort
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
416
|
+
deps.logCmuxEvent(prefs, "All milestones complete.", "success");
|
|
417
|
+
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
418
|
+
}
|
|
419
|
+
else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
420
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
421
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
422
|
+
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
423
|
+
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
424
|
+
}
|
|
425
|
+
else if (state.phase === "blocked") {
|
|
426
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
427
|
+
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
428
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
429
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
430
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
const ids = incomplete.map((m) => m.id).join(", ");
|
|
434
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
435
|
+
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
436
|
+
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
437
|
+
}
|
|
438
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
439
|
+
return { action: "break", reason: "no-active-milestone" };
|
|
440
|
+
}
|
|
441
|
+
if (!midTitle) {
|
|
442
|
+
midTitle = mid;
|
|
443
|
+
ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
|
|
444
|
+
}
|
|
445
|
+
// Mid-merge safety check
|
|
446
|
+
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
447
|
+
deps.invalidateAllCaches();
|
|
448
|
+
state = await deps.deriveState(s.basePath);
|
|
449
|
+
mid = state.activeMilestone?.id;
|
|
450
|
+
midTitle = state.activeMilestone?.title;
|
|
451
|
+
}
|
|
452
|
+
if (!mid || !midTitle) {
|
|
453
|
+
const noMilestoneReason = !mid
|
|
454
|
+
? "No active milestone after merge reconciliation"
|
|
455
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
456
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
457
|
+
debugLog("autoLoop", {
|
|
458
|
+
phase: "exit",
|
|
459
|
+
reason: "no-milestone-after-reconciliation",
|
|
460
|
+
});
|
|
461
|
+
return { action: "break", reason: "no-milestone-after-reconciliation" };
|
|
462
|
+
}
|
|
463
|
+
// Terminal: complete
|
|
464
|
+
if (state.phase === "complete") {
|
|
465
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
466
|
+
if (s.currentMilestoneId) {
|
|
467
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
468
|
+
// Opt-in: create draft PR on milestone completion
|
|
469
|
+
if (prefs?.git?.auto_pr) {
|
|
470
|
+
try {
|
|
471
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
472
|
+
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.`);
|
|
473
|
+
if (prUrl) {
|
|
474
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// Non-fatal — PR creation is best-effort
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
483
|
+
deps.logCmuxEvent(prefs, `Milestone ${mid} complete.`, "success");
|
|
484
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
485
|
+
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
486
|
+
return { action: "break", reason: "milestone-complete" };
|
|
487
|
+
}
|
|
488
|
+
// Terminal: blocked
|
|
489
|
+
if (state.phase === "blocked") {
|
|
490
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
491
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
492
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
493
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
494
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
495
|
+
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
496
|
+
return { action: "break", reason: "blocked" };
|
|
497
|
+
}
|
|
498
|
+
return { action: "next", data: { state, mid, midTitle } };
|
|
499
|
+
}
|
|
500
|
+
// ─── runDispatch ──────────────────────────────────────────────────────────────
|
|
501
|
+
/**
|
|
502
|
+
* Phase 3: Dispatch resolution — resolve next unit, stuck detection, pre-dispatch hooks.
|
|
503
|
+
* Returns break/continue to control the loop, or next with IterationData on success.
|
|
504
|
+
*/
|
|
505
|
+
async function runDispatch(ic, preData, loopState) {
|
|
506
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
507
|
+
const { state, mid, midTitle } = preData;
|
|
508
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
509
|
+
debugLog("autoLoop", { phase: "dispatch-resolve", iteration: ic.iteration });
|
|
510
|
+
const dispatchResult = await deps.resolveDispatch({
|
|
511
|
+
basePath: s.basePath,
|
|
512
|
+
mid,
|
|
513
|
+
midTitle,
|
|
514
|
+
state,
|
|
515
|
+
prefs,
|
|
516
|
+
session: s,
|
|
517
|
+
});
|
|
518
|
+
if (dispatchResult.action === "stop") {
|
|
519
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
520
|
+
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
521
|
+
return { action: "break", reason: "dispatch-stop" };
|
|
522
|
+
}
|
|
523
|
+
if (dispatchResult.action !== "dispatch") {
|
|
524
|
+
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
525
|
+
await new Promise((r) => setImmediate(r));
|
|
526
|
+
return { action: "continue" };
|
|
527
|
+
}
|
|
528
|
+
let unitType = dispatchResult.unitType;
|
|
529
|
+
let unitId = dispatchResult.unitId;
|
|
530
|
+
let prompt = dispatchResult.prompt;
|
|
531
|
+
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
532
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
533
|
+
const derivedKey = `${unitType}/${unitId}`;
|
|
534
|
+
if (!s.pendingVerificationRetry) {
|
|
535
|
+
loopState.recentUnits.push({ key: derivedKey });
|
|
536
|
+
if (loopState.recentUnits.length > STUCK_WINDOW_SIZE)
|
|
537
|
+
loopState.recentUnits.shift();
|
|
538
|
+
const stuckSignal = detectStuck(loopState.recentUnits);
|
|
539
|
+
if (stuckSignal) {
|
|
540
|
+
debugLog("autoLoop", {
|
|
541
|
+
phase: "stuck-check",
|
|
542
|
+
unitType,
|
|
543
|
+
unitId,
|
|
544
|
+
reason: stuckSignal.reason,
|
|
545
|
+
recoveryAttempts: loopState.stuckRecoveryAttempts,
|
|
546
|
+
});
|
|
547
|
+
if (loopState.stuckRecoveryAttempts === 0) {
|
|
548
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
549
|
+
loopState.stuckRecoveryAttempts++;
|
|
550
|
+
const artifactExists = deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
551
|
+
if (artifactExists) {
|
|
552
|
+
debugLog("autoLoop", {
|
|
553
|
+
phase: "stuck-recovery",
|
|
554
|
+
level: 1,
|
|
555
|
+
action: "artifact-found",
|
|
556
|
+
});
|
|
557
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
558
|
+
deps.invalidateAllCaches();
|
|
559
|
+
return { action: "continue" };
|
|
560
|
+
}
|
|
561
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
|
|
562
|
+
deps.invalidateAllCaches();
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// Level 2: hard stop — genuinely stuck
|
|
566
|
+
debugLog("autoLoop", {
|
|
567
|
+
phase: "stuck-detected",
|
|
568
|
+
unitType,
|
|
569
|
+
unitId,
|
|
570
|
+
reason: stuckSignal.reason,
|
|
571
|
+
});
|
|
572
|
+
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
573
|
+
ctx.ui.notify(`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`, "error");
|
|
574
|
+
return { action: "break", reason: "stuck-detected" };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
// Progress detected — reset recovery counter
|
|
579
|
+
if (loopState.stuckRecoveryAttempts > 0) {
|
|
580
|
+
debugLog("autoLoop", {
|
|
581
|
+
phase: "stuck-counter-reset",
|
|
582
|
+
from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
|
|
583
|
+
to: derivedKey,
|
|
584
|
+
});
|
|
585
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Pre-dispatch hooks
|
|
590
|
+
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
591
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
592
|
+
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
593
|
+
}
|
|
594
|
+
if (preDispatchResult.action === "skip") {
|
|
595
|
+
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
596
|
+
await new Promise((r) => setImmediate(r));
|
|
597
|
+
return { action: "continue" };
|
|
598
|
+
}
|
|
599
|
+
if (preDispatchResult.action === "replace") {
|
|
600
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
601
|
+
if (preDispatchResult.unitType)
|
|
602
|
+
unitType = preDispatchResult.unitType;
|
|
603
|
+
}
|
|
604
|
+
else if (preDispatchResult.prompt) {
|
|
605
|
+
prompt = preDispatchResult.prompt;
|
|
606
|
+
}
|
|
607
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(s.basePath, deps.getMainBranch(s.basePath), unitType, unitId);
|
|
608
|
+
if (priorSliceBlocker) {
|
|
609
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
610
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
611
|
+
return { action: "break", reason: "prior-slice-blocker" };
|
|
612
|
+
}
|
|
613
|
+
const observabilityIssues = await deps.collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
614
|
+
return {
|
|
615
|
+
action: "next",
|
|
616
|
+
data: {
|
|
617
|
+
unitType, unitId, prompt, finalPrompt: prompt,
|
|
618
|
+
pauseAfterUatDispatch, observabilityIssues,
|
|
619
|
+
state, mid, midTitle,
|
|
620
|
+
isRetry: false, previousTier: undefined,
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
// ─── runGuards ────────────────────────────────────────────────────────────────
|
|
625
|
+
/**
|
|
626
|
+
* Phase 2: Guards — budget ceiling, context window, secrets re-check.
|
|
627
|
+
* Returns break to exit the loop, or next to proceed to dispatch.
|
|
628
|
+
*/
|
|
629
|
+
async function runGuards(ic, mid) {
|
|
630
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
631
|
+
// Budget ceiling guard
|
|
632
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
633
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
634
|
+
const currentLedger = deps.getLedger();
|
|
635
|
+
const totalCost = currentLedger
|
|
636
|
+
? deps.getProjectTotals(currentLedger.units).cost
|
|
637
|
+
: 0;
|
|
638
|
+
const budgetPct = totalCost / budgetCeiling;
|
|
639
|
+
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
640
|
+
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
641
|
+
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
642
|
+
const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
|
|
643
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
644
|
+
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel >= t.pct);
|
|
645
|
+
if (threshold) {
|
|
646
|
+
s.lastBudgetAlertLevel =
|
|
647
|
+
newBudgetAlertLevel;
|
|
648
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
649
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
650
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
651
|
+
if (budgetEnforcementAction === "halt") {
|
|
652
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
653
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
654
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
655
|
+
return { action: "break", reason: "budget-halt" };
|
|
656
|
+
}
|
|
657
|
+
if (budgetEnforcementAction === "pause") {
|
|
658
|
+
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
659
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
660
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
661
|
+
await deps.pauseAuto(ctx, pi);
|
|
662
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
663
|
+
return { action: "break", reason: "budget-pause" };
|
|
664
|
+
}
|
|
665
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
666
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
667
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
668
|
+
}
|
|
669
|
+
else if (threshold.pct < 100) {
|
|
670
|
+
// Sub-100% — simple notification
|
|
671
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
672
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
673
|
+
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
674
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else if (budgetAlertLevel === 0) {
|
|
678
|
+
s.lastBudgetAlertLevel = 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
s.lastBudgetAlertLevel = 0;
|
|
683
|
+
}
|
|
684
|
+
// Context window guard
|
|
685
|
+
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
686
|
+
if (contextThreshold > 0 && s.cmdCtx) {
|
|
687
|
+
const contextUsage = s.cmdCtx.getContextUsage();
|
|
688
|
+
if (contextUsage &&
|
|
689
|
+
contextUsage.percent !== null &&
|
|
690
|
+
contextUsage.percent >= contextThreshold) {
|
|
691
|
+
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
692
|
+
ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
|
|
693
|
+
deps.sendDesktopNotification("GSD", `Context ${contextUsage.percent}% — paused`, "warning", "attention");
|
|
694
|
+
await deps.pauseAuto(ctx, pi);
|
|
695
|
+
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
696
|
+
return { action: "break", reason: "context-window" };
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Secrets re-check gate
|
|
700
|
+
try {
|
|
701
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
702
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
703
|
+
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
704
|
+
if (result &&
|
|
705
|
+
result.applied &&
|
|
706
|
+
result.skipped &&
|
|
707
|
+
result.existingSkipped) {
|
|
708
|
+
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch (err) {
|
|
716
|
+
ctx.ui.notify(`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, "warning");
|
|
717
|
+
}
|
|
718
|
+
return { action: "next", data: undefined };
|
|
719
|
+
}
|
|
720
|
+
// ─── runUnitPhase ─────────────────────────────────────────────────────────────
|
|
721
|
+
/**
|
|
722
|
+
* Phase 4: Unit execution — dispatch prompt, await agent_end, closeout, artifact verify.
|
|
723
|
+
* Returns break or next with unitStartedAt for downstream phases.
|
|
724
|
+
*/
|
|
725
|
+
async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
726
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
727
|
+
const { unitType, unitId, prompt, observabilityIssues, state, mid } = iterData;
|
|
728
|
+
debugLog("autoLoop", {
|
|
729
|
+
phase: "unit-execution",
|
|
730
|
+
iteration: ic.iteration,
|
|
731
|
+
unitType,
|
|
732
|
+
unitId,
|
|
733
|
+
});
|
|
734
|
+
// Detect retry and capture previous tier for escalation
|
|
735
|
+
const isRetry = !!(s.currentUnit &&
|
|
736
|
+
s.currentUnit.type === unitType &&
|
|
737
|
+
s.currentUnit.id === unitId);
|
|
738
|
+
const previousTier = s.currentUnitRouting?.tier;
|
|
739
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
740
|
+
deps.captureAvailableSkills();
|
|
741
|
+
deps.writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
742
|
+
phase: "dispatched",
|
|
743
|
+
wrapupWarningSent: false,
|
|
744
|
+
timeoutAt: null,
|
|
745
|
+
lastProgressAt: s.currentUnit.startedAt,
|
|
746
|
+
progressCount: 0,
|
|
747
|
+
lastProgressKind: "dispatch",
|
|
748
|
+
});
|
|
749
|
+
// Status bar + progress widget
|
|
750
|
+
ctx.ui.setStatus("gsd-auto", "auto");
|
|
751
|
+
if (mid)
|
|
752
|
+
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
753
|
+
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
754
|
+
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
755
|
+
// Prompt injection
|
|
756
|
+
let finalPrompt = prompt;
|
|
757
|
+
if (s.pendingVerificationRetry) {
|
|
758
|
+
const retryCtx = s.pendingVerificationRetry;
|
|
759
|
+
s.pendingVerificationRetry = null;
|
|
760
|
+
const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
761
|
+
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
762
|
+
"\n\n[...failure context truncated]"
|
|
763
|
+
: retryCtx.failureContext;
|
|
764
|
+
finalPrompt = `**VERIFICATION FAILED — AUTO-FIX ATTEMPT ${retryCtx.attempt}**\n\nThe verification gate ran after your previous attempt and found failures. Fix these issues before completing the task.\n\n${capped}\n\n---\n\n${finalPrompt}`;
|
|
765
|
+
}
|
|
766
|
+
if (s.pendingCrashRecovery) {
|
|
767
|
+
const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
768
|
+
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
769
|
+
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
770
|
+
: s.pendingCrashRecovery;
|
|
771
|
+
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
772
|
+
s.pendingCrashRecovery = null;
|
|
773
|
+
}
|
|
774
|
+
else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
775
|
+
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
776
|
+
if (diagnostic) {
|
|
777
|
+
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
778
|
+
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
779
|
+
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
780
|
+
: diagnostic;
|
|
781
|
+
finalPrompt = `**RETRY — your previous attempt did not produce the required artifact.**\n\nDiagnostic from previous attempt:\n${cappedDiag}\n\nFix whatever went wrong and make sure you write the required file this time.\n\n---\n\n${finalPrompt}`;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const repairBlock = deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
785
|
+
if (repairBlock) {
|
|
786
|
+
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
787
|
+
}
|
|
788
|
+
// Prompt char measurement
|
|
789
|
+
s.lastPromptCharCount = finalPrompt.length;
|
|
790
|
+
s.lastBaselineCharCount = undefined;
|
|
791
|
+
if (deps.isDbAvailable()) {
|
|
792
|
+
try {
|
|
793
|
+
const { inlineGsdRootFile } = await importExtensionModule(import.meta.url, "./auto-prompts.js");
|
|
794
|
+
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
795
|
+
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
796
|
+
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
797
|
+
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
798
|
+
]);
|
|
799
|
+
s.lastBaselineCharCount =
|
|
800
|
+
(decisionsContent?.length ?? 0) +
|
|
801
|
+
(requirementsContent?.length ?? 0) +
|
|
802
|
+
(projectContent?.length ?? 0);
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// Non-fatal
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Cache-optimize prompt section ordering
|
|
809
|
+
try {
|
|
810
|
+
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
811
|
+
}
|
|
812
|
+
catch (reorderErr) {
|
|
813
|
+
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
814
|
+
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
815
|
+
}
|
|
816
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
817
|
+
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
|
|
818
|
+
s.currentUnitRouting =
|
|
819
|
+
modelResult.routing;
|
|
820
|
+
// Start unit supervision
|
|
821
|
+
deps.clearUnitTimeout();
|
|
822
|
+
deps.startUnitSupervision({
|
|
823
|
+
s,
|
|
824
|
+
ctx,
|
|
825
|
+
pi,
|
|
826
|
+
unitType,
|
|
827
|
+
unitId,
|
|
828
|
+
prefs,
|
|
829
|
+
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
830
|
+
buildRecoveryContext: () => ({}),
|
|
831
|
+
pauseAuto: deps.pauseAuto,
|
|
832
|
+
});
|
|
833
|
+
// Session + send + await
|
|
834
|
+
const sessionFile = deps.getSessionFile(ctx);
|
|
835
|
+
deps.updateSessionLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
836
|
+
deps.writeLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
837
|
+
debugLog("autoLoop", {
|
|
838
|
+
phase: "runUnit-start",
|
|
839
|
+
iteration: ic.iteration,
|
|
840
|
+
unitType,
|
|
841
|
+
unitId,
|
|
842
|
+
});
|
|
843
|
+
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt);
|
|
844
|
+
debugLog("autoLoop", {
|
|
845
|
+
phase: "runUnit-end",
|
|
846
|
+
iteration: ic.iteration,
|
|
847
|
+
unitType,
|
|
848
|
+
unitId,
|
|
849
|
+
status: unitResult.status,
|
|
850
|
+
});
|
|
851
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
852
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
853
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
854
|
+
if (lastEntry) {
|
|
855
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
else if (unitResult.event?.messages?.length) {
|
|
859
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
860
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
861
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
862
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
863
|
+
if (lastEntry) {
|
|
864
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (unitResult.status === "cancelled") {
|
|
869
|
+
ctx.ui.notify(`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`, "warning");
|
|
870
|
+
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
871
|
+
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
872
|
+
return { action: "break", reason: "session-failed" };
|
|
873
|
+
}
|
|
874
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
875
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
876
|
+
// crash between iterations.
|
|
877
|
+
await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
|
|
878
|
+
if (s.currentUnitRouting) {
|
|
879
|
+
deps.recordOutcome(unitType, s.currentUnitRouting.tier, true);
|
|
880
|
+
}
|
|
881
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
882
|
+
const artifactVerified = isHookUnit ||
|
|
883
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
884
|
+
if (artifactVerified) {
|
|
885
|
+
s.completedUnits.push({
|
|
886
|
+
type: unitType,
|
|
887
|
+
id: unitId,
|
|
888
|
+
startedAt: s.currentUnit.startedAt,
|
|
889
|
+
finishedAt: Date.now(),
|
|
890
|
+
});
|
|
891
|
+
if (s.completedUnits.length > 200) {
|
|
892
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
893
|
+
}
|
|
894
|
+
// Flush completed-units to disk so the record survives crashes
|
|
895
|
+
try {
|
|
896
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
897
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
898
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
899
|
+
}
|
|
900
|
+
catch { /* non-fatal: disk flush failure */ }
|
|
901
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
902
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
903
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
904
|
+
}
|
|
905
|
+
return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
|
|
906
|
+
}
|
|
907
|
+
// ─── runFinalize ──────────────────────────────────────────────────────────────
|
|
908
|
+
/**
|
|
909
|
+
* Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
|
|
910
|
+
* Returns break/continue/next to control the outer loop.
|
|
911
|
+
*/
|
|
912
|
+
async function runFinalize(ic, iterData, sidecarItem) {
|
|
913
|
+
const { ctx, pi, s, deps } = ic;
|
|
914
|
+
const { pauseAfterUatDispatch } = iterData;
|
|
915
|
+
debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
|
|
916
|
+
// Clear unit timeout (unit completed)
|
|
917
|
+
deps.clearUnitTimeout();
|
|
918
|
+
// Post-unit context for pre/post verification
|
|
919
|
+
const postUnitCtx = {
|
|
920
|
+
s,
|
|
921
|
+
ctx,
|
|
922
|
+
pi,
|
|
923
|
+
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
924
|
+
lockBase: deps.lockBase,
|
|
925
|
+
stopAuto: deps.stopAuto,
|
|
926
|
+
pauseAuto: deps.pauseAuto,
|
|
927
|
+
updateProgressWidget: deps.updateProgressWidget,
|
|
928
|
+
};
|
|
929
|
+
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
930
|
+
// Sidecar items use lightweight pre-verification opts
|
|
931
|
+
const preVerificationOpts = sidecarItem
|
|
932
|
+
? sidecarItem.kind === "hook"
|
|
933
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
934
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
935
|
+
: undefined;
|
|
936
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
937
|
+
if (preResult === "dispatched") {
|
|
938
|
+
debugLog("autoLoop", {
|
|
939
|
+
phase: "exit",
|
|
940
|
+
reason: "pre-verification-dispatched",
|
|
941
|
+
});
|
|
942
|
+
return { action: "break", reason: "pre-verification-dispatched" };
|
|
943
|
+
}
|
|
944
|
+
if (pauseAfterUatDispatch) {
|
|
945
|
+
ctx.ui.notify("UAT requires human execution. Auto-mode will pause after this unit writes the result file.", "info");
|
|
946
|
+
await deps.pauseAuto(ctx, pi);
|
|
947
|
+
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
948
|
+
return { action: "break", reason: "uat-pause" };
|
|
949
|
+
}
|
|
950
|
+
// Verification gate
|
|
951
|
+
// Hook sidecar items skip verification entirely.
|
|
952
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
953
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
954
|
+
if (!skipVerification) {
|
|
955
|
+
const verificationResult = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
956
|
+
if (verificationResult === "pause") {
|
|
957
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
958
|
+
return { action: "break", reason: "verification-pause" };
|
|
959
|
+
}
|
|
960
|
+
if (verificationResult === "retry") {
|
|
961
|
+
if (sidecarItem) {
|
|
962
|
+
// Sidecar verification retries are skipped — just continue
|
|
963
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration });
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
967
|
+
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
968
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration: ic.iteration });
|
|
969
|
+
return { action: "continue" };
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
974
|
+
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
975
|
+
if (postResult === "stopped") {
|
|
976
|
+
debugLog("autoLoop", {
|
|
977
|
+
phase: "exit",
|
|
978
|
+
reason: "post-verification-stopped",
|
|
979
|
+
});
|
|
980
|
+
return { action: "break", reason: "post-verification-stopped" };
|
|
981
|
+
}
|
|
982
|
+
if (postResult === "step-wizard") {
|
|
983
|
+
// Step mode — exit the loop (caller handles wizard)
|
|
984
|
+
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
985
|
+
return { action: "break", reason: "step-wizard" };
|
|
986
|
+
}
|
|
987
|
+
return { action: "next", data: undefined };
|
|
988
|
+
}
|
|
278
989
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
279
990
|
/**
|
|
280
991
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
@@ -287,10 +998,7 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
|
287
998
|
export async function autoLoop(ctx, pi, s, deps) {
|
|
288
999
|
debugLog("autoLoop", { phase: "enter" });
|
|
289
1000
|
let iteration = 0;
|
|
290
|
-
|
|
291
|
-
const recentUnits = [];
|
|
292
|
-
const STUCK_WINDOW_SIZE = 6;
|
|
293
|
-
let stuckRecoveryAttempts = 0;
|
|
1001
|
+
const loopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
294
1002
|
let consecutiveErrors = 0;
|
|
295
1003
|
while (s.active) {
|
|
296
1004
|
iteration++;
|
|
@@ -341,687 +1049,53 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
341
1049
|
break;
|
|
342
1050
|
}
|
|
343
1051
|
}
|
|
344
|
-
|
|
345
|
-
let
|
|
346
|
-
let unitId;
|
|
347
|
-
let prompt;
|
|
348
|
-
let pauseAfterUatDispatch = false;
|
|
349
|
-
let state;
|
|
350
|
-
let mid;
|
|
351
|
-
let midTitle;
|
|
352
|
-
let observabilityIssues = [];
|
|
1052
|
+
const ic = { ctx, pi, s, deps, prefs, iteration };
|
|
1053
|
+
let iterData;
|
|
353
1054
|
if (!sidecarItem) {
|
|
354
|
-
// ── Phase 1: Pre-dispatch
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (staleMsg) {
|
|
358
|
-
await deps.stopAuto(ctx, pi, staleMsg);
|
|
359
|
-
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
|
-
deps.invalidateAllCaches();
|
|
363
|
-
s.lastPromptCharCount = undefined;
|
|
364
|
-
s.lastBaselineCharCount = undefined;
|
|
365
|
-
// Pre-dispatch health gate
|
|
366
|
-
try {
|
|
367
|
-
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
368
|
-
if (healthGate.fixesApplied.length > 0) {
|
|
369
|
-
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
370
|
-
}
|
|
371
|
-
if (!healthGate.proceed) {
|
|
372
|
-
ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
|
|
373
|
-
await deps.pauseAuto(ctx, pi);
|
|
374
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
catch {
|
|
379
|
-
// Non-fatal
|
|
380
|
-
}
|
|
381
|
-
// Sync project root artifacts into worktree
|
|
382
|
-
if (s.originalBasePath &&
|
|
383
|
-
s.basePath !== s.originalBasePath &&
|
|
384
|
-
s.currentMilestoneId) {
|
|
385
|
-
deps.syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
|
|
386
|
-
}
|
|
387
|
-
// Derive state
|
|
388
|
-
state = await deps.deriveState(s.basePath);
|
|
389
|
-
deps.syncCmuxSidebar(prefs, state);
|
|
390
|
-
mid = state.activeMilestone?.id;
|
|
391
|
-
midTitle = state.activeMilestone?.title;
|
|
392
|
-
debugLog("autoLoop", {
|
|
393
|
-
phase: "state-derived",
|
|
394
|
-
iteration,
|
|
395
|
-
mid,
|
|
396
|
-
statePhase: state.phase,
|
|
397
|
-
});
|
|
398
|
-
// ── Milestone transition ────────────────────────────────────────────
|
|
399
|
-
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
400
|
-
ctx.ui.notify(`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`, "info");
|
|
401
|
-
deps.sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
|
|
402
|
-
deps.logCmuxEvent(prefs, `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`, "success");
|
|
403
|
-
const vizPrefs = prefs;
|
|
404
|
-
if (vizPrefs?.auto_visualize) {
|
|
405
|
-
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
406
|
-
}
|
|
407
|
-
if (vizPrefs?.auto_report !== false) {
|
|
408
|
-
try {
|
|
409
|
-
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
410
|
-
}
|
|
411
|
-
catch (err) {
|
|
412
|
-
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// Reset dispatch counters for new milestone
|
|
416
|
-
s.unitDispatchCount.clear();
|
|
417
|
-
s.unitRecoveryCount.clear();
|
|
418
|
-
s.unitLifetimeDispatches.clear();
|
|
419
|
-
recentUnits.length = 0;
|
|
420
|
-
stuckRecoveryAttempts = 0;
|
|
421
|
-
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
422
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
423
|
-
// Opt-in: create draft PR on milestone completion
|
|
424
|
-
if (prefs?.git?.auto_pr) {
|
|
425
|
-
try {
|
|
426
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
427
|
-
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.`);
|
|
428
|
-
if (prUrl) {
|
|
429
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
catch {
|
|
433
|
-
// Non-fatal — PR creation is best-effort
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
deps.invalidateAllCaches();
|
|
437
|
-
state = await deps.deriveState(s.basePath);
|
|
438
|
-
mid = state.activeMilestone?.id;
|
|
439
|
-
midTitle = state.activeMilestone?.title;
|
|
440
|
-
if (mid) {
|
|
441
|
-
if (deps.getIsolationMode() !== "none") {
|
|
442
|
-
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
443
|
-
commitDocs: prefs?.git?.commit_docs,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
// mid is undefined — no milestone to capture integration branch for
|
|
450
|
-
}
|
|
451
|
-
const pendingIds = state.registry
|
|
452
|
-
.filter((m) => m.status !== "complete" && m.status !== "parked")
|
|
453
|
-
.map((m) => m.id);
|
|
454
|
-
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
455
|
-
}
|
|
456
|
-
if (mid) {
|
|
457
|
-
s.currentMilestoneId = mid;
|
|
458
|
-
deps.setActiveMilestoneId(s.basePath, mid);
|
|
459
|
-
}
|
|
460
|
-
// ── Terminal conditions ──────────────────────────────────────────────
|
|
461
|
-
if (!mid) {
|
|
462
|
-
if (s.currentUnit) {
|
|
463
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
464
|
-
}
|
|
465
|
-
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
466
|
-
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
467
|
-
// All milestones complete — merge milestone branch before stopping
|
|
468
|
-
if (s.currentMilestoneId) {
|
|
469
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
470
|
-
// Opt-in: create draft PR on milestone completion
|
|
471
|
-
if (prefs?.git?.auto_pr) {
|
|
472
|
-
try {
|
|
473
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
474
|
-
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.`);
|
|
475
|
-
if (prUrl) {
|
|
476
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
catch {
|
|
480
|
-
// Non-fatal — PR creation is best-effort
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
|
|
485
|
-
deps.logCmuxEvent(prefs, "All milestones complete.", "success");
|
|
486
|
-
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
487
|
-
}
|
|
488
|
-
else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
489
|
-
// Empty registry — no milestones visible, likely a path resolution bug
|
|
490
|
-
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
491
|
-
ctx.ui.notify(`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`, "error");
|
|
492
|
-
await deps.stopAuto(ctx, pi, `No milestones found — check basePath resolution`);
|
|
493
|
-
}
|
|
494
|
-
else if (state.phase === "blocked") {
|
|
495
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
496
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
497
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
498
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
499
|
-
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
const ids = incomplete.map((m) => m.id).join(", ");
|
|
503
|
-
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
504
|
-
ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
|
|
505
|
-
await deps.stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
|
|
506
|
-
}
|
|
507
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
1055
|
+
// ── Phase 1: Pre-dispatch ─────────────────────────────────────────
|
|
1056
|
+
const preDispatchResult = await runPreDispatch(ic, loopState);
|
|
1057
|
+
if (preDispatchResult.action === "break")
|
|
508
1058
|
break;
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
516
|
-
deps.invalidateAllCaches();
|
|
517
|
-
state = await deps.deriveState(s.basePath);
|
|
518
|
-
mid = state.activeMilestone?.id;
|
|
519
|
-
midTitle = state.activeMilestone?.title;
|
|
520
|
-
}
|
|
521
|
-
if (!mid || !midTitle) {
|
|
522
|
-
const noMilestoneReason = !mid
|
|
523
|
-
? "No active milestone after merge reconciliation"
|
|
524
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
525
|
-
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
526
|
-
debugLog("autoLoop", {
|
|
527
|
-
phase: "exit",
|
|
528
|
-
reason: "no-milestone-after-reconciliation",
|
|
529
|
-
});
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
// Terminal: complete
|
|
533
|
-
if (state.phase === "complete") {
|
|
534
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
535
|
-
if (s.currentMilestoneId) {
|
|
536
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
537
|
-
// Opt-in: create draft PR on milestone completion
|
|
538
|
-
if (prefs?.git?.auto_pr) {
|
|
539
|
-
try {
|
|
540
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
541
|
-
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.`);
|
|
542
|
-
if (prUrl) {
|
|
543
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
catch {
|
|
547
|
-
// Non-fatal — PR creation is best-effort
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
552
|
-
deps.logCmuxEvent(prefs, `Milestone ${mid} complete.`, "success");
|
|
553
|
-
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
554
|
-
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
555
|
-
break;
|
|
556
|
-
}
|
|
557
|
-
// Terminal: blocked
|
|
558
|
-
if (state.phase === "blocked") {
|
|
559
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
560
|
-
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
561
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
562
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
563
|
-
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
564
|
-
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
1059
|
+
if (preDispatchResult.action === "continue")
|
|
1060
|
+
continue;
|
|
1061
|
+
const preData = preDispatchResult.data;
|
|
1062
|
+
// ── Phase 2: Guards ───────────────────────────────────────────────
|
|
1063
|
+
const guardsResult = await runGuards(ic, preData.mid);
|
|
1064
|
+
if (guardsResult.action === "break")
|
|
565
1065
|
break;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const budgetCeiling = prefs?.budget_ceiling;
|
|
570
|
-
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
571
|
-
const currentLedger = deps.getLedger();
|
|
572
|
-
const totalCost = currentLedger
|
|
573
|
-
? deps.getProjectTotals(currentLedger.units).cost
|
|
574
|
-
: 0;
|
|
575
|
-
const budgetPct = totalCost / budgetCeiling;
|
|
576
|
-
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
577
|
-
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
578
|
-
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
579
|
-
const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
|
|
580
|
-
// Data-driven threshold check — loop descending, fire first match
|
|
581
|
-
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel >= t.pct);
|
|
582
|
-
if (threshold) {
|
|
583
|
-
s.lastBudgetAlertLevel =
|
|
584
|
-
newBudgetAlertLevel;
|
|
585
|
-
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
586
|
-
// 100% — special enforcement logic (halt/pause/warn)
|
|
587
|
-
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
588
|
-
if (budgetEnforcementAction === "halt") {
|
|
589
|
-
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
590
|
-
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
591
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
if (budgetEnforcementAction === "pause") {
|
|
595
|
-
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
596
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
597
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
598
|
-
await deps.pauseAuto(ctx, pi);
|
|
599
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
603
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
604
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
605
|
-
}
|
|
606
|
-
else if (threshold.pct < 100) {
|
|
607
|
-
// Sub-100% — simple notification
|
|
608
|
-
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
609
|
-
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
610
|
-
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
611
|
-
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
else if (budgetAlertLevel === 0) {
|
|
615
|
-
s.lastBudgetAlertLevel = 0;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
else {
|
|
619
|
-
s.lastBudgetAlertLevel = 0;
|
|
620
|
-
}
|
|
621
|
-
// Context window guard
|
|
622
|
-
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
623
|
-
if (contextThreshold > 0 && s.cmdCtx) {
|
|
624
|
-
const contextUsage = s.cmdCtx.getContextUsage();
|
|
625
|
-
if (contextUsage &&
|
|
626
|
-
contextUsage.percent !== null &&
|
|
627
|
-
contextUsage.percent >= contextThreshold) {
|
|
628
|
-
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
629
|
-
ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
|
|
630
|
-
deps.sendDesktopNotification("GSD", `Context ${contextUsage.percent}% — paused`, "warning", "attention");
|
|
631
|
-
await deps.pauseAuto(ctx, pi);
|
|
632
|
-
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
// Secrets re-check gate
|
|
637
|
-
try {
|
|
638
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
639
|
-
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
640
|
-
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
641
|
-
if (result &&
|
|
642
|
-
result.applied &&
|
|
643
|
-
result.skipped &&
|
|
644
|
-
result.existingSkipped) {
|
|
645
|
-
ctx.ui.notify(`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`, "info");
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
catch (err) {
|
|
653
|
-
ctx.ui.notify(`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`, "warning");
|
|
654
|
-
}
|
|
655
|
-
// ── Phase 3: Dispatch resolution ────────────────────────────────────
|
|
656
|
-
debugLog("autoLoop", { phase: "dispatch-resolve", iteration });
|
|
657
|
-
const dispatchResult = await deps.resolveDispatch({
|
|
658
|
-
basePath: s.basePath,
|
|
659
|
-
mid,
|
|
660
|
-
midTitle: midTitle,
|
|
661
|
-
state,
|
|
662
|
-
prefs,
|
|
663
|
-
session: s,
|
|
664
|
-
});
|
|
665
|
-
if (dispatchResult.action === "stop") {
|
|
666
|
-
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
667
|
-
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1066
|
+
// ── Phase 3: Dispatch ─────────────────────────────────────────────
|
|
1067
|
+
const dispatchResult = await runDispatch(ic, preData, loopState);
|
|
1068
|
+
if (dispatchResult.action === "break")
|
|
668
1069
|
break;
|
|
669
|
-
|
|
670
|
-
if (dispatchResult.action !== "dispatch") {
|
|
671
|
-
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
672
|
-
await new Promise((r) => setImmediate(r));
|
|
1070
|
+
if (dispatchResult.action === "continue")
|
|
673
1071
|
continue;
|
|
674
|
-
|
|
675
|
-
unitType = dispatchResult.unitType;
|
|
676
|
-
unitId = dispatchResult.unitId;
|
|
677
|
-
prompt = dispatchResult.prompt;
|
|
678
|
-
pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
679
|
-
// ── Sliding-window stuck detection with graduated recovery ──
|
|
680
|
-
const derivedKey = `${unitType}/${unitId}`;
|
|
681
|
-
if (!s.pendingVerificationRetry) {
|
|
682
|
-
recentUnits.push({ key: derivedKey });
|
|
683
|
-
if (recentUnits.length > STUCK_WINDOW_SIZE)
|
|
684
|
-
recentUnits.shift();
|
|
685
|
-
const stuckSignal = detectStuck(recentUnits);
|
|
686
|
-
if (stuckSignal) {
|
|
687
|
-
debugLog("autoLoop", {
|
|
688
|
-
phase: "stuck-check",
|
|
689
|
-
unitType,
|
|
690
|
-
unitId,
|
|
691
|
-
reason: stuckSignal.reason,
|
|
692
|
-
recoveryAttempts: stuckRecoveryAttempts,
|
|
693
|
-
});
|
|
694
|
-
if (stuckRecoveryAttempts === 0) {
|
|
695
|
-
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
696
|
-
stuckRecoveryAttempts++;
|
|
697
|
-
const artifactExists = deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
698
|
-
if (artifactExists) {
|
|
699
|
-
debugLog("autoLoop", {
|
|
700
|
-
phase: "stuck-recovery",
|
|
701
|
-
level: 1,
|
|
702
|
-
action: "artifact-found",
|
|
703
|
-
});
|
|
704
|
-
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
705
|
-
deps.invalidateAllCaches();
|
|
706
|
-
continue;
|
|
707
|
-
}
|
|
708
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
|
|
709
|
-
deps.invalidateAllCaches();
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
712
|
-
// Level 2: hard stop — genuinely stuck
|
|
713
|
-
debugLog("autoLoop", {
|
|
714
|
-
phase: "stuck-detected",
|
|
715
|
-
unitType,
|
|
716
|
-
unitId,
|
|
717
|
-
reason: stuckSignal.reason,
|
|
718
|
-
});
|
|
719
|
-
await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
|
|
720
|
-
ctx.ui.notify(`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`, "error");
|
|
721
|
-
break;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
else {
|
|
725
|
-
// Progress detected — reset recovery counter
|
|
726
|
-
if (stuckRecoveryAttempts > 0) {
|
|
727
|
-
debugLog("autoLoop", {
|
|
728
|
-
phase: "stuck-counter-reset",
|
|
729
|
-
from: recentUnits[recentUnits.length - 2]?.key ?? "",
|
|
730
|
-
to: derivedKey,
|
|
731
|
-
});
|
|
732
|
-
stuckRecoveryAttempts = 0;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// Pre-dispatch hooks
|
|
737
|
-
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
738
|
-
if (preDispatchResult.firedHooks.length > 0) {
|
|
739
|
-
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
740
|
-
}
|
|
741
|
-
if (preDispatchResult.action === "skip") {
|
|
742
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
743
|
-
await new Promise((r) => setImmediate(r));
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
if (preDispatchResult.action === "replace") {
|
|
747
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
748
|
-
if (preDispatchResult.unitType)
|
|
749
|
-
unitType = preDispatchResult.unitType;
|
|
750
|
-
}
|
|
751
|
-
else if (preDispatchResult.prompt) {
|
|
752
|
-
prompt = preDispatchResult.prompt;
|
|
753
|
-
}
|
|
754
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(s.basePath, deps.getMainBranch(s.basePath), unitType, unitId);
|
|
755
|
-
if (priorSliceBlocker) {
|
|
756
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
757
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
758
|
-
break;
|
|
759
|
-
}
|
|
760
|
-
observabilityIssues = await deps.collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
|
|
761
|
-
// Derive state for shared use in execution phase
|
|
762
|
-
// (state, mid, midTitle already set above)
|
|
1072
|
+
iterData = dispatchResult.data;
|
|
763
1073
|
}
|
|
764
1074
|
else {
|
|
765
1075
|
// ── Sidecar path: use values from the sidecar item directly ──
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
unitId,
|
|
780
|
-
});
|
|
781
|
-
// Detect retry and capture previous tier for escalation
|
|
782
|
-
const isRetry = !!(s.currentUnit &&
|
|
783
|
-
s.currentUnit.type === unitType &&
|
|
784
|
-
s.currentUnit.id === unitId);
|
|
785
|
-
const previousTier = s.currentUnitRouting?.tier;
|
|
786
|
-
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
787
|
-
deps.captureAvailableSkills();
|
|
788
|
-
deps.writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
|
|
789
|
-
phase: "dispatched",
|
|
790
|
-
wrapupWarningSent: false,
|
|
791
|
-
timeoutAt: null,
|
|
792
|
-
lastProgressAt: s.currentUnit.startedAt,
|
|
793
|
-
progressCount: 0,
|
|
794
|
-
lastProgressKind: "dispatch",
|
|
795
|
-
});
|
|
796
|
-
// Status bar + progress widget
|
|
797
|
-
ctx.ui.setStatus("gsd-auto", "auto");
|
|
798
|
-
if (mid)
|
|
799
|
-
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
800
|
-
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
801
|
-
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
802
|
-
// Prompt injection
|
|
803
|
-
let finalPrompt = prompt;
|
|
804
|
-
if (s.pendingVerificationRetry) {
|
|
805
|
-
const retryCtx = s.pendingVerificationRetry;
|
|
806
|
-
s.pendingVerificationRetry = null;
|
|
807
|
-
const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
808
|
-
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
809
|
-
"\n\n[...failure context truncated]"
|
|
810
|
-
: retryCtx.failureContext;
|
|
811
|
-
finalPrompt = `**VERIFICATION FAILED — AUTO-FIX ATTEMPT ${retryCtx.attempt}**\n\nThe verification gate ran after your previous attempt and found failures. Fix these issues before completing the task.\n\n${capped}\n\n---\n\n${finalPrompt}`;
|
|
812
|
-
}
|
|
813
|
-
if (s.pendingCrashRecovery) {
|
|
814
|
-
const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
815
|
-
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
816
|
-
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
817
|
-
: s.pendingCrashRecovery;
|
|
818
|
-
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
819
|
-
s.pendingCrashRecovery = null;
|
|
820
|
-
}
|
|
821
|
-
else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
822
|
-
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
823
|
-
if (diagnostic) {
|
|
824
|
-
const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
|
|
825
|
-
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
826
|
-
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
827
|
-
: diagnostic;
|
|
828
|
-
finalPrompt = `**RETRY — your previous attempt did not produce the required artifact.**\n\nDiagnostic from previous attempt:\n${cappedDiag}\n\nFix whatever went wrong and make sure you write the required file this time.\n\n---\n\n${finalPrompt}`;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
const repairBlock = deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
832
|
-
if (repairBlock) {
|
|
833
|
-
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
834
|
-
}
|
|
835
|
-
// Prompt char measurement
|
|
836
|
-
s.lastPromptCharCount = finalPrompt.length;
|
|
837
|
-
s.lastBaselineCharCount = undefined;
|
|
838
|
-
if (deps.isDbAvailable()) {
|
|
839
|
-
try {
|
|
840
|
-
const { inlineGsdRootFile } = await importExtensionModule(import.meta.url, "./auto-prompts.js");
|
|
841
|
-
const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
|
|
842
|
-
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
843
|
-
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
844
|
-
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
845
|
-
]);
|
|
846
|
-
s.lastBaselineCharCount =
|
|
847
|
-
(decisionsContent?.length ?? 0) +
|
|
848
|
-
(requirementsContent?.length ?? 0) +
|
|
849
|
-
(projectContent?.length ?? 0);
|
|
850
|
-
}
|
|
851
|
-
catch {
|
|
852
|
-
// Non-fatal
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
// Cache-optimize prompt section ordering
|
|
856
|
-
try {
|
|
857
|
-
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
858
|
-
}
|
|
859
|
-
catch (reorderErr) {
|
|
860
|
-
const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
861
|
-
process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
|
|
862
|
-
}
|
|
863
|
-
// Select and apply model (with tier escalation on retry — normal units only)
|
|
864
|
-
const modelResult = await deps.selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel, sidecarItem ? undefined : { isRetry, previousTier });
|
|
865
|
-
s.currentUnitRouting =
|
|
866
|
-
modelResult.routing;
|
|
867
|
-
// Start unit supervision
|
|
868
|
-
deps.clearUnitTimeout();
|
|
869
|
-
deps.startUnitSupervision({
|
|
870
|
-
s,
|
|
871
|
-
ctx,
|
|
872
|
-
pi,
|
|
873
|
-
unitType,
|
|
874
|
-
unitId,
|
|
875
|
-
prefs,
|
|
876
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
877
|
-
buildRecoveryContext: () => ({}),
|
|
878
|
-
pauseAuto: deps.pauseAuto,
|
|
879
|
-
});
|
|
880
|
-
// Session + send + await
|
|
881
|
-
const sessionFile = deps.getSessionFile(ctx);
|
|
882
|
-
deps.updateSessionLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
883
|
-
deps.writeLock(deps.lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
|
|
884
|
-
debugLog("autoLoop", {
|
|
885
|
-
phase: "runUnit-start",
|
|
886
|
-
iteration,
|
|
887
|
-
unitType,
|
|
888
|
-
unitId,
|
|
889
|
-
});
|
|
890
|
-
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt);
|
|
891
|
-
debugLog("autoLoop", {
|
|
892
|
-
phase: "runUnit-end",
|
|
893
|
-
iteration,
|
|
894
|
-
unitType,
|
|
895
|
-
unitId,
|
|
896
|
-
status: unitResult.status,
|
|
897
|
-
});
|
|
898
|
-
// Tag the most recent window entry with error info for stuck detection
|
|
899
|
-
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
900
|
-
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
901
|
-
if (lastEntry) {
|
|
902
|
-
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
else if (unitResult.event?.messages?.length) {
|
|
906
|
-
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
907
|
-
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
908
|
-
if (/error|fail|exception/i.test(msgStr)) {
|
|
909
|
-
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
910
|
-
if (lastEntry) {
|
|
911
|
-
lastEntry.error = msgStr.slice(0, 200);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
1076
|
+
const sidecarState = await deps.deriveState(s.basePath);
|
|
1077
|
+
iterData = {
|
|
1078
|
+
unitType: sidecarItem.unitType,
|
|
1079
|
+
unitId: sidecarItem.unitId,
|
|
1080
|
+
prompt: sidecarItem.prompt,
|
|
1081
|
+
finalPrompt: sidecarItem.prompt,
|
|
1082
|
+
pauseAfterUatDispatch: false,
|
|
1083
|
+
observabilityIssues: [],
|
|
1084
|
+
state: sidecarState,
|
|
1085
|
+
mid: sidecarState.activeMilestone?.id,
|
|
1086
|
+
midTitle: sidecarState.activeMilestone?.title,
|
|
1087
|
+
isRetry: false, previousTier: undefined,
|
|
1088
|
+
};
|
|
914
1089
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
918
|
-
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
1090
|
+
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState, sidecarItem);
|
|
1091
|
+
if (unitPhaseResult.action === "break")
|
|
919
1092
|
break;
|
|
920
|
-
}
|
|
921
|
-
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
922
|
-
// Run right after runUnit() returns so telemetry is never lost to a
|
|
923
|
-
// crash between iterations.
|
|
924
|
-
await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
|
|
925
|
-
if (s.currentUnitRouting) {
|
|
926
|
-
deps.recordOutcome(unitType, s.currentUnitRouting.tier, true);
|
|
927
|
-
}
|
|
928
|
-
const isHookUnit = unitType.startsWith("hook/");
|
|
929
|
-
const artifactVerified = isHookUnit ||
|
|
930
|
-
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
931
|
-
if (artifactVerified) {
|
|
932
|
-
s.completedUnits.push({
|
|
933
|
-
type: unitType,
|
|
934
|
-
id: unitId,
|
|
935
|
-
startedAt: s.currentUnit.startedAt,
|
|
936
|
-
finishedAt: Date.now(),
|
|
937
|
-
});
|
|
938
|
-
if (s.completedUnits.length > 200) {
|
|
939
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
940
|
-
}
|
|
941
|
-
// Flush completed-units to disk so the record survives crashes
|
|
942
|
-
try {
|
|
943
|
-
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
944
|
-
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
945
|
-
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
946
|
-
}
|
|
947
|
-
catch { /* non-fatal: disk flush failure */ }
|
|
948
|
-
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
949
|
-
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
950
|
-
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
951
|
-
}
|
|
952
1093
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
deps.clearUnitTimeout();
|
|
956
|
-
// Post-unit context for pre/post verification
|
|
957
|
-
const postUnitCtx = {
|
|
958
|
-
s,
|
|
959
|
-
ctx,
|
|
960
|
-
pi,
|
|
961
|
-
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
962
|
-
lockBase: deps.lockBase,
|
|
963
|
-
stopAuto: deps.stopAuto,
|
|
964
|
-
pauseAuto: deps.pauseAuto,
|
|
965
|
-
updateProgressWidget: deps.updateProgressWidget,
|
|
966
|
-
};
|
|
967
|
-
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
968
|
-
// Sidecar items use lightweight pre-verification opts
|
|
969
|
-
const preVerificationOpts = sidecarItem
|
|
970
|
-
? sidecarItem.kind === "hook"
|
|
971
|
-
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
972
|
-
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
973
|
-
: undefined;
|
|
974
|
-
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
975
|
-
if (preResult === "dispatched") {
|
|
976
|
-
debugLog("autoLoop", {
|
|
977
|
-
phase: "exit",
|
|
978
|
-
reason: "pre-verification-dispatched",
|
|
979
|
-
});
|
|
980
|
-
break;
|
|
981
|
-
}
|
|
982
|
-
if (pauseAfterUatDispatch) {
|
|
983
|
-
ctx.ui.notify("UAT requires human execution. Auto-mode will pause after this unit writes the result file.", "info");
|
|
984
|
-
await deps.pauseAuto(ctx, pi);
|
|
985
|
-
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
986
|
-
break;
|
|
987
|
-
}
|
|
988
|
-
// Verification gate
|
|
989
|
-
// Hook sidecar items skip verification entirely.
|
|
990
|
-
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
991
|
-
const skipVerification = sidecarItem?.kind === "hook";
|
|
992
|
-
if (!skipVerification) {
|
|
993
|
-
const verificationResult = await deps.runPostUnitVerification({ s, ctx, pi }, deps.pauseAuto);
|
|
994
|
-
if (verificationResult === "pause") {
|
|
995
|
-
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
996
|
-
break;
|
|
997
|
-
}
|
|
998
|
-
if (verificationResult === "retry") {
|
|
999
|
-
if (sidecarItem) {
|
|
1000
|
-
// Sidecar verification retries are skipped — just continue
|
|
1001
|
-
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration });
|
|
1002
|
-
}
|
|
1003
|
-
else {
|
|
1004
|
-
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1005
|
-
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1006
|
-
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1007
|
-
continue;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
1012
|
-
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
1013
|
-
if (postResult === "stopped") {
|
|
1014
|
-
debugLog("autoLoop", {
|
|
1015
|
-
phase: "exit",
|
|
1016
|
-
reason: "post-verification-stopped",
|
|
1017
|
-
});
|
|
1018
|
-
break;
|
|
1019
|
-
}
|
|
1020
|
-
if (postResult === "step-wizard") {
|
|
1021
|
-
// Step mode — exit the loop (caller handles wizard)
|
|
1022
|
-
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
1094
|
+
const finalizeResult = await runFinalize(ic, iterData, sidecarItem);
|
|
1095
|
+
if (finalizeResult.action === "break")
|
|
1023
1096
|
break;
|
|
1024
|
-
|
|
1097
|
+
if (finalizeResult.action === "continue")
|
|
1098
|
+
continue;
|
|
1025
1099
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1026
1100
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
1027
1101
|
}
|