v0.8.0
This release's headline is not ours. --runtime codex was contributed by
@minkichoe-lbox in
#170 — the first outside
contribution this project has received, 117 files, and it kept every
load-bearing invariant intact.
A second node runtime (#170)
oh-my-graph --runtime codex run graph.yaml
Claude remains the default; one runtime per run. A single CLIRunner owns both
protocols, runtime identity is persisted so resume and browser gate actions
know what they are resuming, Codex sandbox modes map from graph permission
modes, and Codex thread ids carry handoff: session. USD cost is explicitly
unknown rather than zero, with provider token usage preserved through the
ledger, the snapshot, the feed contract and the web UI. state.json and
events.jsonl move to schema 3, so an older binary refuses to misread
unknown cost or runtime identity instead of guessing.
The thing most likely to have broken did not. Adding a second provider had
exactly one quiet failure mode: a runtime branch inside the env scrub, where the
half that fell behind would bill silently with nothing failing to say so.
childenv.Scrub still takes no runtime, still has no branch, and all four exec
seams still call it unconditionally — so a Claude node drops the OpenAI switches
and a Codex node drops the Anthropic ones. Three reviewers checked that
independently; none found a seam.
The Codex disclosure names what you would otherwise meet after paying (#174)
Before a Codex run spends on a node, it now prints four differences alongside
the sandbox mapping:
- No network.
gh,git pushandgit ls-remotefail, so a graph halts at
the first node that publishes — and the disclosure says where that node
sits, because it is not always the last one. Last inadr-driven-devand
every user ofgraphs/fragments/pr-publish.yaml; first inapply-flags,
which pushes fromdevand ends on a read-onlyverify; every node in
merge-shepherd, which isghend to end and so fails at node 1 having done
nothing. Two per-node ways out are named with what each costs. - USD cost is unknown for every Codex node, not merely unbudgetable.
approval_policy="never"is passed unconditionally.- ADR 0009's session-limit pause does not exist on Codex
(#171).
Writing that down truthfully cost us two corrections to claims we had made
about our own graphs. If you have been reading LIMITATIONS, it was wrong about
merge-shepherd and silent about apply-flags; it no longer is.
A budget is never measured against a cost the loop does not have (#173)
--max-goal-budget-usd compared known spend alone, so unknown spend counted as
$0 and a capped goal loop could iterate under a ceiling it could no longer
measure. ADR 0025 states that as a property of the system, and the CLI refusing
the runtime/budget combination up front is not the same thing — a cost can go
unknown at runtime too, from a node killed before it reported or a garbled
envelope.
The ordering keeps the honest half: known spend is a floor on true spend, so
"the known part already reaches the ceiling" stays sound however much went
unreported and still stops with StopBudgetExceeded. Only when the known part
is under the ceiling does the unknown decide, and there the loop stops with the
new StopBudgetUnmeasurable — a stop reason, not an error, so it prints
the remaining: line every clean stop prints and no summary bills a cycle that
never began.
Known blast radius, stated rather than discovered: one ordinary 20-minute
node timeout also sets CostUnknown, so a single timed-out node can end a
budgeted loop that ADR 0011 §2 would otherwise keep iterating.
A node's stderr is bounded again (#173)
Collecting stderr by hand had replaced cmd.Output()'s stdlib cap with an
unbounded buffer — paid by the Claude path too — while the only consumer
reads 500 bytes of it. Restored as a 32 KiB tail: the tail, because every
consumer reads through tailOf and a CLI's fatal line is its last one; 32 KiB,
because that is the ceiling prefixSuffixSaver kept rather than a tighter
number invented here.
make local passes on macOS again (#172)
The new timing tests failed on every mac while CI stayed green. macOS charges a
security scan on the first exec of a newly written file, per file, so a stub
written into a fresh t.TempDir() paid it every run: 384–1607 ms, against
6–15 ms to re-run the same file. Both tests raced that against a wall clock and
lost before the code under test was involved.
Fixed by warming the stub outside the timed window rather than by widening a
deadline. With a cost spanning 4x, no constant is both tight enough to test the
property and loose enough to stay green — a bigger number would have turned a
red suite into a flaky one, which is worse.
Also
- The README is a front page again — 904 lines to 223
(#169). lintandrun --dry-runwarn when asuccess_check.verify.commandsplices
a model's own reply into the shell command line the engine runs
(#168). Measured before
shipping: zero hits across 34 verify blocks in this repo's graphs plus a
20-graph operator corpus.
Version rule
Minor, checked by name rather than by diff line: the seventeen long flags in
flags.go are unchanged, the eleven subcommands are the same eleven, and the
global flags gained exactly one — --runtime. Nothing renamed, nothing removed.
Full detail in CHANGELOG.md.