From acf87059b54d671f78610d1b9724e5d649cc5324 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 4 Sep 2026 18:32:45 +0800 Subject: [PATCH] fix: runAct treats nonzero-exit JSON as a failed act, like runDecide and runProve Bug review of EauDoon/agent-action-stack found: - MEDIUM: runAct (bin/aas.mjs) short-circuits to childProcessError when result.status !== 0, before parseStageJson is reached. Any JSON crctl wrote to stdout is dropped. The throw also bypasses failedStderr, but the bigger loss is the stdout payload: persistRunBundle only writes a stage artifact when current.raw !== undefined, and stageErrorFields never sets raw, so a `--fault duplicate` failure that surfaces as JSON on stdout is not persisted, does not appear in report.stages.act, and printHuman shows only act_code / act_reason / act_stderr. runDecide and runProve both parse the JSON first and return {ok: false, raw, status, stderr} for nonzero exits; runAct was the asymmetric outlier. Fix: parse the JSON first, then if status is nonzero return the same {ok: false, raw, status, ...failedStderr(result)} shape that runDecide and runProve use. Throw childProcessError only when the spawn itself failed (result.error) or when the child exited nonzero with no parseable JSON. Verified: node --test test/stack.test.mjs runs 40 tests, all pass. --- bin/aas.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/aas.mjs b/bin/aas.mjs index d9eb79b..d6c1230 100644 --- a/bin/aas.mjs +++ b/bin/aas.mjs @@ -520,8 +520,15 @@ export function runAct( const result = runner(process.execPath, [crctl, ...args], { cwd: join(depsDir, "consequence-rail"), }); - if (result.error || result.status !== 0) throw childProcessError("act", result); + if (result.error) throw childProcessError("act", result); const payload = parseStageJson("act", result); + if (result.status !== 0) { + // A nonzero exit with parseable JSON is an unsuccessful act (the CLI + // surfaces structured errors as JSON on stdout), not a child-process + // error. Mirror runProve so persistRunBundle and printHuman see the + // structured failure and the GUI can render the stage artifact. + return { ok: false, raw: payload, status: result.status, ...failedStderr(result) }; + } if (!payload || typeof payload !== "object" || Array.isArray(payload) || ![null, "settled", "compensated", "disputed"].includes(payload.outcome)) { throw attachChildDiagnostics(new Error("act did not return a valid outcome"), {