Context
v2 gates every deterministic step on exit code with no opt-out. v1 had failOnError: false, and it existed for one dominant pattern: run a check, let it be red, and hand its output to the agent built to answer it. That pattern — repair before failure — is the whole shape of a validated implementation flow, and in v2 every author has to re-invent a workaround for it.
Found while porting AgentWorkforce/relay's 80-to-100 validation workflows to v2. The repo's own v1-to-v2 bridge documents the workaround in flows/spec-builder.ts:
| v1 | v2 |
| `failOnError: false` | `<command> || true` |
with this comment, which is the tell:
The group braces are load-bearing: || cannot begin a line, so appending "\n|| true" to a multi-line command is a shell syntax error rather than a fallback.
flows/diagnose/orchestration.spec.ts carries the same workaround under the name advisoryGate.
Why || true is not good enough
It discards the exit code. A later gate then has nothing to read, so the flow has to re-run the command to learn what happened, or the author builds an evidence journal outside the kernel. Authoring the relay native-delivery campaign I ended up writing exactly that: a recorder script that runs each command, writes {command, exitCode, verdict, tail} to disk, and always exits 0, plus a require-green gate that reads those files back. That is the kernel's job — every one of those facts is already journal-shaped.
There is a second, worse failure mode: || true is indistinguishable from success, so a flow that forgets one gate ships red work silently.
What to change
Give the deterministic step a first-class non-fatal mode. The shape that fits v2's journal-first design:
- id: run-tests
type: deterministic
command: npm test
onNonZero: record # 'fail' (default) | 'record'
record journals the exit code and output as a step outcome, marks the step complete-but-red, and lets dependents run. A later gate reads the recorded outcome — { type: 'steps_green', ids: [...] } or equivalent — rather than re-running the command.
In TypeScript, the natural form is a result object instead of a throw:
const tests = await f.run('npm test', { onNonZero: 'record' });
if (!tests.ok) await f.agent('fixer', { task: `Fix these failures:\n${tests.output}` });
Acceptance
- A deterministic step can be red without failing the run, and its exit code and output are readable by a later step from the journal.
- A gate can assert "these recorded steps were all green" without re-running them.
|| true is no longer the documented answer; the repair-before-failure pattern appears in the docs as a supported shape.
Out of scope
- Retry policy (
maxIterations).
- Agent-step failure semantics.
Context
v2 gates every
deterministicstep on exit code with no opt-out. v1 hadfailOnError: false, and it existed for one dominant pattern: run a check, let it be red, and hand its output to the agent built to answer it. That pattern — repair before failure — is the whole shape of a validated implementation flow, and in v2 every author has to re-invent a workaround for it.Found while porting AgentWorkforce/relay's 80-to-100 validation workflows to v2. The repo's own v1-to-v2 bridge documents the workaround in
flows/spec-builder.ts:with this comment, which is the tell:
flows/diagnose/orchestration.spec.tscarries the same workaround under the nameadvisoryGate.Why
|| trueis not good enoughIt discards the exit code. A later gate then has nothing to read, so the flow has to re-run the command to learn what happened, or the author builds an evidence journal outside the kernel. Authoring the relay native-delivery campaign I ended up writing exactly that: a recorder script that runs each command, writes
{command, exitCode, verdict, tail}to disk, and always exits 0, plus arequire-greengate that reads those files back. That is the kernel's job — every one of those facts is already journal-shaped.There is a second, worse failure mode:
|| trueis indistinguishable from success, so a flow that forgets one gate ships red work silently.What to change
Give the deterministic step a first-class non-fatal mode. The shape that fits v2's journal-first design:
recordjournals the exit code and output as a step outcome, marks the step complete-but-red, and lets dependents run. A later gate reads the recorded outcome —{ type: 'steps_green', ids: [...] }or equivalent — rather than re-running the command.In TypeScript, the natural form is a result object instead of a throw:
Acceptance
|| trueis no longer the documented answer; the repair-before-failure pattern appears in the docs as a supported shape.Out of scope
maxIterations).