How gateline stays vendor-neutral

No role in gateline knows which model executes it. Roles are contracts over files, models are bindings resolved in a single registry, and runtimes attach through thin, generated adapters. That separation is enforced in the repository, and it is what lets an adopter change vendors, harnesses, or both without rewriting the pipeline.

The principles, and the two that do the work

Six principles govern the design: artifacts over conversation (P1); roles are contracts, models are bindings (P2); a runtime-neutral core with thin adapters (P3); a human at phase gates (P4); decorrelation where it counts (P5); and extension by adding roles, not tuning knobs (P6). Two of them do the neutrality work. P2 rules out hardcoding a claude-* or gpt-* identifier in any role definition, and the rule is enforced as a repository invariant: no vendor or model name may appear in roles/ or contracts/. Model IDs live only in the registry.

P5 is the technical argument. Models from the same family share blind spots: an error the implementer's model reliably makes is often an error that same model reliably fails to catch. Binding the Reviewer and Verifier to a different lineage than the Implementer decorrelates those failure modes cheaply. The design's name for the ruled-out alternative is the same model grading its own homework.

A closed role roster

The software development lifecycle distills to eight operations, and the roster maps them to eight roles. The Orchestrator decomposes intent and routes work without ever writing content. The Analyst turns intent into a testable spec. The Architect produces the technical plan, work breakdown, and decision records. The Implementer executes one work item on a branch. The Reviewer reads the diff adversarially against spec and plan. The Verifier independently exercises the behavior end to end. Ops prepares the release and rollback plans. The Historian periodically reconciles the surrounding prose (docs, changelog, tracker) with the run record. Reviewer and Verifier are separate on purpose: review is reading, verification is running, and collapsing them recreates rubber-stamp reviews.

The roles/ directory holds one more spec: the Integrator, the role that adopts the framework into a host repository, with its own gate (GI). It is not a ninth SDLC role: the pipeline roster is eight roles plus the Integrator. The set is closed: adding a role takes an explicit maintainer decision recorded in an issue, because P6 makes roles the only extension surface. The Historian is the worked example: when doc drift became painful, the role cost one spec, one contract, and one registry binding, with no change to any other role or contract.

DESIGN.md §4.2 draws the line finer. Implementer, Reviewer, Verifier, and the Orchestrator are evidence-bearing positions: the record's claims are about them, so they are as closed as the gates. The other roles are closed by governance. A host repository may add roles of its own, and the rule for them is open table, closed gates: an adopter role may put more evidence on an existing gate's table, and it may never mint a gate.

Each role spec carries its mission, operating instructions, definition of done, and explicit escalation triggers, with frontmatter declaring its capabilities and a capability profile, a tier of model ability, not a model. One role is never rendered to a runner at all: the Orchestrator is executed by a human in v0 or by the engine in v1, so no adapter ships an agent file for it.

One file owns the models

registry/models.yaml is the only place vendor and model IDs exist. Roles bind to capability profiles instead of models: frontier-reasoning for where judgment concentrates (architecture, review, orchestration, cost-insensitive because these roles' errors are the expensive ones), balanced for the workhorse tiers, and fast-cheap for high-volume, low-stakes work. A bindings map attaches each role to a profile, and the Reviewer and Verifier entries carry avoid_vendor_of: implementer, the P5 pin written as data, so a human dispatching by hand or the engine dispatching automatically can enforce it.

Resolution runs role → capability profile → registry profile → concrete vendor/model ID, and then to the dispatching adapter's own spelling of that ID. That spelling is what lands in the ledger and what the pricing lookup keys on, so a dispatch is priced and recorded as what actually ran. Changing vendors is an edit to the registry, then re-spelling the binding in each adapter's manifest and re-rendering; no role spec or contract ever changes. The same file holds the pricing map used to meter dispatches and the static per-role estimates behind the pre-flight budget check.

Two honesty notes. The shipped model IDs are marked ILLUSTRATIVE: pin your organization's approved models and re-verify pricing when you do. And the OpenRouter pricing entries are a wired fallback off the live path: the opencode adapter reports real per-dispatch cost directly, and the entries key the token-based fallback if that report ever stops carrying cost.

From role to running model. The role spec declares a capability profile; the registry resolves the profile to a vendor and model; the adapter manifest re-spells that binding for its runner; the rendered agent file is the output.
Binding resolution: role spec → registry → adapter manifest → rendered agent file. The zone boundary between box 1 and boxes 2–4 is where vendor names become allowed. roles/reviewer.md capability_profile: frontier-reasoning writes_code / capabilities resolves profile registry/models.yaml profiles → vendor/model IDs bindings: reviewer { avoid_vendor_of: implementer } pricing: · dispatch_estimates_usd: re-spells binding adapters/<name>/manifest.json model_map / model_overrides (runner spellings) tool_map · headless dispatch gateline render (+ CI --check) .github/agents/ reviewer.agent.md RENDERED … DO NOT EDIT no vendor or model names (enforced invariant) vendor/model IDs allowed swap vendors = edit registry + manifests, re-render — role specs and contracts untouched

Adapters are rendered, not written

Three adapters ship. claude-code renders .claude/agents/ subagents. copilot-cli renders .github/agents/ custom agents. opencode renders .opencode/agents/ agents against its any-provider catalog. gateline render, from the Node package @gateline/framework, generates each agent file from the role spec (the body verbatim, because agents start cold and the spec doubles as their instructions) plus the adapter's manifest.json, which supplies the frontmatter shape, the capability-to-tool map, and the runner's model spellings. That package is the only reader of role specs and manifests, and it takes no runtime dependencies on purpose, so a host repository's render check (node packages/framework/src/main.ts render --check) runs with nothing installed. Rendered files carry a do-not-edit header, and CI fails any pull request whose renders are stale: drift between a role spec and what a runner executes is structurally impossible.

An adapter may narrow a role (fewer tools, tighter permissions) but never widen it; the role spec is the ceiling. In the opencode adapter the rule is a rendered property: each agent carries a deny-by-default permission map, so a harness capability the role never granted stays denied. The v1 engine dispatches through the same manifests: a headless section gives the invocation template and the usage-report parsing spec, so nothing above the dispatch seam knows which harness ran. Supporting a new runner costs one manifest. The roles are never restated per runner.

The decorrelation gradient, stated honestly

gateline supports cross-vendor pinning by design, and it is exercised where the harness allows. The three adapters sit at different points on that gradient, and the framework's own documentation says so.

AdapterModel surfaceP5 decorrelation
claude-codeOne vendor's models Partially honored. The interim fallback is a lineage split: the reviewer runs a different model lineage (fable) than the implementer (sonnet).
copilot-cliAnthropic, OpenAI, and Google models nativelyFully honored. The manifest binds the reviewer and verifier to different vendors than the implementer (illustrative IDs: gpt-5.4 and gemini-3-flash against a claude-sonnet-5 implementer). Its usage report is static-estimate: until per-invocation usage is verified live, metering records the registry's per-dispatch estimate with token counts null.
opencodeAny provider in its Models.dev catalogThree-way in its proof-of-concept bindings. Implementer, reviewer, and verifier land on three distinct labs (DeepSeek, Moonshot, MiniMax), where "vendor" means the underlying lab, not the OpenRouter routing layer.

Where more than one vendor is live, enforcement is mechanical: the dispatch seam refuses to bind the Reviewer or Verifier to the Implementer's vendor. The run records show the gradient as well: in the human-orchestrated dupefind run, every implementer ledger entry is one model lineage and every reviewer entry another, under the single vendor that harness hosts.

The neutrality claim, whole: the judgment encoded in roles and contracts is portable; model preference is a registry edit; runner preference is one manifest. Each piece is checkable in the repository, and none of it requires trusting a vendor's roadmap — including the framework's own.

For manifest schemas and the headless sections, see the adapter manifests reference; for the handoff formats the roles exchange, the contract grammar reference. For the packages that implement the engine, server, CLI, and UI, see the API documentation.