fix(gsd): remove stale completedUnits refs, fix writeLock callers, add missing imports

- Remove completedUnits from dashboard, context, parallel, guided-flow, merge
- Fix writeLock callers to match new (basePath, unitType, unitId, sessionFile?) signature
- Add gsdRoot, atomicWriteSync, verifyExpectedArtifact, writeUnitRuntimeRecord imports to phases.ts
- Add full_plan_md to workflow-manifest snapshot mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson 2026-03-25 08:37:32 -06:00
parent 3a12089355
commit 63dea156c3
9 changed files with 12 additions and 70 deletions

View file

@ -624,7 +624,7 @@ export async function bootstrapAutoSession(
"starting",
s.currentMilestoneId ?? "unknown",
);
writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown", 0);
writeLock(lockBase(), "starting", s.currentMilestoneId ?? "unknown");
// Secrets collection gate
const mid = state.activeMilestone!.id;

View file

@ -322,7 +322,6 @@ export function getAutoDashboardData(): AutoDashboardData {
? (s.autoStartTime > 0 ? Date.now() - s.autoStartTime : 0)
: 0,
currentUnit: s.currentUnit ? { ...s.currentUnit } : null,
completedUnits: [],
basePath: s.basePath,
totalCost: totals?.cost ?? 0,
totalTokens: totals?.tokens.total ?? 0,
@ -1169,7 +1168,6 @@ export async function startAuto(
lockBase(),
"resuming",
s.currentMilestoneId ?? "unknown",
0,
);
logCmuxEvent(loadEffectiveGSDPreferences()?.preferences, s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "progress");
@ -1391,7 +1389,6 @@ export async function dispatchHookUnit(
lockBase(),
hookUnitType,
triggerUnitId,
0,
sessionFile,
);

View file

@ -29,6 +29,10 @@ import { MergeConflictError } from "../git-service.js";
import { join } from "node:path";
import { existsSync, cpSync } from "node:fs";
import { logWarning, logError } from "../workflow-logger.js";
import { gsdRoot } from "../paths.js";
import { atomicWriteSync } from "../atomic-write.js";
import { verifyExpectedArtifact } from "../auto-recovery.js";
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
// ─── generateMilestoneReport ──────────────────────────────────────────────────
@ -275,11 +279,7 @@ export async function runPreDispatch(
.map((m: { id: string }) => m.id);
deps.pruneQueueOrder(s.basePath, pendingIds);
// Reset completed-units tracking for the new milestone — stale entries
// from the previous milestone cause the dispatch loop to skip units
// that haven't actually been completed in the new milestone's context.
// Archive the old completed-units.json instead of wiping it (#2313).
s.completedUnits = [];
try {
const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
if (existsSync(completedKeysPath) && s.currentMilestoneId) {
@ -538,7 +538,7 @@ export async function runDispatch(
if (loopState.stuckRecoveryAttempts === 0) {
// Level 1: try verifying the artifact, then cache invalidation + retry
loopState.stuckRecoveryAttempts++;
const artifactExists = deps.verifyExpectedArtifact(
const artifactExists = verifyExpectedArtifact(
unitType,
unitId,
s.basePath,
@ -847,7 +847,7 @@ export async function runUnitPhase(
const unitStartSeq = ic.nextSeq();
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: unitStartSeq, eventType: "unit-start", data: { unitType, unitId } });
deps.captureAvailableSkills();
deps.writeUnitRuntimeRecord(
writeUnitRuntimeRecord(
s.basePath,
unitType,
unitId,
@ -1116,7 +1116,7 @@ export async function runUnitPhase(
const skipArtifactVerification = unitType.startsWith("hook/") || unitType === "custom-step";
const artifactVerified =
skipArtifactVerification ||
deps.verifyExpectedArtifact(unitType, unitId, s.basePath);
verifyExpectedArtifact(unitType, unitId, s.basePath);
if (artifactVerified) {
s.unitDispatchCount.delete(`${unitType}/${unitId}`);
s.unitRecoveryCount.delete(`${unitType}/${unitId}`);

View file

@ -47,15 +47,10 @@ export async function guardRemoteSession(
return false;
}
const unitsMsg = remote.completedUnits != null
? `${remote.completedUnits} units completed`
: "";
const choice = await showNextAction(ctx, {
title: `Auto-mode is running in another terminal (PID ${remote.pid})`,
summary: [
`Currently executing: ${unitLabel}`,
...(unitsMsg ? [unitsMsg] : []),
...(remote.startedAt ? [`Started: ${remote.startedAt}`] : []),
],
actions: [

View file

@ -63,7 +63,7 @@ export async function handleParallelCommand(trimmed: string, _ctx: ExtensionComm
}
const lines = ["# Parallel Workers\n"];
for (const worker of workers) {
lines.push(`- **${worker.milestoneId}** (${worker.title}) — ${worker.state}${worker.completedUnits} units — $${worker.cost.toFixed(2)}`);
lines.push(`- **${worker.milestoneId}** (${worker.title}) — ${worker.state}$${worker.cost.toFixed(2)}`);
}
const state = getOrchestratorState();
if (state) {

View file

@ -99,18 +99,11 @@ export class GSDDashboardOverlay {
const currentUnit = dashData.currentUnit
? `${dashData.currentUnit.type}:${dashData.currentUnit.id}:${dashData.currentUnit.startedAt}`
: "-";
const lastCompleted = dashData.completedUnits.length > 0
? dashData.completedUnits[dashData.completedUnits.length - 1]
: null;
const completedKey = lastCompleted
? `${dashData.completedUnits.length}:${lastCompleted.type}:${lastCompleted.id}:${lastCompleted.finishedAt}`
: "0";
return [
base,
dashData.active ? "1" : "0",
dashData.paused ? "1" : "0",
currentUnit,
completedKey,
].join("|");
}
@ -458,49 +451,6 @@ export class GSDDashboardOverlay {
lines.push(centered(th.fg("dim", "No active milestone.")));
}
if (this.dashData.completedUnits.length > 0) {
lines.push(blank());
lines.push(hr());
lines.push(row(th.fg("text", th.bold("Completed"))));
lines.push(blank());
// Build ledger lookup for budget indicators (last entry wins for retries)
const ledgerLookup = new Map<string, UnitMetrics>();
const currentLedger = getLedger();
if (currentLedger) {
for (const lu of currentLedger.units) {
ledgerLookup.set(`${lu.type}:${lu.id}`, lu);
}
}
const recent = [...this.dashData.completedUnits].reverse().slice(0, 10);
for (const u of recent) {
// Budget indicators from ledger — use warning glyph for pressured units
const ledgerEntry = ledgerLookup.get(`${u.type}:${u.id}`);
const hadPressure = ledgerEntry?.continueHereFired === true;
const hadTruncation = (ledgerEntry?.truncationSections ?? 0) > 0;
const unitGlyph = hadPressure
? th.fg(STATUS_COLOR.warning, STATUS_GLYPH.warning)
: th.fg(STATUS_COLOR.done, STATUS_GLYPH.done);
const left = ` ${unitGlyph} ${th.fg("muted", unitLabel(u.type))} ${th.fg("muted", u.id)}`;
let budgetMarkers = "";
if (hadTruncation) {
budgetMarkers += th.fg("warning", `${ledgerEntry!.truncationSections}`);
}
if (hadPressure) {
budgetMarkers += th.fg("error", " → wrap-up");
}
const right = th.fg("dim", formatDuration(u.finishedAt - u.startedAt));
lines.push(row(joinColumns(`${left}${budgetMarkers}`, right, contentWidth)));
}
if (this.dashData.completedUnits.length > 10) {
lines.push(row(th.fg("dim", ` ...and ${this.dashData.completedUnits.length - 10} more`)));
}
}
const ledger = getLedger();
if (ledger && ledger.units.length > 0) {
const totals = getProjectTotals(ledger.units);

View file

@ -910,8 +910,7 @@ export async function showSmartEntry(
// when the user exits during init wizard or discuss phase before any
// real auto-mode work begins.
const isBootstrapCrash = crashLock.unitType === "starting"
&& crashLock.unitId === "bootstrap"
&& crashLock.completedUnits === 0;
&& crashLock.unitId === "bootstrap";
if (!isBootstrapCrash) {
const resume = await showNextAction(ctx, {

View file

@ -37,7 +37,7 @@ export function determineMergeOrder(
workers: WorkerInfo[],
order: MergeOrder = "sequential",
): string[] {
const completed = workers.filter(w => w.state === "stopped" && w.completedUnits > 0);
const completed = workers.filter(w => w.state === "stopped");
if (order === "by-completion") {
return completed
.sort((a, b) => a.startedAt - b.startedAt) // earliest first

View file

@ -128,6 +128,7 @@ export function snapshotState(): StateManifest {
inputs: JSON.parse((r["inputs"] as string) || "[]"),
expected_output: JSON.parse((r["expected_output"] as string) || "[]"),
observability_impact: (r["observability_impact"] as string) ?? "",
full_plan_md: (r["full_plan_md"] as string) ?? "",
sequence: (r["sequence"] as number) ?? 0,
}));