gsd-pi 2.39.0 → 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/async-jobs/index.js +10 -0
- 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 +761 -673
- package/dist/resources/extensions/gsd/auto-post-unit.js +10 -2
- package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
- 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/async-jobs/index.ts +11 -0
- 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 +1075 -921
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
- 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
|
@@ -74,6 +74,47 @@ export interface UnitResult {
|
|
|
74
74
|
event?: AgentEndEvent;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// ─── Phase pipeline types ────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
type PhaseResult<T = void> =
|
|
80
|
+
| { action: "continue" }
|
|
81
|
+
| { action: "break"; reason: string }
|
|
82
|
+
| { action: "next"; data: T }
|
|
83
|
+
|
|
84
|
+
interface IterationContext {
|
|
85
|
+
ctx: ExtensionContext;
|
|
86
|
+
pi: ExtensionAPI;
|
|
87
|
+
s: AutoSession;
|
|
88
|
+
deps: LoopDeps;
|
|
89
|
+
prefs: GSDPreferences | undefined;
|
|
90
|
+
iteration: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface LoopState {
|
|
94
|
+
recentUnits: Array<{ key: string; error?: string }>;
|
|
95
|
+
stuckRecoveryAttempts: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface PreDispatchData {
|
|
99
|
+
state: GSDState;
|
|
100
|
+
mid: string;
|
|
101
|
+
midTitle: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface IterationData {
|
|
105
|
+
unitType: string;
|
|
106
|
+
unitId: string;
|
|
107
|
+
prompt: string;
|
|
108
|
+
finalPrompt: string;
|
|
109
|
+
pauseAfterUatDispatch: boolean;
|
|
110
|
+
observabilityIssues: unknown[];
|
|
111
|
+
state: GSDState;
|
|
112
|
+
mid: string | undefined;
|
|
113
|
+
midTitle: string | undefined;
|
|
114
|
+
isRetry: boolean;
|
|
115
|
+
previousTier: string | undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
77
118
|
// ─── Per-unit one-shot promise state ────────────────────────────────────────
|
|
78
119
|
//
|
|
79
120
|
// A single module-level resolve function scoped to the current unit execution.
|
|
@@ -287,6 +328,20 @@ export async function runUnit(
|
|
|
287
328
|
status: result.status,
|
|
288
329
|
});
|
|
289
330
|
|
|
331
|
+
// Discard trailing follow-up messages (e.g. async_job_result notifications)
|
|
332
|
+
// from the completed unit. Without this, queued follow-ups trigger wasteful
|
|
333
|
+
// LLM turns before the next session can start (#1642).
|
|
334
|
+
// clearQueue() lives on AgentSession but isn't part of the typed
|
|
335
|
+
// ExtensionCommandContext interface — call it via runtime check.
|
|
336
|
+
try {
|
|
337
|
+
const cmdCtxAny = s.cmdCtx as Record<string, unknown> | null;
|
|
338
|
+
if (typeof cmdCtxAny?.clearQueue === "function") {
|
|
339
|
+
(cmdCtxAny.clearQueue as () => unknown)();
|
|
340
|
+
}
|
|
341
|
+
} catch {
|
|
342
|
+
// Non-fatal — clearQueue may not be available in all contexts
|
|
343
|
+
}
|
|
344
|
+
|
|
290
345
|
return result;
|
|
291
346
|
}
|
|
292
347
|
|
|
@@ -642,204 +697,192 @@ async function closeoutAndStop(
|
|
|
642
697
|
await deps.stopAuto(ctx, pi, reason);
|
|
643
698
|
}
|
|
644
699
|
|
|
645
|
-
// ───
|
|
700
|
+
// ─── runPreDispatch ───────────────────────────────────────────────────────────
|
|
646
701
|
|
|
647
702
|
/**
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
* This is the linear replacement for the recursive
|
|
653
|
-
* dispatchNextUnit → handleAgentEnd → dispatchNextUnit chain.
|
|
703
|
+
* Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
|
|
704
|
+
* milestone transition, terminal conditions.
|
|
705
|
+
* Returns break to exit the loop, or next with PreDispatchData on success.
|
|
654
706
|
*/
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
deps
|
|
660
|
-
): Promise<void> {
|
|
661
|
-
debugLog("autoLoop", { phase: "enter" });
|
|
662
|
-
let iteration = 0;
|
|
663
|
-
// ── Sliding-window stuck detection ──
|
|
664
|
-
const recentUnits: Array<{ key: string; error?: string }> = [];
|
|
665
|
-
const STUCK_WINDOW_SIZE = 6;
|
|
666
|
-
let stuckRecoveryAttempts = 0;
|
|
707
|
+
async function runPreDispatch(
|
|
708
|
+
ic: IterationContext,
|
|
709
|
+
loopState: LoopState,
|
|
710
|
+
): Promise<PhaseResult<PreDispatchData>> {
|
|
711
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
667
712
|
|
|
668
|
-
|
|
713
|
+
// Resource version guard
|
|
714
|
+
const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
|
|
715
|
+
if (staleMsg) {
|
|
716
|
+
await deps.stopAuto(ctx, pi, staleMsg);
|
|
717
|
+
debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
|
|
718
|
+
return { action: "break", reason: "resources-stale" };
|
|
719
|
+
}
|
|
669
720
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
721
|
+
deps.invalidateAllCaches();
|
|
722
|
+
s.lastPromptCharCount = undefined;
|
|
723
|
+
s.lastBaselineCharCount = undefined;
|
|
673
724
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
ctx,
|
|
682
|
-
pi,
|
|
683
|
-
`Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`,
|
|
725
|
+
// Pre-dispatch health gate
|
|
726
|
+
try {
|
|
727
|
+
const healthGate = await deps.preDispatchHealthGate(s.basePath);
|
|
728
|
+
if (healthGate.fixesApplied.length > 0) {
|
|
729
|
+
ctx.ui.notify(
|
|
730
|
+
`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`,
|
|
731
|
+
"info",
|
|
684
732
|
);
|
|
685
|
-
break;
|
|
686
733
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
734
|
+
if (!healthGate.proceed) {
|
|
735
|
+
ctx.ui.notify(
|
|
736
|
+
healthGate.reason ?? "Pre-dispatch health check failed.",
|
|
737
|
+
"error",
|
|
738
|
+
);
|
|
739
|
+
await deps.pauseAuto(ctx, pi);
|
|
740
|
+
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
741
|
+
return { action: "break", reason: "health-gate-failed" };
|
|
691
742
|
}
|
|
743
|
+
} catch {
|
|
744
|
+
// Non-fatal
|
|
745
|
+
}
|
|
692
746
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
unitId: sidecarItem.unitId,
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const sessionLockBase = deps.lockBase();
|
|
710
|
-
if (sessionLockBase) {
|
|
711
|
-
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
712
|
-
if (!lockStatus.valid) {
|
|
713
|
-
debugLog("autoLoop", {
|
|
714
|
-
phase: "session-lock-invalid",
|
|
715
|
-
reason: lockStatus.failureReason ?? "unknown",
|
|
716
|
-
existingPid: lockStatus.existingPid,
|
|
717
|
-
expectedPid: lockStatus.expectedPid,
|
|
718
|
-
});
|
|
719
|
-
deps.handleLostSessionLock(ctx, lockStatus);
|
|
720
|
-
debugLog("autoLoop", {
|
|
721
|
-
phase: "exit",
|
|
722
|
-
reason: "session-lock-lost",
|
|
723
|
-
detail: lockStatus.failureReason ?? "unknown",
|
|
724
|
-
});
|
|
725
|
-
break;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
747
|
+
// Sync project root artifacts into worktree
|
|
748
|
+
if (
|
|
749
|
+
s.originalBasePath &&
|
|
750
|
+
s.basePath !== s.originalBasePath &&
|
|
751
|
+
s.currentMilestoneId
|
|
752
|
+
) {
|
|
753
|
+
deps.syncProjectRootToWorktree(
|
|
754
|
+
s.originalBasePath,
|
|
755
|
+
s.basePath,
|
|
756
|
+
s.currentMilestoneId,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
728
759
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
760
|
+
// Derive state
|
|
761
|
+
let state = await deps.deriveState(s.basePath);
|
|
762
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
763
|
+
let mid = state.activeMilestone?.id;
|
|
764
|
+
let midTitle = state.activeMilestone?.title;
|
|
765
|
+
debugLog("autoLoop", {
|
|
766
|
+
phase: "state-derived",
|
|
767
|
+
iteration: ic.iteration,
|
|
768
|
+
mid,
|
|
769
|
+
statePhase: state.phase,
|
|
770
|
+
});
|
|
738
771
|
|
|
739
|
-
|
|
740
|
-
|
|
772
|
+
// ── Milestone transition ────────────────────────────────────────────
|
|
773
|
+
if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
|
|
774
|
+
ctx.ui.notify(
|
|
775
|
+
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
|
|
776
|
+
"info",
|
|
777
|
+
);
|
|
778
|
+
deps.sendDesktopNotification(
|
|
779
|
+
"GSD",
|
|
780
|
+
`Milestone ${s.currentMilestoneId} complete!`,
|
|
781
|
+
"success",
|
|
782
|
+
"milestone",
|
|
783
|
+
);
|
|
784
|
+
deps.logCmuxEvent(
|
|
785
|
+
prefs,
|
|
786
|
+
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
787
|
+
"success",
|
|
788
|
+
);
|
|
741
789
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
790
|
+
const vizPrefs = prefs;
|
|
791
|
+
if (vizPrefs?.auto_visualize) {
|
|
792
|
+
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
793
|
+
}
|
|
794
|
+
if (vizPrefs?.auto_report !== false) {
|
|
795
|
+
try {
|
|
796
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
797
|
+
} catch (err) {
|
|
798
|
+
ctx.ui.notify(
|
|
799
|
+
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
800
|
+
"warning",
|
|
801
|
+
);
|
|
748
802
|
}
|
|
803
|
+
}
|
|
749
804
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
805
|
+
// Reset dispatch counters for new milestone
|
|
806
|
+
s.unitDispatchCount.clear();
|
|
807
|
+
s.unitRecoveryCount.clear();
|
|
808
|
+
s.unitLifetimeDispatches.clear();
|
|
809
|
+
loopState.recentUnits.length = 0;
|
|
810
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
753
811
|
|
|
754
|
-
|
|
812
|
+
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
813
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
814
|
+
|
|
815
|
+
// Opt-in: create draft PR on milestone completion
|
|
816
|
+
if (prefs?.git?.auto_pr) {
|
|
755
817
|
try {
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (
|
|
764
|
-
ctx.ui.notify(
|
|
765
|
-
healthGate.reason ?? "Pre-dispatch health check failed.",
|
|
766
|
-
"error",
|
|
767
|
-
);
|
|
768
|
-
await deps.pauseAuto(ctx, pi);
|
|
769
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
770
|
-
break;
|
|
818
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
819
|
+
const prUrl = createDraftPR(
|
|
820
|
+
s.basePath,
|
|
821
|
+
s.currentMilestoneId!,
|
|
822
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
823
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
824
|
+
);
|
|
825
|
+
if (prUrl) {
|
|
826
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
771
827
|
}
|
|
772
828
|
} catch {
|
|
773
|
-
// Non-fatal
|
|
829
|
+
// Non-fatal — PR creation is best-effort
|
|
774
830
|
}
|
|
831
|
+
}
|
|
775
832
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
);
|
|
833
|
+
deps.invalidateAllCaches();
|
|
834
|
+
|
|
835
|
+
state = await deps.deriveState(s.basePath);
|
|
836
|
+
mid = state.activeMilestone?.id;
|
|
837
|
+
midTitle = state.activeMilestone?.title;
|
|
838
|
+
|
|
839
|
+
if (mid) {
|
|
840
|
+
if (deps.getIsolationMode() !== "none") {
|
|
841
|
+
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
842
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
843
|
+
});
|
|
787
844
|
}
|
|
845
|
+
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
846
|
+
} else {
|
|
847
|
+
// mid is undefined — no milestone to capture integration branch for
|
|
848
|
+
}
|
|
788
849
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
mid,
|
|
798
|
-
statePhase: state.phase,
|
|
799
|
-
});
|
|
850
|
+
const pendingIds = state.registry
|
|
851
|
+
.filter(
|
|
852
|
+
(m: { status: string }) =>
|
|
853
|
+
m.status !== "complete" && m.status !== "parked",
|
|
854
|
+
)
|
|
855
|
+
.map((m: { id: string }) => m.id);
|
|
856
|
+
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
857
|
+
}
|
|
800
858
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
"info",
|
|
806
|
-
);
|
|
807
|
-
deps.sendDesktopNotification(
|
|
808
|
-
"GSD",
|
|
809
|
-
`Milestone ${s.currentMilestoneId} complete!`,
|
|
810
|
-
"success",
|
|
811
|
-
"milestone",
|
|
812
|
-
);
|
|
813
|
-
deps.logCmuxEvent(
|
|
814
|
-
prefs,
|
|
815
|
-
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
816
|
-
"success",
|
|
817
|
-
);
|
|
859
|
+
if (mid) {
|
|
860
|
+
s.currentMilestoneId = mid;
|
|
861
|
+
deps.setActiveMilestoneId(s.basePath, mid);
|
|
862
|
+
}
|
|
818
863
|
|
|
819
|
-
|
|
820
|
-
if (vizPrefs?.auto_visualize) {
|
|
821
|
-
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
822
|
-
}
|
|
823
|
-
if (vizPrefs?.auto_report !== false) {
|
|
824
|
-
try {
|
|
825
|
-
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
826
|
-
} catch (err) {
|
|
827
|
-
ctx.ui.notify(
|
|
828
|
-
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
829
|
-
"warning",
|
|
830
|
-
);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
864
|
+
// ── Terminal conditions ──────────────────────────────────────────────
|
|
833
865
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
866
|
+
if (!mid) {
|
|
867
|
+
if (s.currentUnit) {
|
|
868
|
+
await deps.closeoutUnit(
|
|
869
|
+
ctx,
|
|
870
|
+
s.basePath,
|
|
871
|
+
s.currentUnit.type,
|
|
872
|
+
s.currentUnit.id,
|
|
873
|
+
s.currentUnit.startedAt,
|
|
874
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
875
|
+
);
|
|
876
|
+
}
|
|
840
877
|
|
|
841
|
-
|
|
842
|
-
|
|
878
|
+
const incomplete = state.registry.filter(
|
|
879
|
+
(m: { status: string }) =>
|
|
880
|
+
m.status !== "complete" && m.status !== "parked",
|
|
881
|
+
);
|
|
882
|
+
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
883
|
+
// All milestones complete — merge milestone branch before stopping
|
|
884
|
+
if (s.currentMilestoneId) {
|
|
885
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
843
886
|
|
|
844
887
|
// Opt-in: create draft PR on milestone completion
|
|
845
888
|
if (prefs?.git?.auto_pr) {
|
|
@@ -847,7 +890,7 @@ export async function autoLoop(
|
|
|
847
890
|
const { createDraftPR } = await import("./git-service.js");
|
|
848
891
|
const prUrl = createDraftPR(
|
|
849
892
|
s.basePath,
|
|
850
|
-
s.currentMilestoneId
|
|
893
|
+
s.currentMilestoneId,
|
|
851
894
|
`[GSD] ${s.currentMilestoneId} complete`,
|
|
852
895
|
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
853
896
|
);
|
|
@@ -858,841 +901,952 @@ export async function autoLoop(
|
|
|
858
901
|
// Non-fatal — PR creation is best-effort
|
|
859
902
|
}
|
|
860
903
|
}
|
|
904
|
+
}
|
|
905
|
+
deps.sendDesktopNotification(
|
|
906
|
+
"GSD",
|
|
907
|
+
"All milestones complete!",
|
|
908
|
+
"success",
|
|
909
|
+
"milestone",
|
|
910
|
+
);
|
|
911
|
+
deps.logCmuxEvent(
|
|
912
|
+
prefs,
|
|
913
|
+
"All milestones complete.",
|
|
914
|
+
"success",
|
|
915
|
+
);
|
|
916
|
+
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
917
|
+
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
918
|
+
// Empty registry — no milestones visible, likely a path resolution bug
|
|
919
|
+
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
920
|
+
ctx.ui.notify(
|
|
921
|
+
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
922
|
+
"error",
|
|
923
|
+
);
|
|
924
|
+
await deps.stopAuto(
|
|
925
|
+
ctx,
|
|
926
|
+
pi,
|
|
927
|
+
`No milestones found — check basePath resolution`,
|
|
928
|
+
);
|
|
929
|
+
} else if (state.phase === "blocked") {
|
|
930
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
931
|
+
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
932
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
933
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
934
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
935
|
+
} else {
|
|
936
|
+
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
937
|
+
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
938
|
+
ctx.ui.notify(
|
|
939
|
+
`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`,
|
|
940
|
+
"error",
|
|
941
|
+
);
|
|
942
|
+
await deps.stopAuto(
|
|
943
|
+
ctx,
|
|
944
|
+
pi,
|
|
945
|
+
`No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
949
|
+
return { action: "break", reason: "no-active-milestone" };
|
|
950
|
+
}
|
|
861
951
|
|
|
862
|
-
|
|
952
|
+
if (!midTitle) {
|
|
953
|
+
midTitle = mid;
|
|
954
|
+
ctx.ui.notify(
|
|
955
|
+
`Milestone ${mid} has no title in roadmap — using ID as fallback.`,
|
|
956
|
+
"warning",
|
|
957
|
+
);
|
|
958
|
+
}
|
|
863
959
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
960
|
+
// Mid-merge safety check
|
|
961
|
+
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
962
|
+
deps.invalidateAllCaches();
|
|
963
|
+
state = await deps.deriveState(s.basePath);
|
|
964
|
+
mid = state.activeMilestone?.id;
|
|
965
|
+
midTitle = state.activeMilestone?.title;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (!mid || !midTitle) {
|
|
969
|
+
const noMilestoneReason = !mid
|
|
970
|
+
? "No active milestone after merge reconciliation"
|
|
971
|
+
: `Milestone ${mid} has no title after reconciliation`;
|
|
972
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
973
|
+
debugLog("autoLoop", {
|
|
974
|
+
phase: "exit",
|
|
975
|
+
reason: "no-milestone-after-reconciliation",
|
|
976
|
+
});
|
|
977
|
+
return { action: "break", reason: "no-milestone-after-reconciliation" };
|
|
978
|
+
}
|
|
867
979
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
980
|
+
// Terminal: complete
|
|
981
|
+
if (state.phase === "complete") {
|
|
982
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
983
|
+
if (s.currentMilestoneId) {
|
|
984
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
985
|
+
|
|
986
|
+
// Opt-in: create draft PR on milestone completion
|
|
987
|
+
if (prefs?.git?.auto_pr) {
|
|
988
|
+
try {
|
|
989
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
990
|
+
const prUrl = createDraftPR(
|
|
991
|
+
s.basePath,
|
|
992
|
+
s.currentMilestoneId,
|
|
993
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
994
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
995
|
+
);
|
|
996
|
+
if (prUrl) {
|
|
997
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
873
998
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
// mid is undefined — no milestone to capture integration branch for
|
|
999
|
+
} catch {
|
|
1000
|
+
// Non-fatal — PR creation is best-effort
|
|
877
1001
|
}
|
|
878
|
-
|
|
879
|
-
const pendingIds = state.registry
|
|
880
|
-
.filter(
|
|
881
|
-
(m: { status: string }) =>
|
|
882
|
-
m.status !== "complete" && m.status !== "parked",
|
|
883
|
-
)
|
|
884
|
-
.map((m: { id: string }) => m.id);
|
|
885
|
-
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
886
1002
|
}
|
|
1003
|
+
}
|
|
1004
|
+
deps.sendDesktopNotification(
|
|
1005
|
+
"GSD",
|
|
1006
|
+
`Milestone ${mid} complete!`,
|
|
1007
|
+
"success",
|
|
1008
|
+
"milestone",
|
|
1009
|
+
);
|
|
1010
|
+
deps.logCmuxEvent(
|
|
1011
|
+
prefs,
|
|
1012
|
+
`Milestone ${mid} complete.`,
|
|
1013
|
+
"success",
|
|
1014
|
+
);
|
|
1015
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
1016
|
+
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
1017
|
+
return { action: "break", reason: "milestone-complete" };
|
|
1018
|
+
}
|
|
887
1019
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1020
|
+
// Terminal: blocked
|
|
1021
|
+
if (state.phase === "blocked") {
|
|
1022
|
+
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
1023
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
1024
|
+
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
1025
|
+
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
1026
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
1027
|
+
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
1028
|
+
return { action: "break", reason: "blocked" };
|
|
1029
|
+
}
|
|
892
1030
|
|
|
893
|
-
|
|
1031
|
+
return { action: "next", data: { state, mid, midTitle } };
|
|
1032
|
+
}
|
|
894
1033
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1034
|
+
// ─── runDispatch ──────────────────────────────────────────────────────────────
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Phase 3: Dispatch resolution — resolve next unit, stuck detection, pre-dispatch hooks.
|
|
1038
|
+
* Returns break/continue to control the loop, or next with IterationData on success.
|
|
1039
|
+
*/
|
|
1040
|
+
async function runDispatch(
|
|
1041
|
+
ic: IterationContext,
|
|
1042
|
+
preData: PreDispatchData,
|
|
1043
|
+
loopState: LoopState,
|
|
1044
|
+
): Promise<PhaseResult<IterationData>> {
|
|
1045
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
1046
|
+
const { state, mid, midTitle } = preData;
|
|
1047
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
906
1048
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1049
|
+
debugLog("autoLoop", { phase: "dispatch-resolve", iteration: ic.iteration });
|
|
1050
|
+
const dispatchResult = await deps.resolveDispatch({
|
|
1051
|
+
basePath: s.basePath,
|
|
1052
|
+
mid,
|
|
1053
|
+
midTitle,
|
|
1054
|
+
state,
|
|
1055
|
+
prefs,
|
|
1056
|
+
session: s,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
if (dispatchResult.action === "stop") {
|
|
1060
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1061
|
+
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1062
|
+
return { action: "break", reason: "dispatch-stop" };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (dispatchResult.action !== "dispatch") {
|
|
1066
|
+
// Non-dispatch action (e.g. "skip") — re-derive state
|
|
1067
|
+
await new Promise((r) => setImmediate(r));
|
|
1068
|
+
return { action: "continue" };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
let unitType = dispatchResult.unitType;
|
|
1072
|
+
let unitId = dispatchResult.unitId;
|
|
1073
|
+
let prompt = dispatchResult.prompt;
|
|
1074
|
+
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1075
|
+
|
|
1076
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1077
|
+
const derivedKey = `${unitType}/${unitId}`;
|
|
1078
|
+
|
|
1079
|
+
if (!s.pendingVerificationRetry) {
|
|
1080
|
+
loopState.recentUnits.push({ key: derivedKey });
|
|
1081
|
+
if (loopState.recentUnits.length > STUCK_WINDOW_SIZE) loopState.recentUnits.shift();
|
|
1082
|
+
|
|
1083
|
+
const stuckSignal = detectStuck(loopState.recentUnits);
|
|
1084
|
+
if (stuckSignal) {
|
|
1085
|
+
debugLog("autoLoop", {
|
|
1086
|
+
phase: "stuck-check",
|
|
1087
|
+
unitType,
|
|
1088
|
+
unitId,
|
|
1089
|
+
reason: stuckSignal.reason,
|
|
1090
|
+
recoveryAttempts: loopState.stuckRecoveryAttempts,
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
if (loopState.stuckRecoveryAttempts === 0) {
|
|
1094
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
1095
|
+
loopState.stuckRecoveryAttempts++;
|
|
1096
|
+
const artifactExists = deps.verifyExpectedArtifact(
|
|
1097
|
+
unitType,
|
|
1098
|
+
unitId,
|
|
1099
|
+
s.basePath,
|
|
910
1100
|
);
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
if (prefs?.git?.auto_pr) {
|
|
918
|
-
try {
|
|
919
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
920
|
-
const prUrl = createDraftPR(
|
|
921
|
-
s.basePath,
|
|
922
|
-
s.currentMilestoneId,
|
|
923
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
924
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
925
|
-
);
|
|
926
|
-
if (prUrl) {
|
|
927
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
928
|
-
}
|
|
929
|
-
} catch {
|
|
930
|
-
// Non-fatal — PR creation is best-effort
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
deps.sendDesktopNotification(
|
|
935
|
-
"GSD",
|
|
936
|
-
"All milestones complete!",
|
|
937
|
-
"success",
|
|
938
|
-
"milestone",
|
|
939
|
-
);
|
|
940
|
-
deps.logCmuxEvent(
|
|
941
|
-
prefs,
|
|
942
|
-
"All milestones complete.",
|
|
943
|
-
"success",
|
|
944
|
-
);
|
|
945
|
-
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
946
|
-
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
947
|
-
// Empty registry — no milestones visible, likely a path resolution bug
|
|
948
|
-
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
949
|
-
ctx.ui.notify(
|
|
950
|
-
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
951
|
-
"error",
|
|
952
|
-
);
|
|
953
|
-
await deps.stopAuto(
|
|
954
|
-
ctx,
|
|
955
|
-
pi,
|
|
956
|
-
`No milestones found — check basePath resolution`,
|
|
957
|
-
);
|
|
958
|
-
} else if (state.phase === "blocked") {
|
|
959
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
960
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
961
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
962
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
963
|
-
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
964
|
-
} else {
|
|
965
|
-
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
966
|
-
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
1101
|
+
if (artifactExists) {
|
|
1102
|
+
debugLog("autoLoop", {
|
|
1103
|
+
phase: "stuck-recovery",
|
|
1104
|
+
level: 1,
|
|
1105
|
+
action: "artifact-found",
|
|
1106
|
+
});
|
|
967
1107
|
ctx.ui.notify(
|
|
968
|
-
`
|
|
969
|
-
"
|
|
970
|
-
);
|
|
971
|
-
await deps.stopAuto(
|
|
972
|
-
ctx,
|
|
973
|
-
pi,
|
|
974
|
-
`No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`,
|
|
1108
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1109
|
+
"info",
|
|
975
1110
|
);
|
|
1111
|
+
deps.invalidateAllCaches();
|
|
1112
|
+
return { action: "continue" };
|
|
976
1113
|
}
|
|
977
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (!midTitle) {
|
|
982
|
-
midTitle = mid;
|
|
983
1114
|
ctx.ui.notify(
|
|
984
|
-
`
|
|
1115
|
+
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
985
1116
|
"warning",
|
|
986
1117
|
);
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Mid-merge safety check
|
|
990
|
-
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
991
1118
|
deps.invalidateAllCaches();
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
midTitle = state.activeMilestone?.title;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
if (!mid || !midTitle) {
|
|
998
|
-
const noMilestoneReason = !mid
|
|
999
|
-
? "No active milestone after merge reconciliation"
|
|
1000
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
1001
|
-
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
1119
|
+
} else {
|
|
1120
|
+
// Level 2: hard stop — genuinely stuck
|
|
1002
1121
|
debugLog("autoLoop", {
|
|
1003
|
-
phase: "
|
|
1004
|
-
|
|
1122
|
+
phase: "stuck-detected",
|
|
1123
|
+
unitType,
|
|
1124
|
+
unitId,
|
|
1125
|
+
reason: stuckSignal.reason,
|
|
1005
1126
|
});
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (state.phase === "complete") {
|
|
1011
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
1012
|
-
if (s.currentMilestoneId) {
|
|
1013
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
1014
|
-
|
|
1015
|
-
// Opt-in: create draft PR on milestone completion
|
|
1016
|
-
if (prefs?.git?.auto_pr) {
|
|
1017
|
-
try {
|
|
1018
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
1019
|
-
const prUrl = createDraftPR(
|
|
1020
|
-
s.basePath,
|
|
1021
|
-
s.currentMilestoneId,
|
|
1022
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
1023
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
1024
|
-
);
|
|
1025
|
-
if (prUrl) {
|
|
1026
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
1027
|
-
}
|
|
1028
|
-
} catch {
|
|
1029
|
-
// Non-fatal — PR creation is best-effort
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
deps.sendDesktopNotification(
|
|
1034
|
-
"GSD",
|
|
1035
|
-
`Milestone ${mid} complete!`,
|
|
1036
|
-
"success",
|
|
1037
|
-
"milestone",
|
|
1127
|
+
await deps.stopAuto(
|
|
1128
|
+
ctx,
|
|
1129
|
+
pi,
|
|
1130
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
1038
1131
|
);
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
"success",
|
|
1132
|
+
ctx.ui.notify(
|
|
1133
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1134
|
+
"error",
|
|
1043
1135
|
);
|
|
1044
|
-
|
|
1045
|
-
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
1046
|
-
break;
|
|
1136
|
+
return { action: "break", reason: "stuck-detected" };
|
|
1047
1137
|
}
|
|
1048
|
-
|
|
1049
|
-
//
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
break;
|
|
1138
|
+
} else {
|
|
1139
|
+
// Progress detected — reset recovery counter
|
|
1140
|
+
if (loopState.stuckRecoveryAttempts > 0) {
|
|
1141
|
+
debugLog("autoLoop", {
|
|
1142
|
+
phase: "stuck-counter-reset",
|
|
1143
|
+
from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
|
|
1144
|
+
to: derivedKey,
|
|
1145
|
+
});
|
|
1146
|
+
loopState.stuckRecoveryAttempts = 0;
|
|
1058
1147
|
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1059
1150
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1088
|
-
|
|
1089
|
-
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
1090
|
-
// 100% — special enforcement logic (halt/pause/warn)
|
|
1091
|
-
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
1092
|
-
if (budgetEnforcementAction === "halt") {
|
|
1093
|
-
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1094
|
-
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1095
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
1098
|
-
if (budgetEnforcementAction === "pause") {
|
|
1099
|
-
ctx.ui.notify(
|
|
1100
|
-
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1101
|
-
"warning",
|
|
1102
|
-
);
|
|
1103
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1104
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1105
|
-
await deps.pauseAuto(ctx, pi);
|
|
1106
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1107
|
-
break;
|
|
1108
|
-
}
|
|
1109
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
1110
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1111
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1112
|
-
} else if (threshold.pct < 100) {
|
|
1113
|
-
// Sub-100% — simple notification
|
|
1114
|
-
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1115
|
-
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1116
|
-
deps.sendDesktopNotification(
|
|
1117
|
-
"GSD",
|
|
1118
|
-
msg,
|
|
1119
|
-
threshold.notifyLevel,
|
|
1120
|
-
"budget",
|
|
1121
|
-
);
|
|
1122
|
-
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
1123
|
-
}
|
|
1124
|
-
} else if (budgetAlertLevel === 0) {
|
|
1125
|
-
s.lastBudgetAlertLevel = 0;
|
|
1126
|
-
}
|
|
1127
|
-
} else {
|
|
1128
|
-
s.lastBudgetAlertLevel = 0;
|
|
1129
|
-
}
|
|
1151
|
+
// Pre-dispatch hooks
|
|
1152
|
+
const preDispatchResult = deps.runPreDispatchHooks(
|
|
1153
|
+
unitType,
|
|
1154
|
+
unitId,
|
|
1155
|
+
prompt,
|
|
1156
|
+
s.basePath,
|
|
1157
|
+
);
|
|
1158
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
1159
|
+
ctx.ui.notify(
|
|
1160
|
+
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
1161
|
+
"info",
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
if (preDispatchResult.action === "skip") {
|
|
1165
|
+
ctx.ui.notify(
|
|
1166
|
+
`Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
|
|
1167
|
+
"info",
|
|
1168
|
+
);
|
|
1169
|
+
await new Promise((r) => setImmediate(r));
|
|
1170
|
+
return { action: "continue" };
|
|
1171
|
+
}
|
|
1172
|
+
if (preDispatchResult.action === "replace") {
|
|
1173
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
1174
|
+
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1175
|
+
} else if (preDispatchResult.prompt) {
|
|
1176
|
+
prompt = preDispatchResult.prompt;
|
|
1177
|
+
}
|
|
1130
1178
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1179
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
|
|
1180
|
+
s.basePath,
|
|
1181
|
+
deps.getMainBranch(s.basePath),
|
|
1182
|
+
unitType,
|
|
1183
|
+
unitId,
|
|
1184
|
+
);
|
|
1185
|
+
if (priorSliceBlocker) {
|
|
1186
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
1187
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
1188
|
+
return { action: "break", reason: "prior-slice-blocker" };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const observabilityIssues = await deps.collectObservabilityWarnings(
|
|
1192
|
+
ctx,
|
|
1193
|
+
s.basePath,
|
|
1194
|
+
unitType,
|
|
1195
|
+
unitId,
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
return {
|
|
1199
|
+
action: "next",
|
|
1200
|
+
data: {
|
|
1201
|
+
unitType, unitId, prompt, finalPrompt: prompt,
|
|
1202
|
+
pauseAfterUatDispatch, observabilityIssues,
|
|
1203
|
+
state, mid, midTitle,
|
|
1204
|
+
isRetry: false, previousTier: undefined,
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// ─── runGuards ────────────────────────────────────────────────────────────────
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Phase 2: Guards — budget ceiling, context window, secrets re-check.
|
|
1213
|
+
* Returns break to exit the loop, or next to proceed to dispatch.
|
|
1214
|
+
*/
|
|
1215
|
+
async function runGuards(
|
|
1216
|
+
ic: IterationContext,
|
|
1217
|
+
mid: string,
|
|
1218
|
+
): Promise<PhaseResult> {
|
|
1219
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
1220
|
+
|
|
1221
|
+
// Budget ceiling guard
|
|
1222
|
+
const budgetCeiling = prefs?.budget_ceiling;
|
|
1223
|
+
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
1224
|
+
const currentLedger = deps.getLedger() as { units: unknown } | null;
|
|
1225
|
+
const totalCost = currentLedger
|
|
1226
|
+
? deps.getProjectTotals(currentLedger.units).cost
|
|
1227
|
+
: 0;
|
|
1228
|
+
const budgetPct = totalCost / budgetCeiling;
|
|
1229
|
+
const budgetAlertLevel = deps.getBudgetAlertLevel(budgetPct);
|
|
1230
|
+
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(
|
|
1231
|
+
s.lastBudgetAlertLevel,
|
|
1232
|
+
budgetPct,
|
|
1233
|
+
);
|
|
1234
|
+
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
1235
|
+
const budgetEnforcementAction = deps.getBudgetEnforcementAction(
|
|
1236
|
+
enforcement,
|
|
1237
|
+
budgetPct,
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
1241
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1242
|
+
(t) => newBudgetAlertLevel >= t.pct,
|
|
1243
|
+
);
|
|
1244
|
+
if (threshold) {
|
|
1245
|
+
s.lastBudgetAlertLevel =
|
|
1246
|
+
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1247
|
+
|
|
1248
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
1249
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
1250
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
1251
|
+
if (budgetEnforcementAction === "halt") {
|
|
1252
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1253
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1254
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1255
|
+
return { action: "break", reason: "budget-halt" };
|
|
1256
|
+
}
|
|
1257
|
+
if (budgetEnforcementAction === "pause") {
|
|
1141
1258
|
ctx.ui.notify(
|
|
1142
|
-
`${msg}
|
|
1143
|
-
"warning",
|
|
1144
|
-
);
|
|
1145
|
-
deps.sendDesktopNotification(
|
|
1146
|
-
"GSD",
|
|
1147
|
-
`Context ${contextUsage.percent}% — paused`,
|
|
1259
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1148
1260
|
"warning",
|
|
1149
|
-
"attention",
|
|
1150
1261
|
);
|
|
1262
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1263
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1151
1264
|
await deps.pauseAuto(ctx, pi);
|
|
1152
|
-
debugLog("autoLoop", { phase: "exit", reason: "
|
|
1153
|
-
break;
|
|
1265
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1266
|
+
return { action: "break", reason: "budget-pause" };
|
|
1154
1267
|
}
|
|
1268
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
1269
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1270
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1271
|
+
} else if (threshold.pct < 100) {
|
|
1272
|
+
// Sub-100% — simple notification
|
|
1273
|
+
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1274
|
+
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1275
|
+
deps.sendDesktopNotification(
|
|
1276
|
+
"GSD",
|
|
1277
|
+
msg,
|
|
1278
|
+
threshold.notifyLevel,
|
|
1279
|
+
"budget",
|
|
1280
|
+
);
|
|
1281
|
+
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
1155
1282
|
}
|
|
1283
|
+
} else if (budgetAlertLevel === 0) {
|
|
1284
|
+
s.lastBudgetAlertLevel = 0;
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
s.lastBudgetAlertLevel = 0;
|
|
1288
|
+
}
|
|
1156
1289
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1290
|
+
// Context window guard
|
|
1291
|
+
const contextThreshold = prefs?.context_pause_threshold ?? 0;
|
|
1292
|
+
if (contextThreshold > 0 && s.cmdCtx) {
|
|
1293
|
+
const contextUsage = s.cmdCtx.getContextUsage();
|
|
1294
|
+
if (
|
|
1295
|
+
contextUsage &&
|
|
1296
|
+
contextUsage.percent !== null &&
|
|
1297
|
+
contextUsage.percent >= contextThreshold
|
|
1298
|
+
) {
|
|
1299
|
+
const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
|
|
1300
|
+
ctx.ui.notify(
|
|
1301
|
+
`${msg} Run /gsd auto to continue (will start fresh session).`,
|
|
1302
|
+
"warning",
|
|
1303
|
+
);
|
|
1304
|
+
deps.sendDesktopNotification(
|
|
1305
|
+
"GSD",
|
|
1306
|
+
`Context ${contextUsage.percent}% — paused`,
|
|
1307
|
+
"warning",
|
|
1308
|
+
"attention",
|
|
1309
|
+
);
|
|
1310
|
+
await deps.pauseAuto(ctx, pi);
|
|
1311
|
+
debugLog("autoLoop", { phase: "exit", reason: "context-window" });
|
|
1312
|
+
return { action: "break", reason: "context-window" };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Secrets re-check gate
|
|
1317
|
+
try {
|
|
1318
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
1319
|
+
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
1320
|
+
const result = await deps.collectSecretsFromManifest(
|
|
1321
|
+
s.basePath,
|
|
1322
|
+
mid,
|
|
1323
|
+
ctx,
|
|
1324
|
+
);
|
|
1325
|
+
if (
|
|
1326
|
+
result &&
|
|
1327
|
+
result.applied &&
|
|
1328
|
+
result.skipped &&
|
|
1329
|
+
result.existingSkipped
|
|
1330
|
+
) {
|
|
1181
1331
|
ctx.ui.notify(
|
|
1182
|
-
`Secrets
|
|
1183
|
-
"
|
|
1332
|
+
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
1333
|
+
"info",
|
|
1184
1334
|
);
|
|
1335
|
+
} else {
|
|
1336
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
1185
1337
|
}
|
|
1338
|
+
}
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
ctx.ui.notify(
|
|
1341
|
+
`Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`,
|
|
1342
|
+
"warning",
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1186
1345
|
|
|
1187
|
-
|
|
1346
|
+
return { action: "next", data: undefined as void };
|
|
1347
|
+
}
|
|
1188
1348
|
|
|
1189
|
-
|
|
1190
|
-
const dispatchResult = await deps.resolveDispatch({
|
|
1191
|
-
basePath: s.basePath,
|
|
1192
|
-
mid,
|
|
1193
|
-
midTitle: midTitle!,
|
|
1194
|
-
state,
|
|
1195
|
-
prefs,
|
|
1196
|
-
session: s,
|
|
1197
|
-
});
|
|
1349
|
+
// ─── runUnitPhase ─────────────────────────────────────────────────────────────
|
|
1198
1350
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1351
|
+
/**
|
|
1352
|
+
* Phase 4: Unit execution — dispatch prompt, await agent_end, closeout, artifact verify.
|
|
1353
|
+
* Returns break or next with unitStartedAt for downstream phases.
|
|
1354
|
+
*/
|
|
1355
|
+
async function runUnitPhase(
|
|
1356
|
+
ic: IterationContext,
|
|
1357
|
+
iterData: IterationData,
|
|
1358
|
+
loopState: LoopState,
|
|
1359
|
+
sidecarItem?: SidecarItem,
|
|
1360
|
+
): Promise<PhaseResult<{ unitStartedAt: number }>> {
|
|
1361
|
+
const { ctx, pi, s, deps, prefs } = ic;
|
|
1362
|
+
const { unitType, unitId, prompt, observabilityIssues, state, mid } = iterData;
|
|
1363
|
+
|
|
1364
|
+
debugLog("autoLoop", {
|
|
1365
|
+
phase: "unit-execution",
|
|
1366
|
+
iteration: ic.iteration,
|
|
1367
|
+
unitType,
|
|
1368
|
+
unitId,
|
|
1369
|
+
});
|
|
1204
1370
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1371
|
+
// Detect retry and capture previous tier for escalation
|
|
1372
|
+
const isRetry = !!(
|
|
1373
|
+
s.currentUnit &&
|
|
1374
|
+
s.currentUnit.type === unitType &&
|
|
1375
|
+
s.currentUnit.id === unitId
|
|
1376
|
+
);
|
|
1377
|
+
const previousTier = s.currentUnitRouting?.tier;
|
|
1210
1378
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1379
|
+
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1380
|
+
deps.captureAvailableSkills();
|
|
1381
|
+
deps.writeUnitRuntimeRecord(
|
|
1382
|
+
s.basePath,
|
|
1383
|
+
unitType,
|
|
1384
|
+
unitId,
|
|
1385
|
+
s.currentUnit.startedAt,
|
|
1386
|
+
{
|
|
1387
|
+
phase: "dispatched",
|
|
1388
|
+
wrapupWarningSent: false,
|
|
1389
|
+
timeoutAt: null,
|
|
1390
|
+
lastProgressAt: s.currentUnit.startedAt,
|
|
1391
|
+
progressCount: 0,
|
|
1392
|
+
lastProgressKind: "dispatch",
|
|
1393
|
+
},
|
|
1394
|
+
);
|
|
1215
1395
|
|
|
1216
|
-
|
|
1217
|
-
|
|
1396
|
+
// Status bar + progress widget
|
|
1397
|
+
ctx.ui.setStatus("gsd-auto", "auto");
|
|
1398
|
+
if (mid)
|
|
1399
|
+
deps.updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
|
|
1400
|
+
deps.updateProgressWidget(ctx, unitType, unitId, state);
|
|
1401
|
+
|
|
1402
|
+
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1403
|
+
|
|
1404
|
+
// Prompt injection
|
|
1405
|
+
let finalPrompt = prompt;
|
|
1406
|
+
|
|
1407
|
+
if (s.pendingVerificationRetry) {
|
|
1408
|
+
const retryCtx = s.pendingVerificationRetry;
|
|
1409
|
+
s.pendingVerificationRetry = null;
|
|
1410
|
+
const capped =
|
|
1411
|
+
retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
1412
|
+
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
1413
|
+
"\n\n[...failure context truncated]"
|
|
1414
|
+
: retryCtx.failureContext;
|
|
1415
|
+
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}`;
|
|
1416
|
+
}
|
|
1218
1417
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1418
|
+
if (s.pendingCrashRecovery) {
|
|
1419
|
+
const capped =
|
|
1420
|
+
s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
|
|
1421
|
+
? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) +
|
|
1422
|
+
"\n\n[...recovery briefing truncated to prevent memory exhaustion]"
|
|
1423
|
+
: s.pendingCrashRecovery;
|
|
1424
|
+
finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
|
|
1425
|
+
s.pendingCrashRecovery = null;
|
|
1426
|
+
} else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
|
|
1427
|
+
const diagnostic = deps.getDeepDiagnostic(s.basePath);
|
|
1428
|
+
if (diagnostic) {
|
|
1429
|
+
const cappedDiag =
|
|
1430
|
+
diagnostic.length > MAX_RECOVERY_CHARS
|
|
1431
|
+
? diagnostic.slice(0, MAX_RECOVERY_CHARS) +
|
|
1432
|
+
"\n\n[...diagnostic truncated to prevent memory exhaustion]"
|
|
1433
|
+
: diagnostic;
|
|
1434
|
+
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}`;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1222
1437
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
unitId,
|
|
1229
|
-
reason: stuckSignal.reason,
|
|
1230
|
-
recoveryAttempts: stuckRecoveryAttempts,
|
|
1231
|
-
});
|
|
1438
|
+
const repairBlock =
|
|
1439
|
+
deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
1440
|
+
if (repairBlock) {
|
|
1441
|
+
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
1442
|
+
}
|
|
1232
1443
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
ctx.ui.notify(
|
|
1255
|
-
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
1256
|
-
"warning",
|
|
1257
|
-
);
|
|
1258
|
-
deps.invalidateAllCaches();
|
|
1259
|
-
} else {
|
|
1260
|
-
// Level 2: hard stop — genuinely stuck
|
|
1261
|
-
debugLog("autoLoop", {
|
|
1262
|
-
phase: "stuck-detected",
|
|
1263
|
-
unitType,
|
|
1264
|
-
unitId,
|
|
1265
|
-
reason: stuckSignal.reason,
|
|
1266
|
-
});
|
|
1267
|
-
await deps.stopAuto(
|
|
1268
|
-
ctx,
|
|
1269
|
-
pi,
|
|
1270
|
-
`Stuck: ${stuckSignal.reason}`,
|
|
1271
|
-
);
|
|
1272
|
-
ctx.ui.notify(
|
|
1273
|
-
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1274
|
-
"error",
|
|
1275
|
-
);
|
|
1276
|
-
break;
|
|
1277
|
-
}
|
|
1278
|
-
} else {
|
|
1279
|
-
// Progress detected — reset recovery counter
|
|
1280
|
-
if (stuckRecoveryAttempts > 0) {
|
|
1281
|
-
debugLog("autoLoop", {
|
|
1282
|
-
phase: "stuck-counter-reset",
|
|
1283
|
-
from: recentUnits[recentUnits.length - 2]?.key ?? "",
|
|
1284
|
-
to: derivedKey,
|
|
1285
|
-
});
|
|
1286
|
-
stuckRecoveryAttempts = 0;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1444
|
+
// Prompt char measurement
|
|
1445
|
+
s.lastPromptCharCount = finalPrompt.length;
|
|
1446
|
+
s.lastBaselineCharCount = undefined;
|
|
1447
|
+
if (deps.isDbAvailable()) {
|
|
1448
|
+
try {
|
|
1449
|
+
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1450
|
+
const [decisionsContent, requirementsContent, projectContent] =
|
|
1451
|
+
await Promise.all([
|
|
1452
|
+
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
1453
|
+
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
1454
|
+
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
1455
|
+
]);
|
|
1456
|
+
s.lastBaselineCharCount =
|
|
1457
|
+
(decisionsContent?.length ?? 0) +
|
|
1458
|
+
(requirementsContent?.length ?? 0) +
|
|
1459
|
+
(projectContent?.length ?? 0);
|
|
1460
|
+
} catch {
|
|
1461
|
+
// Non-fatal
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1290
1464
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
"info",
|
|
1302
|
-
);
|
|
1303
|
-
}
|
|
1304
|
-
if (preDispatchResult.action === "skip") {
|
|
1305
|
-
ctx.ui.notify(
|
|
1306
|
-
`Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
|
|
1307
|
-
"info",
|
|
1308
|
-
);
|
|
1309
|
-
await new Promise((r) => setImmediate(r));
|
|
1310
|
-
continue;
|
|
1311
|
-
}
|
|
1312
|
-
if (preDispatchResult.action === "replace") {
|
|
1313
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
1314
|
-
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1315
|
-
} else if (preDispatchResult.prompt) {
|
|
1316
|
-
prompt = preDispatchResult.prompt;
|
|
1317
|
-
}
|
|
1465
|
+
// Cache-optimize prompt section ordering
|
|
1466
|
+
try {
|
|
1467
|
+
finalPrompt = deps.reorderForCaching(finalPrompt);
|
|
1468
|
+
} catch (reorderErr) {
|
|
1469
|
+
const msg =
|
|
1470
|
+
reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
|
|
1471
|
+
process.stderr.write(
|
|
1472
|
+
`[gsd] prompt reorder failed (non-fatal): ${msg}\n`,
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1318
1475
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1476
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1477
|
+
const modelResult = await deps.selectAndApplyModel(
|
|
1478
|
+
ctx,
|
|
1479
|
+
pi,
|
|
1480
|
+
unitType,
|
|
1481
|
+
unitId,
|
|
1482
|
+
s.basePath,
|
|
1483
|
+
prefs,
|
|
1484
|
+
s.verbose,
|
|
1485
|
+
s.autoModeStartModel,
|
|
1486
|
+
sidecarItem ? undefined : { isRetry, previousTier },
|
|
1487
|
+
);
|
|
1488
|
+
s.currentUnitRouting =
|
|
1489
|
+
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
1490
|
+
|
|
1491
|
+
// Start unit supervision
|
|
1492
|
+
deps.clearUnitTimeout();
|
|
1493
|
+
deps.startUnitSupervision({
|
|
1494
|
+
s,
|
|
1495
|
+
ctx,
|
|
1496
|
+
pi,
|
|
1497
|
+
unitType,
|
|
1498
|
+
unitId,
|
|
1499
|
+
prefs,
|
|
1500
|
+
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
1501
|
+
buildRecoveryContext: () => ({}),
|
|
1502
|
+
pauseAuto: deps.pauseAuto,
|
|
1503
|
+
});
|
|
1330
1504
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1505
|
+
// Session + send + await
|
|
1506
|
+
const sessionFile = deps.getSessionFile(ctx);
|
|
1507
|
+
deps.updateSessionLock(
|
|
1508
|
+
deps.lockBase(),
|
|
1509
|
+
unitType,
|
|
1510
|
+
unitId,
|
|
1511
|
+
s.completedUnits.length,
|
|
1512
|
+
sessionFile,
|
|
1513
|
+
);
|
|
1514
|
+
deps.writeLock(
|
|
1515
|
+
deps.lockBase(),
|
|
1516
|
+
unitType,
|
|
1517
|
+
unitId,
|
|
1518
|
+
s.completedUnits.length,
|
|
1519
|
+
sessionFile,
|
|
1520
|
+
);
|
|
1337
1521
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1522
|
+
debugLog("autoLoop", {
|
|
1523
|
+
phase: "runUnit-start",
|
|
1524
|
+
iteration: ic.iteration,
|
|
1525
|
+
unitType,
|
|
1526
|
+
unitId,
|
|
1527
|
+
});
|
|
1528
|
+
const unitResult = await runUnit(
|
|
1529
|
+
ctx,
|
|
1530
|
+
pi,
|
|
1531
|
+
s,
|
|
1532
|
+
unitType,
|
|
1533
|
+
unitId,
|
|
1534
|
+
finalPrompt,
|
|
1535
|
+
);
|
|
1536
|
+
debugLog("autoLoop", {
|
|
1537
|
+
phase: "runUnit-end",
|
|
1538
|
+
iteration: ic.iteration,
|
|
1539
|
+
unitType,
|
|
1540
|
+
unitId,
|
|
1541
|
+
status: unitResult.status,
|
|
1542
|
+
});
|
|
1340
1543
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1544
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
1545
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
1546
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
1547
|
+
if (lastEntry) {
|
|
1548
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
1549
|
+
}
|
|
1550
|
+
} else if (unitResult.event?.messages?.length) {
|
|
1551
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
1552
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
1553
|
+
if (/error|fail|exception/i.test(msgStr)) {
|
|
1554
|
+
const lastEntry = loopState.recentUnits[loopState.recentUnits.length - 1];
|
|
1555
|
+
if (lastEntry) {
|
|
1556
|
+
lastEntry.error = msgStr.slice(0, 200);
|
|
1350
1557
|
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1351
1560
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
// Detect retry and capture previous tier for escalation
|
|
1362
|
-
const isRetry = !!(
|
|
1363
|
-
s.currentUnit &&
|
|
1364
|
-
s.currentUnit.type === unitType &&
|
|
1365
|
-
s.currentUnit.id === unitId
|
|
1366
|
-
);
|
|
1367
|
-
const previousTier = s.currentUnitRouting?.tier;
|
|
1561
|
+
if (unitResult.status === "cancelled") {
|
|
1562
|
+
ctx.ui.notify(
|
|
1563
|
+
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
1564
|
+
"warning",
|
|
1565
|
+
);
|
|
1566
|
+
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
1567
|
+
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
1568
|
+
return { action: "break", reason: "session-failed" };
|
|
1569
|
+
}
|
|
1368
1570
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
lastProgressAt: s.currentUnit.startedAt,
|
|
1381
|
-
progressCount: 0,
|
|
1382
|
-
lastProgressKind: "dispatch",
|
|
1383
|
-
},
|
|
1384
|
-
);
|
|
1571
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
1572
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
1573
|
+
// crash between iterations.
|
|
1574
|
+
await deps.closeoutUnit(
|
|
1575
|
+
ctx,
|
|
1576
|
+
s.basePath,
|
|
1577
|
+
unitType,
|
|
1578
|
+
unitId,
|
|
1579
|
+
s.currentUnit.startedAt,
|
|
1580
|
+
deps.buildSnapshotOpts(unitType, unitId),
|
|
1581
|
+
);
|
|
1385
1582
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
// Prompt injection
|
|
1395
|
-
let finalPrompt = prompt;
|
|
1396
|
-
|
|
1397
|
-
if (s.pendingVerificationRetry) {
|
|
1398
|
-
const retryCtx = s.pendingVerificationRetry;
|
|
1399
|
-
s.pendingVerificationRetry = null;
|
|
1400
|
-
const capped =
|
|
1401
|
-
retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
1402
|
-
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
1403
|
-
"\n\n[...failure context truncated]"
|
|
1404
|
-
: retryCtx.failureContext;
|
|
1405
|
-
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}`;
|
|
1406
|
-
}
|
|
1583
|
+
if (s.currentUnitRouting) {
|
|
1584
|
+
deps.recordOutcome(
|
|
1585
|
+
unitType,
|
|
1586
|
+
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1587
|
+
true, // success assumed; dispatch will re-dispatch if artifact missing
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1407
1590
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1591
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
1592
|
+
const artifactVerified =
|
|
1593
|
+
isHookUnit ||
|
|
1594
|
+
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1595
|
+
if (artifactVerified) {
|
|
1596
|
+
s.completedUnits.push({
|
|
1597
|
+
type: unitType,
|
|
1598
|
+
id: unitId,
|
|
1599
|
+
startedAt: s.currentUnit.startedAt,
|
|
1600
|
+
finishedAt: Date.now(),
|
|
1601
|
+
});
|
|
1602
|
+
if (s.completedUnits.length > 200) {
|
|
1603
|
+
s.completedUnits = s.completedUnits.slice(-200);
|
|
1604
|
+
}
|
|
1605
|
+
// Flush completed-units to disk so the record survives crashes
|
|
1606
|
+
try {
|
|
1607
|
+
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
1608
|
+
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
1609
|
+
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
1610
|
+
} catch { /* non-fatal: disk flush failure */ }
|
|
1611
|
+
|
|
1612
|
+
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
1613
|
+
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
1614
|
+
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1615
|
+
}
|
|
1427
1616
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (repairBlock) {
|
|
1431
|
-
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
1432
|
-
}
|
|
1617
|
+
return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
|
|
1618
|
+
}
|
|
1433
1619
|
|
|
1434
|
-
|
|
1435
|
-
s.lastPromptCharCount = finalPrompt.length;
|
|
1436
|
-
s.lastBaselineCharCount = undefined;
|
|
1437
|
-
if (deps.isDbAvailable()) {
|
|
1438
|
-
try {
|
|
1439
|
-
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1440
|
-
const [decisionsContent, requirementsContent, projectContent] =
|
|
1441
|
-
await Promise.all([
|
|
1442
|
-
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
1443
|
-
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
1444
|
-
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
1445
|
-
]);
|
|
1446
|
-
s.lastBaselineCharCount =
|
|
1447
|
-
(decisionsContent?.length ?? 0) +
|
|
1448
|
-
(requirementsContent?.length ?? 0) +
|
|
1449
|
-
(projectContent?.length ?? 0);
|
|
1450
|
-
} catch {
|
|
1451
|
-
// Non-fatal
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1620
|
+
// ─── runFinalize ──────────────────────────────────────────────────────────────
|
|
1454
1621
|
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1622
|
+
/**
|
|
1623
|
+
* Phase 5: Post-unit finalize — pre/post verification, UAT pause, step-wizard.
|
|
1624
|
+
* Returns break/continue/next to control the outer loop.
|
|
1625
|
+
*/
|
|
1626
|
+
async function runFinalize(
|
|
1627
|
+
ic: IterationContext,
|
|
1628
|
+
iterData: IterationData,
|
|
1629
|
+
sidecarItem?: SidecarItem,
|
|
1630
|
+
): Promise<PhaseResult> {
|
|
1631
|
+
const { ctx, pi, s, deps } = ic;
|
|
1632
|
+
const { pauseAfterUatDispatch } = iterData;
|
|
1633
|
+
|
|
1634
|
+
debugLog("autoLoop", { phase: "finalize", iteration: ic.iteration });
|
|
1635
|
+
|
|
1636
|
+
// Clear unit timeout (unit completed)
|
|
1637
|
+
deps.clearUnitTimeout();
|
|
1638
|
+
|
|
1639
|
+
// Post-unit context for pre/post verification
|
|
1640
|
+
const postUnitCtx: PostUnitContext = {
|
|
1641
|
+
s,
|
|
1642
|
+
ctx,
|
|
1643
|
+
pi,
|
|
1644
|
+
buildSnapshotOpts: deps.buildSnapshotOpts,
|
|
1645
|
+
lockBase: deps.lockBase,
|
|
1646
|
+
stopAuto: deps.stopAuto,
|
|
1647
|
+
pauseAuto: deps.pauseAuto,
|
|
1648
|
+
updateProgressWidget: deps.updateProgressWidget,
|
|
1649
|
+
};
|
|
1465
1650
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1651
|
+
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1652
|
+
// Sidecar items use lightweight pre-verification opts
|
|
1653
|
+
const preVerificationOpts: PreVerificationOpts | undefined = sidecarItem
|
|
1654
|
+
? sidecarItem.kind === "hook"
|
|
1655
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1656
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1657
|
+
: undefined;
|
|
1658
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1659
|
+
if (preResult === "dispatched") {
|
|
1660
|
+
debugLog("autoLoop", {
|
|
1661
|
+
phase: "exit",
|
|
1662
|
+
reason: "pre-verification-dispatched",
|
|
1663
|
+
});
|
|
1664
|
+
return { action: "break", reason: "pre-verification-dispatched" };
|
|
1665
|
+
}
|
|
1480
1666
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
1491
|
-
buildRecoveryContext: () => ({}),
|
|
1492
|
-
pauseAuto: deps.pauseAuto,
|
|
1493
|
-
});
|
|
1667
|
+
if (pauseAfterUatDispatch) {
|
|
1668
|
+
ctx.ui.notify(
|
|
1669
|
+
"UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
|
|
1670
|
+
"info",
|
|
1671
|
+
);
|
|
1672
|
+
await deps.pauseAuto(ctx, pi);
|
|
1673
|
+
debugLog("autoLoop", { phase: "exit", reason: "uat-pause" });
|
|
1674
|
+
return { action: "break", reason: "uat-pause" };
|
|
1675
|
+
}
|
|
1494
1676
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
deps.writeLock(
|
|
1505
|
-
deps.lockBase(),
|
|
1506
|
-
unitType,
|
|
1507
|
-
unitId,
|
|
1508
|
-
s.completedUnits.length,
|
|
1509
|
-
sessionFile,
|
|
1510
|
-
);
|
|
1677
|
+
// Verification gate
|
|
1678
|
+
// Hook sidecar items skip verification entirely.
|
|
1679
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
1680
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
1681
|
+
if (!skipVerification) {
|
|
1682
|
+
const verificationResult = await deps.runPostUnitVerification(
|
|
1683
|
+
{ s, ctx, pi },
|
|
1684
|
+
deps.pauseAuto,
|
|
1685
|
+
);
|
|
1511
1686
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
unitId,
|
|
1517
|
-
});
|
|
1518
|
-
const unitResult = await runUnit(
|
|
1519
|
-
ctx,
|
|
1520
|
-
pi,
|
|
1521
|
-
s,
|
|
1522
|
-
unitType,
|
|
1523
|
-
unitId,
|
|
1524
|
-
finalPrompt,
|
|
1525
|
-
);
|
|
1526
|
-
debugLog("autoLoop", {
|
|
1527
|
-
phase: "runUnit-end",
|
|
1528
|
-
iteration,
|
|
1529
|
-
unitType,
|
|
1530
|
-
unitId,
|
|
1531
|
-
status: unitResult.status,
|
|
1532
|
-
});
|
|
1687
|
+
if (verificationResult === "pause") {
|
|
1688
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1689
|
+
return { action: "break", reason: "verification-pause" };
|
|
1690
|
+
}
|
|
1533
1691
|
|
|
1534
|
-
|
|
1535
|
-
if (
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
if (/error|fail|exception/i.test(msgStr)) {
|
|
1544
|
-
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1545
|
-
if (lastEntry) {
|
|
1546
|
-
lastEntry.error = msgStr.slice(0, 200);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1692
|
+
if (verificationResult === "retry") {
|
|
1693
|
+
if (sidecarItem) {
|
|
1694
|
+
// Sidecar verification retries are skipped — just continue
|
|
1695
|
+
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration });
|
|
1696
|
+
} else {
|
|
1697
|
+
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1698
|
+
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1699
|
+
debugLog("autoLoop", { phase: "verification-retry", iteration: ic.iteration });
|
|
1700
|
+
return { action: "continue" };
|
|
1549
1701
|
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1550
1704
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
1554
|
-
"warning",
|
|
1555
|
-
);
|
|
1556
|
-
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
1557
|
-
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
1558
|
-
break;
|
|
1559
|
-
}
|
|
1705
|
+
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
1706
|
+
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
1560
1707
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
unitId,
|
|
1569
|
-
s.currentUnit.startedAt,
|
|
1570
|
-
deps.buildSnapshotOpts(unitType, unitId),
|
|
1571
|
-
);
|
|
1708
|
+
if (postResult === "stopped") {
|
|
1709
|
+
debugLog("autoLoop", {
|
|
1710
|
+
phase: "exit",
|
|
1711
|
+
reason: "post-verification-stopped",
|
|
1712
|
+
});
|
|
1713
|
+
return { action: "break", reason: "post-verification-stopped" };
|
|
1714
|
+
}
|
|
1572
1715
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
);
|
|
1579
|
-
}
|
|
1716
|
+
if (postResult === "step-wizard") {
|
|
1717
|
+
// Step mode — exit the loop (caller handles wizard)
|
|
1718
|
+
debugLog("autoLoop", { phase: "exit", reason: "step-wizard" });
|
|
1719
|
+
return { action: "break", reason: "step-wizard" };
|
|
1720
|
+
}
|
|
1580
1721
|
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
isHookUnit ||
|
|
1584
|
-
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1585
|
-
if (artifactVerified) {
|
|
1586
|
-
s.completedUnits.push({
|
|
1587
|
-
type: unitType,
|
|
1588
|
-
id: unitId,
|
|
1589
|
-
startedAt: s.currentUnit.startedAt,
|
|
1590
|
-
finishedAt: Date.now(),
|
|
1591
|
-
});
|
|
1592
|
-
if (s.completedUnits.length > 200) {
|
|
1593
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1594
|
-
}
|
|
1595
|
-
// Flush completed-units to disk so the record survives crashes
|
|
1596
|
-
try {
|
|
1597
|
-
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
1598
|
-
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
1599
|
-
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
1600
|
-
} catch { /* non-fatal: disk flush failure */ }
|
|
1601
|
-
|
|
1602
|
-
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
1603
|
-
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
1604
|
-
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1605
|
-
}
|
|
1722
|
+
return { action: "next", data: undefined as void };
|
|
1723
|
+
}
|
|
1606
1724
|
|
|
1607
|
-
|
|
1725
|
+
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
1608
1726
|
|
|
1609
|
-
|
|
1727
|
+
/**
|
|
1728
|
+
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
1729
|
+
* runUnit → finalize → repeat. Exits when s.active becomes false or a
|
|
1730
|
+
* terminal condition is reached.
|
|
1731
|
+
*
|
|
1732
|
+
* This is the linear replacement for the recursive
|
|
1733
|
+
* dispatchNextUnit → handleAgentEnd → dispatchNextUnit chain.
|
|
1734
|
+
*/
|
|
1735
|
+
export async function autoLoop(
|
|
1736
|
+
ctx: ExtensionContext,
|
|
1737
|
+
pi: ExtensionAPI,
|
|
1738
|
+
s: AutoSession,
|
|
1739
|
+
deps: LoopDeps,
|
|
1740
|
+
): Promise<void> {
|
|
1741
|
+
debugLog("autoLoop", { phase: "enter" });
|
|
1742
|
+
let iteration = 0;
|
|
1743
|
+
const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
|
|
1744
|
+
let consecutiveErrors = 0;
|
|
1610
1745
|
|
|
1611
|
-
|
|
1612
|
-
|
|
1746
|
+
while (s.active) {
|
|
1747
|
+
iteration++;
|
|
1748
|
+
debugLog("autoLoop", { phase: "loop-top", iteration });
|
|
1613
1749
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1750
|
+
if (iteration > MAX_LOOP_ITERATIONS) {
|
|
1751
|
+
debugLog("autoLoop", {
|
|
1752
|
+
phase: "exit",
|
|
1753
|
+
reason: "max-iterations",
|
|
1754
|
+
iteration,
|
|
1755
|
+
});
|
|
1756
|
+
await deps.stopAuto(
|
|
1617
1757
|
ctx,
|
|
1618
1758
|
pi,
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1759
|
+
`Safety: loop exceeded ${MAX_LOOP_ITERATIONS} iterations — possible runaway`,
|
|
1760
|
+
);
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (!s.cmdCtx) {
|
|
1765
|
+
debugLog("autoLoop", { phase: "exit", reason: "no-cmdCtx" });
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
try {
|
|
1770
|
+
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
1771
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
1625
1772
|
|
|
1626
|
-
//
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1631
|
-
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1632
|
-
: undefined;
|
|
1633
|
-
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1634
|
-
if (preResult === "dispatched") {
|
|
1773
|
+
// ── Check sidecar queue before deriveState ──
|
|
1774
|
+
let sidecarItem: SidecarItem | undefined;
|
|
1775
|
+
if (s.sidecarQueue.length > 0) {
|
|
1776
|
+
sidecarItem = s.sidecarQueue.shift()!;
|
|
1635
1777
|
debugLog("autoLoop", {
|
|
1636
|
-
phase: "
|
|
1637
|
-
|
|
1778
|
+
phase: "sidecar-dequeue",
|
|
1779
|
+
kind: sidecarItem.kind,
|
|
1780
|
+
unitType: sidecarItem.unitType,
|
|
1781
|
+
unitId: sidecarItem.unitId,
|
|
1638
1782
|
});
|
|
1639
|
-
break;
|
|
1640
1783
|
}
|
|
1641
1784
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
{ s, ctx, pi },
|
|
1659
|
-
deps.pauseAuto,
|
|
1660
|
-
);
|
|
1661
|
-
|
|
1662
|
-
if (verificationResult === "pause") {
|
|
1663
|
-
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1785
|
+
const sessionLockBase = deps.lockBase();
|
|
1786
|
+
if (sessionLockBase) {
|
|
1787
|
+
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
1788
|
+
if (!lockStatus.valid) {
|
|
1789
|
+
debugLog("autoLoop", {
|
|
1790
|
+
phase: "session-lock-invalid",
|
|
1791
|
+
reason: lockStatus.failureReason ?? "unknown",
|
|
1792
|
+
existingPid: lockStatus.existingPid,
|
|
1793
|
+
expectedPid: lockStatus.expectedPid,
|
|
1794
|
+
});
|
|
1795
|
+
deps.handleLostSessionLock(ctx, lockStatus);
|
|
1796
|
+
debugLog("autoLoop", {
|
|
1797
|
+
phase: "exit",
|
|
1798
|
+
reason: "session-lock-lost",
|
|
1799
|
+
detail: lockStatus.failureReason ?? "unknown",
|
|
1800
|
+
});
|
|
1664
1801
|
break;
|
|
1665
1802
|
}
|
|
1666
|
-
|
|
1667
|
-
if (verificationResult === "retry") {
|
|
1668
|
-
if (sidecarItem) {
|
|
1669
|
-
// Sidecar verification retries are skipped — just continue
|
|
1670
|
-
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration });
|
|
1671
|
-
} else {
|
|
1672
|
-
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1673
|
-
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1674
|
-
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
1803
|
}
|
|
1679
1804
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1805
|
+
const ic: IterationContext = { ctx, pi, s, deps, prefs, iteration };
|
|
1806
|
+
let iterData: IterationData;
|
|
1682
1807
|
|
|
1683
|
-
if (
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1808
|
+
if (!sidecarItem) {
|
|
1809
|
+
// ── Phase 1: Pre-dispatch ─────────────────────────────────────────
|
|
1810
|
+
const preDispatchResult = await runPreDispatch(ic, loopState);
|
|
1811
|
+
if (preDispatchResult.action === "break") break;
|
|
1812
|
+
if (preDispatchResult.action === "continue") continue;
|
|
1813
|
+
|
|
1814
|
+
const preData = preDispatchResult.data;
|
|
1815
|
+
|
|
1816
|
+
// ── Phase 2: Guards ───────────────────────────────────────────────
|
|
1817
|
+
const guardsResult = await runGuards(ic, preData.mid);
|
|
1818
|
+
if (guardsResult.action === "break") break;
|
|
1819
|
+
|
|
1820
|
+
// ── Phase 3: Dispatch ─────────────────────────────────────────────
|
|
1821
|
+
const dispatchResult = await runDispatch(ic, preData, loopState);
|
|
1822
|
+
if (dispatchResult.action === "break") break;
|
|
1823
|
+
if (dispatchResult.action === "continue") continue;
|
|
1824
|
+
iterData = dispatchResult.data;
|
|
1825
|
+
} else {
|
|
1826
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
1827
|
+
const sidecarState = await deps.deriveState(s.basePath);
|
|
1828
|
+
iterData = {
|
|
1829
|
+
unitType: sidecarItem.unitType,
|
|
1830
|
+
unitId: sidecarItem.unitId,
|
|
1831
|
+
prompt: sidecarItem.prompt,
|
|
1832
|
+
finalPrompt: sidecarItem.prompt,
|
|
1833
|
+
pauseAfterUatDispatch: false,
|
|
1834
|
+
observabilityIssues: [],
|
|
1835
|
+
state: sidecarState,
|
|
1836
|
+
mid: sidecarState.activeMilestone?.id,
|
|
1837
|
+
midTitle: sidecarState.activeMilestone?.title,
|
|
1838
|
+
isRetry: false, previousTier: undefined,
|
|
1839
|
+
};
|
|
1689
1840
|
}
|
|
1690
1841
|
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1842
|
+
const unitPhaseResult = await runUnitPhase(ic, iterData, loopState, sidecarItem);
|
|
1843
|
+
if (unitPhaseResult.action === "break") break;
|
|
1844
|
+
|
|
1845
|
+
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
1846
|
+
|
|
1847
|
+
const finalizeResult = await runFinalize(ic, iterData, sidecarItem);
|
|
1848
|
+
if (finalizeResult.action === "break") break;
|
|
1849
|
+
if (finalizeResult.action === "continue") continue;
|
|
1696
1850
|
|
|
1697
1851
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1698
1852
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|