Flight control config — landed evidence
Worker routing, gate strictness and worker fences now resolve from four layered YAML sources through one schema-validated command, and every resolved value reports the layer it came from. Seven declared measures, seven pieces of evidence, no negative results.
Declared measures and what each produced
| Measure | Evidence | Verdict |
|---|---|---|
| Schema round-trip | Both committed artifacts regenerate byte-identically from the LinkML source — § schema | pass |
| Layer precedence | Project override of one backend key leaves five sibling keys standing — § resolution | pass |
| Provenance | 36 resolved keys, each attributed; all four layers visible in one run — § provenance | pass |
| Malformed config fails loudly | Invalid enum, unknown key, out-of-range and bad duration each name file, key path and constraint — § failures | pass |
| Availability | Both CLI backends located on PATH; missing-binary report recorded — § availability | pass |
| Machine contract | Single-line sorted JSON, byte-identical across runs, exit 0 valid / 1 invalid — § contract | pass |
| Suite green | 1,495 tests pass, 35 of them new — § suite | pass |
Four layers, merged per key
Resolution runs shipped → host → project → override, merging mappings recursively and replacing everything else. The property that matters is that a layer overriding one key of one backend leaves that backend's other keys standing, so a project does not restate a backend to change its model.
backends.codex.effort in the project layer costs nothing
from backends.codex.model beside it.The deep-merge measure was exercised against a host layer defining a backend with five keys and a project layer overriding one of them:
host: backends.alpha = {launch: cli, command: alpha-cli, model: some-model,
effort: high, concurrency: 3}
project: backends.alpha = {model: other-model}
resolved backends.alpha:
model = other-model (project)
command = alpha-cli (host) <- survives
effort = high (host) <- survives
concurrency = 3 (host) <- survives
launch = cli (host) <- survives
gates.enforce = strict (shipped) <- untouched layer still contributes
Adding a backend in a higher layer also leaves the shipped one in place, so the in-harness
backend is always available as a degradation target. Lists replace rather than accumulate:
summary.at is one choice, not a set to append to.
Per-key provenance across a deliberate three-layer setup
A temporary project layer was written to
docs/state/reckon/flight.yaml setting backends.codex.effort,
backends.claude.concurrency and gates.on_fail, over the real host
config and the shipped defaults, then removed. reckon flight --project reckon
attributed all 36 resolved keys, with every layer represented:
project backends.codex.effort = medium
host backends.codex.model = gpt-5.6-sol
host backends.codex.concurrency = 3
project backends.claude.concurrency = 1
host backends.claude.effort = medium
host backends.native.concurrency = 4
project gates.on_fail = warn
host gates.enforce = strict
shipped summary.reflex = what-why-how-when
shipped fences.needs_help_after_failures = 2
shipped worktree.cleanup = conservative
shipped roles.implement = {}
keys with an origin: 36 | layers: shipped=present host=present project=present override=absent
A test asserts that no leaf of the resolved config is missing from the provenance map, so the attribution cannot silently fall behind a schema addition.
Generated artifacts match their source
reckon/schema/flight.yaml is the LinkML model. Two artifacts are generated from
it and committed: reckon/_flight_schema.py, the Pydantic validator reckon imports at
runtime, and docs/_shared/flight.schema.json, served live at
/_shared/flight.schema.json (HTTP 200 confirmed) so a
yaml-language-server header gives editor completion while tuning. Two tests
regenerate both and compare; both matched byte-for-byte.
The generators live in the dev dependency group, where uv run installs them with
no flag to forget, and uv run python scripts/regen_flight_schema.py rewrites both
artifacts. The generated module is excluded from lint for the same reason it is generated: a
lint fix applied by hand is exactly the drift the round-trip measure exists to catch.
The runtime claim was checked against a built wheel rather than asserted. A clean environment installed from the wheel alone resolves configuration with none of the toolchain present:
| Environment | Installed packages | LinkML present | reckon flight |
|---|---|---|---|
| Wheel only | 32 | no | resolves; 20 attributed keys from shipped defaults |
| Development, dev group installed | 104 | yes | resolves; regeneration available |
That is the measurement behind the toolchain decision: authoring weight is 72 packages on top
of a 32-package runtime, carried only by developers who regenerate the schema. The wheel also
carries reckon/schema/flight-defaults.yaml, confirmed present in the built archive,
so a fresh install works with no configuration at all.
A malformed layer stops, and says why
Every failure names the file, the key path inside it and the violated constraint. No path through resolution falls back to defaults on a bad value — misconfigured routing that looks like it worked is the failure this rules out.
$ reckon flight --set gates.enforce=aggressive Error: <override>: gates.enforce: Input should be 'strict', 'advisory' or 'disabled' [exit 1] $ reckon flight --set gates.enfoce=strict Error: <override>: gates.enfoce: Extra inputs are not permitted [exit 1] $ RECKON_FLIGHT_CONFIG=bad.yaml reckon flight Error: bad.yaml: backends.codex.concurrency: Input should be greater than or equal to 1 [exit 1]
Tests additionally cover an unparseable YAML layer, a duration that does not match the
schema's pattern, a default_backend naming a backend no layer defines, and a role
dispatching to an undefined backend. The last two are whole-config rules: they can only be judged
after every layer has merged, so they are checked there rather than per layer.
One rejection is worth naming because it is a trap rather than a typo. gates: {enforce:
off} parses as the boolean false in YAML, not the string off, so the schema
spells the value disabled and the boolean is rejected with the enum message above.
The same coercion silently renamed the value inside the generated Python enum during
implementation, which is how it was found.
Backend availability, reported and not acted on
codex command_found=true /home/ITER/mcintos/.npm/packages/bin/codex
claude command_found=true /home/ITER/mcintos/.local/bin/claude
native command_found=true in-harness backend needs no external command
# with the command pointed at a binary that is not installed:
codex command_found=false command_path=null
detail="'codex-not-installed' is not on PATH"
A missing backend stays in the resolved config and is reported as missing; nothing reroutes silently, because silent rerouting hides exactly the misconfiguration this reports. The caller decides whether to degrade to the in-harness backend.
Authentication cannot be checked without provider-specific knowledge, which the constraints
forbid reckon from holding. A backend may therefore declare an auth_check argument
vector — user data, like its command — whose exit status is reported. Because running it spawns a
process, it runs only under --probe-auth; otherwise authentication is reported as
unprobed rather than guessed. No auth_check is configured on this host, so the
recorded report shows the unprobed state.
The machine contract
$ reckon flight -> exit 0, one line of JSON $ reckon flight -> byte-identical to the previous run top-level keys sorted = True (availability, config, layers, project, provenance) config keys sorted = True --pretty = the same document, indented invalid config -> exit 1, message names file / key path / constraint
Sorted keys at every level are what make the output diffable: two runs differ only where a
value differs. --set dotted.key=value feeds the override layer, parsing values as
YAML scalars so concurrency=3 is an integer rather than a string the schema would
reject.
The host config (not committed)
~/.config/reckon/flight.yaml is user data and is recorded here rather than in the
repository. It states only what it changes from the shipped defaults, which is the discipline the
deep merge exists to enable:
# yaml-language-server: $schema=http://127.0.0.1:8765/_shared/flight.schema.json
version: 1
default_backend: codex
backends:
codex:
launch: cli
command: codex
model: gpt-5.6-sol
effort: high
sandbox: worktree-full
session_reuse: true
concurrency: 3
time_budget: 25m
claude:
launch: cli
command: claude
model: claude-sonnet-5
effort: medium
sandbox: worktree-full
session_reuse: true
concurrency: 3
time_budget: 25m
native:
launch: in-harness
concurrency: 4
time_budget: 25m
session_reuse: false
roles:
review:
sandbox: read-only
investigate:
sandbox: read-only
gates:
enforce: strict
require_evidence: true
on_fail: hold
fences, worktree and summary are absent because the
host agrees with the shipped values for all of them; they resolve with a shipped
origin, visible in the provenance output above.
Where the implementation departs from the design sketch
Three departures, each forced by something the sketch could not have known, and each recorded because a later reader will otherwise assume the sketch was implemented literally.
| Design sketch | Implemented | Why |
|---|---|---|
| Shipped defaults live in the generated model | Shipped defaults are a validated data layer, reckon/schema/flight-defaults.yaml, in the wheel |
Per-key provenance is the deliverable that makes the surface debuggable, and a value coming from a Pydantic field default is indistinguishable from a key nobody set. A real layer keeps the two apart. The LinkML generator also emits syntactically invalid Python for a default whose enum value contains a hyphen, and cannot clear an inherited default on a partial overlay class — both would have needed hand edits to the generated file. |
gates.enforce: off |
gates.enforce: disabled |
YAML reads off as the boolean false, in the schema and in every config file it validates. |
| Report whether a backend “appears authenticated” | Report the exit status of a backend's own declared auth_check, under an explicit flag |
Any built-in authentication probe would encode provider-specific knowledge in reckon source, which the constraints forbid. Putting the check in the config's values keeps that knowledge in user data. |
The schema names no backend, command, model identifier or effort ladder; a test asserts the shipped layer defines only the in-harness backend and that the published JSON Schema fixes no backend key. Effort is deliberately free text rather than an enum, because each backend defines its own vocabulary and a closed list would encode a hierarchy.
Suite
$ uv run pytest -q 1495 passed in 42.00s $ uv run pytest tests/test_flight.py -q 35 passed $ uv run ruff check reckon/ tests/ scripts/ All checks passed!
35 new tests, no failures, no skips — the two round-trip tests skip only where the LinkML
generators are absent, and both ran here. Commit
98257f477af5d894227d7475da914ec4ad61f5d6.