gsd-pi 2.38.0-dev.4d4d14a → 2.38.0-dev.5492881
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +538 -469
- package/dist/resources/extensions/gsd/auto-post-unit.js +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +18 -14
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/files.js +4 -0
- package/dist/resources/extensions/gsd/git-service.js +22 -11
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +342 -304
- package/src/resources/extensions/gsd/auto-post-unit.ts +10 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -14
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/commands.ts +2 -2
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/files.ts +3 -1
- package/src/resources/extensions/gsd/git-service.ts +31 -9
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +106 -31
- package/src/resources/extensions/mcp-client/index.ts +17 -1
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* session rotation). No queue — stale agent_end events are dropped.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type
|
|
13
|
+
import { importExtensionModule, type ExtensionAPI, type ExtensionContext } from "@gsd/pi-coding-agent";
|
|
14
14
|
|
|
15
|
-
import type { AutoSession } from "./auto/session.js";
|
|
15
|
+
import type { AutoSession, SidecarItem } from "./auto/session.js";
|
|
16
16
|
import { NEW_SESSION_TIMEOUT_MS } from "./auto/session.js";
|
|
17
17
|
import type { GSDPreferences } from "./preferences.js";
|
|
18
18
|
import type { SessionLockStatus } from "./session-lock.js";
|
|
@@ -26,6 +26,9 @@ import type {
|
|
|
26
26
|
import type { DispatchAction } from "./auto-dispatch.js";
|
|
27
27
|
import type { WorktreeResolver } from "./worktree-resolver.js";
|
|
28
28
|
import { debugLog } from "./debug-logger.js";
|
|
29
|
+
import { gsdRoot } from "./paths.js";
|
|
30
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
31
|
+
import { join } from "node:path";
|
|
29
32
|
import type { CmuxLogLevel } from "../cmux/index.js";
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -35,6 +38,8 @@ import type { CmuxLogLevel } from "../cmux/index.js";
|
|
|
35
38
|
* generous headroom including retries and sidecar work.
|
|
36
39
|
*/
|
|
37
40
|
const MAX_LOOP_ITERATIONS = 500;
|
|
41
|
+
/** Maximum characters of failure/crash context included in recovery prompts. */
|
|
42
|
+
const MAX_RECOVERY_CHARS = 50_000;
|
|
38
43
|
|
|
39
44
|
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
40
45
|
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
@@ -130,6 +135,63 @@ export function _setActiveSession(_session: AutoSession | null): void {
|
|
|
130
135
|
// No-op — kept for test backward compatibility
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
// ─── detectStuck ─────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
type WindowEntry = { key: string; error?: string };
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Analyze a sliding window of recent unit dispatches for stuck patterns.
|
|
144
|
+
* Returns a signal with reason if stuck, null otherwise.
|
|
145
|
+
*
|
|
146
|
+
* Rule 1: Same error string twice in a row → stuck immediately.
|
|
147
|
+
* Rule 2: Same unit key 3+ consecutive times → stuck (preserves prior behavior).
|
|
148
|
+
* Rule 3: Oscillation A→B→A→B in last 4 entries → stuck.
|
|
149
|
+
*/
|
|
150
|
+
export function detectStuck(
|
|
151
|
+
window: readonly WindowEntry[],
|
|
152
|
+
): { stuck: true; reason: string } | null {
|
|
153
|
+
if (window.length < 2) return null;
|
|
154
|
+
|
|
155
|
+
const last = window[window.length - 1];
|
|
156
|
+
const prev = window[window.length - 2];
|
|
157
|
+
|
|
158
|
+
// Rule 1: Same error repeated consecutively
|
|
159
|
+
if (last.error && prev.error && last.error === prev.error) {
|
|
160
|
+
return {
|
|
161
|
+
stuck: true,
|
|
162
|
+
reason: `Same error repeated: ${last.error.slice(0, 200)}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Rule 2: Same unit 3+ consecutive times
|
|
167
|
+
if (window.length >= 3) {
|
|
168
|
+
const lastThree = window.slice(-3);
|
|
169
|
+
if (lastThree.every((u) => u.key === last.key)) {
|
|
170
|
+
return {
|
|
171
|
+
stuck: true,
|
|
172
|
+
reason: `${last.key} derived 3 consecutive times without progress`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Rule 3: Oscillation (A→B→A→B in last 4)
|
|
178
|
+
if (window.length >= 4) {
|
|
179
|
+
const w = window.slice(-4);
|
|
180
|
+
if (
|
|
181
|
+
w[0].key === w[2].key &&
|
|
182
|
+
w[1].key === w[3].key &&
|
|
183
|
+
w[0].key !== w[1].key
|
|
184
|
+
) {
|
|
185
|
+
return {
|
|
186
|
+
stuck: true,
|
|
187
|
+
reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
133
195
|
// ─── runUnit ─────────────────────────────────────────────────────────────────
|
|
134
196
|
|
|
135
197
|
/**
|
|
@@ -501,9 +563,9 @@ async function generateMilestoneReport(
|
|
|
501
563
|
ctx: ExtensionContext,
|
|
502
564
|
milestoneId: string,
|
|
503
565
|
): Promise<void> {
|
|
504
|
-
const { loadVisualizerData } = await import("./visualizer-data.js");
|
|
505
|
-
const { generateHtmlReport } = await import("./export-html.js");
|
|
506
|
-
const { writeReportSnapshot } = await import("./reports.js");
|
|
566
|
+
const { loadVisualizerData } = await importExtensionModule<typeof import("./visualizer-data.js")>(import.meta.url, "./visualizer-data.js");
|
|
567
|
+
const { generateHtmlReport } = await importExtensionModule<typeof import("./export-html.js")>(import.meta.url, "./export-html.js");
|
|
568
|
+
const { writeReportSnapshot } = await importExtensionModule<typeof import("./reports.js")>(import.meta.url, "./reports.js");
|
|
507
569
|
const { basename } = await import("node:path");
|
|
508
570
|
|
|
509
571
|
const snapData = await loadVisualizerData(s.basePath);
|
|
@@ -598,8 +660,10 @@ export async function autoLoop(
|
|
|
598
660
|
): Promise<void> {
|
|
599
661
|
debugLog("autoLoop", { phase: "enter" });
|
|
600
662
|
let iteration = 0;
|
|
601
|
-
|
|
602
|
-
|
|
663
|
+
// ── Sliding-window stuck detection ──
|
|
664
|
+
const recentUnits: Array<{ key: string; error?: string }> = [];
|
|
665
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
666
|
+
let stuckRecoveryAttempts = 0;
|
|
603
667
|
|
|
604
668
|
let consecutiveErrors = 0;
|
|
605
669
|
|
|
@@ -628,6 +692,19 @@ export async function autoLoop(
|
|
|
628
692
|
|
|
629
693
|
try {
|
|
630
694
|
// ── Blanket try/catch: one bad iteration must not kill the session
|
|
695
|
+
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
696
|
+
|
|
697
|
+
// ── Check sidecar queue before deriveState ──
|
|
698
|
+
let sidecarItem: SidecarItem | undefined;
|
|
699
|
+
if (s.sidecarQueue.length > 0) {
|
|
700
|
+
sidecarItem = s.sidecarQueue.shift()!;
|
|
701
|
+
debugLog("autoLoop", {
|
|
702
|
+
phase: "sidecar-dequeue",
|
|
703
|
+
kind: sidecarItem.kind,
|
|
704
|
+
unitType: sidecarItem.unitType,
|
|
705
|
+
unitId: sidecarItem.unitId,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
631
708
|
|
|
632
709
|
const sessionLockBase = deps.lockBase();
|
|
633
710
|
if (sessionLockBase) {
|
|
@@ -649,6 +726,17 @@ export async function autoLoop(
|
|
|
649
726
|
}
|
|
650
727
|
}
|
|
651
728
|
|
|
729
|
+
// Variables shared between the sidecar and normal paths
|
|
730
|
+
let unitType: string;
|
|
731
|
+
let unitId: string;
|
|
732
|
+
let prompt: string;
|
|
733
|
+
let pauseAfterUatDispatch = false;
|
|
734
|
+
let state: GSDState;
|
|
735
|
+
let mid: string | undefined;
|
|
736
|
+
let midTitle: string | undefined;
|
|
737
|
+
let observabilityIssues: unknown[] = [];
|
|
738
|
+
|
|
739
|
+
if (!sidecarItem) {
|
|
652
740
|
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
653
741
|
|
|
654
742
|
// Resource version guard
|
|
@@ -699,10 +787,10 @@ export async function autoLoop(
|
|
|
699
787
|
}
|
|
700
788
|
|
|
701
789
|
// Derive state
|
|
702
|
-
|
|
703
|
-
deps.syncCmuxSidebar(
|
|
704
|
-
|
|
705
|
-
|
|
790
|
+
state = await deps.deriveState(s.basePath);
|
|
791
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
792
|
+
mid = state.activeMilestone?.id;
|
|
793
|
+
midTitle = state.activeMilestone?.title;
|
|
706
794
|
debugLog("autoLoop", {
|
|
707
795
|
phase: "state-derived",
|
|
708
796
|
iteration,
|
|
@@ -723,12 +811,12 @@ export async function autoLoop(
|
|
|
723
811
|
"milestone",
|
|
724
812
|
);
|
|
725
813
|
deps.logCmuxEvent(
|
|
726
|
-
|
|
814
|
+
prefs,
|
|
727
815
|
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
728
816
|
"success",
|
|
729
817
|
);
|
|
730
818
|
|
|
731
|
-
const vizPrefs =
|
|
819
|
+
const vizPrefs = prefs;
|
|
732
820
|
if (vizPrefs?.auto_visualize) {
|
|
733
821
|
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
734
822
|
}
|
|
@@ -747,11 +835,30 @@ export async function autoLoop(
|
|
|
747
835
|
s.unitDispatchCount.clear();
|
|
748
836
|
s.unitRecoveryCount.clear();
|
|
749
837
|
s.unitLifetimeDispatches.clear();
|
|
750
|
-
|
|
751
|
-
|
|
838
|
+
recentUnits.length = 0;
|
|
839
|
+
stuckRecoveryAttempts = 0;
|
|
752
840
|
|
|
753
841
|
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
754
842
|
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
843
|
+
|
|
844
|
+
// Opt-in: create draft PR on milestone completion
|
|
845
|
+
if (prefs?.git?.auto_pr) {
|
|
846
|
+
try {
|
|
847
|
+
const { createDraftPR } = await import("./git-service.js");
|
|
848
|
+
const prUrl = createDraftPR(
|
|
849
|
+
s.basePath,
|
|
850
|
+
s.currentMilestoneId!,
|
|
851
|
+
`[GSD] ${s.currentMilestoneId} complete`,
|
|
852
|
+
`Milestone ${s.currentMilestoneId} completed by GSD auto-mode.\n\nSee .gsd/${s.currentMilestoneId}/ for details.`,
|
|
853
|
+
);
|
|
854
|
+
if (prUrl) {
|
|
855
|
+
ctx.ui.notify(`Draft PR created: ${prUrl}`, "info");
|
|
856
|
+
}
|
|
857
|
+
} catch {
|
|
858
|
+
// Non-fatal — PR creation is best-effort
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
755
862
|
deps.invalidateAllCaches();
|
|
756
863
|
|
|
757
864
|
state = await deps.deriveState(s.basePath);
|
|
@@ -761,9 +868,7 @@ export async function autoLoop(
|
|
|
761
868
|
if (mid) {
|
|
762
869
|
if (deps.getIsolationMode() !== "none") {
|
|
763
870
|
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
764
|
-
commitDocs:
|
|
765
|
-
deps.loadEffectiveGSDPreferences()?.preferences?.git
|
|
766
|
-
?.commit_docs,
|
|
871
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
767
872
|
});
|
|
768
873
|
}
|
|
769
874
|
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
@@ -807,6 +912,24 @@ export async function autoLoop(
|
|
|
807
912
|
// All milestones complete — merge milestone branch before stopping
|
|
808
913
|
if (s.currentMilestoneId) {
|
|
809
914
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
915
|
+
|
|
916
|
+
// Opt-in: create draft PR on milestone completion
|
|
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
|
+
}
|
|
810
933
|
}
|
|
811
934
|
deps.sendDesktopNotification(
|
|
812
935
|
"GSD",
|
|
@@ -815,7 +938,7 @@ export async function autoLoop(
|
|
|
815
938
|
"milestone",
|
|
816
939
|
);
|
|
817
940
|
deps.logCmuxEvent(
|
|
818
|
-
|
|
941
|
+
prefs,
|
|
819
942
|
"All milestones complete.",
|
|
820
943
|
"success",
|
|
821
944
|
);
|
|
@@ -837,7 +960,7 @@ export async function autoLoop(
|
|
|
837
960
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
838
961
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
839
962
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
840
|
-
deps.logCmuxEvent(
|
|
963
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
841
964
|
} else {
|
|
842
965
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
843
966
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -888,6 +1011,24 @@ export async function autoLoop(
|
|
|
888
1011
|
// Milestone merge on complete (before closeout so branch state is clean)
|
|
889
1012
|
if (s.currentMilestoneId) {
|
|
890
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
|
+
}
|
|
891
1032
|
}
|
|
892
1033
|
deps.sendDesktopNotification(
|
|
893
1034
|
"GSD",
|
|
@@ -896,7 +1037,7 @@ export async function autoLoop(
|
|
|
896
1037
|
"milestone",
|
|
897
1038
|
);
|
|
898
1039
|
deps.logCmuxEvent(
|
|
899
|
-
|
|
1040
|
+
prefs,
|
|
900
1041
|
`Milestone ${mid} complete.`,
|
|
901
1042
|
"success",
|
|
902
1043
|
);
|
|
@@ -911,15 +1052,13 @@ export async function autoLoop(
|
|
|
911
1052
|
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
912
1053
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
913
1054
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
914
|
-
deps.logCmuxEvent(
|
|
1055
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
915
1056
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
916
1057
|
break;
|
|
917
1058
|
}
|
|
918
1059
|
|
|
919
1060
|
// ── Phase 2: Guards ─────────────────────────────────────────────────
|
|
920
1061
|
|
|
921
|
-
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
922
|
-
|
|
923
1062
|
// Budget ceiling guard
|
|
924
1063
|
const budgetCeiling = prefs?.budget_ceiling;
|
|
925
1064
|
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
@@ -1069,76 +1208,84 @@ export async function autoLoop(
|
|
|
1069
1208
|
continue;
|
|
1070
1209
|
}
|
|
1071
1210
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1211
|
+
unitType = dispatchResult.unitType;
|
|
1212
|
+
unitId = dispatchResult.unitId;
|
|
1213
|
+
prompt = dispatchResult.prompt;
|
|
1214
|
+
pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1076
1215
|
|
|
1077
|
-
// ──
|
|
1216
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1078
1217
|
const derivedKey = `${unitType}/${unitId}`;
|
|
1079
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
1080
|
-
sameUnitCount++;
|
|
1081
|
-
debugLog("autoLoop", {
|
|
1082
|
-
phase: "stuck-check",
|
|
1083
|
-
unitType,
|
|
1084
|
-
unitId,
|
|
1085
|
-
sameUnitCount,
|
|
1086
|
-
});
|
|
1087
1218
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1219
|
+
if (!s.pendingVerificationRetry) {
|
|
1220
|
+
recentUnits.push({ key: derivedKey });
|
|
1221
|
+
if (recentUnits.length > STUCK_WINDOW_SIZE) recentUnits.shift();
|
|
1222
|
+
|
|
1223
|
+
const stuckSignal = detectStuck(recentUnits);
|
|
1224
|
+
if (stuckSignal) {
|
|
1225
|
+
debugLog("autoLoop", {
|
|
1226
|
+
phase: "stuck-check",
|
|
1091
1227
|
unitType,
|
|
1092
1228
|
unitId,
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1229
|
+
reason: stuckSignal.reason,
|
|
1230
|
+
recoveryAttempts: stuckRecoveryAttempts,
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
if (stuckRecoveryAttempts === 0) {
|
|
1234
|
+
// Level 1: try verifying the artifact, then cache invalidation + retry
|
|
1235
|
+
stuckRecoveryAttempts++;
|
|
1236
|
+
const artifactExists = deps.verifyExpectedArtifact(
|
|
1237
|
+
unitType,
|
|
1238
|
+
unitId,
|
|
1239
|
+
s.basePath,
|
|
1240
|
+
);
|
|
1241
|
+
if (artifactExists) {
|
|
1242
|
+
debugLog("autoLoop", {
|
|
1243
|
+
phase: "stuck-recovery",
|
|
1244
|
+
level: 1,
|
|
1245
|
+
action: "artifact-found",
|
|
1246
|
+
});
|
|
1247
|
+
ctx.ui.notify(
|
|
1248
|
+
`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
|
|
1249
|
+
"info",
|
|
1250
|
+
);
|
|
1251
|
+
deps.invalidateAllCaches();
|
|
1252
|
+
continue;
|
|
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
|
|
1096
1261
|
debugLog("autoLoop", {
|
|
1097
|
-
phase: "stuck-
|
|
1098
|
-
|
|
1099
|
-
|
|
1262
|
+
phase: "stuck-detected",
|
|
1263
|
+
unitType,
|
|
1264
|
+
unitId,
|
|
1265
|
+
reason: stuckSignal.reason,
|
|
1100
1266
|
});
|
|
1267
|
+
await deps.stopAuto(
|
|
1268
|
+
ctx,
|
|
1269
|
+
pi,
|
|
1270
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
1271
|
+
);
|
|
1101
1272
|
ctx.ui.notify(
|
|
1102
|
-
`Stuck
|
|
1103
|
-
"
|
|
1273
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1274
|
+
"error",
|
|
1104
1275
|
);
|
|
1105
|
-
|
|
1106
|
-
|
|
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;
|
|
1107
1287
|
}
|
|
1108
|
-
ctx.ui.notify(
|
|
1109
|
-
`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`,
|
|
1110
|
-
"warning",
|
|
1111
|
-
);
|
|
1112
|
-
deps.invalidateAllCaches();
|
|
1113
|
-
} else if (sameUnitCount === 5) {
|
|
1114
|
-
// Level 2: hard stop — genuinely stuck
|
|
1115
|
-
debugLog("autoLoop", {
|
|
1116
|
-
phase: "stuck-detected",
|
|
1117
|
-
unitType,
|
|
1118
|
-
unitId,
|
|
1119
|
-
sameUnitCount,
|
|
1120
|
-
});
|
|
1121
|
-
await deps.stopAuto(
|
|
1122
|
-
ctx,
|
|
1123
|
-
pi,
|
|
1124
|
-
`Stuck: ${unitType} ${unitId} derived ${sameUnitCount} consecutive times without progress`,
|
|
1125
|
-
);
|
|
1126
|
-
ctx.ui.notify(
|
|
1127
|
-
`Stuck on ${unitType} ${unitId} — deriveState returns the same unit after ${sameUnitCount} attempts. The expected artifact was not written.`,
|
|
1128
|
-
"error",
|
|
1129
|
-
);
|
|
1130
|
-
break;
|
|
1131
|
-
}
|
|
1132
|
-
} else {
|
|
1133
|
-
if (derivedKey !== lastDerivedUnit) {
|
|
1134
|
-
debugLog("autoLoop", {
|
|
1135
|
-
phase: "stuck-counter-reset",
|
|
1136
|
-
from: lastDerivedUnit,
|
|
1137
|
-
to: derivedKey,
|
|
1138
|
-
});
|
|
1139
1288
|
}
|
|
1140
|
-
lastDerivedUnit = derivedKey;
|
|
1141
|
-
sameUnitCount = 0;
|
|
1142
1289
|
}
|
|
1143
1290
|
|
|
1144
1291
|
// Pre-dispatch hooks
|
|
@@ -1181,13 +1328,27 @@ export async function autoLoop(
|
|
|
1181
1328
|
break;
|
|
1182
1329
|
}
|
|
1183
1330
|
|
|
1184
|
-
|
|
1331
|
+
observabilityIssues = await deps.collectObservabilityWarnings(
|
|
1185
1332
|
ctx,
|
|
1186
1333
|
s.basePath,
|
|
1187
1334
|
unitType,
|
|
1188
1335
|
unitId,
|
|
1189
1336
|
);
|
|
1190
1337
|
|
|
1338
|
+
// Derive state for shared use in execution phase
|
|
1339
|
+
// (state, mid, midTitle already set above)
|
|
1340
|
+
|
|
1341
|
+
} else {
|
|
1342
|
+
// ── Sidecar path: use values from the sidecar item directly ──
|
|
1343
|
+
unitType = sidecarItem.unitType;
|
|
1344
|
+
unitId = sidecarItem.unitId;
|
|
1345
|
+
prompt = sidecarItem.prompt;
|
|
1346
|
+
// Derive minimal state for progress widget / execution context
|
|
1347
|
+
state = await deps.deriveState(s.basePath);
|
|
1348
|
+
mid = state.activeMilestone?.id;
|
|
1349
|
+
midTitle = state.activeMilestone?.title;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1191
1352
|
// ── Phase 4: Unit execution ─────────────────────────────────────────
|
|
1192
1353
|
|
|
1193
1354
|
debugLog("autoLoop", {
|
|
@@ -1205,61 +1366,6 @@ export async function autoLoop(
|
|
|
1205
1366
|
);
|
|
1206
1367
|
const previousTier = s.currentUnitRouting?.tier;
|
|
1207
1368
|
|
|
1208
|
-
// Closeout previous unit
|
|
1209
|
-
if (s.currentUnit) {
|
|
1210
|
-
await deps.closeoutUnit(
|
|
1211
|
-
ctx,
|
|
1212
|
-
s.basePath,
|
|
1213
|
-
s.currentUnit.type,
|
|
1214
|
-
s.currentUnit.id,
|
|
1215
|
-
s.currentUnit.startedAt,
|
|
1216
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1217
|
-
);
|
|
1218
|
-
|
|
1219
|
-
if (s.currentUnitRouting) {
|
|
1220
|
-
const isRetryForOutcome =
|
|
1221
|
-
s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
1222
|
-
deps.recordOutcome(
|
|
1223
|
-
s.currentUnit.type,
|
|
1224
|
-
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1225
|
-
!isRetryForOutcome,
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
1230
|
-
const incomingKey = `${unitType}/${unitId}`;
|
|
1231
|
-
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
1232
|
-
const artifactVerified =
|
|
1233
|
-
isHookUnit ||
|
|
1234
|
-
deps.verifyExpectedArtifact(
|
|
1235
|
-
s.currentUnit.type,
|
|
1236
|
-
s.currentUnit.id,
|
|
1237
|
-
s.basePath,
|
|
1238
|
-
);
|
|
1239
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
1240
|
-
s.completedUnits.push({
|
|
1241
|
-
type: s.currentUnit.type,
|
|
1242
|
-
id: s.currentUnit.id,
|
|
1243
|
-
startedAt: s.currentUnit.startedAt,
|
|
1244
|
-
finishedAt: Date.now(),
|
|
1245
|
-
});
|
|
1246
|
-
if (s.completedUnits.length > 200) {
|
|
1247
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1248
|
-
}
|
|
1249
|
-
deps.clearUnitRuntimeRecord(
|
|
1250
|
-
s.basePath,
|
|
1251
|
-
s.currentUnit.type,
|
|
1252
|
-
s.currentUnit.id,
|
|
1253
|
-
);
|
|
1254
|
-
s.unitDispatchCount.delete(
|
|
1255
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1256
|
-
);
|
|
1257
|
-
s.unitRecoveryCount.delete(
|
|
1258
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
1369
|
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1264
1370
|
deps.captureAvailableSkills();
|
|
1265
1371
|
deps.writeUnitRuntimeRecord(
|
|
@@ -1286,7 +1392,6 @@ export async function autoLoop(
|
|
|
1286
1392
|
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1287
1393
|
|
|
1288
1394
|
// Prompt injection
|
|
1289
|
-
const MAX_RECOVERY_CHARS = 50_000;
|
|
1290
1395
|
let finalPrompt = prompt;
|
|
1291
1396
|
|
|
1292
1397
|
if (s.pendingVerificationRetry) {
|
|
@@ -1331,7 +1436,7 @@ export async function autoLoop(
|
|
|
1331
1436
|
s.lastBaselineCharCount = undefined;
|
|
1332
1437
|
if (deps.isDbAvailable()) {
|
|
1333
1438
|
try {
|
|
1334
|
-
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
1439
|
+
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1335
1440
|
const [decisionsContent, requirementsContent, projectContent] =
|
|
1336
1441
|
await Promise.all([
|
|
1337
1442
|
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
@@ -1358,7 +1463,7 @@ export async function autoLoop(
|
|
|
1358
1463
|
);
|
|
1359
1464
|
}
|
|
1360
1465
|
|
|
1361
|
-
// Select and apply model (with tier escalation on retry)
|
|
1466
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1362
1467
|
const modelResult = await deps.selectAndApplyModel(
|
|
1363
1468
|
ctx,
|
|
1364
1469
|
pi,
|
|
@@ -1368,7 +1473,7 @@ export async function autoLoop(
|
|
|
1368
1473
|
prefs,
|
|
1369
1474
|
s.verbose,
|
|
1370
1475
|
s.autoModeStartModel,
|
|
1371
|
-
{ isRetry, previousTier },
|
|
1476
|
+
sidecarItem ? undefined : { isRetry, previousTier },
|
|
1372
1477
|
);
|
|
1373
1478
|
s.currentUnitRouting =
|
|
1374
1479
|
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
@@ -1426,6 +1531,23 @@ export async function autoLoop(
|
|
|
1426
1531
|
status: unitResult.status,
|
|
1427
1532
|
});
|
|
1428
1533
|
|
|
1534
|
+
// Tag the most recent window entry with error info for stuck detection
|
|
1535
|
+
if (unitResult.status === "error" || unitResult.status === "cancelled") {
|
|
1536
|
+
const lastEntry = recentUnits[recentUnits.length - 1];
|
|
1537
|
+
if (lastEntry) {
|
|
1538
|
+
lastEntry.error = `${unitResult.status}:${unitType}/${unitId}`;
|
|
1539
|
+
}
|
|
1540
|
+
} else if (unitResult.event?.messages?.length) {
|
|
1541
|
+
const lastMsg = unitResult.event.messages[unitResult.event.messages.length - 1];
|
|
1542
|
+
const msgStr = typeof lastMsg === "string" ? lastMsg : JSON.stringify(lastMsg);
|
|
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
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1429
1551
|
if (unitResult.status === "cancelled") {
|
|
1430
1552
|
ctx.ui.notify(
|
|
1431
1553
|
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
@@ -1436,6 +1558,52 @@ export async function autoLoop(
|
|
|
1436
1558
|
break;
|
|
1437
1559
|
}
|
|
1438
1560
|
|
|
1561
|
+
// ── Immediate unit closeout (metrics, activity log, memory) ────────
|
|
1562
|
+
// Run right after runUnit() returns so telemetry is never lost to a
|
|
1563
|
+
// crash between iterations.
|
|
1564
|
+
await deps.closeoutUnit(
|
|
1565
|
+
ctx,
|
|
1566
|
+
s.basePath,
|
|
1567
|
+
unitType,
|
|
1568
|
+
unitId,
|
|
1569
|
+
s.currentUnit.startedAt,
|
|
1570
|
+
deps.buildSnapshotOpts(unitType, unitId),
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
if (s.currentUnitRouting) {
|
|
1574
|
+
deps.recordOutcome(
|
|
1575
|
+
unitType,
|
|
1576
|
+
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1577
|
+
true, // success assumed; dispatch will re-dispatch if artifact missing
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
const isHookUnit = unitType.startsWith("hook/");
|
|
1582
|
+
const artifactVerified =
|
|
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
|
+
}
|
|
1606
|
+
|
|
1439
1607
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
1440
1608
|
|
|
1441
1609
|
debugLog("autoLoop", { phase: "finalize", iteration });
|
|
@@ -1456,7 +1624,13 @@ export async function autoLoop(
|
|
|
1456
1624
|
};
|
|
1457
1625
|
|
|
1458
1626
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1459
|
-
|
|
1627
|
+
// Sidecar items use lightweight pre-verification opts
|
|
1628
|
+
const preVerificationOpts: PreVerificationOpts | undefined = sidecarItem
|
|
1629
|
+
? sidecarItem.kind === "hook"
|
|
1630
|
+
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1631
|
+
: { skipSettleDelay: true, skipStateRebuild: true }
|
|
1632
|
+
: undefined;
|
|
1633
|
+
const preResult = await deps.postUnitPreVerification(postUnitCtx, preVerificationOpts);
|
|
1460
1634
|
if (preResult === "dispatched") {
|
|
1461
1635
|
debugLog("autoLoop", {
|
|
1462
1636
|
phase: "exit",
|
|
@@ -1475,22 +1649,32 @@ export async function autoLoop(
|
|
|
1475
1649
|
break;
|
|
1476
1650
|
}
|
|
1477
1651
|
|
|
1478
|
-
// Verification gate
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
)
|
|
1652
|
+
// Verification gate
|
|
1653
|
+
// Hook sidecar items skip verification entirely.
|
|
1654
|
+
// Non-hook sidecar items run verification but skip retries (just continue).
|
|
1655
|
+
const skipVerification = sidecarItem?.kind === "hook";
|
|
1656
|
+
if (!skipVerification) {
|
|
1657
|
+
const verificationResult = await deps.runPostUnitVerification(
|
|
1658
|
+
{ s, ctx, pi },
|
|
1659
|
+
deps.pauseAuto,
|
|
1660
|
+
);
|
|
1483
1661
|
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1662
|
+
if (verificationResult === "pause") {
|
|
1663
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1664
|
+
break;
|
|
1665
|
+
}
|
|
1488
1666
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
+
}
|
|
1494
1678
|
}
|
|
1495
1679
|
|
|
1496
1680
|
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
@@ -1510,152 +1694,6 @@ export async function autoLoop(
|
|
|
1510
1694
|
break;
|
|
1511
1695
|
}
|
|
1512
1696
|
|
|
1513
|
-
// ── Sidecar drain: dispatch enqueued hooks/triage/quick-tasks ──
|
|
1514
|
-
let sidecarBroke = false;
|
|
1515
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
1516
|
-
const item = s.sidecarQueue.shift()!;
|
|
1517
|
-
debugLog("autoLoop", {
|
|
1518
|
-
phase: "sidecar-dequeue",
|
|
1519
|
-
kind: item.kind,
|
|
1520
|
-
unitType: item.unitType,
|
|
1521
|
-
unitId: item.unitId,
|
|
1522
|
-
});
|
|
1523
|
-
|
|
1524
|
-
// Set up as current unit
|
|
1525
|
-
const sidecarStartedAt = Date.now();
|
|
1526
|
-
s.currentUnit = {
|
|
1527
|
-
type: item.unitType,
|
|
1528
|
-
id: item.unitId,
|
|
1529
|
-
startedAt: sidecarStartedAt,
|
|
1530
|
-
};
|
|
1531
|
-
deps.writeUnitRuntimeRecord(
|
|
1532
|
-
s.basePath,
|
|
1533
|
-
item.unitType,
|
|
1534
|
-
item.unitId,
|
|
1535
|
-
sidecarStartedAt,
|
|
1536
|
-
{
|
|
1537
|
-
phase: "dispatched",
|
|
1538
|
-
wrapupWarningSent: false,
|
|
1539
|
-
timeoutAt: null,
|
|
1540
|
-
lastProgressAt: sidecarStartedAt,
|
|
1541
|
-
progressCount: 0,
|
|
1542
|
-
lastProgressKind: "dispatch",
|
|
1543
|
-
},
|
|
1544
|
-
);
|
|
1545
|
-
|
|
1546
|
-
// Model selection (handles hook model override)
|
|
1547
|
-
await deps.selectAndApplyModel(
|
|
1548
|
-
ctx,
|
|
1549
|
-
pi,
|
|
1550
|
-
item.unitType,
|
|
1551
|
-
item.unitId,
|
|
1552
|
-
s.basePath,
|
|
1553
|
-
prefs,
|
|
1554
|
-
s.verbose,
|
|
1555
|
-
s.autoModeStartModel,
|
|
1556
|
-
);
|
|
1557
|
-
|
|
1558
|
-
// Supervision
|
|
1559
|
-
deps.clearUnitTimeout();
|
|
1560
|
-
deps.startUnitSupervision({
|
|
1561
|
-
s,
|
|
1562
|
-
ctx,
|
|
1563
|
-
pi,
|
|
1564
|
-
unitType: item.unitType,
|
|
1565
|
-
unitId: item.unitId,
|
|
1566
|
-
prefs,
|
|
1567
|
-
buildSnapshotOpts: () =>
|
|
1568
|
-
deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
1569
|
-
buildRecoveryContext: () => ({}),
|
|
1570
|
-
pauseAuto: deps.pauseAuto,
|
|
1571
|
-
});
|
|
1572
|
-
|
|
1573
|
-
// Write lock
|
|
1574
|
-
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
1575
|
-
deps.writeLock(
|
|
1576
|
-
deps.lockBase(),
|
|
1577
|
-
item.unitType,
|
|
1578
|
-
item.unitId,
|
|
1579
|
-
s.completedUnits.length,
|
|
1580
|
-
sidecarSessionFile,
|
|
1581
|
-
);
|
|
1582
|
-
|
|
1583
|
-
// Execute via standard runUnit
|
|
1584
|
-
const sidecarResult = await runUnit(
|
|
1585
|
-
ctx,
|
|
1586
|
-
pi,
|
|
1587
|
-
s,
|
|
1588
|
-
item.unitType,
|
|
1589
|
-
item.unitId,
|
|
1590
|
-
item.prompt,
|
|
1591
|
-
);
|
|
1592
|
-
deps.clearUnitTimeout();
|
|
1593
|
-
|
|
1594
|
-
if (sidecarResult.status === "cancelled") {
|
|
1595
|
-
ctx.ui.notify(
|
|
1596
|
-
`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`,
|
|
1597
|
-
"warning",
|
|
1598
|
-
);
|
|
1599
|
-
await deps.stopAuto(ctx, pi, "Sidecar session creation failed");
|
|
1600
|
-
sidecarBroke = true;
|
|
1601
|
-
break;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// Run pre-verification for the sidecar unit (lightweight path)
|
|
1605
|
-
const sidecarPreOpts: PreVerificationOpts = item.kind === "hook"
|
|
1606
|
-
? { skipSettleDelay: true, skipDoctor: true, skipStateRebuild: true, skipWorktreeSync: true }
|
|
1607
|
-
: { skipSettleDelay: true, skipStateRebuild: true };
|
|
1608
|
-
const sidecarPreResult =
|
|
1609
|
-
await deps.postUnitPreVerification(postUnitCtx, sidecarPreOpts);
|
|
1610
|
-
if (sidecarPreResult === "dispatched") {
|
|
1611
|
-
// Pre-verification caused stop/pause
|
|
1612
|
-
debugLog("autoLoop", {
|
|
1613
|
-
phase: "exit",
|
|
1614
|
-
reason: "sidecar-pre-verification-stop",
|
|
1615
|
-
});
|
|
1616
|
-
sidecarBroke = true;
|
|
1617
|
-
break;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
// Verification gate for non-hook sidecar units (triage, quick-tasks)
|
|
1621
|
-
// Hook units are lightweight and don't need verification.
|
|
1622
|
-
if (item.kind !== "hook") {
|
|
1623
|
-
const sidecarVerification = await deps.runPostUnitVerification(
|
|
1624
|
-
{ s, ctx, pi },
|
|
1625
|
-
deps.pauseAuto,
|
|
1626
|
-
);
|
|
1627
|
-
if (sidecarVerification === "pause") {
|
|
1628
|
-
debugLog("autoLoop", {
|
|
1629
|
-
phase: "exit",
|
|
1630
|
-
reason: "sidecar-verification-pause",
|
|
1631
|
-
});
|
|
1632
|
-
sidecarBroke = true;
|
|
1633
|
-
break;
|
|
1634
|
-
}
|
|
1635
|
-
// "retry" for sidecars — skip retry, just continue (sidecar retries are not worth the complexity)
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
// Post-verification (may enqueue more sidecar items)
|
|
1639
|
-
const sidecarPostResult =
|
|
1640
|
-
await deps.postUnitPostVerification(postUnitCtx);
|
|
1641
|
-
if (sidecarPostResult === "stopped") {
|
|
1642
|
-
debugLog("autoLoop", { phase: "exit", reason: "sidecar-stopped" });
|
|
1643
|
-
sidecarBroke = true;
|
|
1644
|
-
break;
|
|
1645
|
-
}
|
|
1646
|
-
if (sidecarPostResult === "step-wizard") {
|
|
1647
|
-
debugLog("autoLoop", {
|
|
1648
|
-
phase: "exit",
|
|
1649
|
-
reason: "sidecar-step-wizard",
|
|
1650
|
-
});
|
|
1651
|
-
sidecarBroke = true;
|
|
1652
|
-
break;
|
|
1653
|
-
}
|
|
1654
|
-
// "continue" — loop checks sidecarQueue again
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
if (sidecarBroke) break;
|
|
1658
|
-
|
|
1659
1697
|
consecutiveErrors = 0; // Iteration completed successfully
|
|
1660
1698
|
debugLog("autoLoop", { phase: "iteration-complete", iteration });
|
|
1661
1699
|
} catch (loopErr) {
|