From a54a92ace4ddec2480604e0cf5f85a99b328af6c Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 25 Aug 2026 19:50:22 -0400 Subject: [PATCH] fix(pi-workflow): reset failed verification on retry --- docs/pi-workflow/README.md | 2 +- docs/pi-workflow/design.md | 1 + extensions/pi-workflow/engine.test.ts | 13 +++++++++++++ extensions/pi-workflow/engine.ts | 19 +++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/pi-workflow/README.md b/docs/pi-workflow/README.md index 6cb50ae..09de2a8 100644 --- a/docs/pi-workflow/README.md +++ b/docs/pi-workflow/README.md @@ -70,7 +70,7 @@ An analysis run uses `action: "start"` with `workflow: { mode: "analysis-script" ## Recovery -Interrupted work opens a Recovery Gate with retry, inspect, and abort choices. Completed Assignments and Decisions remain persisted. Potentially mutating interrupted Assignments are preserved and are not replayed automatically; an explicit retry creates newly fenced work where policy permits it. Startup recovery leaves an idle run already waiting at Recovery under its current parent instead of letting another Pi session claim it. If that parent's process has exited, an explicit retry from a previously attached parent session may reclaim the stale lease before continuing. +Interrupted work opens a Recovery Gate with retry, inspect, and abort choices. Completed Assignments and Decisions remain persisted. Potentially mutating interrupted Assignments are preserved and are not replayed automatically; an explicit retry creates newly fenced work where policy permits it. Startup recovery leaves an idle run already waiting at Recovery under its current parent instead of letting another Pi session claim it. If that parent's process has exited, an explicit retry from a previously attached parent session may reclaim the stale lease before continuing. Retry clears prior failed-command output and reruns verification from a pending state. ## Artifacts diff --git a/docs/pi-workflow/design.md b/docs/pi-workflow/design.md index 6d847d8..08dba33 100644 --- a/docs/pi-workflow/design.md +++ b/docs/pi-workflow/design.md @@ -519,6 +519,7 @@ On process or session interruption: - Active Assignments become `interrupted` idempotently. - Potentially mutating Assignments are never replayed automatically. - The run opens a Recovery Gate offering retry, inspect, or abort. +- Explicit retry resets failed verification commands to pending and clears their prior output before host execution. - Completed Assignments and accepted Decisions remain intact. ## Child Execution diff --git a/extensions/pi-workflow/engine.test.ts b/extensions/pi-workflow/engine.test.ts index ef83c4b..13ac6dd 100644 --- a/extensions/pi-workflow/engine.test.ts +++ b/extensions/pi-workflow/engine.test.ts @@ -1512,9 +1512,20 @@ test("Recovery retry reruns failed verification after the remediation limit", as await withTempDir(async (directory) => { const setup = await fixture(directory); let commandRuns = 0; + let runId: string | undefined; const hostCommands: HostCommandExecutor = { async run() { commandRuns++; + if (commandRuns === 2) { + assert.ok(runId); + const retrying = await setup.store.loadRun(runId); + const [command] = retrying.verificationEvidence?.commands ?? []; + assert.equal(command?.status, "running"); + assert.equal(command?.stdout, undefined); + assert.equal(command?.stderr, undefined); + assert.equal(command?.exitCode, undefined); + assert.equal(command?.error, undefined); + } return { result: { code: commandRuns === 1 ? 1 : 0, @@ -1552,6 +1563,7 @@ test("Recovery retry reruns failed verification after the remediation limit", as hostCommands, effectiveConfig, }).execute(startCommand()); + runId = planned.runId; const approved = await setup.engine("session-1", { hostCommands, effectiveConfig, @@ -1598,6 +1610,7 @@ test("Recovery retry reruns failed verification after the remediation limit", as const liveStolen = structuredClone(untouched); liveStolen.revision++; liveStolen.parentSessionIds.push("transient-session"); + liveStolen.verificationEvidence!.commands[0]!.error = "stale host exception"; liveStolen.lease = { processId: 200, activeParentSessionId: "transient-session", diff --git a/extensions/pi-workflow/engine.ts b/extensions/pi-workflow/engine.ts index a6d8d43..09e2cd9 100644 --- a/extensions/pi-workflow/engine.ts +++ b/extensions/pi-workflow/engine.ts @@ -4279,6 +4279,10 @@ export class WorkflowEngine { run.verificationEvidence?.commands.filter( (command) => command.status === "running" || command.status === "interrupted", ) ?? []; + const failedCommands = + run.verificationEvidence?.commands.filter( + (command) => command.status === "failed", + ) ?? []; run = await this.transition(run, (next) => { for (const assignment of next.assignments) { if ( @@ -4309,6 +4313,21 @@ export class WorkflowEngine { next.script.error = undefined; } if (next.verificationEvidence) { + for (const command of failedCommands) { + const persisted = next.verificationEvidence.commands.find( + (candidate) => candidate.id === command.id, + ); + if (!persisted) continue; + persisted.status = "pending"; + persisted.exitCode = undefined; + persisted.stdout = undefined; + persisted.stderr = undefined; + persisted.error = undefined; + } + if (failedCommands.length > 0) { + next.verificationEvidence.focusedBehavioralTests = false; + next.verificationEvidence.verifierSucceeded = false; + } for (const command of interruptedCommands) { const persisted = next.verificationEvidence.commands.find( (candidate) => candidate.id === command.id,