OpenAI Codex · Code Mode

Write architecture decisions as lint

In the same AGENTS.md, a rule that has a command will light red on three operating systems. The one that is only a path — after a rename, nobody notices.

Course goalAfter this lesson you can explain three things. How a call-site rule becomes a rustc plugin and blocks on Linux, macOS, and Windows at once. Why a path written as prose goes stale. Why a missing feature registration can be caught by an exhaustive table.
Try it first · One commit walking architecture checks
One spec, five change cards: watch which layer blocks it, and what that layer is trying to keep
This change
Hit Play to watch the gates. Or tap a layer on the right to see pass or block.
Commit and gatesStandby
create_openai_url(None)The call site wrote a bare None. It compiles; a reader has to jump to the definition to know what it governs.
This step’s verdict
Change in handBare None
Gate hitNot fired yet
What this keepsWalk the gates first
EndingStandby
Waiting to start.
Logic trail · each animation step maps to a source span
  1. Is the call-site arg an anonymous literallib.rs L261
  2. Does the comment name equal the parameter namelib.rs L222
  3. Is the callee a workspace cratelib.rs L177
  4. Does CI run on three platforms at oncerust-ci.yml L174
  5. Does the Markdown path still existAGENTS.md L35
  6. Is the Feature registered in the exhaustive tablelib.rs L379
  7. In-development features must default offtests.rs L18
Hit Play to see where this change stops across six gates.
Who blocked it
What it keeps
Swap a cardWith a red light, a rename or a miss goes red the same day. Without one, the words stay and the object has already moved.
Teaching sketch: the gate layers are a lesson grouping, to contrast “has a check” with “prose only.” Trail line numbers map to openai/codex commit 4f39251a01.
Idea 1 · A locally checkable decision becomes a machine-run red light
What problem it solves

A new hire gets the task: change an MCP tool call. They open AGENTS.md and copy the path on line 35. The file does not exist. The real file is connection_manager.rs, in the same directory. The name with an mcp_ prefix in the docs is leftover from a rename that was not cleaned up.

Source: AGENTS.md lines 32–36; codex-rs/codex-mcp/src/connection_manager.rs lines 1–15

In the same file, a positional argument missing /*base_url*/ goes red on the local command. Change Cargo.toml and forget to refresh the Bazel lock, and CI goes red. The path on line 35 has no checker. Markdown will not verify the file is still there.

What the idea is

Change the API first so the call site can be read. A reader of foo(false) has to jump to the definition to know what that false governs. Only if you cannot change the API is /*param_name*/ allowed. Lint is the fallback.

Source: AGENTS.md lines 14–20

The implementation lives in a standalone Dylint crate and runs as rustc. Only after type resolution can you get the callee’s parameter names. The entry only looks at function and method calls; anything from macro expansion is skipped.

Checks walk in this order.

1. Only this-repo crates; std and tokio are skipped.

2. Comments are sought in three places: the gap before the argument, the previous 64 bytes, and the argument text itself.

3. A wrong name reports mismatch. A wrong comment will not fall through to the “no comment” rule.

4. When none is written, a method name equal to the only parameter name is exempt, e.g. .enabled(false).

5. The rest only blocks anonymous literals. None, bools, and numbers need a comment; strings and chars are skipped.

Source: tools/argument-comment-lint/src/lib.rs lines 165–180; tools/argument-comment-lint/src/lib.rs lines 261–274

Call site bare None workspace? only then keep checking Valid comment? the name must match deny, then three CI runs Linux / macOS / Windows In: one call. Out: a red light before merge. What it keeps: a self-explaining call site
Teaching structure diagram: only a local call whose parameter names can be resolved is worth a rustc plugin.

The repo entry raises the default-Allow rule to deny. CI runs once each on Linux, macOS, and Windows; if one fails the other two still finish. A person went green on macOS; if the Windows target’s macro expansion adds one more None, the third machine still blocks.

Source: .github/workflows/rust-ci.yml lines 164–187

Why it lasts

The call site is local, the name is resolvable, and false positives can be held by an exemption. Change the language and the shape is the same: rename first; if you cannot, require an in-line name. TypeScript uses ESLint, Python uses ruff — both fit.

Idea 2 · Prose rots; being countable is not the same as being counted
What problem it solves

Line 35 and line 265 are the same rot. The app-server guide still writes v2.rs; it is now a v2/ directory, split into thirty-plus files. Near 800 lines, a file should split. After the split, nobody changed the single-file path in the guide.

Source: AGENTS.md lines 260–266

The module-size rule names five hot files; four already crossed 800, one sits near 900. chat_composer.rs is 12859 lines. The repo has no command that counts lines. Lines can be counted; CI does not count them. Whether a change is mechanical is a poor machine job, so the 800-line cap stops at review.

Source: AGENTS.md lines 49–61; AGENTS.md lines 125–131

What the idea is

Read the rules as two sets. One has a command or a compiler and goes red before merge. One can only be read by people and review — miss it and it passes. Path existence is the easiest check: extract backtick paths and test them against the repo root. The repo did not. Budget went to call-site readability, not path existence.

Rule written in AGENTS.md Command or compiler? yes → into the machine lint, test, or schema job Prose only three-platform CI blocks merge a person or review might catch it path renamed, words remain
Teaching fork diagram: a checked rule goes red the same day; prose-only breaks in silence.
Why it lasts

Docs will not re-check themselves. A locally checkable rule written only in Markdown — on the day of a rename or a split, the words stay and the object has moved. The smallest form is a twenty-line script that checks paths. You do not need a rustc plugin.

A rule written as lint goes red the day the file is renamed.
Idea 3 · A lifecycle written as an enum plus an exhaustive table
What problem it solves

If a feature flag is only a bool and a paragraph, a missed registration, an in-development default-on, or a Deprecated that just sits there will not go red at once.

What the idea is

Beside the Feature enum sits a FEATURES table. FeatureSpec welds id, config key, stage, and default-on onto one row. A missing row is unreachable!. One extra enum variant and one missing table row — run to key() and it panics.

Source: codex-rs/features/src/lib.rs lines 41–58; codex-rs/features/src/lib.rs lines 819–826; codex-rs/features/src/lib.rs lines 379–384

Two nearby tests lock the defaults. In-development features must default off. A default-on feature can only be Stable or Removed. There are five stages; the extra Experimental carries a menu name and an announcement. Deprecated has no expiry; three Deprecated items can still be turned on. A stage can say “do not use”; it cannot say “delete next version.”

Source: codex-rs/features/src/tests.rs lines 17–28; codex-rs/features/src/tests.rs lines 82–94

UnderDevelopment must default off Experimental menu plus announcement Stable only then default-on Deprecated Removed In: enum plus one table row. Out: miss a row and it panics. Deprecated to Removed has no timer
Teaching state diagram: the exhaustive table locks registration and defaults; it cannot lock auto-delete.
Why it lasts

An exhaustive table plus two tests still holds in another language. Miss a row and it panics; defaults stay locked. You do not get auto-delete — you get these two invariants.

Side-by-side · Another answer to the same question

DSH: every package must show up; empty still needs an explanation

DeepSeek Harness writes “every package must own ./invariant” as both prose and a gate. The prose is in packages/AGENTS.md. The gate is the 21-line verify-package-invariants; failure is process.exit(1). An empty installer must carry the fixed prefix No runtime invariant:. Empty is an explicit architecture conclusion — later mutable state must become a real check.

The note answers why empty is allowed; the checker guarantees empty must be explained. Miss either and you are back at Codex line 35: the words stay, the object has moved. DSH has no rustc plugin for foo(false). Codex has no exhaustive package gate for path existence.

Source: packages/AGENTS.md line 18; scripts/verify-package-invariants.ts lines 1–21

Both sides checked against source · 2026-08-22

Grok: a localizable decision goes straight into clippy

The Grok Build repo root has no AGENTS.md. It still writes one architecture decision as lint: clippy.toml bans canonicalize, because Windows gets a verbatim prefix that breaks git and leaks into model context. The exec boundary is in the same file: this ban is run by each crate’s cargo clippy presubmit; Bazel-only crates rely on a person.

Same class of judgment as Codex’s argument comments: the call site is local, the false-positive surface is controllable. Grok admits Bazel coverage is incomplete. Codex admits local only runs the current OS. A small team should copy path existence and the 21-line verify script first — cheaper than copying Dylint.

Source: clippy.toml lines 9–28

Both sides checked against source · 2026-08-22
Classroom Exercise
01

Which automatic check do you build first?

Line 35 and line 265 of AGENTS.md are both stale paths. If you can only build one automatic check first, do you check paths prefixed codex-rs/, or every backtick string that contains /?

The first misses a relative form like app-server-protocol/src/protocol/v2.rs. The second false-hits command names, crate names, and URL fragments. Write your filter rule, and use these two stale paths as positive examples.

Takeaway: A locally checkable decision should not live only in Markdown. The clause tells a person what to review; the red light still shines when nobody is looking. Path existence and an exhaustive table are cheaper than a rustc plugin — and should come first.