You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(runner): an inapplicable cap is not an unsafe one (#185)
* fix(runner): an inapplicable cap is not an unsafe one
Codex preflight refused `agent:` and `budget_usd` with the same sentence,
and they are not the same kind of thing. `agent:` names a Claude Code
subagent — without it the node runs without that agent's system prompt,
so it is a different node, and refusing stays correct. `budget_usd` is a
USD ceiling over a runtime that reports no USD: there is no quantity to
bound, which makes it inapplicable rather than unsafe.
ValidateGraphForRuntime now returns warnings alongside its error. A
budgeted node under codex loads and warns that the cap cannot apply,
naming the guard still in force — its own `timeout:` or the runner's
default — quoted from the constant the CLIRunner applies. The runaway
guard was never the budget: graphs/fragments/e2e-verify.yaml says so
beside its own `budget_usd: 10.00` ("The hang guard is `timeout:`, not
the budget").
Measured, `--runtime codex lint graphs/*.yaml`: five of eight graphs
refused before, one after — adr-driven-dev, for `agent:`.
`auto --max-goal-budget-usd` stays refused, deliberately: a goal ceiling
is the ONLY bound on an iterating loop, so an unmeasurable one would stop
at its first cycle boundary with StopBudgetUnmeasurable having bought a
cycle to learn what preflight says for free.
All five call sites surface the warnings through one helper; `run` alone
discards executeGraph's second copy, having already printed the list in
its pre-run disclosure. A nil writer there means stderr, never silence.
shipped_graphs_runtime_test.go lints every embedded graph under both
runtimes and asserts the verdict per named graph, with its reason, so a
graph that becomes unloadable under Codex fails `make test` by name.
The Claude path does not move: ValidateGraphForRuntime still returns on
its first line for RuntimeClaude. --help, version, lint and --dry-run
over all eight shipped graphs are byte-identical to HEAD's binary.
Refs: ADR 0026, amending ADR 0025.
Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
Signed-off-by: jitokim <pigberger70@gmail.com>
* fix(runner): the goal ceiling is not the only bound, and say so
ADR 0026 justified refusing `auto --max-goal-budget-usd` on a premise the
code denies: that a goal ceiling is the ONLY bound on an iterating loop.
flags.go refuses the flag unless --max-cycles >= 2, and its own comment
says the cycle flag IS the bound with no unbounded spelling (ADR 0011 §1).
Drop the ceiling and the loop is still hard-bounded — the exact parallel
to a node keeping its `timeout:` that the record spends a section denying.
The sound argument was already there and is verifiable: goal.go checks the
ceiling only inside its `cycle > 1` block, so accepting an unmeasurable one
buys a whole cycle before StopBudgetUnmeasurable says what preflight says
for free, while an inapplicable node cap costs nothing extra. That is now
the reason in all six places the claim appears — preflight.go's doc,
noteCodexRuntimePolicy, ADR 0026 §2, CHANGELOG, LIMITATIONS, EXAMPLES —
with "only bound" demoted to "only spend-shaped bound".
README.ko.md still said all three declarations are refused before
execution; the English paragraph was rewritten in 773ba51 and its mirror
was not. Ported. This mirror has drifted twice before (CHANGELOG:487,
:1336).
Three new tests in cmd/oh-my-graph/runtime_test.go pin the claim that made
the split worth making — that every call site PRINTS. `lint` and
`--dry-run` are pinned at their spawn-free sites, and `run` is pinned to
print the list EXACTLY once counted across stdout and stderr together, so
the fix for a double print cannot be to move one copy where the test is
not looking. Mutation-checked: `_ = runtimeWarnings` at either spawn-free
site, or dropping `run`'s io.Discard, turns each red; before this they
stayed green.
shipped_graphs_runtime_test.go counts a graph's warnings instead of
asserting presence, because ADR 0026 §1's table records counts: stripping
one of review-loop's two caps was green under the bool and now fails by
name with both numbers.
`run` also passes flags.graphPath rather than "", so its warning carries
the same `<path>: ` prefix as the other four sites, and warnRuntimePreflight
now documents why `run` alone routes to stdout — the disclosure block below
refers back to those lines, and a reference split from its referent across
two streams reads worse than the inconsistency. The 2026-08-14 plan record
gets a banner for the one item ADR 0026 reversed.
The Claude path still does not move: lint and --dry-run over all eight
shipped graphs, --help, bare `run` and bare `auto` are byte-identical in
stdout, stderr and exit to 773ba51's binary.
Refs: ADR 0026.
Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
Signed-off-by: jitokim <pigberger70@gmail.com>
---------
Signed-off-by: jitokim <pigberger70@gmail.com>
fmt.Fprintln(w, " Last node: adr-driven-dev (finalize), and every user of graphs/fragments/pr-publish.yaml (self-dev, dev-review-pr, backlog-batch). First node: apply-flags (dev pushes before verify reads). Every node: merge-shepherd, which is `gh` end to end and fails at node 1 having done nothing.")
1086
1104
fmt.Fprintln(w, " Two remedies, both per node: permission_mode: bypassPermissions maps to danger-full-access, which is no sandbox — that node keeps network AND keyring. Or Codex's sandbox_workspace_write.network_access=true, which lifts the block for `git push`/`git ls-remote` but not for `gh` on a machine where gh's token is in an OS keyring the sandbox denies (measured 2026-08-14, macOS: \"no oauth token found for github.com\"; where no keyring exists gh reads ~/.config/gh/hosts.yml, which the sandbox can read).")
1087
1105
fmt.Fprintln(w, " Cost is unknown for every Codex node: tokens are counted, USD never is, so this run reports no dollar figure per node or in total.")
1106
+
fmt.Fprintln(w, " A node's budget_usd therefore loads but cannot apply — there is no spend to compare it against, and that node's runaway guard is its timeout: (each such node is warned by name). `auto --max-goal-budget-usd` is refused instead, because it is checked only at a cycle boundary: an unmeasurable ceiling would buy a whole cycle before stopping to say it cannot be checked, where an inapplicable node cap costs nothing extra. The loop stays bounded either way — --max-cycles is what bounds iterations (ADR 0026).")
1088
1107
fmt.Fprintln(w, " approval_policy=\"never\" is passed on every node: a non-interactive run cannot answer a prompt, so nothing is escalated for approval.")
1089
1108
fmt.Fprintln(w, " No session-limit pause: ADR 0009's resumable pause is Claude-only, so a Codex session limit is an ordinary node failure (ADR 0009 scopes it to the Claude runtime).")
0 commit comments