# AUTOPILOT-116 — which engine settings each host actually honours

**The failure mode was SILENCE.** A key accepted by `autopilot.config.json`
(and reported as saved) had no effect on one host: the engine ran its built-in
defaults. Nothing crashed and nothing warned, so the only way to notice was to
read the construction site.

This file is the fact table an operator could not previously obtain. It records
what was true **before** the fix and what the fix changed.

**Baseline for every "before" below: the released 0.9.0, commit `7ac0a84`**
("version bump 0.8.0 -> 0.9.0", the cut covering the AUTOPILOT-100..115 batch).
The numbers are stated against that commit, not against whatever branch this
file happens to sit on: a branch cut before AUTOPILOT-115 landed shows a
different pi constructor, which is the trap that made the first version of this
table wrong. Re-derive any cell with `git show 7ac0a84:<file>`.

## The two construction sites (non-test)

`Autopilot` is constructed in exactly two production places; every other
construction is a test:

| Site | Before AUTOPILOT-116 |
| --- | --- |
| `src/hosts/pi-extension.ts` (`ensureAutopilot`) | `new Autopilot({ stateDir, maxSlots, queueLowThreshold, workerAgents, reviewerAgents, reviewCap, intakeSuppressionHours, log })` — a **hand-picked subset**: six resolved engine keys |
| `src/hosts/opencode-plugin.ts` (`createOpenCodeFramework`) | `new Autopilot({ stateDir, intakeSuppressionHours: cfg.intakeSuppressionHours, ...opts.config })` — and **no production caller supplies `opts.config`** |

`opts.config` is the opencode injection seam. The only caller of
`createOpenCodeFramework` in production is the plugin factory
`OrchestratorAutopilot`, which passes `{ stateDir, hostConfigDir, delivery }` —
**no `config`**. No test supplies it either. So the opencode engine ran the
engine's built-in defaults for every engine key **except
`intakeSuppressionHours`** (AUTOPILOT-115 had already wired that one at both
hosts before 0.9.0); the only other file value it honoured was
`sweepIntervalMs`, which is a **runner** setting, not an engine one.

## Per-host table (BEFORE the fix, at baseline `7ac0a84`)

Values are the engine's built-in defaults in `src/core.ts` unless the host
plumbed the resolved key. `loadAutopilotConfig` resolves file + env overrides;
a resolved value that never reaches the constructor is *silently ignored*.
Each cell below is re-derived from `git show 7ac0a84:<host file>`.

| Engine key (`AutopilotConfig`) | Config source | pi host | opencode host |
| --- | --- | --- | --- |
| `stateDir` | host resolution | passed | passed |
| `maxSlots` | file / `AUTOPILOT_MAX_SLOTS` (default 3) | **passed** | **IGNORED → 3** |
| `queueLowThreshold` | file / `AUTOPILOT_QUEUE_LOW` (default 2) | **passed** | **IGNORED → 2** |
| `workerAgents` | file / `AUTOPILOT_WORKER_AGENTS` (default `["worker"]`) | **passed** | **IGNORED → `["worker"]`** |
| `reviewerAgents` | file / `AUTOPILOT_REVIEWER_AGENTS` (default `["orchestrator-reviewer"]`) | **passed** | **IGNORED → `["orchestrator-reviewer"]`** |
| `reviewCap` | file / `AUTOPILOT_REVIEW_CAP` (default 5) | **passed** | **IGNORED → 5** |
| `zombieGraceMinutes` | file / `AUTOPILOT_ZOMBIE_GRACE_MINUTES` (default 30) | **IGNORED → 30** | **IGNORED → 30** |
| `intakeSuppressionHours` | file (default 24) | **passed** | **passed** |
| `quietPeriodMs` | *no config-file key* (default 60 000) | built-in (test-only override) | built-in (test-only override) |
| `log` | host seam, **not a config key** | wired (gated on `isAutopilotOn`) | not wired |
| `now` | host seam, **not a config key** | built-in `Date.now` | built-in `Date.now` |

So the two hosts **differed in what they honoured**: pi honoured the six engine
keys it named (`maxSlots`, `queueLowThreshold`, `workerAgents`,
`reviewerAgents`, `reviewCap`, `intakeSuppressionHours`), while opencode
honoured `intakeSuppressionHours` **only**. `zombieGraceMinutes` alone was
inert on **both** hosts.

Runner-level settings (both hosts honour them, so they are listed only to mark
the boundary): `sweepIntervalMs` — pi reads `loadAutopilotConfig(stateDir).sweepIntervalMs`,
opencode reads `opts.sweepIntervalMs ?? cfg.sweepIntervalMs`.

## The fix: pass the WHOLE resolved config

Adding a third single-key line would have **confirmed** the defect class (this
repo had already fixed two single-key instances: model resolution, then intake
suppression). Instead both hosts now spread the entire `loadAutopilotConfig()`
result:

```ts
// opencode
new Autopilot({ stateDir, ...cfg, ...opts.config })
// pi
new Autopilot({ stateDir, ...cfg, log: /* host seam */ })
```

This extends the existing mechanism rather than inventing a second path: any
engine key `loadAutopilotConfig` resolves now reaches the engine automatically,
so a future key cannot go inert at a host without a conscious removal of the
spread. Defaults, meanings and units are unchanged; `opts.config` remains the
last-wins test/injection seam.

## The narrower PARTIAL case (state it: resolved here, residual separate)

The same class had a **partial** form: several host-level readers call
`loadAutopilotConfig(stateDir)` **live** each sweep —
`framework/runner.ts`, `tools/queue-ops.ts`, `framework/auto-dispatch.ts` read
`maxSlots` / `workerAgents` / `reviewerAgents` / `zombieGraceMinutes` straight
from the file — while the engine's own reconcile/decide paths read the
constructor value. On opencode, before the fix, the runner's status snapshot
used the file's `zombieGraceMinutes` (say 5) while the engine's
`zombieReconcile` used the built-in 30: **a display and an action disagreeing
about the same setting**.

For the record on the item's framing: `intakeSuppressionHours` has **no**
host-level reader — grep shows it read only in `src/core.ts` (`decideTick`) — so
it never had a runner-vs-engine split. At `7ac0a84` it already reached the
constructor on **both** hosts (AUTOPILOT-115 wired it before 0.9.0), so it is
not part of this item's "before" defect. The concrete live-vs-constructor split
is the four keys above, with `zombieGraceMinutes` the sharp one because both the
runner and the engine act on it.

The whole-config spread **resolves the file-vs-default half** of that case: the
engine now consumes the same resolved values the host-level readers do.

**Residual, genuinely separate:** the engine takes a *snapshot at construction*,
while the host-level readers *re-read every sweep*. So a config file edited
while a host process is live is seen immediately by the runner but by the engine
only when the host rebuilds it. pi rebuilds on `/autopilot capacity <n>`
(`autopilot = null`); opencode **never** rebuilds the engine, so an in-session
`capacity <n>` there writes the file and does not move the running engine until
the plugin reloads. That is a **runtime-reload lifecycle** defect, not a
"value never left the file" defect, and it needs a different seam (the runner
holds the engine object by reference). It is deliberately **not** folded into
this item.

## Load-time warning for keys that cannot take effect? — no

The honest pattern here is "a key either takes effect or is not offered". After
this fix **every** engine key a host can resolve is plumbed on **both** hosts;
there is no host for which wiring is impossible, so a load-time "ignored keys"
warning would have nothing to name and would be noise. No warning was added.
`log` stays unwired on opencode, but it is a host seam, not a config key, so it
is out of scope for such a warning.

## Acceptance

Each host has a test that sets NON-default values and asserts the engine
received them — the trap the earlier instances hid behind is that a
default-valued test cannot tell "wired" from "ignored".

- opencode: `maxSlots: 7` (dispatch tick FLEET line + `slotsFree`),
  `queueLowThreshold: 4` (intake tick `facts.threshold`), `intakeSuppressionHours: 0`
  (a fresh proposal otherwise suppresses the intake nudge for the default 24h),
  plus a default-config control.
- pi: `maxSlots: 7` (FLEET line) and `intakeSuppressionHours: 0`, plus a
default-config control.

The two tests prove different things against the `7ac0a84` baseline:

- The **opencode** test is **red-to-green** for this item: at `7ac0a84` the
  opencode engine ran `maxSlots` 3 and `queueLowThreshold` 2, so the test fails
  against that host source. `intakeSuppressionHours` was already wired there
  (AUTOPILOT-115), so that key alone would not have failed.
- The **pi** test is a **regression guard**, not red-to-green: pi already passed
  both asserted keys at `7ac0a84`, so the test does not fail against the
  pre-AUTOPILOT-116 pi source. It fails only against the pre-AUTOPILOT-115 base
  (`b08f0ea`), where `intakeSuppressionHours` was still inert on pi.
