Model Citizen by Jake Selby v0.13.1
GitHub
View source
Doc

What is supported

Path
docs/compatibility.md
Lines
262

The architecture is model-provider agnostic: your rules, skills, roles, workflows and personal stances have one source. That does not mean every runtime implements every capability. compatibility/catalog.json is the versioned authority; harness compatibility --json emits it. A separate compatibility and release policy defines the stable v1 interfaces, preview boundary, versioning, deprecation and migration rules. A qualified entry requires native evidence for its exact runtime, client and platform. Unqualified means no complete passing evidence, planned means no current integration, and unsupported means a combination explicitly outside the integration contract.

Claude Code and Codex are this release’s integration targets. The v0.13.1 stable support floor qualifies the Claude Code CLI on macOS and Linux; against v0.13.0 it changes the landing copy only. The Codex CLI is outside that contract until a scripted qualification round agrees with a hand-driven one, so the v0.11.1 stable floor remains the last one qualifying the Codex CLI on macOS and Linux. The Claude Code and Codex VS Code surfaces, Codex Desktop on macOS and the Claude Code plugin-marketplace install remain unqualified previews. A marketplace install carries the skills, roles, commands and output style only; the ownership journal, stance selection, the Codex projection and the hooks come from bin/harness install, and runtime installation states the difference. Do not read successful source generation or deterministic tests as native client qualification.

What qualified means at each level

Client qualification and capability qualification are separate claims, and this is the default rule reconciling them; a maintainer decision may replace it, in this section:

A client is qualified when its required acceptance cases pass natively. A capability is qualified for a client only when that client is qualified AND a native acceptance case exercising that capability exists and passed; otherwise the capability is unqualified and inherits nothing from the client. The catalog is the single authority: per-capability state is derived from the adapters’ capability files at harness compatibility time and rendered beside the client row, never hand-edited in two places.

An adapter names the cases that exercise a capability with an optional acceptance_cases list on a stance entry or on role_execution in adapters/<runtime>/capabilities.json; every name in it must be one of the catalog’s required_cases. Listing cases under a capability that is not qualified, or claiming qualified without them, is a contradiction between the two files: harness compatibility --release-check blocks the release and the test suite fails. Today no adapter names a case and no client is qualified, so every cell below reads unqualified, which is what the table says. harness compatibility --json emits both levels, each client row carrying its derived capabilities. The capability-by-client layout follows the generated matrix in wshobson/agents’ docs/harnesses.md.

The table’s last row answers a different question: not whether a capability carries native evidence, but whether the delegation stance’s model-tier ceiling binds on that surface. tier_restriction in adapters/<runtime>/capabilities.json declares it per runtime, with a without_hooks entry for a client that installs no hooks — "installs_hooks": false on a catalog client, which today is the plugin-marketplace install. The Claude Code CLI and VS Code surfaces read enforced, because claude/hooks/tier-agent-spawns.py rewrites an Agent call that asks for the strongest class by model: down to the class below unless the role it names declares that class itself. Every Codex surface reads advisory: the coordinator does run there, and a Codex Agent call still passes the role, marker, evasion and brief checks, but the tier rewrite is behind a runtime == "claude-code" gate in lib/harness_core/lifecycle.py, so the ceiling reaches Codex as projected prose alone. The marketplace install reads advisory for the plainer reason that it installs no hooks at all.

Qualified: claude-code-cli-macos, claude-code-cli-linux.

Unqualified: claude-code-vscode-macos, claude-code-plugin-marketplace, codex-cli-macos, codex-vscode-macos, codex-desktop-macos, codex-cli-linux.

Planned: cursor, grok.

A client’s status is not a capability’s status. Each cell is derived from that runtime’s adapters/<runtime>/capabilities.json at generation time:

Capabilityclaude-code-cli-macosclaude-code-vscode-macosclaude-code-cli-linuxclaude-code-plugin-marketplacecodex-cli-macoscodex-vscode-macoscodex-desktop-macoscodex-cli-linux
autonomyunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
build-vs-buyunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
commitsunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
costunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
delegationunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
licensingunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
plan-ceremonyunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
role_executionunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
testingunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
voiceunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualifiedunqualified
tier restrictionenforcedenforcedenforcedadvisoryadvisoryadvisoryadvisoryadvisory

The last row is not a qualification state. It says whether the delegation stance’s model-tier ceiling is enforced (a hook rewrites or refuses the spawn), advisory (prompt text only) or none, carried advisory by primitives/skills/delegation-tiering/SKILL.md, primitives/stances/delegation/tiered.md; enforced by claude/hooks/tier-agent-spawns.py. enforced is narrower than it sounds. It never reaches the session’s own model: the model settings key is one this harness never writes (docs/settings-ownership.md). Within a session it rewrites a spawn only while the selected delegation variant is tiered — off stops the spawn instead, and any other variant leaves it alone — and only while the adapter’s class table maps at least two models, since one class is no ladder to move a spawn down. Under every other condition the ceiling is prose, exactly as advisory is everywhere.

Hosted agents and native memory merging remain deferred. The architecture-viewer binding is a preview for separately installed custom adapters. A local protocol 1 candidate passed process-level harness acceptance; no viewer is bundled, and native viewer interaction and distribution/license clearance remain unverified. Native Windows is unsupported; WSL2 has not been qualified.

A model provider supplies the model. An agent runtime orchestrates its tools and context. A client surface is the CLI, editor integration or desktop app exposing that runtime. Cursor belongs in the runtime/client integration catalog, with model providers described separately: selecting the same underlying provider does not prove equivalent Cursor behavior.

Qualification procedure

The qualification runbook covers the mechanics of a run: the acceptance runner’s invocation, its disposable homes and the credentials it passes through. Start with an isolated test user/configuration home and a disposable repository. Record exact runtime and client versions, operating system, source commit, date, configuration, commands, and observed results. Never commit credentials, full private transcripts or personal settings. On macOS, run harness keychain <home> on any home you build by hand before a client is launched under it; a home without a default keychain raises a system dialog, and a home whose keychain cannot be created does not launch a client. Never repoint your own default keychain or search list. For every target listed in the catalog, verify every required_cases entry natively:

  1. Install, restart, and inspect effective instructions, discovered skills and registered roles.
  2. Switch a representative communication and delegation stance; observe both instruction text and agent/tool behavior. Repeat with a custom dimension, project override and invalid choice.
  3. Exercise manual, auto and acknowledged bypass postures against native restrictions. Read each posture twice: the permission mode the sync wrote into the client’s own settings, and what the client then did with one file write. A bypass posture must be refused by the sync until it is acknowledged, and the refused sync must leave the mode where it was. Judge the acknowledged bypass from the turn’s own permission denials and mode, never from the written file alone: a turn the model declined on its own judgement observed no permission control and is unverified, not a block.
  4. Check hook trust, composition, denials and multi-file patches; attempt writes from read-only roles and outside the planner artifact scope. Configuration defaults are insufficient proof.
  5. Change staged and untracked files after a green gate; check reruns and unverified failures.
  6. Drive the spawn hook in a disposable home against a fixture recipe built from a descriptor in policy/integrations/: confirm a brief carrying a constrained role’s work is denied with the isolated-worker instruction whether it is spawned unnamed, under a generic subagent type or under a band worker’s name; and that the refusal offers exactly that descriptor’s declared input roots, and no others, as the read roots the worker is limited to. The recipe spawn is denied, so routing is observed on a separate plain spawn that carries none of the recipe’s text: confirm it runs as the cost variant’s default band worker at that row’s class and effort with the budget sentence in its brief, and that a null variant rewrites nothing. No third-party framework’s own workflow is run in a qualification round; that is the optional suite in BMad.
  7. Continue the same task Claude→Codex and Codex→Claude, including changed-tree and stale-writer cases; establish permissions anew. Verify migration, drift and uninstall preserve user data.
  8. Select a non-default cost variant and sync; confirm that only the roles it changes are rewritten and that every other role keeps its link. In a session started after that sync, spawn a subagent that names no role, and confirm from the subagent’s own transcript that it ran as the variant’s default band worker at that row’s model and effort, that its brief ends with the budget sentence, that the usage feed reported its spend against that budget, and that harness usage --rescan --by role records the routed row. Confirm that a session started before the workers were installed keeps them out of its session record and that its spawn still succeeds. A headless client runs each turn as its own process, so such a session is continued by resuming it, and the resumed process loads the workers from disk and announces them; its spawn may then route to the announced band worker, but never to one that neither its record nor that announcement named. Then select a variant with the feed off, no default band and no budgets, and confirm that none of this occurs. On a runtime that does not route native spawns, verify the posture through an isolated role worker’s model and effort and the budget sentence in a named role’s brief, and record the feed as not applicable with that reason.
  9. Run a review layer of a framework named by a descriptor in policy/integrations/ as a native subagent that names no role, with the brief carrying the framework’s own spawn text as its workflow hands it to the client, and confirm the spawn is refused and the refusal names the framework, the layer and harness role run <role>. Recognition is lexical: a brief the client rewrites in its own words, keeping the layer’s prompt file but none of the descriptor’s phrases, is not refused. Run that rewording as well and record what it did, as the observed limit of the claim rather than as a pass or a failure. Confirm the same layer run the routed way writes isolated worker state and returns findings, and that a session with no worker state written is a failed case rather than a passed review. Then the false positive: spawn ordinary work whose brief mentions review, a diff or findings in passing, and one that edits the framework’s own input roots, and confirm both run.

Store a redacted JSON evidence artifact with kind: native, client, harness_version, source_commit, runtime_version, client_version, platform, observations, cases and invalidation_scope, with the case values passed, failed, or unverified. Each case observation opens with its case name and a colon, one per case in the record’s sorted case order, so pairing never depends on position; a round-level note the round runner appends carries no case prefix. Add its path and SHA256 to the client entry. Evidence cannot be reused for another client or harness version. Its full source commit must be an ancestor of the release with no subsequent change under the paths that invalidate this target. Set exact runtime/client versions before changing status to qualified. Each linked record must match the catalog’s exact runtime version, client version and platform. Linked failed or unverified results block qualification even if another record passes the same case. When a rerun supersedes a record, remove the old reference from the active claim while preserving the historical evidence file. Unknown cases and result values are rejected. The runner appends each finished case to a durable log as the case completes, so a killed round costs the case it was running rather than the round; rebuild the surviving cases into a record with --from-progress, and link that partial record as the partial record it is. The CLI verifies these records and harness compatibility --release-check fails until all required clients are qualified. A reviewer must assess the observations; a JSON label alone is not empirical evidence.

Where each target runs, and what it needs

The required targets are the four CLI rows the v0.11.1 stable floor qualified. Every round to date, 0.9.0 through 0.11.1 on all four, ran on one maintainer-owned Apple-silicon Mac; there is no other qualification host and no hosted runner. What each target needs on that host:

  • codex-cli-macos. The binary is the one bundled in the ChatGPT desktop app, /Applications/ChatGPT.app/Contents/Resources/codex, linked onto PATH as ~/.local/bin/codex. The app updates it, so the version is whatever codex --version reports on the day and the record names it (0.11.1 recorded 0.154.0-alpha.6.2). It authenticates with a ChatGPT account session, codex login, which writes ~/.codex/auth.json; no API key is used. The host is the Mac itself.
  • codex-cli-linux. npm’s @openai/codex at the version the round pins (0.11.1 recorded 0.155.1), installed by scripts/linux-target.Dockerfile. The host is a linux/arm64 container under Docker Desktop on the same Mac, with the frozen clone mounted into it. How it authenticated is not recorded: the 0.11.x records say only that each probe ran with a throwaway home and Codex home. The runbook’s in-container codex login is a suggestion, not what those rounds are known to have done.
  • claude-code-cli-macos. The native Claude Code install, version from claude --version. It authenticates with a credential the acceptance runner passes through by name, an Anthropic API key or a cloud profile; an interactive claude login does not reach the runner’s disposable home. The host is the Mac itself.
  • claude-code-cli-linux. npm’s @anthropic-ai/claude-code at the pinned version (0.11.1 recorded 2.1.278), in the same container and authenticated the same way, the variable passed in by name with docker run -e.

The runbook has the commands that establish each of these before a round, and python3 scripts/smoke_tier.py --targets <ids> refuses the round at once when a client, a Codex login or the Docker daemon is missing. A Codex login is a session, not a key, and the acceptance runner’s disposable CODEX_HOME does not yet carry it, so a Codex target’s evidence can only be produced by hand until it does; no Codex target is qualified for the current source.

If a release cannot qualify the Codex or the Linux targets, it narrows the support floor the last qualified release set. That narrowing is stated in this page’s opening section as a decision, as v0.12.0’s is, and is never a target silently left out of a round.

Which source change invalidates which evidence

Evidence is invalidated per target, not per repository. A target’s path set is the shared runtime source — VERSION, bin, lib, adapters, primitives, policy, templates, config.example.json — minus every other runtime’s adapter directory, as the catalog’s evidence_invalidation block maps them, except for the files inside such a directory that shared code reads whatever runtime is running. Those are carved back into the shared set and invalidate every target. The block names them: today bindings.json (harness tiers checks both adapters’ class tables in one command), capabilities.json (stance coverage and catalog reconciliation read every runtime’s) and worker.py (harness role run --runtime chooses the adapter by flag, so either runtime’s worker is reachable from either session). Only hook.py is private to its runtime: a client executes its own runtime’s hook, and harness sync writes the other one’s path into a config file without reading it.

So a fix confined to adapters/codex/hook.py leaves the Claude Code targets of a round standing, and the reverse holds. A change to shared source, to a carved-out file in any adapter directory, or to a file under adapters/ that no runtime owns, still invalidates every target.

The scope fails closed. A runtime the catalog does not map is excluded from nothing and keeps the whole-source rule; a declared path that is not that runtime’s own adapters/<runtime> directory, or that names a runtime no client runs, is rejected; a declaration that carves out no shared file at all is rejected rather than trusted; exclusions are emitted as literal pathspecs so no glob or .. can widen them; and a record that carries no invalidation_scope, or one whose scope is malformed or differs from what the catalog grants, is checked against the whole source or refused. Each record states the scope it was validated under, so a reviewer reads the assumption from the artifact instead of recomputing it.

tests/test_adapter_directory_isolation.py holds the declaration to the source: it parses every tracked Python file under the shared paths — failing on one it cannot parse, rather than skipping it — asserts it still finds the loaders it is meant to cover, and fails when shared code builds a path to an adapter file the block does not declare, names another runtime’s directory outright, or reaches an adapter through a symlink. What it cannot prove is the runtime_files half: that hook.py is only ever loaded for the runtime whose session is running is a maintainer’s reading of the call sites, and a wrong entry there is coupling this scope would miss.

Per-case scoping, which would invalidate only the acceptance cases whose declared source paths changed, is not implemented. It would replace the published requirement with “no change under the paths a maintainer believes this case depends on”, losing any coupling the map does not model, and the evidence requirements are a v1 stable interface. That is an owner decision and a policy edit, not a quiet patch; it waits on one. See #333.

A released catalog pins the exact source commit its evidence qualifies. Later development does not rewrite or invalidate that historical release record, but any change under the runtime-source paths makes harness compatibility --release-check fail until the new source has its own candidate catalog and native evidence.

Runtime controls records current enforcement gaps. Adapter coverage in harness stances --json distinguishes instruction policy, hooks and settings, including custom stances which are advisory by default. No preference overrides a native restriction.

↑↓ move↵ openPagefind · indexed at build