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
|
@@ -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.
|
|
@@ -656,204 +697,192 @@ async function closeoutAndStop(
|
|
|
656
697
|
await deps.stopAuto(ctx, pi, reason);
|
|
657
698
|
}
|
|
658
699
|
|
|
659
|
-
// ───
|
|
700
|
+
// ─── runPreDispatch ───────────────────────────────────────────────────────────
|
|
660
701
|
|
|
661
702
|
/**
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
* This is the linear replacement for the recursive
|
|
667
|
-
* 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.
|
|
668
706
|
*/
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
deps
|
|
674
|
-
): Promise<void> {
|
|
675
|
-
debugLog("autoLoop", { phase: "enter" });
|
|
676
|
-
let iteration = 0;
|
|
677
|
-
// ── Sliding-window stuck detection ──
|
|
678
|
-
const recentUnits: Array<{ key: string; error?: string }> = [];
|
|
679
|
-
const STUCK_WINDOW_SIZE = 6;
|
|
680
|
-
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;
|
|
681
712
|
|
|
682
|
-
|
|
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
|
+
}
|
|
683
720
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
721
|
+
deps.invalidateAllCaches();
|
|
722
|
+
s.lastPromptCharCount = undefined;
|
|
723
|
+
s.lastBaselineCharCount = undefined;
|
|
687
724
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
ctx,
|
|
696
|
-
pi,
|
|
697
|
-
`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",
|
|
698
732
|
);
|
|
699
|
-
break;
|
|
700
733
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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" };
|
|
705
742
|
}
|
|
743
|
+
} catch {
|
|
744
|
+
// Non-fatal
|
|
745
|
+
}
|
|
706
746
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
unitId: sidecarItem.unitId,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const sessionLockBase = deps.lockBase();
|
|
724
|
-
if (sessionLockBase) {
|
|
725
|
-
const lockStatus = deps.validateSessionLock(sessionLockBase);
|
|
726
|
-
if (!lockStatus.valid) {
|
|
727
|
-
debugLog("autoLoop", {
|
|
728
|
-
phase: "session-lock-invalid",
|
|
729
|
-
reason: lockStatus.failureReason ?? "unknown",
|
|
730
|
-
existingPid: lockStatus.existingPid,
|
|
731
|
-
expectedPid: lockStatus.expectedPid,
|
|
732
|
-
});
|
|
733
|
-
deps.handleLostSessionLock(ctx, lockStatus);
|
|
734
|
-
debugLog("autoLoop", {
|
|
735
|
-
phase: "exit",
|
|
736
|
-
reason: "session-lock-lost",
|
|
737
|
-
detail: lockStatus.failureReason ?? "unknown",
|
|
738
|
-
});
|
|
739
|
-
break;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
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
|
+
}
|
|
742
759
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
+
});
|
|
752
771
|
|
|
753
|
-
|
|
754
|
-
|
|
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
|
+
);
|
|
755
789
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
+
);
|
|
762
802
|
}
|
|
803
|
+
}
|
|
804
|
+
|
|
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;
|
|
763
811
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
s.lastBaselineCharCount = undefined;
|
|
812
|
+
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
813
|
+
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
767
814
|
|
|
768
|
-
|
|
815
|
+
// Opt-in: create draft PR on milestone completion
|
|
816
|
+
if (prefs?.git?.auto_pr) {
|
|
769
817
|
try {
|
|
770
|
-
const
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
if (
|
|
778
|
-
ctx.ui.notify(
|
|
779
|
-
healthGate.reason ?? "Pre-dispatch health check failed.",
|
|
780
|
-
"error",
|
|
781
|
-
);
|
|
782
|
-
await deps.pauseAuto(ctx, pi);
|
|
783
|
-
debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
|
|
784
|
-
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");
|
|
785
827
|
}
|
|
786
828
|
} catch {
|
|
787
|
-
// Non-fatal
|
|
829
|
+
// Non-fatal — PR creation is best-effort
|
|
788
830
|
}
|
|
831
|
+
}
|
|
789
832
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
);
|
|
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
|
+
});
|
|
801
844
|
}
|
|
845
|
+
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
846
|
+
} else {
|
|
847
|
+
// mid is undefined — no milestone to capture integration branch for
|
|
848
|
+
}
|
|
802
849
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
mid,
|
|
812
|
-
statePhase: state.phase,
|
|
813
|
-
});
|
|
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
|
+
}
|
|
814
858
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
"info",
|
|
820
|
-
);
|
|
821
|
-
deps.sendDesktopNotification(
|
|
822
|
-
"GSD",
|
|
823
|
-
`Milestone ${s.currentMilestoneId} complete!`,
|
|
824
|
-
"success",
|
|
825
|
-
"milestone",
|
|
826
|
-
);
|
|
827
|
-
deps.logCmuxEvent(
|
|
828
|
-
prefs,
|
|
829
|
-
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
830
|
-
"success",
|
|
831
|
-
);
|
|
859
|
+
if (mid) {
|
|
860
|
+
s.currentMilestoneId = mid;
|
|
861
|
+
deps.setActiveMilestoneId(s.basePath, mid);
|
|
862
|
+
}
|
|
832
863
|
|
|
833
|
-
|
|
834
|
-
if (vizPrefs?.auto_visualize) {
|
|
835
|
-
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
836
|
-
}
|
|
837
|
-
if (vizPrefs?.auto_report !== false) {
|
|
838
|
-
try {
|
|
839
|
-
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
840
|
-
} catch (err) {
|
|
841
|
-
ctx.ui.notify(
|
|
842
|
-
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
843
|
-
"warning",
|
|
844
|
-
);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
864
|
+
// ── Terminal conditions ──────────────────────────────────────────────
|
|
847
865
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
+
}
|
|
854
877
|
|
|
855
|
-
|
|
856
|
-
|
|
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);
|
|
857
886
|
|
|
858
887
|
// Opt-in: create draft PR on milestone completion
|
|
859
888
|
if (prefs?.git?.auto_pr) {
|
|
@@ -861,7 +890,7 @@ export async function autoLoop(
|
|
|
861
890
|
const { createDraftPR } = await import("./git-service.js");
|
|
862
891
|
const prUrl = createDraftPR(
|
|
863
892
|
s.basePath,
|
|
864
|
-
s.currentMilestoneId
|
|
893
|
+
s.currentMilestoneId,
|
|
865
894
|
`[GSD] ${s.currentMilestoneId} complete`,
|
|
866
895
|
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
867
896
|
);
|
|
@@ -872,841 +901,952 @@ export async function autoLoop(
|
|
|
872
901
|
// Non-fatal — PR creation is best-effort
|
|
873
902
|
}
|
|
874
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
|
+
}
|
|
875
951
|
|
|
876
|
-
|
|
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
|
+
}
|
|
959
|
+
|
|
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
|
+
}
|
|
877
979
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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);
|
|
881
985
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
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");
|
|
887
998
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
// mid is undefined — no milestone to capture integration branch for
|
|
999
|
+
} catch {
|
|
1000
|
+
// Non-fatal — PR creation is best-effort
|
|
891
1001
|
}
|
|
892
|
-
|
|
893
|
-
const pendingIds = state.registry
|
|
894
|
-
.filter(
|
|
895
|
-
(m: { status: string }) =>
|
|
896
|
-
m.status !== "complete" && m.status !== "parked",
|
|
897
|
-
)
|
|
898
|
-
.map((m: { id: string }) => m.id);
|
|
899
|
-
deps.pruneQueueOrder(s.basePath, pendingIds);
|
|
900
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
|
+
}
|
|
901
1019
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
}
|
|
906
1030
|
|
|
907
|
-
|
|
1031
|
+
return { action: "next", data: { state, mid, midTitle } };
|
|
1032
|
+
}
|
|
908
1033
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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;
|
|
1048
|
+
|
|
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
|
+
});
|
|
920
1058
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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,
|
|
924
1100
|
);
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (prefs?.git?.auto_pr) {
|
|
932
|
-
try {
|
|
933
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
934
|
-
const prUrl = createDraftPR(
|
|
935
|
-
s.basePath,
|
|
936
|
-
s.currentMilestoneId,
|
|
937
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
938
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
939
|
-
);
|
|
940
|
-
if (prUrl) {
|
|
941
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
942
|
-
}
|
|
943
|
-
} catch {
|
|
944
|
-
// Non-fatal — PR creation is best-effort
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
deps.sendDesktopNotification(
|
|
949
|
-
"GSD",
|
|
950
|
-
"All milestones complete!",
|
|
951
|
-
"success",
|
|
952
|
-
"milestone",
|
|
953
|
-
);
|
|
954
|
-
deps.logCmuxEvent(
|
|
955
|
-
prefs,
|
|
956
|
-
"All milestones complete.",
|
|
957
|
-
"success",
|
|
958
|
-
);
|
|
959
|
-
await deps.stopAuto(ctx, pi, "All milestones complete");
|
|
960
|
-
} else if (incomplete.length === 0 && state.registry.length === 0) {
|
|
961
|
-
// Empty registry — no milestones visible, likely a path resolution bug
|
|
962
|
-
const diag = `basePath=${s.basePath}, phase=${state.phase}`;
|
|
963
|
-
ctx.ui.notify(
|
|
964
|
-
`No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
|
|
965
|
-
"error",
|
|
966
|
-
);
|
|
967
|
-
await deps.stopAuto(
|
|
968
|
-
ctx,
|
|
969
|
-
pi,
|
|
970
|
-
`No milestones found — check basePath resolution`,
|
|
971
|
-
);
|
|
972
|
-
} else if (state.phase === "blocked") {
|
|
973
|
-
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
974
|
-
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
975
|
-
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
976
|
-
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
977
|
-
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
978
|
-
} else {
|
|
979
|
-
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
980
|
-
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
|
+
});
|
|
981
1107
|
ctx.ui.notify(
|
|
982
|
-
`
|
|
983
|
-
"
|
|
984
|
-
);
|
|
985
|
-
await deps.stopAuto(
|
|
986
|
-
ctx,
|
|
987
|
-
pi,
|
|
988
|
-
`No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`,
|
|
1108
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1109
|
+
"info",
|
|
989
1110
|
);
|
|
1111
|
+
deps.invalidateAllCaches();
|
|
1112
|
+
return { action: "continue" };
|
|
990
1113
|
}
|
|
991
|
-
debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
if (!midTitle) {
|
|
996
|
-
midTitle = mid;
|
|
997
1114
|
ctx.ui.notify(
|
|
998
|
-
`
|
|
1115
|
+
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
999
1116
|
"warning",
|
|
1000
1117
|
);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// Mid-merge safety check
|
|
1004
|
-
if (deps.reconcileMergeState(s.basePath, ctx)) {
|
|
1005
1118
|
deps.invalidateAllCaches();
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
midTitle = state.activeMilestone?.title;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (!mid || !midTitle) {
|
|
1012
|
-
const noMilestoneReason = !mid
|
|
1013
|
-
? "No active milestone after merge reconciliation"
|
|
1014
|
-
: `Milestone ${mid} has no title after reconciliation`;
|
|
1015
|
-
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
1119
|
+
} else {
|
|
1120
|
+
// Level 2: hard stop — genuinely stuck
|
|
1016
1121
|
debugLog("autoLoop", {
|
|
1017
|
-
phase: "
|
|
1018
|
-
|
|
1122
|
+
phase: "stuck-detected",
|
|
1123
|
+
unitType,
|
|
1124
|
+
unitId,
|
|
1125
|
+
reason: stuckSignal.reason,
|
|
1019
1126
|
});
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (state.phase === "complete") {
|
|
1025
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
1026
|
-
if (s.currentMilestoneId) {
|
|
1027
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
1028
|
-
|
|
1029
|
-
// Opt-in: create draft PR on milestone completion
|
|
1030
|
-
if (prefs?.git?.auto_pr) {
|
|
1031
|
-
try {
|
|
1032
|
-
const { createDraftPR } = await import("./git-service.js");
|
|
1033
|
-
const prUrl = createDraftPR(
|
|
1034
|
-
s.basePath,
|
|
1035
|
-
s.currentMilestoneId,
|
|
1036
|
-
`[GSD] ${s.currentMilestoneId} complete`,
|
|
1037
|
-
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
1038
|
-
);
|
|
1039
|
-
if (prUrl) {
|
|
1040
|
-
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
1041
|
-
}
|
|
1042
|
-
} catch {
|
|
1043
|
-
// Non-fatal — PR creation is best-effort
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
deps.sendDesktopNotification(
|
|
1048
|
-
"GSD",
|
|
1049
|
-
`Milestone ${mid} complete!`,
|
|
1050
|
-
"success",
|
|
1051
|
-
"milestone",
|
|
1127
|
+
await deps.stopAuto(
|
|
1128
|
+
ctx,
|
|
1129
|
+
pi,
|
|
1130
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
1052
1131
|
);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
"success",
|
|
1132
|
+
ctx.ui.notify(
|
|
1133
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1134
|
+
"error",
|
|
1057
1135
|
);
|
|
1058
|
-
|
|
1059
|
-
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
1060
|
-
break;
|
|
1136
|
+
return { action: "break", reason: "stuck-detected" };
|
|
1061
1137
|
}
|
|
1062
|
-
|
|
1063
|
-
//
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
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;
|
|
1072
1147
|
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1073
1150
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
+
}
|
|
1094
1178
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (budgetEnforcementAction === "halt") {
|
|
1107
|
-
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1108
|
-
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1109
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1110
|
-
break;
|
|
1111
|
-
}
|
|
1112
|
-
if (budgetEnforcementAction === "pause") {
|
|
1113
|
-
ctx.ui.notify(
|
|
1114
|
-
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1115
|
-
"warning",
|
|
1116
|
-
);
|
|
1117
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1118
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1119
|
-
await deps.pauseAuto(ctx, pi);
|
|
1120
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1121
|
-
break;
|
|
1122
|
-
}
|
|
1123
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
1124
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1125
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1126
|
-
} else if (threshold.pct < 100) {
|
|
1127
|
-
// Sub-100% — simple notification
|
|
1128
|
-
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
1129
|
-
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
1130
|
-
deps.sendDesktopNotification(
|
|
1131
|
-
"GSD",
|
|
1132
|
-
msg,
|
|
1133
|
-
threshold.notifyLevel,
|
|
1134
|
-
"budget",
|
|
1135
|
-
);
|
|
1136
|
-
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
1137
|
-
}
|
|
1138
|
-
} else if (budgetAlertLevel === 0) {
|
|
1139
|
-
s.lastBudgetAlertLevel = 0;
|
|
1140
|
-
}
|
|
1141
|
-
} else {
|
|
1142
|
-
s.lastBudgetAlertLevel = 0;
|
|
1143
|
-
}
|
|
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
|
+
}
|
|
1144
1190
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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") {
|
|
1155
1258
|
ctx.ui.notify(
|
|
1156
|
-
`${msg}
|
|
1157
|
-
"warning",
|
|
1158
|
-
);
|
|
1159
|
-
deps.sendDesktopNotification(
|
|
1160
|
-
"GSD",
|
|
1161
|
-
`Context ${contextUsage.percent}% — paused`,
|
|
1259
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1162
1260
|
"warning",
|
|
1163
|
-
"attention",
|
|
1164
1261
|
);
|
|
1262
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1263
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1165
1264
|
await deps.pauseAuto(ctx, pi);
|
|
1166
|
-
debugLog("autoLoop", { phase: "exit", reason: "
|
|
1167
|
-
break;
|
|
1265
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1266
|
+
return { action: "break", reason: "budget-pause" };
|
|
1168
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);
|
|
1169
1282
|
}
|
|
1283
|
+
} else if (budgetAlertLevel === 0) {
|
|
1284
|
+
s.lastBudgetAlertLevel = 0;
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
s.lastBudgetAlertLevel = 0;
|
|
1288
|
+
}
|
|
1170
1289
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
+
) {
|
|
1195
1331
|
ctx.ui.notify(
|
|
1196
|
-
`Secrets
|
|
1197
|
-
"
|
|
1332
|
+
`Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
|
|
1333
|
+
"info",
|
|
1198
1334
|
);
|
|
1335
|
+
} else {
|
|
1336
|
+
ctx.ui.notify("Secrets collection skipped.", "info");
|
|
1199
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
|
+
}
|
|
1200
1345
|
|
|
1201
|
-
|
|
1346
|
+
return { action: "next", data: undefined as void };
|
|
1347
|
+
}
|
|
1202
1348
|
|
|
1203
|
-
|
|
1204
|
-
const dispatchResult = await deps.resolveDispatch({
|
|
1205
|
-
basePath: s.basePath,
|
|
1206
|
-
mid,
|
|
1207
|
-
midTitle: midTitle!,
|
|
1208
|
-
state,
|
|
1209
|
-
prefs,
|
|
1210
|
-
session: s,
|
|
1211
|
-
});
|
|
1349
|
+
// ─── runUnitPhase ─────────────────────────────────────────────────────────────
|
|
1212
1350
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
+
});
|
|
1218
1370
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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;
|
|
1224
1378
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
+
);
|
|
1229
1395
|
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
+
}
|
|
1232
1417
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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
|
+
}
|
|
1236
1437
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
unitId,
|
|
1243
|
-
reason: stuckSignal.reason,
|
|
1244
|
-
recoveryAttempts: stuckRecoveryAttempts,
|
|
1245
|
-
});
|
|
1438
|
+
const repairBlock =
|
|
1439
|
+
deps.buildObservabilityRepairBlock(observabilityIssues);
|
|
1440
|
+
if (repairBlock) {
|
|
1441
|
+
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
1442
|
+
}
|
|
1246
1443
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
ctx.ui.notify(
|
|
1269
|
-
`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`,
|
|
1270
|
-
"warning",
|
|
1271
|
-
);
|
|
1272
|
-
deps.invalidateAllCaches();
|
|
1273
|
-
} else {
|
|
1274
|
-
// Level 2: hard stop — genuinely stuck
|
|
1275
|
-
debugLog("autoLoop", {
|
|
1276
|
-
phase: "stuck-detected",
|
|
1277
|
-
unitType,
|
|
1278
|
-
unitId,
|
|
1279
|
-
reason: stuckSignal.reason,
|
|
1280
|
-
});
|
|
1281
|
-
await deps.stopAuto(
|
|
1282
|
-
ctx,
|
|
1283
|
-
pi,
|
|
1284
|
-
`Stuck: ${stuckSignal.reason}`,
|
|
1285
|
-
);
|
|
1286
|
-
ctx.ui.notify(
|
|
1287
|
-
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1288
|
-
"error",
|
|
1289
|
-
);
|
|
1290
|
-
break;
|
|
1291
|
-
}
|
|
1292
|
-
} else {
|
|
1293
|
-
// Progress detected — reset recovery counter
|
|
1294
|
-
if (stuckRecoveryAttempts > 0) {
|
|
1295
|
-
debugLog("autoLoop", {
|
|
1296
|
-
phase: "stuck-counter-reset",
|
|
1297
|
-
from: recentUnits[recentUnits.length - 2]?.key ?? "",
|
|
1298
|
-
to: derivedKey,
|
|
1299
|
-
});
|
|
1300
|
-
stuckRecoveryAttempts = 0;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
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
|
+
}
|
|
1304
1464
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
"info",
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
if (preDispatchResult.action === "skip") {
|
|
1319
|
-
ctx.ui.notify(
|
|
1320
|
-
`Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
|
|
1321
|
-
"info",
|
|
1322
|
-
);
|
|
1323
|
-
await new Promise((r) => setImmediate(r));
|
|
1324
|
-
continue;
|
|
1325
|
-
}
|
|
1326
|
-
if (preDispatchResult.action === "replace") {
|
|
1327
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
1328
|
-
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1329
|
-
} else if (preDispatchResult.prompt) {
|
|
1330
|
-
prompt = preDispatchResult.prompt;
|
|
1331
|
-
}
|
|
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
|
+
}
|
|
1332
1475
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
+
});
|
|
1344
1504
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
+
);
|
|
1351
1521
|
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
+
});
|
|
1354
1543
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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);
|
|
1364
1557
|
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1365
1560
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
// Detect retry and capture previous tier for escalation
|
|
1376
|
-
const isRetry = !!(
|
|
1377
|
-
s.currentUnit &&
|
|
1378
|
-
s.currentUnit.type === unitType &&
|
|
1379
|
-
s.currentUnit.id === unitId
|
|
1380
|
-
);
|
|
1381
|
-
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
|
+
}
|
|
1382
1570
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
lastProgressAt: s.currentUnit.startedAt,
|
|
1395
|
-
progressCount: 0,
|
|
1396
|
-
lastProgressKind: "dispatch",
|
|
1397
|
-
},
|
|
1398
|
-
);
|
|
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
|
+
);
|
|
1399
1582
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
// Prompt injection
|
|
1409
|
-
let finalPrompt = prompt;
|
|
1410
|
-
|
|
1411
|
-
if (s.pendingVerificationRetry) {
|
|
1412
|
-
const retryCtx = s.pendingVerificationRetry;
|
|
1413
|
-
s.pendingVerificationRetry = null;
|
|
1414
|
-
const capped =
|
|
1415
|
-
retryCtx.failureContext.length > MAX_RECOVERY_CHARS
|
|
1416
|
-
? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) +
|
|
1417
|
-
"\n\n[...failure context truncated]"
|
|
1418
|
-
: retryCtx.failureContext;
|
|
1419
|
-
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}`;
|
|
1420
|
-
}
|
|
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
|
+
}
|
|
1421
1590
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
+
}
|
|
1441
1616
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
if (repairBlock) {
|
|
1445
|
-
finalPrompt = `${finalPrompt}${repairBlock}`;
|
|
1446
|
-
}
|
|
1617
|
+
return { action: "next", data: { unitStartedAt: s.currentUnit.startedAt } };
|
|
1618
|
+
}
|
|
1447
1619
|
|
|
1448
|
-
|
|
1449
|
-
s.lastPromptCharCount = finalPrompt.length;
|
|
1450
|
-
s.lastBaselineCharCount = undefined;
|
|
1451
|
-
if (deps.isDbAvailable()) {
|
|
1452
|
-
try {
|
|
1453
|
-
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1454
|
-
const [decisionsContent, requirementsContent, projectContent] =
|
|
1455
|
-
await Promise.all([
|
|
1456
|
-
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
1457
|
-
inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
|
|
1458
|
-
inlineGsdRootFile(s.basePath, "project.md", "Project"),
|
|
1459
|
-
]);
|
|
1460
|
-
s.lastBaselineCharCount =
|
|
1461
|
-
(decisionsContent?.length ?? 0) +
|
|
1462
|
-
(requirementsContent?.length ?? 0) +
|
|
1463
|
-
(projectContent?.length ?? 0);
|
|
1464
|
-
} catch {
|
|
1465
|
-
// Non-fatal
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1620
|
+
// ─── runFinalize ──────────────────────────────────────────────────────────────
|
|
1468
1621
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
+
};
|
|
1479
1650
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
+
}
|
|
1494
1666
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
buildSnapshotOpts: () => deps.buildSnapshotOpts(unitType, unitId),
|
|
1505
|
-
buildRecoveryContext: () => ({}),
|
|
1506
|
-
pauseAuto: deps.pauseAuto,
|
|
1507
|
-
});
|
|
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
|
+
}
|
|
1508
1676
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
deps.writeLock(
|
|
1519
|
-
deps.lockBase(),
|
|
1520
|
-
unitType,
|
|
1521
|
-
unitId,
|
|
1522
|
-
s.completedUnits.length,
|
|
1523
|
-
sessionFile,
|
|
1524
|
-
);
|
|
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
|
+
);
|
|
1525
1686
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
unitId,
|
|
1531
|
-
});
|
|
1532
|
-
const unitResult = await runUnit(
|
|
1533
|
-
ctx,
|
|
1534
|
-
pi,
|
|
1535
|
-
s,
|
|
1536
|
-
unitType,
|
|
1537
|
-
unitId,
|
|
1538
|
-
finalPrompt,
|
|
1539
|
-
);
|
|
1540
|
-
debugLog("autoLoop", {
|
|
1541
|
-
phase: "runUnit-end",
|
|
1542
|
-
iteration,
|
|
1543
|
-
unitType,
|
|
1544
|
-
unitId,
|
|
1545
|
-
status: unitResult.status,
|
|
1546
|
-
});
|
|
1687
|
+
if (verificationResult === "pause") {
|
|
1688
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1689
|
+
return { action: "break", reason: "verification-pause" };
|
|
1690
|
+
}
|
|
1547
1691
|
|
|
1548
|
-
|
|
1549
|
-
if (
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
if (/error|fail|exception/i.test(msgStr)) {
|
|
1558
|
-
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1559
|
-
if (lastEntry) {
|
|
1560
|
-
lastEntry.error = msgStr.slice(0, 200);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
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" };
|
|
1563
1701
|
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1564
1704
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
1568
|
-
"warning",
|
|
1569
|
-
);
|
|
1570
|
-
await deps.stopAuto(ctx, pi, "Session creation failed");
|
|
1571
|
-
debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
|
|
1572
|
-
break;
|
|
1573
|
-
}
|
|
1705
|
+
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
1706
|
+
const postResult = await deps.postUnitPostVerification(postUnitCtx);
|
|
1574
1707
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
unitId,
|
|
1583
|
-
s.currentUnit.startedAt,
|
|
1584
|
-
deps.buildSnapshotOpts(unitType, unitId),
|
|
1585
|
-
);
|
|
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
|
+
}
|
|
1586
1715
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
);
|
|
1593
|
-
}
|
|
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
|
+
}
|
|
1594
1721
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
isHookUnit ||
|
|
1598
|
-
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
|
|
1599
|
-
if (artifactVerified) {
|
|
1600
|
-
s.completedUnits.push({
|
|
1601
|
-
type: unitType,
|
|
1602
|
-
id: unitId,
|
|
1603
|
-
startedAt: s.currentUnit.startedAt,
|
|
1604
|
-
finishedAt: Date.now(),
|
|
1605
|
-
});
|
|
1606
|
-
if (s.completedUnits.length > 200) {
|
|
1607
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1608
|
-
}
|
|
1609
|
-
// Flush completed-units to disk so the record survives crashes
|
|
1610
|
-
try {
|
|
1611
|
-
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
|
|
1612
|
-
const keys = s.completedUnits.map((u) => `${u.type}/${u.id}`);
|
|
1613
|
-
atomicWriteSync(completedKeysPath, JSON.stringify(keys, null, 2));
|
|
1614
|
-
} catch { /* non-fatal: disk flush failure */ }
|
|
1615
|
-
|
|
1616
|
-
deps.clearUnitRuntimeRecord(s.basePath, unitType, unitId);
|
|
1617
|
-
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
|
|
1618
|
-
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);
|
|
1619
|
-
}
|
|
1722
|
+
return { action: "next", data: undefined as void };
|
|
1723
|
+
}
|
|
1620
1724
|
|
|
1621
|
-
|
|
1725
|
+
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
1622
1726
|
|
|
1623
|
-
|
|
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;
|
|
1624
1745
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1746
|
+
while (s.active) {
|
|
1747
|
+
iteration++;
|
|
1748
|
+
debugLog("autoLoop", { phase: "loop-top", iteration });
|
|
1627
1749
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1750
|
+
if (iteration > MAX_LOOP_ITERATIONS) {
|
|
1751
|
+
debugLog("autoLoop", {
|
|
1752
|
+
phase: "exit",
|
|
1753
|
+
reason: "max-iterations",
|
|
1754
|
+
iteration,
|
|
1755
|
+
});
|
|
1756
|
+
await deps.stopAuto(
|
|
1631
1757
|
ctx,
|
|
1632
1758
|
pi,
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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;
|
|
1639
1772
|
|
|
1640
|
-
//
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1645
|
-
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1646
|
-
: undefined;
|
|
1647
|
-
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1648
|
-
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()!;
|
|
1649
1777
|
debugLog("autoLoop", {
|
|
1650
|
-
phase: "
|
|
1651
|
-
|
|
1778
|
+
phase: "sidecar-dequeue",
|
|
1779
|
+
kind: sidecarItem.kind,
|
|
1780
|
+
unitType: sidecarItem.unitType,
|
|
1781
|
+
unitId: sidecarItem.unitId,
|
|
1652
1782
|
});
|
|
1653
|
-
break;
|
|
1654
1783
|
}
|
|
1655
1784
|
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
{ s, ctx, pi },
|
|
1673
|
-
deps.pauseAuto,
|
|
1674
|
-
);
|
|
1675
|
-
|
|
1676
|
-
if (verificationResult === "pause") {
|
|
1677
|
-
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
|
+
});
|
|
1678
1801
|
break;
|
|
1679
1802
|
}
|
|
1680
|
-
|
|
1681
|
-
if (verificationResult === "retry") {
|
|
1682
|
-
if (sidecarItem) {
|
|
1683
|
-
// Sidecar verification retries are skipped — just continue
|
|
1684
|
-
debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration });
|
|
1685
|
-
} else {
|
|
1686
|
-
// s.pendingVerificationRetry was set by runPostUnitVerification.
|
|
1687
|
-
// Continue the loop — next iteration will inject the retry context into the prompt.
|
|
1688
|
-
debugLog("autoLoop", { phase: "verification-retry", iteration });
|
|
1689
|
-
continue;
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
1803
|
}
|
|
1693
1804
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1805
|
+
const ic: IterationContext = { ctx, pi, s, deps, prefs, iteration };
|
|
1806
|
+
let iterData: IterationData;
|
|
1696
1807
|
|
|
1697
|
-
if (
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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
|
+
};
|
|
1703
1840
|
}
|
|
1704
1841
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
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;
|
|
1710
1850
|
|
|
1711
1851
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1712
1852
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|