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

MeasureEvidenceVerdict
Schema round-tripBoth committed artifacts regenerate byte-identically from the LinkML source — § schemapass
Layer precedenceProject override of one backend key leaves five sibling keys standing — § resolutionpass
Provenance36 resolved keys, each attributed; all four layers visible in one run — § provenancepass
Malformed config fails loudlyInvalid enum, unknown key, out-of-range and bad duration each name file, key path and constraint — § failurespass
AvailabilityBoth CLI backends located on PATH; missing-binary report recorded — § availabilitypass
Machine contractSingle-line sorted JSON, byte-identical across runs, exit 0 valid / 1 invalid — § contractpass
Suite green1,495 tests pass, 35 of them new — § suitepass

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.

shipped host project override resolved worktree.cleanup backends.codex.model backends.codex.effort gates.enforce conservative gpt-5.6-sol medium advisory
Every key resolves on its own track. A filled dot supplies the resolved value; a hollow dot is a value that layer set and a higher one shadowed. Because the tracks are independent, changing 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:

EnvironmentInstalled packagesLinkML presentreckon flight
Wheel only32noresolves; 20 attributed keys from shipped defaults
Development, dev group installed104yesresolves; 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 sketchImplementedWhy
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.