Skip to content

feat(runner): a planned node answers with the model the operator chose - #248

Open
jitokim wants to merge 10 commits into
mainfrom
lane-model
Open

feat(runner): a planned node answers with the model the operator chose#248
jitokim wants to merge 10 commits into
mainfrom
lane-model

Conversation

@jitokim

@jitokim jitokim commented Aug 24, 2026

Copy link
Copy Markdown
Owner

The maintainer's decision: inherit the user's setting. A planned node was answering with whatever the CLI defaults to, because --setting-sources "" drops the file the operator's model lives in — and oh-my-graph never passed --model at all.

The argument this rests on

The auto ceiling bounds capability: which tools a node holds, which files load, whose hooks run. Model choice grants none of those. Inheriting it does not weaken the ceiling; it separates two things that were only ever coupled by the bluntness of --setting-sources "".

The tension it had to resolve

Reading ~/.claude/settings.json means parsing a file another product owns — the same shape ADR 0009 refused for the CLI's prose reset time, and the #218 measurement refused for transcripts. The brief required an answer for all four failure cases (key absent, file absent, malformed, unknown value) with one rule fixed in advance:

A wrong model name must not become a silent fallback — answering with a different model than the operator chose is the defect under repair, and doing it in the fix would be worse than the bug.

Status

10 commits, make local green. Goal not met — read the run's own remaining note before merging; it is on the branch and this PR is not claiming it is finished work.

🤖 Generated with Claude Code

jitokim and others added 10 commits August 24, 2026 10:23
…operator chose

`--setting-sources ""` withholds the whole of ~/.claude/settings.json from a
planned node, because withholding the document is the only lever the CLI
offers. The operator's model preference happens to live in that document, so a
planned node answers with whatever the CLI defaults to — measured across this
repository's corpus, 181 of 187 planned nodes ran a model nobody selected
(docs/measurements/0034-planned-node-model.go).

internal/usermodel reads exactly one key, `model`, out of
$CLAUDE_CONFIG_DIR/settings.json (else ~/.claude/settings.json), into a struct
with exactly one field, and returns it verbatim. No allowlist: an allowlist
goes stale with the CLI's release cadence and would then substitute a default
for a name the operator really chose, which is the defect under repair.

Nothing about the ceiling moves. It bounds CAPABILITY — which tools a node
holds, which files load into it, whose hooks run — and a model name grants none
of those; it reaches argv, not a prompt, and cannot come from planner output
(there is no `model` key in the graph schema). The package doc carries that
argument, and the terms on which we parse a file another product owns: a
renamed key or a changed type degrades to no flag, never to a guess.

The five cases are the tests: key present (verbatim, "[1m]" suffix intact), key
absent, blank value (absent — the CLI rejects an empty --model), file absent
(not an error), malformed and unreadable (one error naming the path, never the
contents — that file also holds credentials). Temp dirs throughout, never the
real home.

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>

Signed-off-by: jitokim <pigberger70@gmail.com>
…an agent

NodeInvocation gains `Model string`, and claudeProtocol.buildArgs emits
`--model <value>` immediately after --permission-mode and before
--max-budget-usd — neither of those is a ceiling flag, so both sit ahead of the
ones that are. Emitted verbatim: the vocabulary belongs to the CLI, an unknown
name is a node failure carrying the CLI's own "Unknown model" stderr, and there
is no allowlist to go stale and substitute a default for a name the operator
really chose.

Two suppressions, both deliberate:

  - empty Model emits nothing, so a node that expressed no choice keeps a
    byte-identical argv and the CLI's own default. That is every hand-written
    node and every machine with no settings key.
  - an agent-mapped node gets no --model at all. `--agent` supplies a model
    from the definition's frontmatter — 6 of the 187 planned nodes measured for
    this change take that route — and the CLI tracks `modelCli` and
    `agentSelectedByCli` as distinct sources with no documented winner. The
    more specific choice wins by the flag being withheld, rather than by us
    finding out which wins in production.

codexProtocol.buildArgs ignores Model, and now says why in place: Codex carries
the identical defect (--ignore-user-config withholds ~/.codex/config.toml,
where the operator's model lives) and the mechanism to fix it exists, but no
codex node's model is observable in this repository's corpus, so there is no
measured population to fix it for. That silence is now legible instead of
accidental, and pinned by a test.

DESIGN.md's argv block and claude_test.go's element-by-element `want` move
together, as that block requires.

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>

Signed-off-by: jitokim <pigberger70@gmail.com>
… does not move

The coordinator reads the operator's model choice once per plan
(WithUserSettingsPath -> usermodel.Read), carries it on Plan.Model, and the CLI
hands it to schedule.Options.Model, which the scheduler forwards to every
NodeInvocation. A resumed planned leg re-reads the same file, because without
it the second leg reproduces the defect one leg late.

Byte-for-byte unchanged: SettingSources "", --allowedTools, --tools, the deny
list, --strict-mcp-config, enterprise policy, internal/childenv.Scrub, and the
count of exec seams. This is argv. A model name grants no tool, loads no file
and runs no hook — the assertion is a test, not a claim: the ceiling comparison
in TestPlan_ModelLeavesTheCeilingUntouched diffs every layer of a plan that read
a model against one that did not.

The three open questions, answered as ADR 0034 decides, each with its silence
made legible at the call site:

  - planner and assessor: no --model. The planner already loads the operator's
    settings (it sets no SettingSources), so there is nothing to fix; both are
    left alone because THE ENGINE PARSES THESE REPLIES, and changing the model
    behind a parser is a compatibility change wearing a preference's clothes.
    coordinatorInvocation and assessorInvocation now say so.
  - codex: no code, documented asymmetry. The defect is identical and the
    mechanism is known, but no codex node's model is observable in this
    repository's corpus, so it would be a fix for an unmeasured population.
    docs/LIMITATIONS.md states it where the user meets it; #245 carries the
    research.
  - per-run flag: none. One surface, the settings file — a flag would need
    persisting and re-applying on resume, or a resumed leg silently runs a
    different model than its first, which is the class of bug being fixed.

Disclosure amended where it was made: toolPolicyFor's comment and the README
both promised a planned node loses that whole file, and one key of it now
arrives. The node's capability ceiling is unchanged; one preference crosses it,
by name, and only that one.

Measurement committed with the decision: 181/6 planned, 851/267 hand-written,
the contaminated directory census printed beside the state.json join so the
difference is visible, and the limit stated out loud — message.model is a
FAMILY census, so the [1m] half of the defect is neither confirmed nor refuted
by it. Re-running the program reproduced every node figure; recomputing the
aggregates off the raw rows a second way agrees.

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>

Signed-off-by: jitokim <pigberger70@gmail.com>
…fter

The Unreleased entry already said the key is read and passed as `--model`; it
did not say, in those terms, what the node answered with BEFORE. "A model
nobody selected" is the finding, not the mechanism — the mechanism is that the
CLI falls back to its own default when no settings source and no flag names
one. Both halves are now stated as a pair, and the AFTER half says verbatim
out loud: no normalisation, no case-folding, and no stripping of a variant
suffix like [1m], because that suffix is exactly the value an author of a
well-meaning allowlist would drop.

No release, no tag, no version bump; no released heading touched.

Signed-off-by: jitokim <pigberger70@gmail.com>

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
…t one

SECURITY.md is the document the README sends a reader to for "the layer-by-layer
stance", and it was the file ADR 0034's own sweep missed: three passages here
enumerate what a planned node loses, and after the change one key of that file
does reach it. A sweep that stops at the README leaves the authoritative
statement contradicting the shipped argv.

Three sites, each stating the same distinction rather than repeating a feature
note:

  - after the layer table, where the reader learns what the five layers ARE:
    every row bounds CAPABILITY — grants, tools, settings, hooks, CLAUDE.md —
    and `--model` grants none of them. Not one row moves. The value reaches
    argv and not a prompt, and the graph schema has no `model` key, so no
    planner output can select it. One key; a second needs its own ADR, because
    the same file's permissions block holds the standing grants layer 1 exists
    to withhold.
  - "Planned nodes are more isolated and less capable": "less capable" is now
    said to be about tools, files and hooks and never about which model does
    the thinking — and the codex asymmetry is stated where that claim is made,
    not left to LIMITATIONS.md alone.
  - the mapped-node cost paragraph: an agent-mapped node is the one planned
    node that gets NO --model, since its definition declares one. That is where
    a reader asks, having just been told what mapping costs.

Documentation only; no code, no ceiling, no argv.

Signed-off-by: jitokim <pigberger70@gmail.com>

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
…a decision

The argv block already carries `[ --model <value> ]` (3d5671a). The two places
a reader goes to ask what that flag MEANS did not:

  - "The tool ceiling — one layered policy" is the enumeration of what bounds a
    planned node, and it now says what --model is not: not a row of the table,
    not a sixth layer, not even a ToolPolicy field — it rides on
    NodeInvocation beside the prompt. Every layer bounds capability; a model
    name binds no grant, adds no tool and loads no file, so ONE key crosses
    layer 1 without moving a row of it. Cited to the test that proves it rather
    than asserted: TestPlan_ModelLeavesTheCeilingUntouched diffs every layer of
    a plan that read a model against one that did not.
  - the Codex protocol block, whose argv listing has no --model beside a Claude
    block that now does. Left implied, that reads as an oversight. It is not:
    the defect is identical (--ignore-user-config withholds
    $CODEX_HOME/config.toml) and the mechanism exists (`-c model="…"`, the same
    override this protocol already uses for approval_policy) — what is missing
    is a measured population, because a codex thread writes no
    ~/.claude/projects transcript and the corpus therefore holds no codex node's
    model at all. Shipping it would be a fix for nobody we have counted.

Documentation only. Code and DESIGN.md move together, as this file requires.

Signed-off-by: jitokim <pigberger70@gmail.com>

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
README.md gained the paragraph in fd82b7a; README.ko.md, which mirrors the same
"one boundary to read before you trust it" section for a Korean reader, did not.
A Korean reader was left with the pre-change claim that a planned node keeps
nothing of their configuration, which the shipped argv contradicts.

Same three statements as the English paragraph, plus the codex half stated in
place rather than by link alone: the model key is read on its own and passed as
`--model`; the capability ceiling does not move, because a model name grants no
tool, loads no file and runs no hook; nothing else in the file is read, and a
`--runtime codex` run reads nothing at all, so its planned nodes answer with
codex's own default.

Documentation only.

Signed-off-by: jitokim <pigberger70@gmail.com>

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
…oses

docs/EXAMPLES.md is the document internal/docsclaims exists because of — the
last sweep to condition an absolute conditioned it in DESIGN, SECURITY, both
READMEs and LIMITATIONS, and left this file stating it flat (see that package's
doc.go). Three passages here enumerate the loss, so three are amended:

  - the "two things that come with it" bullet: your model choice is the one
    thing a planned node keeps, so "less capable" is about tools, files and
    hooks and never about which model answers. Claude-only, said there.
  - the agent-mapping cost paragraph: a mapped node takes its model from the
    agent file, and is the one planned node that gets no --model at all.
  - the skill-activation "the tool ceiling does not move" claim: "load none of
    your settings" now names the one key read back out of that file by name.

Each says the same distinction the ceiling rests on — capability versus
preference — rather than repeating the feature note.

Documentation only.

Signed-off-by: jitokim <pigberger70@gmail.com>

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>
…e-run moves

The 0034 writeup already carried the question, the method, the two buckets and
the [1m] limit. Two things it did not carry, both about the edges of the claim
rather than the claim:

WHAT IS NOT MEASURED, as a list rather than as prose scattered through the
argument. Six lines, each marked, each with the address of the number on it:
the [1m] context-window variant; a codex node's model (12 records join to no
claude transcript, because a codex node writes none); the 77 records with no
session_id and the 3 with no assistant record; whether the operator's settings
held one model value across the corpus window; the hand-written bucket split by
agent, which was never run; and whether ADR 0034's --model repairs any of it,
which no run in this corpus is late enough to show.

WHAT A RE-RUN NOW REPORTS. Re-running the program today does NOT reproduce the
figures, and saying "re-running reproduced them" without a date would have gone
stale silently. The runs directory grew: 45 -> 46 planned runs, 181 -> 186
planned claude-opus-5, 1397 -> 1402 raw rows. The hand-written bucket does not
move by a single record (851/267, 1118 of 1202), and the whole delta is planned
nodes this branch's own work created after the raw file was written. The
document therefore states which column it commits to — the committed raw JSON,
the one with a durable address — and gives the other as a dated observation.

The re-run command in the file runs from a scratch directory on purpose: the
program writes its raw output to a path relative to the working directory, so
reproducing it inside the repo overwrites the very file the figures cite.

Also stated plainly, because the writeup implied it without answering it: the
hand-written bucket is NOT contaminated, by construction — it is reached only
through state.json node records. The 1156 transcripts claimed by no graph node
are the contamination a directory census would have added, measured instead of
asserted.

No number in this commit was re-derived without its command beside it, and no
figure already committed was changed.

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>

Signed-off-by: jitokim <pigberger70@gmail.com>
…umber

The Codex model-asymmetry follow-up was filed in the public tracker and then
cited from six places. Our own follow-ups belong in the operator's private
backlog (oh-my-graph-hq notes/open.md), so the citations now name that address
instead of an issue number. Where a citation carried the research as well as
the follow-up, the research now points at the place it actually lives — ADR
0034 section 2.6, or the comment above codexProtocol.buildArgs.

No claim, wording or other address changed: the asymmetry, its one reason (no
codex node's model is observable in this corpus) and the mechanism stay exactly
as written.

Co-Authored-By: oh-my-graph <graphs@oh-my-graph.dev>

Signed-off-by: jitokim <pigberger70@gmail.com>
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 283de6ea-cedd-48d2-9799-a7785c3f1cff


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant