gsd-pi 2.38.0-dev.63ad7e5 → 2.38.0-dev.8f5c161
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.
|
@@ -18,9 +18,11 @@ import { debugLog } from "./debug-logger.js";
|
|
|
18
18
|
* generous headroom including retries and sidecar work.
|
|
19
19
|
*/
|
|
20
20
|
const MAX_LOOP_ITERATIONS = 500;
|
|
21
|
-
/** Data-driven budget threshold notifications (
|
|
22
|
-
*
|
|
21
|
+
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
22
|
+
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
23
|
+
* a simple notification. */
|
|
23
24
|
const BUDGET_THRESHOLDS = [
|
|
25
|
+
{ pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
|
|
24
26
|
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
25
27
|
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
26
28
|
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
@@ -88,7 +90,7 @@ export function _setActiveSession(_session) {
|
|
|
88
90
|
* On session creation failure or timeout, returns { status: 'cancelled' }
|
|
89
91
|
* without awaiting the promise.
|
|
90
92
|
*/
|
|
91
|
-
export async function runUnit(ctx, pi, s, unitType, unitId, prompt
|
|
93
|
+
export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
92
94
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
93
95
|
// ── Session creation with timeout ──
|
|
94
96
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
@@ -155,6 +157,60 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
155
157
|
});
|
|
156
158
|
return result;
|
|
157
159
|
}
|
|
160
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
161
|
+
/**
|
|
162
|
+
* Generate and write an HTML milestone report snapshot.
|
|
163
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
164
|
+
*/
|
|
165
|
+
async function generateMilestoneReport(s, ctx, milestoneId) {
|
|
166
|
+
const { loadVisualizerData } = await import("./visualizer-data.js");
|
|
167
|
+
const { generateHtmlReport } = await import("./export-html.js");
|
|
168
|
+
const { writeReportSnapshot } = await import("./reports.js");
|
|
169
|
+
const { basename } = await import("node:path");
|
|
170
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
171
|
+
const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
|
|
172
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
173
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
174
|
+
const projName = basename(s.basePath);
|
|
175
|
+
const doneSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.filter((sl) => sl.done).length, 0);
|
|
176
|
+
const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
|
|
177
|
+
const outPath = writeReportSnapshot({
|
|
178
|
+
basePath: s.basePath,
|
|
179
|
+
html: generateHtmlReport(snapData, {
|
|
180
|
+
projectName: projName,
|
|
181
|
+
projectPath: s.basePath,
|
|
182
|
+
gsdVersion,
|
|
183
|
+
milestoneId,
|
|
184
|
+
indexRelPath: "index.html",
|
|
185
|
+
}),
|
|
186
|
+
milestoneId,
|
|
187
|
+
milestoneTitle: msTitle,
|
|
188
|
+
kind: "milestone",
|
|
189
|
+
projectName: projName,
|
|
190
|
+
projectPath: s.basePath,
|
|
191
|
+
gsdVersion,
|
|
192
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
193
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
194
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
195
|
+
doneSlices,
|
|
196
|
+
totalSlices,
|
|
197
|
+
doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
|
|
198
|
+
totalMilestones: snapData.milestones.length,
|
|
199
|
+
phase: snapData.phase,
|
|
200
|
+
});
|
|
201
|
+
ctx.ui.notify(`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`, "info");
|
|
202
|
+
}
|
|
203
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
204
|
+
/**
|
|
205
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
206
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
207
|
+
*/
|
|
208
|
+
async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
209
|
+
if (s.currentUnit) {
|
|
210
|
+
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
211
|
+
}
|
|
212
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
213
|
+
}
|
|
158
214
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
159
215
|
/**
|
|
160
216
|
* Main auto-mode execution loop. Iterates: derive → dispatch → guards →
|
|
@@ -262,43 +318,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
262
318
|
}
|
|
263
319
|
if (vizPrefs?.auto_report !== false) {
|
|
264
320
|
try {
|
|
265
|
-
|
|
266
|
-
const { generateHtmlReport } = await import("./export-html.js");
|
|
267
|
-
const { writeReportSnapshot } = await import("./reports.js");
|
|
268
|
-
const { basename } = await import("node:path");
|
|
269
|
-
const snapData = await loadVisualizerData(s.basePath);
|
|
270
|
-
const completedMs = snapData.milestones.find((m) => m.id === s.currentMilestoneId);
|
|
271
|
-
const msTitle = completedMs?.title ?? s.currentMilestoneId;
|
|
272
|
-
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
273
|
-
const projName = basename(s.basePath);
|
|
274
|
-
const doneSlices = snapData.milestones.reduce((acc, m) => acc +
|
|
275
|
-
m.slices.filter((sl) => sl.done).length, 0);
|
|
276
|
-
const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
|
|
277
|
-
const outPath = writeReportSnapshot({
|
|
278
|
-
basePath: s.basePath,
|
|
279
|
-
html: generateHtmlReport(snapData, {
|
|
280
|
-
projectName: projName,
|
|
281
|
-
projectPath: s.basePath,
|
|
282
|
-
gsdVersion,
|
|
283
|
-
milestoneId: s.currentMilestoneId,
|
|
284
|
-
indexRelPath: "index.html",
|
|
285
|
-
}),
|
|
286
|
-
milestoneId: s.currentMilestoneId,
|
|
287
|
-
milestoneTitle: msTitle,
|
|
288
|
-
kind: "milestone",
|
|
289
|
-
projectName: projName,
|
|
290
|
-
projectPath: s.basePath,
|
|
291
|
-
gsdVersion,
|
|
292
|
-
totalCost: snapData.totals?.cost ?? 0,
|
|
293
|
-
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
294
|
-
totalDuration: snapData.totals?.duration ?? 0,
|
|
295
|
-
doneSlices,
|
|
296
|
-
totalSlices,
|
|
297
|
-
doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
|
|
298
|
-
totalMilestones: snapData.milestones.length,
|
|
299
|
-
phase: snapData.phase,
|
|
300
|
-
});
|
|
301
|
-
ctx.ui.notify(`Report saved: .gsd/reports/${(await import("node:path")).basename(outPath)} — open index.html to browse progression.`, "info");
|
|
321
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId);
|
|
302
322
|
}
|
|
303
323
|
catch (err) {
|
|
304
324
|
ctx.ui.notify(`Report generation failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
@@ -386,13 +406,10 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
386
406
|
midTitle = state.activeMilestone?.title;
|
|
387
407
|
}
|
|
388
408
|
if (!mid || !midTitle) {
|
|
389
|
-
if (s.currentUnit) {
|
|
390
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
391
|
-
}
|
|
392
409
|
const noMilestoneReason = !mid
|
|
393
410
|
? "No active milestone after merge reconciliation"
|
|
394
411
|
: `Milestone ${mid} has no title after reconciliation`;
|
|
395
|
-
await
|
|
412
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
396
413
|
debugLog("autoLoop", {
|
|
397
414
|
phase: "exit",
|
|
398
415
|
reason: "no-milestone-after-reconciliation",
|
|
@@ -401,26 +418,20 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
401
418
|
}
|
|
402
419
|
// Terminal: complete
|
|
403
420
|
if (state.phase === "complete") {
|
|
404
|
-
|
|
405
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
406
|
-
}
|
|
407
|
-
// Milestone merge on complete
|
|
421
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
408
422
|
if (s.currentMilestoneId) {
|
|
409
423
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
410
424
|
}
|
|
411
425
|
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone");
|
|
412
426
|
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, `Milestone ${mid} complete.`, "success");
|
|
413
|
-
await
|
|
427
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
414
428
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
415
429
|
break;
|
|
416
430
|
}
|
|
417
431
|
// Terminal: blocked
|
|
418
432
|
if (state.phase === "blocked") {
|
|
419
|
-
if (s.currentUnit) {
|
|
420
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
421
|
-
}
|
|
422
433
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
423
|
-
await
|
|
434
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
424
435
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
425
436
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
426
437
|
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
@@ -441,42 +452,42 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
441
452
|
const newBudgetAlertLevel = deps.getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
|
|
442
453
|
const enforcement = prefs?.budget_enforcement ?? "pause";
|
|
443
454
|
const budgetEnforcementAction = deps.getBudgetEnforcementAction(enforcement, budgetPct);
|
|
444
|
-
|
|
445
|
-
|
|
455
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
456
|
+
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel >= t.pct);
|
|
457
|
+
if (threshold) {
|
|
446
458
|
s.lastBudgetAlertLevel =
|
|
447
459
|
newBudgetAlertLevel;
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
460
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
461
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
462
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
463
|
+
if (budgetEnforcementAction === "halt") {
|
|
464
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
465
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
466
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
if (budgetEnforcementAction === "pause") {
|
|
470
|
+
ctx.ui.notify(`${msg} Pausing auto-mode — /gsd auto to override and continue.`, "warning");
|
|
471
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
472
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
473
|
+
await deps.pauseAuto(ctx, pi);
|
|
474
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
456
478
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
457
479
|
deps.logCmuxEvent(prefs, msg, "warning");
|
|
458
|
-
await deps.pauseAuto(ctx, pi);
|
|
459
|
-
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
460
|
-
break;
|
|
461
480
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
deps.logCmuxEvent(prefs, msg, "warning");
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
// Data-driven 75/80/90% threshold notifications
|
|
468
|
-
const threshold = BUDGET_THRESHOLDS.find((t) => newBudgetAlertLevel === t.pct);
|
|
469
|
-
if (threshold) {
|
|
470
|
-
s.lastBudgetAlertLevel =
|
|
471
|
-
newBudgetAlertLevel;
|
|
481
|
+
else if (threshold.pct < 100) {
|
|
482
|
+
// Sub-100% — simple notification
|
|
472
483
|
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
473
484
|
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
474
485
|
deps.sendDesktopNotification("GSD", msg, threshold.notifyLevel, "budget");
|
|
475
486
|
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
476
487
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
488
|
+
}
|
|
489
|
+
else if (budgetAlertLevel === 0) {
|
|
490
|
+
s.lastBudgetAlertLevel = 0;
|
|
480
491
|
}
|
|
481
492
|
}
|
|
482
493
|
else {
|
|
@@ -527,10 +538,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
527
538
|
session: s,
|
|
528
539
|
});
|
|
529
540
|
if (dispatchResult.action === "stop") {
|
|
530
|
-
|
|
531
|
-
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
532
|
-
}
|
|
533
|
-
await deps.stopAuto(ctx, pi, dispatchResult.reason);
|
|
541
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
534
542
|
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
535
543
|
break;
|
|
536
544
|
}
|
|
@@ -634,8 +642,8 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
634
642
|
if (s.currentUnit) {
|
|
635
643
|
await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
|
|
636
644
|
if (s.currentUnitRouting) {
|
|
637
|
-
const
|
|
638
|
-
deps.recordOutcome(s.currentUnit.type, s.currentUnitRouting.tier, !
|
|
645
|
+
const isRetryForOutcome = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
646
|
+
deps.recordOutcome(s.currentUnit.type, s.currentUnitRouting.tier, !isRetryForOutcome);
|
|
639
647
|
}
|
|
640
648
|
const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
|
|
641
649
|
const incomingKey = `${unitType}/${unitId}`;
|
|
@@ -762,7 +770,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
762
770
|
unitType,
|
|
763
771
|
unitId,
|
|
764
772
|
});
|
|
765
|
-
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt
|
|
773
|
+
const unitResult = await runUnit(ctx, pi, s, unitType, unitId, finalPrompt);
|
|
766
774
|
debugLog("autoLoop", {
|
|
767
775
|
phase: "runUnit-end",
|
|
768
776
|
iteration,
|
|
@@ -876,7 +884,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
876
884
|
const sidecarSessionFile = deps.getSessionFile(ctx);
|
|
877
885
|
deps.writeLock(deps.lockBase(), item.unitType, item.unitId, s.completedUnits.length, sidecarSessionFile);
|
|
878
886
|
// Execute via standard runUnit
|
|
879
|
-
const sidecarResult = await runUnit(ctx, pi, s, item.unitType, item.unitId, item.prompt
|
|
887
|
+
const sidecarResult = await runUnit(ctx, pi, s, item.unitType, item.unitId, item.prompt);
|
|
880
888
|
deps.clearUnitTimeout();
|
|
881
889
|
if (sidecarResult.status === "cancelled") {
|
|
882
890
|
ctx.ui.notify(`Sidecar unit ${item.unitType} ${item.unitId} session cancelled. Stopping.`, "warning");
|
package/package.json
CHANGED
|
@@ -36,14 +36,16 @@ import type { CmuxLogLevel } from "../cmux/index.js";
|
|
|
36
36
|
*/
|
|
37
37
|
const MAX_LOOP_ITERATIONS = 500;
|
|
38
38
|
|
|
39
|
-
/** Data-driven budget threshold notifications (
|
|
40
|
-
*
|
|
39
|
+
/** Data-driven budget threshold notifications (descending). The 100% entry
|
|
40
|
+
* triggers special enforcement logic (halt/pause/warn); sub-100 entries fire
|
|
41
|
+
* a simple notification. */
|
|
41
42
|
const BUDGET_THRESHOLDS: Array<{
|
|
42
43
|
pct: number;
|
|
43
44
|
label: string;
|
|
44
|
-
notifyLevel: "info" | "warning";
|
|
45
|
-
cmuxLevel: "progress" | "warning";
|
|
45
|
+
notifyLevel: "info" | "warning" | "error";
|
|
46
|
+
cmuxLevel: "progress" | "warning" | "error";
|
|
46
47
|
}> = [
|
|
48
|
+
{ pct: 100, label: "Budget ceiling reached", notifyLevel: "error", cmuxLevel: "error" },
|
|
47
49
|
{ pct: 90, label: "Budget 90%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
48
50
|
{ pct: 80, label: "Approaching budget ceiling — 80%", notifyLevel: "warning", cmuxLevel: "warning" },
|
|
49
51
|
{ pct: 75, label: "Budget 75%", notifyLevel: "info", cmuxLevel: "progress" },
|
|
@@ -145,7 +147,6 @@ export async function runUnit(
|
|
|
145
147
|
unitType: string,
|
|
146
148
|
unitId: string,
|
|
147
149
|
prompt: string,
|
|
148
|
-
_prefs: GSDPreferences | undefined,
|
|
149
150
|
): Promise<UnitResult> {
|
|
150
151
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
151
152
|
|
|
@@ -489,6 +490,96 @@ export interface LoopDeps {
|
|
|
489
490
|
getSessionFile: (ctx: ExtensionContext) => string;
|
|
490
491
|
}
|
|
491
492
|
|
|
493
|
+
// ─── generateMilestoneReport ──────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Generate and write an HTML milestone report snapshot.
|
|
497
|
+
* Extracted from the milestone-transition block in autoLoop.
|
|
498
|
+
*/
|
|
499
|
+
async function generateMilestoneReport(
|
|
500
|
+
s: AutoSession,
|
|
501
|
+
ctx: ExtensionContext,
|
|
502
|
+
milestoneId: string,
|
|
503
|
+
): 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");
|
|
507
|
+
const { basename } = await import("node:path");
|
|
508
|
+
|
|
509
|
+
const snapData = await loadVisualizerData(s.basePath);
|
|
510
|
+
const completedMs = snapData.milestones.find(
|
|
511
|
+
(m: { id: string }) => m.id === milestoneId,
|
|
512
|
+
);
|
|
513
|
+
const msTitle = completedMs?.title ?? milestoneId;
|
|
514
|
+
const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
|
|
515
|
+
const projName = basename(s.basePath);
|
|
516
|
+
const doneSlices = snapData.milestones.reduce(
|
|
517
|
+
(acc: number, m: { slices: { done: boolean }[] }) =>
|
|
518
|
+
acc + m.slices.filter((sl: { done: boolean }) => sl.done).length,
|
|
519
|
+
0,
|
|
520
|
+
);
|
|
521
|
+
const totalSlices = snapData.milestones.reduce(
|
|
522
|
+
(acc: number, m: { slices: unknown[] }) => acc + m.slices.length,
|
|
523
|
+
0,
|
|
524
|
+
);
|
|
525
|
+
const outPath = writeReportSnapshot({
|
|
526
|
+
basePath: s.basePath,
|
|
527
|
+
html: generateHtmlReport(snapData, {
|
|
528
|
+
projectName: projName,
|
|
529
|
+
projectPath: s.basePath,
|
|
530
|
+
gsdVersion,
|
|
531
|
+
milestoneId,
|
|
532
|
+
indexRelPath: "index.html",
|
|
533
|
+
}),
|
|
534
|
+
milestoneId,
|
|
535
|
+
milestoneTitle: msTitle,
|
|
536
|
+
kind: "milestone",
|
|
537
|
+
projectName: projName,
|
|
538
|
+
projectPath: s.basePath,
|
|
539
|
+
gsdVersion,
|
|
540
|
+
totalCost: snapData.totals?.cost ?? 0,
|
|
541
|
+
totalTokens: snapData.totals?.tokens.total ?? 0,
|
|
542
|
+
totalDuration: snapData.totals?.duration ?? 0,
|
|
543
|
+
doneSlices,
|
|
544
|
+
totalSlices,
|
|
545
|
+
doneMilestones: snapData.milestones.filter(
|
|
546
|
+
(m: { status: string }) => m.status === "complete",
|
|
547
|
+
).length,
|
|
548
|
+
totalMilestones: snapData.milestones.length,
|
|
549
|
+
phase: snapData.phase,
|
|
550
|
+
});
|
|
551
|
+
ctx.ui.notify(
|
|
552
|
+
`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
|
|
553
|
+
"info",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ─── closeoutAndStop ──────────────────────────────────────────────────────────
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* If a unit is in-flight, close it out, then stop auto-mode.
|
|
561
|
+
* Extracted from ~4 identical if-closeout-then-stop sequences in autoLoop.
|
|
562
|
+
*/
|
|
563
|
+
async function closeoutAndStop(
|
|
564
|
+
ctx: ExtensionContext,
|
|
565
|
+
pi: ExtensionAPI,
|
|
566
|
+
s: AutoSession,
|
|
567
|
+
deps: LoopDeps,
|
|
568
|
+
reason: string,
|
|
569
|
+
): Promise<void> {
|
|
570
|
+
if (s.currentUnit) {
|
|
571
|
+
await deps.closeoutUnit(
|
|
572
|
+
ctx,
|
|
573
|
+
s.basePath,
|
|
574
|
+
s.currentUnit.type,
|
|
575
|
+
s.currentUnit.id,
|
|
576
|
+
s.currentUnit.startedAt,
|
|
577
|
+
deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
581
|
+
}
|
|
582
|
+
|
|
492
583
|
// ─── autoLoop ────────────────────────────────────────────────────────────────
|
|
493
584
|
|
|
494
585
|
/**
|
|
@@ -643,57 +734,7 @@ export async function autoLoop(
|
|
|
643
734
|
}
|
|
644
735
|
if (vizPrefs?.auto_report !== false) {
|
|
645
736
|
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
|
-
);
|
|
737
|
+
await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
|
|
697
738
|
} catch (err) {
|
|
698
739
|
ctx.ui.notify(
|
|
699
740
|
`Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -831,20 +872,10 @@ export async function autoLoop(
|
|
|
831
872
|
}
|
|
832
873
|
|
|
833
874
|
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
875
|
const noMilestoneReason = !mid
|
|
845
876
|
? "No active milestone after merge reconciliation"
|
|
846
877
|
: `Milestone ${mid} has no title after reconciliation`;
|
|
847
|
-
await
|
|
878
|
+
await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
|
|
848
879
|
debugLog("autoLoop", {
|
|
849
880
|
phase: "exit",
|
|
850
881
|
reason: "no-milestone-after-reconciliation",
|
|
@@ -854,17 +885,7 @@ export async function autoLoop(
|
|
|
854
885
|
|
|
855
886
|
// Terminal: complete
|
|
856
887
|
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
|
|
888
|
+
// Milestone merge on complete (before closeout so branch state is clean)
|
|
868
889
|
if (s.currentMilestoneId) {
|
|
869
890
|
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
870
891
|
}
|
|
@@ -879,25 +900,15 @@ export async function autoLoop(
|
|
|
879
900
|
`Milestone ${mid} complete.`,
|
|
880
901
|
"success",
|
|
881
902
|
);
|
|
882
|
-
await
|
|
903
|
+
await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
|
|
883
904
|
debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
|
|
884
905
|
break;
|
|
885
906
|
}
|
|
886
907
|
|
|
887
908
|
// Terminal: blocked
|
|
888
909
|
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
910
|
const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
|
|
900
|
-
await
|
|
911
|
+
await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
|
|
901
912
|
ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
|
|
902
913
|
deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention");
|
|
903
914
|
deps.logCmuxEvent(deps.loadEffectiveGSDPreferences()?.preferences, blockerMsg, "error");
|
|
@@ -928,38 +939,39 @@ export async function autoLoop(
|
|
|
928
939
|
budgetPct,
|
|
929
940
|
);
|
|
930
941
|
|
|
931
|
-
|
|
932
|
-
|
|
942
|
+
// Data-driven threshold check — loop descending, fire first match
|
|
943
|
+
const threshold = BUDGET_THRESHOLDS.find(
|
|
944
|
+
(t) => newBudgetAlertLevel >= t.pct,
|
|
945
|
+
);
|
|
946
|
+
if (threshold) {
|
|
933
947
|
s.lastBudgetAlertLevel =
|
|
934
948
|
newBudgetAlertLevel as AutoSession["lastBudgetAlertLevel"];
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
)
|
|
949
|
+
|
|
950
|
+
if (threshold.pct === 100 && budgetEnforcementAction !== "none") {
|
|
951
|
+
// 100% — special enforcement logic (halt/pause/warn)
|
|
952
|
+
const msg = `Budget ceiling ${deps.formatCost(budgetCeiling)} reached (spent ${deps.formatCost(totalCost)}).`;
|
|
953
|
+
if (budgetEnforcementAction === "halt") {
|
|
954
|
+
deps.sendDesktopNotification("GSD", msg, "error", "budget");
|
|
955
|
+
await deps.stopAuto(ctx, pi, "Budget ceiling reached");
|
|
956
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-halt" });
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
if (budgetEnforcementAction === "pause") {
|
|
960
|
+
ctx.ui.notify(
|
|
961
|
+
`${msg} Pausing auto-mode — /gsd auto to override and continue.`,
|
|
962
|
+
"warning",
|
|
963
|
+
);
|
|
964
|
+
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
965
|
+
deps.logCmuxEvent(prefs, msg, "warning");
|
|
966
|
+
await deps.pauseAuto(ctx, pi);
|
|
967
|
+
debugLog("autoLoop", { phase: "exit", reason: "budget-pause" });
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
|
|
946
971
|
deps.sendDesktopNotification("GSD", msg, "warning", "budget");
|
|
947
972
|
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"];
|
|
973
|
+
} else if (threshold.pct < 100) {
|
|
974
|
+
// Sub-100% — simple notification
|
|
963
975
|
const msg = `${threshold.label}: ${deps.formatCost(totalCost)} / ${deps.formatCost(budgetCeiling)}`;
|
|
964
976
|
ctx.ui.notify(msg, threshold.notifyLevel);
|
|
965
977
|
deps.sendDesktopNotification(
|
|
@@ -969,9 +981,9 @@ export async function autoLoop(
|
|
|
969
981
|
"budget",
|
|
970
982
|
);
|
|
971
983
|
deps.logCmuxEvent(prefs, msg, threshold.cmuxLevel);
|
|
972
|
-
} else if (budgetAlertLevel === 0) {
|
|
973
|
-
s.lastBudgetAlertLevel = 0;
|
|
974
984
|
}
|
|
985
|
+
} else if (budgetAlertLevel === 0) {
|
|
986
|
+
s.lastBudgetAlertLevel = 0;
|
|
975
987
|
}
|
|
976
988
|
} else {
|
|
977
989
|
s.lastBudgetAlertLevel = 0;
|
|
@@ -1046,17 +1058,7 @@ export async function autoLoop(
|
|
|
1046
1058
|
});
|
|
1047
1059
|
|
|
1048
1060
|
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);
|
|
1061
|
+
await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
|
|
1060
1062
|
debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
|
|
1061
1063
|
break;
|
|
1062
1064
|
}
|
|
@@ -1215,12 +1217,12 @@ export async function autoLoop(
|
|
|
1215
1217
|
);
|
|
1216
1218
|
|
|
1217
1219
|
if (s.currentUnitRouting) {
|
|
1218
|
-
const
|
|
1220
|
+
const isRetryForOutcome =
|
|
1219
1221
|
s.currentUnit.type === unitType && s.currentUnit.id === unitId;
|
|
1220
1222
|
deps.recordOutcome(
|
|
1221
1223
|
s.currentUnit.type,
|
|
1222
1224
|
s.currentUnitRouting.tier as "light" | "standard" | "heavy",
|
|
1223
|
-
!
|
|
1225
|
+
!isRetryForOutcome,
|
|
1224
1226
|
);
|
|
1225
1227
|
}
|
|
1226
1228
|
|
|
@@ -1415,7 +1417,6 @@ export async function autoLoop(
|
|
|
1415
1417
|
unitType,
|
|
1416
1418
|
unitId,
|
|
1417
1419
|
finalPrompt,
|
|
1418
|
-
prefs,
|
|
1419
1420
|
);
|
|
1420
1421
|
debugLog("autoLoop", {
|
|
1421
1422
|
phase: "runUnit-end",
|
|
@@ -1587,7 +1588,6 @@ export async function autoLoop(
|
|
|
1587
1588
|
item.unitType,
|
|
1588
1589
|
item.unitId,
|
|
1589
1590
|
item.prompt,
|
|
1590
|
-
prefs,
|
|
1591
1591
|
);
|
|
1592
1592
|
deps.clearUnitTimeout();
|
|
1593
1593
|
|
|
@@ -104,7 +104,6 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
|
|
|
104
104
|
"task",
|
|
105
105
|
"T01",
|
|
106
106
|
"do stuff",
|
|
107
|
-
undefined,
|
|
108
107
|
);
|
|
109
108
|
|
|
110
109
|
// Give the microtask queue a tick so runUnit reaches the await
|
|
@@ -136,7 +135,7 @@ test("double resolveAgentEnd only resolves once (second is dropped)", async () =
|
|
|
136
135
|
const event1 = makeEvent([{ id: 1 }]);
|
|
137
136
|
const event2 = makeEvent([{ id: 2 }]);
|
|
138
137
|
|
|
139
|
-
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
138
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
140
139
|
|
|
141
140
|
await new Promise((r) => setTimeout(r, 10));
|
|
142
141
|
|
|
@@ -161,7 +160,7 @@ test("runUnit returns cancelled when session creation fails", async () => {
|
|
|
161
160
|
const pi = makeMockPi();
|
|
162
161
|
const s = makeMockSession({ newSessionThrows: "connection refused" });
|
|
163
162
|
|
|
164
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
163
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
165
164
|
|
|
166
165
|
assert.equal(result.status, "cancelled");
|
|
167
166
|
assert.equal(result.event, undefined);
|
|
@@ -177,7 +176,7 @@ test("runUnit returns cancelled when session creation times out", async () => {
|
|
|
177
176
|
// Session returns cancelled: true (simulates the timeout race outcome)
|
|
178
177
|
const s = makeMockSession({ newSessionResult: { cancelled: true } });
|
|
179
178
|
|
|
180
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
179
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
181
180
|
|
|
182
181
|
assert.equal(result.status, "cancelled");
|
|
183
182
|
assert.equal(result.event, undefined);
|
|
@@ -192,7 +191,7 @@ test("runUnit returns cancelled when s.active is false before sendMessage", asyn
|
|
|
192
191
|
const s = makeMockSession();
|
|
193
192
|
s.active = false;
|
|
194
193
|
|
|
195
|
-
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
194
|
+
const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
196
195
|
|
|
197
196
|
assert.equal(result.status, "cancelled");
|
|
198
197
|
assert.equal(pi.calls.length, 0);
|
|
@@ -212,7 +211,7 @@ test("runUnit only arms resolve after newSession completes", async () => {
|
|
|
212
211
|
},
|
|
213
212
|
});
|
|
214
213
|
|
|
215
|
-
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt"
|
|
214
|
+
const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
|
|
216
215
|
|
|
217
216
|
await new Promise((r) => setTimeout(r, 30));
|
|
218
217
|
|