ADR 0022: Declarative model policy
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-06-01 |
| Deciders | Project maintainers |
| Supersedes | — |
| Superseded by | — |
| Related ADRs | ADR-0011 (Zod schema library), ADR-0008 (no auto-anything) |
Summary
aidokit gains an optional, purely declarative model policy:
.aidokit/model-policy.json. A new @aidokit/core Zod schema
(ModelPolicySchema) describes it, loadModelPolicy(projectRoot) reads and
validates it, and an opt-in aidokit doctor --model-policy check reports its
status. The file maps a task type to a recommended model. aidokit
recommends; it never selects, routes, or invokes a model. There is no
orchestration, no routing layer, and zero LLM calls.
The file is not emitted at init — emitting it would change the
byte-compare dogfood gate fixtures. It is a runtime artifact the user authors;
aidokit only validates and surfaces it.
Context
Issue #7 asked for model routing inside the emitted workflow. True routing —
aidokit choosing and dispatching a model per task — is out of scope: it
implies LLM traffic and orchestration, both of which are permanent non-goals
(aidokit makes zero LLM calls; the user's own CLI handles all model traffic).
The legitimate, in-scope kernel of #7 is recording a project's preferred model per task type so the user's agent runtime can consult it. That is a declarative data file plus validation — deterministic, hermetic, no network.
The shape mirrors existing data-driven catalogs (ADR-0007): a typed Zod schema, a one-file edit to change, validated at load time.
Decision
1. Schema in @aidokit/core
const ModelPolicyEntrySchema = z
.object({
model: z.string().min(1),
reasoning: z.string().min(1).optional(),
maxTokens: z.number().int().positive().optional(),
})
.strict();
const ModelPolicySchema = z
.object({
version: z.literal(1),
policies: z.record(z.string().min(1), ModelPolicyEntrySchema),
default: ModelPolicyEntrySchema.optional(),
})
.strict();
.strict() follows the catalog convention — unknown fields are rejected to
catch typos at load time.
2. Loader + validator
loadModelPolicy(projectRoot)reads.aidokit/model-policy.json. Missing file →{ status: 'absent' }(not an error — the file is optional). Unparseable JSON or schema violation →{ status: 'invalid', error }. Valid →{ status: 'valid', policy }. Usesfs/promises; never throws on a missing or malformed file.validateModelPolicy(value)validates an already-parsed value and returns a flattened, human-readable error string on failure.
3. Opt-in doctor check
A DoctorCheckSource (model-policy, requiresFlag: 'modelPolicy', flag
--model-policy):
- absent →
MODEL_POLICY_ABSENT(pass, informational) - present-and-invalid →
MODEL_POLICY_INVALID(warn, includes the Zod error) - valid →
MODEL_POLICY_OK(pass)
Opt-in so projects that don't use a policy file see no noise.
4. Not emitted at init
aidokit init does not write the file. The byte-compare dogfood gate (CLAUDE.md
§10) compares emitted trees byte-for-byte against committed fixtures; emitting a
new file would require regenerating every fixture for no behavioural gain. The
policy is a runtime convenience, not part of the scaffolded surface.
Consequences
Positive
- Captures the in-scope kernel of #7 (recording a model preference) without any routing, orchestration, or LLM traffic.
- One typed schema, one-file edit to change a policy — matches the catalog convention.
- Opt-in doctor check keeps the default output quiet.
Negative
aidokitdoes not act on the policy. Consumption is entirely the responsibility of the user's agent runtime. This is intentional but may surprise users expecting routing.
Neutral
- The
version: 1literal leaves room for a future migration if the shape needs to grow (ADR-0007-style non-breaking field additions remain available).
Alternatives considered
Alternative 1: True model routing inside the emitted workflow
Rejected — implies LLM calls and orchestration from aidokit itself, both
permanent non-goals.
Alternative 2: Emit a starter model-policy.json at init
Rejected — changes byte-compare fixtures across all adapters for no behavioural gain. The file is optional and user-authored.
Validation
This decision is validated when:
- [ ]
ModelPolicySchemaround-trips a valid document and rejects unknown fields, wrongversion, and entries missingmodel. - [ ]
loadModelPolicyreturnsabsent/invalid/validcorrectly. - [ ]
aidokit doctor --model-policyreportsMODEL_POLICY_ABSENT,MODEL_POLICY_INVALID, orMODEL_POLICY_OK. - [ ]
aidokit initemits no.aidokit/model-policy.json(byte-compare gate unchanged).
References
Internal
- ADR-0007 — MCP catalog shape (data-driven catalog convention).
- ADR-0011 — Zod schema library.
- CLAUDE.md §10 — the dogfood byte-compare gate.