gsd-pi 2.38.0-dev.63ad7e5 → 2.38.0-dev.785052f
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/README.md +15 -11
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +593 -516
- package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +197 -19
- 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-providers.js +3 -0
- 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 +46 -7
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/gitignore.js +16 -3
- package/dist/resources/extensions/gsd/guided-flow.js +149 -38
- package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
- package/dist/resources/extensions/gsd/health-widget.js +3 -86
- package/dist/resources/extensions/gsd/index.js +22 -19
- package/dist/resources/extensions/gsd/migrate-external.js +18 -1
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
- package/dist/resources/extensions/gsd/preferences.js +20 -9
- package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -1
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/state.js +41 -22
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/dist/resources/extensions/remote-questions/status.js +4 -2
- package/dist/resources/extensions/remote-questions/store.js +4 -2
- package/dist/resources/extensions/shared/frontmatter.js +1 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- 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/dist/core/skills.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +6 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/packages/pi-coding-agent/src/core/skills.ts +9 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +472 -434
- package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +242 -19
- 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-providers.ts +4 -0
- 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 +49 -9
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/gitignore.ts +17 -3
- package/src/resources/extensions/gsd/guided-flow.ts +177 -44
- package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
- package/src/resources/extensions/gsd/health-widget.ts +3 -89
- package/src/resources/extensions/gsd/index.ts +21 -16
- package/src/resources/extensions/gsd/migrate-external.ts +18 -1
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-types.ts +4 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
- package/src/resources/extensions/gsd/preferences.ts +23 -9
- package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +3 -1
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/state.ts +38 -20
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +111 -37
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
- package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
- package/src/resources/extensions/gsd/types.ts +18 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/mcp-client/index.ts +17 -1
- package/src/resources/extensions/remote-questions/status.ts +4 -2
- package/src/resources/extensions/remote-questions/store.ts +4 -2
- package/src/resources/extensions/shared/frontmatter.ts +1 -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,15 +38,19 @@ 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
|
-
/** Data-driven budget threshold notifications (
|
|
40
|
-
*
|
|
44
|
+
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
45
|
+
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
46
|
+
* a simple notification. */
|
|
41
47
|
const BUDGET_THRESHOLDS: Array<{
|
|
42
48
|
pct: number;
|
|
43
49
|
label: string;
|
|
44
|
-
notifyLevel: "info" | "warning";
|
|
45
|
-
cmuxLevel: "progress" | "warning";
|
|
50
|
+
notifyLevel: "info" | "warning" | "error";
|
|
51
|
+
cmuxLevel: "progress" | "warning" | "error";
|
|
46
52
|
}> = [
|
|
53
|
+
{ pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
|
|
47
54
|
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
48
55
|
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
49
56
|
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
@@ -128,6 +135,63 @@ export function _setActiveSession(_session: AutoSession | null): void {
|
|
|
128
135
|
// No-op — kept for test backward compatibility
|
|
129
136
|
}
|
|
130
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
|
+
|
|
131
195
|
// ─── runUnit ─────────────────────────────────────────────────────────────────
|
|
132
196
|
|
|
133
197
|
/**
|
|
@@ -145,7 +209,6 @@ export async function runUnit(
|
|
|
145
209
|
unitType: string,
|
|
146
210
|
unitId: string,
|
|
147
211
|
prompt: string,
|
|
148
|
-
_prefs: GSDPreferences | undefined,
|
|
149
212
|
): Promise<UnitResult> {
|
|
150
213
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
151
214
|
|
|
@@ -489,6 +552,96 @@ export interface LoopDeps {
|
|
|
489
552
|
getSessionFile: (ctx: ExtensionContext) => string;
|
|
490
553
|
}
|
|
491
554
|
|
|
555
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Generate and write an HTML milestone report snapshot.
|
|
559
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
560
|
+
*/
|
|
561
|
+
async function generateMilestoneReport(
|
|
562
|
+
s: AutoSession,
|
|
563
|
+
ctx: ExtensionContext,
|
|
564
|
+
milestoneId: string,
|
|
565
|
+
): Promise<void> {
|
|
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");
|
|
569
|
+
const { basename } = await import("node:path");
|
|
570
|
+
|
|
571
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
572
|
+
const completedMs = snapData.milestones.find(
|
|
573
|
+
(m: { id: string }) => m.id === milestoneId,
|
|
574
|
+
);
|
|
575
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
576
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
577
|
+
const projName = basename(s.basePath);
|
|
578
|
+
const doneSlices = snapData.milestones.reduce(
|
|
579
|
+
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
580
|
+
acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
581
|
+
0,
|
|
582
|
+
);
|
|
583
|
+
const totalSlices = snapData.milestones.reduce(
|
|
584
|
+
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
585
|
+
0,
|
|
586
|
+
);
|
|
587
|
+
const outPath = writeReportSnapshot({
|
|
588
|
+
basePath: s.basePath,
|
|
589
|
+
html: generateHtmlReport(snapData, {
|
|
590
|
+
projectName: projName,
|
|
591
|
+
projectPath: s.basePath,
|
|
592
|
+
gsdVersion,
|
|
593
|
+
milestoneId,
|
|
594
|
+
indexRelPath: "index.html",
|
|
595
|
+
}),
|
|
596
|
+
milestoneId,
|
|
597
|
+
milestoneTitle: msTitle,
|
|
598
|
+
kind: "milestone",
|
|
599
|
+
projectName: projName,
|
|
600
|
+
projectPath: s.basePath,
|
|
601
|
+
gsdVersion,
|
|
602
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
603
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
604
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
605
|
+
doneSlices,
|
|
606
|
+
totalSlices,
|
|
607
|
+
doneMilestones: snapData.milestones.filter(
|
|
608
|
+
(m: { status: string }) => m.status === "complete",
|
|
609
|
+
).length,
|
|
610
|
+
totalMilestones: snapData.milestones.length,
|
|
611
|
+
phase: snapData.phase,
|
|
612
|
+
});
|
|
613
|
+
ctx.ui.notify(
|
|
614
|
+
`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
|
|
615
|
+
"info",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
623
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
624
|
+
*/
|
|
625
|
+
async function closeoutAndStop(
|
|
626
|
+
ctx: ExtensionContext,
|
|
627
|
+
pi: ExtensionAPI,
|
|
628
|
+
s: AutoSession,
|
|
629
|
+
deps: LoopDeps,
|
|
630
|
+
reason: string,
|
|
631
|
+
): Promise<void> {
|
|
632
|
+
if (s.currentUnit) {
|
|
633
|
+
await deps.closeoutUnit(
|
|
634
|
+
ctx,
|
|
635
|
+
s.basePath,
|
|
636
|
+
s.currentUnit.type,
|
|
637
|
+
s.currentUnit.id,
|
|
638
|
+
s.currentUnit.startedAt,
|
|
639
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
643
|
+
}
|
|
644
|
+
|
|
492
645
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
493
646
|
|
|
494
647
|
/**
|
|
@@ -507,8 +660,10 @@ export async function autoLoop(
|
|
|
507
660
|
): Promise<void> {
|
|
508
661
|
debugLog("autoLoop", { phase: "enter" });
|
|
509
662
|
let iteration = 0;
|
|
510
|
-
|
|
511
|
-
|
|
663
|
+
// ── Sliding-window stuck detection ──
|
|
664
|
+
const recentUnits: Array<{ key: string; error?: string }> = [];
|
|
665
|
+
const STUCK_WINDOW_SIZE = 6;
|
|
666
|
+
let stuckRecoveryAttempts = 0;
|
|
512
667
|
|
|
513
668
|
let consecutiveErrors = 0;
|
|
514
669
|
|
|
@@ -537,6 +692,19 @@ export async function autoLoop(
|
|
|
537
692
|
|
|
538
693
|
try {
|
|
539
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
|
+
}
|
|
540
708
|
|
|
541
709
|
const sessionLockBase = deps.lockBase();
|
|
542
710
|
if (sessionLockBase) {
|
|
@@ -558,6 +726,17 @@ export async function autoLoop(
|
|
|
558
726
|
}
|
|
559
727
|
}
|
|
560
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) {
|
|
561
740
|
// ── Phase 1: Pre-dispatch ───────────────────────────────────────────
|
|
562
741
|
|
|
563
742
|
// Resource version guard
|
|
@@ -608,10 +787,10 @@ export async function autoLoop(
|
|
|
608
787
|
}
|
|
609
788
|
|
|
610
789
|
// Derive state
|
|
611
|
-
|
|
612
|
-
deps.syncCmuxSidebar(
|
|
613
|
-
|
|
614
|
-
|
|
790
|
+
state = await deps.deriveState(s.basePath);
|
|
791
|
+
deps.syncCmuxSidebar(prefs, state);
|
|
792
|
+
mid = state.activeMilestone?.id;
|
|
793
|
+
midTitle = state.activeMilestone?.title;
|
|
615
794
|
debugLog("autoLoop", {
|
|
616
795
|
phase: "state-derived",
|
|
617
796
|
iteration,
|
|
@@ -632,68 +811,18 @@ export async function autoLoop(
|
|
|
632
811
|
"milestone",
|
|
633
812
|
);
|
|
634
813
|
deps.logCmuxEvent(
|
|
635
|
-
|
|
814
|
+
prefs,
|
|
636
815
|
`Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
|
|
637
816
|
"success",
|
|
638
817
|
);
|
|
639
818
|
|
|
640
|
-
const vizPrefs =
|
|
819
|
+
const vizPrefs = prefs;
|
|
641
820
|
if (vizPrefs?.auto_visualize) {
|
|
642
821
|
ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
|
|
643
822
|
}
|
|
644
823
|
if (vizPrefs?.auto_report !== false) {
|
|
645
824
|
try {
|
|
646
|
-
|
|
647
|
-
const { generateHtmlReport } = await import("./export-html.js");
|
|
648
|
-
const { writeReportSnapshot } = await import("./reports.js");
|
|
649
|
-
const { basename } = await import("node:path");
|
|
650
|
-
const snapData = await loadVisualizerData(s.basePath);
|
|
651
|
-
const completedMs = snapData.milestones.find(
|
|
652
|
-
(m: { id: string }) => m.id === s.currentMilestoneId,
|
|
653
|
-
);
|
|
654
|
-
const msTitle = completedMs?.title ?? s.currentMilestoneId;
|
|
655
|
-
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
656
|
-
const projName = basename(s.basePath);
|
|
657
|
-
const doneSlices = snapData.milestones.reduce(
|
|
658
|
-
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
659
|
-
acc +
|
|
660
|
-
m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
661
|
-
0,
|
|
662
|
-
);
|
|
663
|
-
const totalSlices = snapData.milestones.reduce(
|
|
664
|
-
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
665
|
-
0,
|
|
666
|
-
);
|
|
667
|
-
const outPath = writeReportSnapshot({
|
|
668
|
-
basePath: s.basePath,
|
|
669
|
-
html: generateHtmlReport(snapData, {
|
|
670
|
-
projectName: projName,
|
|
671
|
-
projectPath: s.basePath,
|
|
672
|
-
gsdVersion,
|
|
673
|
-
milestoneId: s.currentMilestoneId,
|
|
674
|
-
indexRelPath: "index.html",
|
|
675
|
-
}),
|
|
676
|
-
milestoneId: s.currentMilestoneId!,
|
|
677
|
-
milestoneTitle: msTitle,
|
|
678
|
-
kind: "milestone",
|
|
679
|
-
projectName: projName,
|
|
680
|
-
projectPath: s.basePath,
|
|
681
|
-
gsdVersion,
|
|
682
|
-
totalCost: snapData.totals?.cost ?? 0,
|
|
683
|
-
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
684
|
-
totalDuration: snapData.totals?.duration ?? 0,
|
|
685
|
-
doneSlices,
|
|
686
|
-
totalSlices,
|
|
687
|
-
doneMilestones: snapData.milestones.filter(
|
|
688
|
-
(m: { status: string }) => m.status === "complete",
|
|
689
|
-
).length,
|
|
690
|
-
totalMilestones: snapData.milestones.length,
|
|
691
|
-
phase: snapData.phase,
|
|
692
|
-
});
|
|
693
|
-
ctx.ui.notify(
|
|
694
|
-
`Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`,
|
|
695
|
-
"info",
|
|
696
|
-
);
|
|
825
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
697
826
|
} catch (err) {
|
|
698
827
|
ctx.ui.notify(
|
|
699
828
|
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -706,11 +835,30 @@ export async function autoLoop(
|
|
|
706
835
|
s.unitDispatchCount.clear();
|
|
707
836
|
s.unitRecoveryCount.clear();
|
|
708
837
|
s.unitLifetimeDispatches.clear();
|
|
709
|
-
|
|
710
|
-
|
|
838
|
+
recentUnits.length = 0;
|
|
839
|
+
stuckRecoveryAttempts = 0;
|
|
711
840
|
|
|
712
841
|
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
713
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
|
+
|
|
714
862
|
deps.invalidateAllCaches();
|
|
715
863
|
|
|
716
864
|
state = await deps.deriveState(s.basePath);
|
|
@@ -720,9 +868,7 @@ export async function autoLoop(
|
|
|
720
868
|
if (mid) {
|
|
721
869
|
if (deps.getIsolationMode() !== "none") {
|
|
722
870
|
deps.captureIntegrationBranch(s.basePath, mid, {
|
|
723
|
-
commitDocs:
|
|
724
|
-
deps.loadEffectiveGSDPreferences()?.preferences?.git
|
|
725
|
-
?.commit_docs,
|
|
871
|
+
commitDocs: prefs?.git?.commit_docs,
|
|
726
872
|
});
|
|
727
873
|
}
|
|
728
874
|
deps.resolver.enterMilestone(mid, ctx.ui);
|
|
@@ -766,6 +912,24 @@ export async function autoLoop(
|
|
|
766
912
|
// All milestones complete — merge milestone branch before stopping
|
|
767
913
|
if (s.currentMilestoneId) {
|
|
768
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
|
+
}
|
|
769
933
|
}
|
|
770
934
|
deps.sendDesktopNotification(
|
|
771
935
|
"GSD",
|
|
@@ -774,7 +938,7 @@ export async function autoLoop(
|
|
|
774
938
|
"milestone",
|
|
775
939
|
);
|
|
776
940
|
deps.logCmuxEvent(
|
|
777
|
-
|
|
941
|
+
prefs,
|
|
778
942
|
"All milestones complete.",
|
|
779
943
|
"success",
|
|
780
944
|
);
|
|
@@ -796,7 +960,7 @@ export async function autoLoop(
|
|
|
796
960
|
await deps.stopAuto(ctx, pi, blockerMsg);
|
|
797
961
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
798
962
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
799
|
-
deps.logCmuxEvent(
|
|
963
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
800
964
|
} else {
|
|
801
965
|
const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
|
|
802
966
|
const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
|
|
@@ -831,20 +995,10 @@ export async function autoLoop(
|
|
|
831
995
|
}
|
|
832
996
|
|
|
833
997
|
if (!mid || !midTitle) {
|
|
834
|
-
if (s.currentUnit) {
|
|
835
|
-
await deps.closeoutUnit(
|
|
836
|
-
ctx,
|
|
837
|
-
s.basePath,
|
|
838
|
-
s.currentUnit.type,
|
|
839
|
-
s.currentUnit.id,
|
|
840
|
-
s.currentUnit.startedAt,
|
|
841
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
842
|
-
);
|
|
843
|
-
}
|
|
844
998
|
const noMilestoneReason = !mid
|
|
845
999
|
? "No active milestone after merge reconciliation"
|
|
846
1000
|
: `Milestone ${mid} has no title after reconciliation`;
|
|
847
|
-
await
|
|
1001
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
848
1002
|
debugLog("autoLoop", {
|
|
849
1003
|
phase: "exit",
|
|
850
1004
|
reason: "no-milestone-after-reconciliation",
|
|
@@ -854,19 +1008,27 @@ export async function autoLoop(
|
|
|
854
1008
|
|
|
855
1009
|
// Terminal: complete
|
|
856
1010
|
if (state.phase === "complete") {
|
|
857
|
-
|
|
858
|
-
await deps.closeoutUnit(
|
|
859
|
-
ctx,
|
|
860
|
-
s.basePath,
|
|
861
|
-
s.currentUnit.type,
|
|
862
|
-
s.currentUnit.id,
|
|
863
|
-
s.currentUnit.startedAt,
|
|
864
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
// Milestone merge on complete
|
|
1011
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
868
1012
|
if (s.currentMilestoneId) {
|
|
869
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
|
+
}
|
|
870
1032
|
}
|
|
871
1033
|
deps.sendDesktopNotification(
|
|
872
1034
|
"GSD",
|
|
@@ -875,40 +1037,28 @@ export async function autoLoop(
|
|
|
875
1037
|
"milestone",
|
|
876
1038
|
);
|
|
877
1039
|
deps.logCmuxEvent(
|
|
878
|
-
|
|
1040
|
+
prefs,
|
|
879
1041
|
`Milestone ${mid} complete.`,
|
|
880
1042
|
"success",
|
|
881
1043
|
);
|
|
882
|
-
await
|
|
1044
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
883
1045
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
884
1046
|
break;
|
|
885
1047
|
}
|
|
886
1048
|
|
|
887
1049
|
// Terminal: blocked
|
|
888
1050
|
if (state.phase === "blocked") {
|
|
889
|
-
if (s.currentUnit) {
|
|
890
|
-
await deps.closeoutUnit(
|
|
891
|
-
ctx,
|
|
892
|
-
s.basePath,
|
|
893
|
-
s.currentUnit.type,
|
|
894
|
-
s.currentUnit.id,
|
|
895
|
-
s.currentUnit.startedAt,
|
|
896
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
1051
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
900
|
-
await
|
|
1052
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
901
1053
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
902
1054
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
903
|
-
deps.logCmuxEvent(
|
|
1055
|
+
deps.logCmuxEvent(prefs, blockerMsg, "error");
|
|
904
1056
|
debugLog("autoLoop", { phase: "exit", reason: "blocked" });
|
|
905
1057
|
break;
|
|
906
1058
|
}
|
|
907
1059
|
|
|
908
1060
|
// ── Phase 2: Guards ─────────────────────────────────────────────────
|
|
909
1061
|
|
|
910
|
-
const prefs = deps.loadEffectiveGSDPreferences()?.preferences;
|
|
911
|
-
|
|
912
1062
|
// Budget ceiling guard
|
|
913
1063
|
const budgetCeiling = prefs?.budget_ceiling;
|
|
914
1064
|
if (budgetCeiling !== undefined && budgetCeiling > 0) {
|
|
@@ -928,38 +1078,39 @@ export async function autoLoop(
|
|
|
928
1078
|
budgetPct,
|
|
929
1079
|
);
|
|
930
1080
|
|
|
931
|
-
|
|
932
|
-
|
|
1081
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
1082
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
1083
|
+
(t) => newBudgetAlertLevel >= t.pct,
|
|
1084
|
+
);
|
|
1085
|
+
if (threshold) {
|
|
933
1086
|
s.lastBudgetAlertLevel =
|
|
934
1087
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
)
|
|
1088
|
+
|
|
1089
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
1090
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
1091
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
1092
|
+
if (budgetEnforcementAction === "halt") {
|
|
1093
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
1094
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
1095
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
if (budgetEnforcementAction === "pause") {
|
|
1099
|
+
ctx.ui.notify(
|
|
1100
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
1101
|
+
"warning",
|
|
1102
|
+
);
|
|
1103
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
1104
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
1105
|
+
await deps.pauseAuto(ctx, pi);
|
|
1106
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
946
1110
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
947
1111
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
break;
|
|
951
|
-
}
|
|
952
|
-
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
953
|
-
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
954
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
955
|
-
} else {
|
|
956
|
-
// Data-driven 75/80/90% threshold notifications
|
|
957
|
-
const threshold = BUDGET_THRESHOLDS.find(
|
|
958
|
-
(t) => newBudgetAlertLevel === t.pct,
|
|
959
|
-
);
|
|
960
|
-
if (threshold) {
|
|
961
|
-
s.lastBudgetAlertLevel =
|
|
962
|
-
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
1112
|
+
} else if (threshold.pct < 100) {
|
|
1113
|
+
// Sub-100% — simple notification
|
|
963
1114
|
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
964
1115
|
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
965
1116
|
deps.sendDesktopNotification(
|
|
@@ -969,9 +1120,9 @@ export async function autoLoop(
|
|
|
969
1120
|
"budget",
|
|
970
1121
|
);
|
|
971
1122
|
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
972
|
-
} else if (budgetAlertLevel === 0) {
|
|
973
|
-
s.lastBudgetAlertLevel = 0;
|
|
974
1123
|
}
|
|
1124
|
+
} else if (budgetAlertLevel === 0) {
|
|
1125
|
+
s.lastBudgetAlertLevel = 0;
|
|
975
1126
|
}
|
|
976
1127
|
} else {
|
|
977
1128
|
s.lastBudgetAlertLevel = 0;
|
|
@@ -1046,17 +1197,7 @@ export async function autoLoop(
|
|
|
1046
1197
|
});
|
|
1047
1198
|
|
|
1048
1199
|
if (dispatchResult.action === "stop") {
|
|
1049
|
-
|
|
1050
|
-
await deps.closeoutUnit(
|
|
1051
|
-
ctx,
|
|
1052
|
-
s.basePath,
|
|
1053
|
-
s.currentUnit.type,
|
|
1054
|
-
s.currentUnit.id,
|
|
1055
|
-
s.currentUnit.startedAt,
|
|
1056
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
await deps.stopAuto(ctx, pi, dispatchResult.reason);
|
|
1200
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1060
1201
|
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1061
1202
|
break;
|
|
1062
1203
|
}
|
|
@@ -1067,76 +1208,84 @@ export async function autoLoop(
|
|
|
1067
1208
|
continue;
|
|
1068
1209
|
}
|
|
1069
1210
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1211
|
+
unitType = dispatchResult.unitType;
|
|
1212
|
+
unitId = dispatchResult.unitId;
|
|
1213
|
+
prompt = dispatchResult.prompt;
|
|
1214
|
+
pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1074
1215
|
|
|
1075
|
-
// ──
|
|
1216
|
+
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1076
1217
|
const derivedKey = `${unitType}/${unitId}`;
|
|
1077
|
-
if (derivedKey === lastDerivedUnit && !s.pendingVerificationRetry) {
|
|
1078
|
-
sameUnitCount++;
|
|
1079
|
-
debugLog("autoLoop", {
|
|
1080
|
-
phase: "stuck-check",
|
|
1081
|
-
unitType,
|
|
1082
|
-
unitId,
|
|
1083
|
-
sameUnitCount,
|
|
1084
|
-
});
|
|
1085
1218
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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",
|
|
1089
1227
|
unitType,
|
|
1090
1228
|
unitId,
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
|
1094
1261
|
debugLog("autoLoop", {
|
|
1095
|
-
phase: "stuck-
|
|
1096
|
-
|
|
1097
|
-
|
|
1262
|
+
phase: "stuck-detected",
|
|
1263
|
+
unitType,
|
|
1264
|
+
unitId,
|
|
1265
|
+
reason: stuckSignal.reason,
|
|
1098
1266
|
});
|
|
1267
|
+
await deps.stopAuto(
|
|
1268
|
+
ctx,
|
|
1269
|
+
pi,
|
|
1270
|
+
`Stuck: ${stuckSignal.reason}`,
|
|
1271
|
+
);
|
|
1099
1272
|
ctx.ui.notify(
|
|
1100
|
-
`Stuck
|
|
1101
|
-
"
|
|
1273
|
+
`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}. The expected artifact was not written.`,
|
|
1274
|
+
"error",
|
|
1102
1275
|
);
|
|
1103
|
-
|
|
1104
|
-
|
|
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;
|
|
1105
1287
|
}
|
|
1106
|
-
ctx.ui.notify(
|
|
1107
|
-
`Stuck on ${unitType} ${unitId} (attempt ${sameUnitCount}). Invalidating caches and retrying.`,
|
|
1108
|
-
"warning",
|
|
1109
|
-
);
|
|
1110
|
-
deps.invalidateAllCaches();
|
|
1111
|
-
} else if (sameUnitCount === 5) {
|
|
1112
|
-
// Level 2: hard stop — genuinely stuck
|
|
1113
|
-
debugLog("autoLoop", {
|
|
1114
|
-
phase: "stuck-detected",
|
|
1115
|
-
unitType,
|
|
1116
|
-
unitId,
|
|
1117
|
-
sameUnitCount,
|
|
1118
|
-
});
|
|
1119
|
-
await deps.stopAuto(
|
|
1120
|
-
ctx,
|
|
1121
|
-
pi,
|
|
1122
|
-
`Stuck: ${unitType} ${unitId} derived ${sameUnitCount} consecutive times without progress`,
|
|
1123
|
-
);
|
|
1124
|
-
ctx.ui.notify(
|
|
1125
|
-
`Stuck on ${unitType} ${unitId} — deriveState returns the same unit after ${sameUnitCount} attempts. The expected artifact was not written.`,
|
|
1126
|
-
"error",
|
|
1127
|
-
);
|
|
1128
|
-
break;
|
|
1129
|
-
}
|
|
1130
|
-
} else {
|
|
1131
|
-
if (derivedKey !== lastDerivedUnit) {
|
|
1132
|
-
debugLog("autoLoop", {
|
|
1133
|
-
phase: "stuck-counter-reset",
|
|
1134
|
-
from: lastDerivedUnit,
|
|
1135
|
-
to: derivedKey,
|
|
1136
|
-
});
|
|
1137
1288
|
}
|
|
1138
|
-
lastDerivedUnit = derivedKey;
|
|
1139
|
-
sameUnitCount = 0;
|
|
1140
1289
|
}
|
|
1141
1290
|
|
|
1142
1291
|
// Pre-dispatch hooks
|
|
@@ -1179,13 +1328,27 @@ export async function autoLoop(
|
|
|
1179
1328
|
break;
|
|
1180
1329
|
}
|
|
1181
1330
|
|
|
1182
|
-
|
|
1331
|
+
observabilityIssues = await deps.collectObservabilityWarnings(
|
|
1183
1332
|
ctx,
|
|
1184
1333
|
s.basePath,
|
|
1185
1334
|
unitType,
|
|
1186
1335
|
unitId,
|
|
1187
1336
|
);
|
|
1188
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
|
+
|
|
1189
1352
|
// ── Phase 4: Unit execution ─────────────────────────────────────────
|
|
1190
1353
|
|
|
1191
1354
|
debugLog("autoLoop", {
|
|
@@ -1203,61 +1366,6 @@ export async function autoLoop(
|
|
|
1203
1366
|
);
|
|
1204
1367
|
const previousTier = s.currentUnitRouting?.tier;
|
|
1205
1368
|
|
|
1206
|
-
// Closeout previous unit
|
|
1207
|
-
if (s.currentUnit) {
|
|
1208
|
-
await deps.closeoutUnit(
|
|
1209
|
-
ctx,
|
|
1210
|
-
s.basePath,
|
|
1211
|
-
s.currentUnit.type,
|
|
1212
|
-
s.currentUnit.id,
|
|
1213
|
-
s.currentUnit.startedAt,
|
|
1214
|
-
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
1215
|
-
);
|
|
1216
|
-
|
|
1217
|
-
if (s.currentUnitRouting) {
|
|
1218
|
-
const isRetry =
|
|
1219
|
-
s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
1220
|
-
deps.recordOutcome(
|
|
1221
|
-
s.currentUnit.type,
|
|
1222
|
-
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1223
|
-
!isRetry,
|
|
1224
|
-
);
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
1228
|
-
const incomingKey = `${unitType}/${unitId}`;
|
|
1229
|
-
const isHookUnit = s.currentUnit.type.startsWith("hook/");
|
|
1230
|
-
const artifactVerified =
|
|
1231
|
-
isHookUnit ||
|
|
1232
|
-
deps.verifyExpectedArtifact(
|
|
1233
|
-
s.currentUnit.type,
|
|
1234
|
-
s.currentUnit.id,
|
|
1235
|
-
s.basePath,
|
|
1236
|
-
);
|
|
1237
|
-
if (closeoutKey !== incomingKey && artifactVerified) {
|
|
1238
|
-
s.completedUnits.push({
|
|
1239
|
-
type: s.currentUnit.type,
|
|
1240
|
-
id: s.currentUnit.id,
|
|
1241
|
-
startedAt: s.currentUnit.startedAt,
|
|
1242
|
-
finishedAt: Date.now(),
|
|
1243
|
-
});
|
|
1244
|
-
if (s.completedUnits.length > 200) {
|
|
1245
|
-
s.completedUnits = s.completedUnits.slice(-200);
|
|
1246
|
-
}
|
|
1247
|
-
deps.clearUnitRuntimeRecord(
|
|
1248
|
-
s.basePath,
|
|
1249
|
-
s.currentUnit.type,
|
|
1250
|
-
s.currentUnit.id,
|
|
1251
|
-
);
|
|
1252
|
-
s.unitDispatchCount.delete(
|
|
1253
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1254
|
-
);
|
|
1255
|
-
s.unitRecoveryCount.delete(
|
|
1256
|
-
`${s.currentUnit.type}/${s.currentUnit.id}`,
|
|
1257
|
-
);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
1369
|
s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
|
|
1262
1370
|
deps.captureAvailableSkills();
|
|
1263
1371
|
deps.writeUnitRuntimeRecord(
|
|
@@ -1284,7 +1392,6 @@ export async function autoLoop(
|
|
|
1284
1392
|
deps.ensurePreconditions(unitType, unitId, s.basePath, state);
|
|
1285
1393
|
|
|
1286
1394
|
// Prompt injection
|
|
1287
|
-
const MAX_RECOVERY_CHARS = 50_000;
|
|
1288
1395
|
let finalPrompt = prompt;
|
|
1289
1396
|
|
|
1290
1397
|
if (s.pendingVerificationRetry) {
|
|
@@ -1329,7 +1436,7 @@ export async function autoLoop(
|
|
|
1329
1436
|
s.lastBaselineCharCount = undefined;
|
|
1330
1437
|
if (deps.isDbAvailable()) {
|
|
1331
1438
|
try {
|
|
1332
|
-
const { inlineGsdRootFile } = await import("./auto-prompts.js");
|
|
1439
|
+
const { inlineGsdRootFile } = await importExtensionModule<typeof import("./auto-prompts.js")>(import.meta.url, "./auto-prompts.js");
|
|
1333
1440
|
const [decisionsContent, requirementsContent, projectContent] =
|
|
1334
1441
|
await Promise.all([
|
|
1335
1442
|
inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
|
|
@@ -1356,7 +1463,7 @@ export async function autoLoop(
|
|
|
1356
1463
|
);
|
|
1357
1464
|
}
|
|
1358
1465
|
|
|
1359
|
-
// Select and apply model (with tier escalation on retry)
|
|
1466
|
+
// Select and apply model (with tier escalation on retry — normal units only)
|
|
1360
1467
|
const modelResult = await deps.selectAndApplyModel(
|
|
1361
1468
|
ctx,
|
|
1362
1469
|
pi,
|
|
@@ -1366,7 +1473,7 @@ export async function autoLoop(
|
|
|
1366
1473
|
prefs,
|
|
1367
1474
|
s.verbose,
|
|
1368
1475
|
s.autoModeStartModel,
|
|
1369
|
-
{ isRetry, previousTier },
|
|
1476
|
+
sidecarItem ? undefined : { isRetry, previousTier },
|
|
1370
1477
|
);
|
|
1371
1478
|
s.currentUnitRouting =
|
|
1372
1479
|
modelResult.routing as AutoSession["currentUnitRouting"];
|
|
@@ -1415,7 +1522,6 @@ export async function autoLoop(
|
|
|
1415
1522
|
unitType,
|
|
1416
1523
|
unitId,
|
|
1417
1524
|
finalPrompt,
|
|
1418
|
-
prefs,
|
|
1419
1525
|
);
|
|
1420
1526
|
debugLog("autoLoop", {
|
|
1421
1527
|
phase: "runUnit-end",
|
|
@@ -1425,6 +1531,23 @@ export async function autoLoop(
|
|
|
1425
1531
|
status: unitResult.status,
|
|
1426
1532
|
});
|
|
1427
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
|
+
|
|
1428
1551
|
if (unitResult.status === "cancelled") {
|
|
1429
1552
|
ctx.ui.notify(
|
|
1430
1553
|
`Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
|
|
@@ -1435,6 +1558,52 @@ export async function autoLoop(
|
|
|
1435
1558
|
break;
|
|
1436
1559
|
}
|
|
1437
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
|
+
|
|
1438
1607
|
// ── Phase 5: Finalize ───────────────────────────────────────────────
|
|
1439
1608
|
|
|
1440
1609
|
debugLog("autoLoop", { phase: "finalize", iteration });
|
|
@@ -1455,7 +1624,13 @@ export async function autoLoop(
|
|
|
1455
1624
|
};
|
|
1456
1625
|
|
|
1457
1626
|
// Pre-verification processing (commit, doctor, state rebuild, etc.)
|
|
1458
|
-
|
|
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);
|
|
1459
1634
|
if (preResult === "dispatched") {
|
|
1460
1635
|
debugLog("autoLoop", {
|
|
1461
1636
|
phase: "exit",
|
|
@@ -1474,22 +1649,32 @@ export async function autoLoop(
|
|
|
1474
1649
|
break;
|
|
1475
1650
|
}
|
|
1476
1651
|
|
|
1477
|
-
// Verification gate
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
)
|
|
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
|
+
);
|
|
1482
1661
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1662
|
+
if (verificationResult === "pause") {
|
|
1663
|
+
debugLog("autoLoop", { phase: "exit", reason: "verification-pause" });
|
|
1664
|
+
break;
|
|
1665
|
+
}
|
|
1487
1666
|
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
+
}
|
|
1493
1678
|
}
|
|
1494
1679
|
|
|
1495
1680
|
// Post-verification processing (DB dual-write, hooks, triage, quick-tasks)
|
|
@@ -1509,153 +1694,6 @@ export async function autoLoop(
|
|
|
1509
1694
|
break;
|
|
1510
1695
|
}
|
|
1511
1696
|
|
|
1512
|
-
// ── Sidecar drain: dispatch enqueued hooks/triage/quick-tasks ──
|
|
1513
|
-
let sidecarBroke = false;
|
|
1514
|
-
while (s.sidecarQueue.length > 0 && s.active) {
|
|
1515
|
-
const item = s.sidecarQueue.shift()!;
|
|
1516
|
-
debugLog("autoLoop", {
|
|
1517
|
-
phase: "sidecar-dequeue",
|
|
1518
|
-
kind: item.kind,
|
|
1519
|
-
unitType: item.unitType,
|
|
1520
|
-
unitId: item.unitId,
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
// Set up as current unit
|
|
1524
|
-
const sidecarStartedAt = Date.now();
|
|
1525
|
-
s.currentUnit = {
|
|
1526
|
-
type: item.unitType,
|
|
1527
|
-
id: item.unitId,
|
|
1528
|
-
startedAt: sidecarStartedAt,
|
|
1529
|
-
};
|
|
1530
|
-
deps.writeUnitRuntimeRecord(
|
|
1531
|
-
s.basePath,
|
|
1532
|
-
item.unitType,
|
|
1533
|
-
item.unitId,
|
|
1534
|
-
sidecarStartedAt,
|
|
1535
|
-
{
|
|
1536
|
-
phase: "dispatched",
|
|
1537
|
-
wrapupWarningSent: false,
|
|
1538
|
-
timeoutAt: null,
|
|
1539
|
-
lastProgressAt: sidecarStartedAt,
|
|
1540
|
-
progressCount: 0,
|
|
1541
|
-
lastProgressKind: "dispatch",
|
|
1542
|
-
},
|
|
1543
|
-
);
|
|
1544
|
-
|
|
1545
|
-
// Model selection (handles hook model override)
|
|
1546
|
-
await deps.selectAndApplyModel(
|
|
1547
|
-
ctx,
|
|
1548
|
-
pi,
|
|
1549
|
-
item.unitType,
|
|
1550
|
-
item.unitId,
|
|
1551
|
-
s.basePath,
|
|
1552
|
-
prefs,
|
|
1553
|
-
s.verbose,
|
|
1554
|
-
s.autoModeStartModel,
|
|
1555
|
-
);
|
|
1556
|
-
|
|
1557
|
-
// Supervision
|
|
1558
|
-
deps.clearUnitTimeout();
|
|
1559
|
-
deps.startUnitSupervision({
|
|
1560
|
-
s,
|
|
1561
|
-
ctx,
|
|
1562
|
-
pi,
|
|
1563
|
-
unitType: item.unitType,
|
|
1564
|
-
unitId: item.unitId,
|
|
1565
|
-
prefs,
|
|
1566
|
-
buildSnapshotOpts: () =>
|
|
1567
|
-
deps.buildSnapshotOpts(item.unitType, item.unitId),
|
|
1568
|
-
buildRecoveryContext: () => ({}),
|
|
1569
|
-
pauseAuto: deps.pauseAuto,
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1572
|
-
// Write lock
|
|
1573
|
-
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
1574
|
-
deps.writeLock(
|
|
1575
|
-
deps.lockBase(),
|
|
1576
|
-
item.unitType,
|
|
1577
|
-
item.unitId,
|
|
1578
|
-
s.completedUnits.length,
|
|
1579
|
-
sidecarSessionFile,
|
|
1580
|
-
);
|
|
1581
|
-
|
|
1582
|
-
// Execute via standard runUnit
|
|
1583
|
-
const sidecarResult = await runUnit(
|
|
1584
|
-
ctx,
|
|
1585
|
-
pi,
|
|
1586
|
-
s,
|
|
1587
|
-
item.unitType,
|
|
1588
|
-
item.unitId,
|
|
1589
|
-
item.prompt,
|
|
1590
|
-
prefs,
|
|
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) {
|