Development record
abcd proposes the model each agent deserves, and facilitates it where it can
Summary
spc-2609180535002478 delivers itd-2609170822093401: a routing table with one
row per agent — a model tier from a closed set and a fan-out bound — that
abcd ships as its own proposal, renders, and applies only once accepted. Four
layers resolve to one row per agent: an invocation --route flag over the
repository's committed file over the machine's accepted file over the bundled
proposal. At run time the winning row is resolved against the connections the
machine has configured: a reachable provider that can serve the tier takes the
step directly; otherwise the step goes to the harness with the tier and bound
in its request block, the fallback announced on stderr and recorded on the
receipt beside the model the host or provider reported. Provider settings
default on the connection and are overridden per row and per flag. Nothing
here enforces the payload's model or agents_used fields (that is
itd-2609180517121254), wires a provider (itd-2609081951381895), or learns
from outcomes (itd-17).
Scope
- One resolver in core.
internal/core/oracle(new) owns the routing types and the resolution:Row{Tier, FanOut, Settings},Table(agent → Row),Layered{Flag, Repo, Machine, Bundled}, andResolve(agent, layered, connections) → Route{Row, Connection, Fallback, Source}. The resolver never writes and never reaches a network; it takes aconnectionsvalue the caller supplies, so a test can hand it a provider that is "reachable" without a socket. Every front door renders aRoute; none invents one. - The bundled proposal. A Go value in
internal/core/oracle/proposal.go, one row per file inagents/, keyed on the agent'sname. A test asserts the proposal names every agent in the roster and no other, so an agent added without a row, or a row for an agent removed, fails the build rather than surfacing at run time (the intent's second open question, answered here). The initial rows:frontierforintent-auditor,lifeboat-reviewer,ruthless-reviewer,security-reviewer,release-changelog-composer;economyfor the four cold-reading positions,docs-currency-reviewer,graveyard-interpreter,principle-distiller,press-release-composer,scribe,sota-researcher; fan-out taken from each agent's contract ceiling. These are the best guess the press release promises, and the calibration for them is itd-17's. - Two stores, one shape.
~/.abcd/oracle-routing.json(machine) and.abcd/config/oracle-routing.json(repository), both{"schema_version": 1, "agents": {"<name>": {"tier": …, "fan_out": …, "settings": {…}}}}— thepositions-map shapereading-presets.jsonalready uses. Both are read through the guarded reader; a repo file is read only from the checkout root the session resolved. A row naming an agent not in the roster is reported on stderr by name and skipped; the remaining rows apply (AC 9). - The tier vocabulary.
local,economy,frontier,host-decides; one closed enum in core, rendered by every refusal that names it. A row'sfan_outabove the agent contract's ceiling is reported and clamped to the ceiling at read time, never raised. - Connections. The resolver consumes a
Connectionsinterface —Serves(tier) (Connection, bool)— that the api adapter intent implements. Until it ships, the only implementation is the empty one, so every row resolves to the harness and AC 1 holds on every machine. - The consent step.
ahoy installgains a fourth consent category,oracle-routing, afterstatus-line: it renders the proposal as a table (agent, tier, fan-out), asks once, and writes the machine file on yes; a second, separate prompt offers the repository file.--yesskips both and reports them underoptional_skipped, the same shape as the status line.ahoy uninstallleaves both files, because they are the user's configuration. - The invocation override.
--route <agent>=<tier>[@<connection>][?k=v,…], repeatable, registered on every delegating verb through one cobra helper so a verb cannot gain delegation without gaining the flag. A--routenaming an agent the verb does not dispatch, a tier outside the enum, or a connection not configured on this machine is refused at exit 2 before any step runs. - The request block and the receipt. Every delegating verb already writes
a request block for the host; it gains a
routing:section carrying the resolved tier, bound and source layer. Every ingest already writes a receipt; it gainsroute: {tier_asked, connection_tried, connection_used, fallback_reason, override, settings_sent, model_reported}, wheremodel_reportedis whatever the payload'smodelfield carries today (its requirement isitd-2609180517121254's). - The board. The bare
abcdboard gains one line per agent underoracle:showing the four layers and marking the winner (AC 6), rendered from the sameRoutevalues.
Approach
- Land the core package with the types, the enum, the proposal and its
roster test, and
Resolveover an in-memoryConnections— every layer and fallback case table-tested with no filesystem. - Add the two store readers behind the guarded reader, with the orphan-row report and the ceiling clamp; table-test precedence with all four layers populated and disagreeing.
- Register the
--routeflag through the shared helper on the delegating verbs (intent audit,launch ship,disembark review|principles| press-release|graveyard,reading ingest,memory ingest); refuse the three malformed cases before dispatch. - Thread the
Routeinto each verb's request block and each ingest's receipt; golden-test one request block and one receipt per verb. - Add the
oracle-routingconsent category toahoy install, mirroring the status-line category; test the prompt order,--yes, andoptional_skipped. - Render the board line.
Each step carries a test watched failing first; the whole is one PR or a short stack, the spec closed in the change that lands step 6.
How the Acceptance Criteria are satisfied
- Empty
Connectionsand no files → everyResolvereturns the bundled row withConnection: harness, tierhost-decidesonly because nothing is accepted: with no machine or repo file the bundled proposal is not applied, soResolvereportsSource: noneand the harness leg athost-decides; the request block carries the contract's ceiling. - The
oracle-routingconsent category renders and writes on yes, and only on yes; declining writes nothing and the next install offers again. - A
ConnectionsthatServes(tier)returns a provider →Route.Connectionis that provider and the verb dispatches to it, not to the host. Servesreturns false →Route.Connection: harness,Fallbackset with the reason; the verb prints one stderr line from it before dispatch and the receipt carries all three connection fields.- The receipt's
routeblock carriestier_asked,connection_usedandmodel_reportedverbatim from the payload. - Precedence is repo → machine → bundled in
Resolve; the board renders all three with the winner marked. --routeis layer zero inLayered; a second flag for another agent merges; the receipt'soverridefield carries the flag verbatim; the three refusals fire in flag parsing.- Settings merge connection → row → flag in
Resolve; the receipt'ssettings_sentrecords the merged map; an unaccepted key is refused by the adapter'sConnectionbefore dispatch (the adapter intent's contract). - The store reader reports and skips an orphan row.
Tests
internal/core/oracle: enum refusals; proposal-roster bijection;Resolveprecedence table (all 4-layer permutations for one agent); fallback reasons; settings merge order; ceiling clamp.- Store readers: guarded-read refusals, schema_version, orphan row.
- CLI:
--routeparse and the three refusals; one golden request block and one golden receipt per delegating verb. - Ahoy: consent order (
…, status-line, oracle-routing, user-state, …),--yes→optional_skipped, decline writes nothing. - Board: one agent with four disagreeing layers renders the winner marked.
Out of scope
- Requiring
model/agents_usedon payloads —itd-2609180517121254. - Any real
Connectionsimplementation, provider configuration, reachability probing, and the set of settings a provider accepts —itd-2609081951381895,iss-2609081951416843. - Writing rows from measurements — itd-17.
- Enforcing a fan-out bound at ingest.
- A lint rule over the routing files.
Progress
Part 1 (branch feat/layered-config-resolver, autonomous run A, 2026-09-25)
lands Approach steps 1, 2 and 6 and leaves the spec open.
The layered resolver has one home, internal/core/layered, and every
consumer reads through it. Both file families are recorded in
dcn-2609250000000005:
layered.Config:.abcd/config.jsonand~/.abcd/config.json, holdingpace.*,oracle.review,roles.<role>.runnerandmatch.threshold;layered.OracleRouting: this spec's two routing files.
Its API:
Load(File, Roots{Repo, Home}) (*Stack, error): guarded reads, loud on any fault; an absent file is an absent layer.(*Stack).SetFlag(key, v, origin): the flag layer, with the flag text kept verbatim as the value's origin.(*Stack).Claim(namespace, keys...): refuses an unknown key under a claimed namespace;""claims the top level and*matches an open segment.Get[T](s, key, bundled, check) (Value[T]{V, Layer, Origin}, error): strict decode; a bad winning value refuses and never falls through.(*Stack).Lookup(key) []Found{Layer, Origin, Raw}: every layer, highest first.(*Stack).Members(layer, key),(*Stack).Present(layer)andDecode[T](raw).Layer:None,Bundled,Machine,RepoandFlag.
Delivered here, each with tests:
- AC 1, the resolution half:
Resolvegivesnone, the harness athost-decidesunder the contract ceiling, and consults no connection. - AC 3 and AC 4, the resolution halves: a provider takes the step, or the step falls back to the harness with a reason.
- AC 6 in full: repo over machine over bundled, and the bare board's
oracle:lines. - AC 7, the core half:
ParseRoutesandApply, with the five refusals. - AC 8, the merge half: connection, then row, then flag.
- AC 9 in full: an orphan row is reported and skipped.
Also delivered: the tier enum, the bundled proposal and its roster test, and the ceiling clamp.
Two rulings this part took (dcn-2609250000000006):
- The bundled proposal applies only once a repository or machine table exists,
and a
--routeaccepts nothing. - Every agent's fan-out ceiling is 1, because no contract declares one.
What remains:
- Step 3,
--routeon the delegating verbs through one cobra helper (AC 7 at the surface). - Step 4, the request block's
routing:section and the receipt'sroute:block. These give the stderr fallback line of AC 4 and the receipts of AC 4, 5, 7 and 8. Steps 3 and 4 land together, because a flag that changes no request would half-work. - Step 5, the
oracle-routingconsent category inahoy install(AC 2). - AC 10 (escalation on a failed fix round) and AC 11 (the provider allowlist). They follow the implement loop and the API adapter.
The spec closes in the change that lands the last of steps 3 to 5.
Part 2 (branch feat/model-tier-routes, autonomous run A, 2026-09-25) lands
Approach steps 3, 4 and 5 and closes this spec with a remainder.
Delivered here, each with tests:
- AC 2 in full:
ahoy installasks theoracle-routingcategory afterstatus-line. It renders the proposal as a table and writes~/.abcd/oracle-routing.json(owner-only) on consent. A separate question offers.abcd/config/oracle-routing.json, written only on its own consent.--yeswrites neither and names both underoptional_skipped. A decline is not recorded, and uninstall leaves both files. - AC 4, the surface half: the stderr line before the step, and the receipt's three connection fields and reason.
- AC 5 in full: the receipt's
routeblock carriestier_asked,connection_usedandmodel_reportedside by side.model_reportedis the payload's ownmodel, or a reading'sinstrument.model. - AC 7, the surface half:
--routeon the delegating verbs through one helper. Its refusals exit 2 before anything is written, and the receipt carries the override verbatim. - AC 8, the receipt half:
settings_sentrecords the merged map.
Where the blocks land:
- The request block is a
routingmember onintent audit <itd-N>and on a readylaunch shipcut. For the audit it is also a## Routingsection in the request document, outside the hashed prompt. - The receipt is a
routemember on every ingest's result.
Shape decisions (recorded in dcn-2609250000000008):
- The verbs are seven, not eight.
memory ingesthas no roster agent: its distiller is the host session. - The receipt is the ingest's result, not a new durable field.
- The repository layer is read from
layered.RootsFor, the rules loader's root. - Settings are bounded and refused unclean where they are read. The limits are
16 per row, 256 bytes per value, no hidden runes in a string value, and a
--routetext of at most 1024 bytes.
What remains goes to the remainder spec this close mints:
- AC 10: one tier up after a failed fix round, recorded in the state file. It needs the implement loop's state file.
- AC 11: a provider route only on that provider's allowlist. It needs the API adapter (itd-2609081951381895) and adr-2609221009491186's allowlist.
- AC 3's dispatch half and AC 8's adapter half. The resolution of a provider leg and the settings merge shipped here; sending a step to the provider and refusing a setting the adapter does not accept both rest on the same API adapter as AC 11, so the remainder holds them and names that adapter in its Blocked on. AC 3 and AC 8 are met when it lands, not before.
Fix round 1 on the same branch (autonomous run A, 2026-09-25), after review:
- AC 7 is ruled, not met as written. It says a second
--routefor a different agent "applies alongside" the first. Every delegating invocation dispatches exactly one agent, so the surface refuses a--routenaming any other agent, before anything runs, rather than merging or ignoring it. The refusal names the invocation, not the verb. The plan's "a second flag for another agent merges" (item 7 above) is superseded; the ruling is in dcn-2609250000000009. - The request
spec closeemits when it ships an intent carries the## Routingsection, asintent audit <itd-N>does.
Frontmatter
| id | spc-2609180535002478 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-2609180535002478-the-oracle-choice-is-one-repo-wide-value.md
open on GitHub ↗ · commit history ↗
References1
- itd-2609170822093401abcd proposes the model each agent deserves, and facilitates it where it canimplements
mentions13
- adr-2609221009491186A provider adapter serves only the models it lists, under a vendor denylist no listing overrides; everything else runs on the host
- dcn-26092500000000052026-09-25 — The layered configuration resolver has one home, `internal/core/layered`, and two file families; every later consumer reads through it rather than opening a file of its own (lane implementer, autonomous run A, on spc-2609180535002478 part 1). Precedence is flag, then repository, then machine, then bundled, and every value comes back with the layer and the origin that supplied it. The families are `layered.Config`, which is `.abcd/config.json` in the checkout the session resolved and `~/.abcd/config.json` on the machine, and `layered.OracleRouting`, which is `.abcd/config/oracle-routing.json` and `~/.abcd/oracle-routing.json`, the files itd-2609170822093401 names. Scalar keys go in `Config` under a namespace: `pace.*` (itd-2609201925079472), `oracle.review` (itd-6), `roles.<role>.runner` (itd-2609201916056194) and `match.threshold` (itd-2609212137116617). The per-agent routing table keeps its own file, as its intent names, and no new file family is added without an entry here. Loudness: an absent file is an absent layer; a present file that is malformed, symlinked, carries a key twice, trails content, declares the wrong `schema_version`, or (machine layer) is not the caller's own owner-only-writable file is an error naming it; a consumer claims its namespace and every key in it (`Stack.Claim`, with `*` for an open segment such as the role name), and an unknown key under a claimed namespace is refused. Because ahoy writes `oracle.backend` into the shared file, whoever claims `oracle` lists `backend` too. A winning value that does not decode or fails its check is refused, and nothing falls through to a lower layer or to the default.
- dcn-26092500000000062026-09-25 — Two rulings for the model-tier routing table, which the spec leaves open (lane implementer, autonomous run A, on spc-2609180535002478 part 1). First, a table is accepted when a routing file exists at the repository or the machine layer. Only then does the bundled proposal fill in an agent the table has no row for; with neither file, every agent resolves to `none`, the harness at `host-decides`, as AC 1 and the spec's criteria section say. A `--route` alone accepts nothing: it overrides the one agent it names for one run. Second, no agent contract under `agents/` declares a fan-out ceiling, and every agent in the roster is a single prompt that spawns no sub-agent, so each ceiling is 1. The proposal carries it (`oracle.Ceiling`), and a row's `fan_out` above it is reported and clamped. The roster test holds the proposal to `agents/`, so an agent that gains a ceiling field moves the number there.
- dcn-26092500000000082026-09-25 — Three shape rulings for the model-tier routing's surface (lane implementer, autonomous run A, on spc-2609180535002478 part 2). First, `--route` sits on the seven verbs whose step an agent in the `agents/` roster runs: `intent audit` and its ingest, `launch ship`, `disembark review`, `principles`, `press-release` and `graveyard`, and `reading ingest`. The spec listed `memory ingest` as an eighth, but its distiller is the host session itself (`commands/memory.md`), not a roster agent, so no row could govern it and a flag there could only refuse; it gains the flag when a roster agent takes that step. A dual-mode verb's deterministic mode dispatches no agent, reads no routing table and refuses `--route`. Second, the receipt is the ingest's result: a `route` member in its JSON and a `route:` line in its text. It is not a new field in any durable record, because each verb's durable record differs (a CHANGELOG, a lifeboat file, an intent's Audit Notes, a reading run) and a routing fact belongs in none of them; the request block is likewise a `routing` member on the steps that precede an agent (`intent audit <itd-N>`, a ready `launch ship` cut), plus a `## Routing` section in the audit's request document outside the hashed prompt. Third, a routing table that cannot be read refuses a delegating verb at exit 2, where the bare board only omits its lines: the verb is about to ask for a model, and the loud-staging rule does not let it guess past the file that decides which.
- dcn-26092500000000092026-09-25 — One `--route` per delegating invocation, for the one agent it dispatches; a `--route` naming any other agent is refused before anything runs, never merged and never ignored (lane implementer, autonomous run A, fix round 1 on spc-2609180535002478, after the tier2 review's finding 1). AC 7 of itd-2609170822093401 says a second `--route` for a different agent "applies alongside" the first, and the closed spec's plan said it merges. Every delegating invocation dispatches exactly one agent (a dual-mode verb in its payload mode, or `reading ingest` for the position its output names), so a second agent has no step to apply to: merging would record an override nothing used, and ignoring it would let a mistyped agent name read as honoured. The refusal speaks of the invocation, not the verb ("this invocation dispatches <agent>, not <other>"), because `reading ingest` registers all four positions and dispatches one per output. The flag's help and the four command pages say one per invocation instead of "repeatable". A verb that one day dispatches two agents in one invocation passes both to `ParseRoutes` and gets AC 7's "alongside" as written.
- dcn-26092500000000102026-09-25 — The request `spec close` emits when it ships an intent carries the `## Routing` section `intent audit <itd-N>` writes; an unreadable routing table does not refuse the close (lane implementer, autonomous run A, fix round 1 on spc-2609180535002478, after the tier2 review's finding 3). The route is a front-door fact, so the core close still emits the request without it and the front door re-emits the owed request with the section once the intent-auditor's route resolves, with the same one-line fallback on stderr. The close is a record move and its emit is report-only (a failed emit already leaves the intent shipped), so a table that cannot be read leaves that request without the section and one stderr warning names the fault and `abcd intent audit <itd-N>` as the re-emit; refusing the close over a file only its report-only step reads would block the record on the wrong thing. The close carries no `--route` and no `routing` member in its result: it is not a delegating verb, and `intent audit <itd-N>` is the documented way to hand the auditor its request.
- iss-2609081951416843Setup discovery (ahoy) could probe OpenRouter reachability and, when reachable, offer it to the user as an opt-in code-review oracle — once the OpenAI-compatible api oracle adapter ships, OpenRouter is provider config (baseURL https://openrouter.ai/api/v1 + key), and the offer is an ahoy-time question, never an auto-selected backend.
- iss-2609240227236354itd-6 passes its readiness gate (intent ready itd-6: READY, all seven checks ok) but cannot be built from today's tree: its spec spc-2609211950427074 stands on four things that do not exist. (1) The build loop's validator stage the adapter implements, itd-2609201916151817 (planned, spec spc-2609202134338445 open; there is no build verb). (2) The layered oracle.review resolver the model-tier and pacing intents share, itd-2609170822093401 and itd-2609201925079472 (both planned, specs spc-2609180535002478 and spc-2609202134341288 open). (3) The command-line runner the adapters chapter entry sits beside, itd-2609201916056194 (planned, spec spc-2609221533057881 open). (4) An MCP client, which the spec's Approach names as a new dependency subject to the new-dependency sign-off; go.mod has none. itd-6 declares builds_on [itd-2] only, so the gate has no edge to refuse on and READY reads as buildable to a run that picks it.
- iss-2610262036163231itd-2609170822093401's press release says the oracle choice is one repo-wide value that nothing reads; the reading chapter has the ingest resolve that intent's model-tier route before anything runs.
- itd-17Pick the Right Oracle for the Job, Automatically
- itd-2609081951381895abcd ships an OpenAI-compatible API adapter, and a provider serves only the models it lists
- itd-2609180517121254Every payload a host hands back from a delegated step names the model that produced it and the number of agents that ran, and the ingesting verb refuses one that does not
- spc-2609251028149555A failed fix round escalates one tier, and a provider route stays on its allowlist