opencode-era record. Superseded where moot by the pi migration (ADR-0055). Retained as historical context.
Date: 2026-07-28
Accepted
Supersedes ADR-0029 (unified setup.json config — its .opencode/setup.json
location, jq reader, schema-v4 whole-file selection, and 3-tier identity
fallback are replaced by the dual-prism.jsonc design below). Supersedes
ADR-0032's explicit rejection of JSONC (§6 and the "Migrate setup.json →
setup.jsonc" alternative) — JSONC is now adopted project-wide for the
manifest. ADR-0032's empty-committed-env.* security invariant is preserved.
Implementation is tracked by issue #276 and the plan at
docs/plans/2026-07-28-prism-jsonc-migration.md. This ADR records the
decisions; the codebase cutover follows the plan's 13 TDD tasks.
ADR-0029 consolidated three config files (.opencode/models.default.env,
.opencode/experimental.default.env, .opencode/setup.json) into a single
.opencode/setup.json (schema v4) parsed by jq, with a 3-tier identity
fallback (user → project → git config). ADR-0032 added an optional env
key for MCP-server secrets and explicitly rejected migrating to JSONC
because jq cannot parse it.
Two forces now drive a change:
- Users want to comment their configuration. The manifest carries five
model tiers, five variant tiers, three experimental flags, identity
fields, scaffold bookkeeping, and an
envsecret section. JSON's comment-hostility makes a documented, self-explaining manifest impossible inline; users must cross-referencemcp.mdandmodel-configuration.mdto understand each field. - The project has matured to deserve a top-level manifest identity.
.opencode/setup.jsonis an implementation detail masquerading as a config location. A root-levelprism.jsoncmakes the manifest a first-class project artifact, visible at the repository root alongsideAGENTS.md,CONTEXT.md, andopencode.jsonc.
Active readers/writers/enforcement of .opencode/setup.json (pre-migration
blast radius, 35+ touchpoints): .envrc, resolve-identity.sh,
setup-scaffold.sh, /setup (.opencode/commands/setup.md),
migrate-setup.sh, setup-write-user-config.sh, check-setup-secrets.sh,
pre-commit (staged-blob guard), ci.yml (both jobs), seven test files,
and eight living-docs files. Historical ADRs (0024, 0026, 0029–0033, 0040)
and completed specs/plans reference the old path but are immutable records.
We replace both legacy setup.json manifests with schema-v5 prism.jsonc
manifests, parsed by one dependency-free, comment-preserving PHP 8.5 JSONC
reader shared across all consumers. The ten settled decisions follow.
The project manifest moves from .opencode/setup.json to prism.jsonc
at the repository root. It is the single source of project-level truth.
.opencode/setup.json is removed entirely after migration. There is
no project-level fallback — no "both exist" branch, no silent
fall-through. The migration engine deletes the legacy file only after the
replacement is atomically written, reparsed, and verified as schema v5.
The user manifest moves from ~/.config/opencode/setup.json to
~/.config/opencode/prism.jsonc. Resolution is a recursive
field-by-field overlay: project defaults first, then user values.
Object keys merge recursively; arrays and scalars are replaced atomically
by the user value. Missing user fields inherit project values.
~/.config/opencode/prism.jsonc is genuine JSONC parsed by the same
shared reader as the project manifest. One parser serves both tiers.
/setup migrates ~/.config/opencode/setup.json → prism.jsonc, removes
the old file, and emits a deprecation warning if the old setup.json
is detected afterward (e.g. a user re-edits the wrong file). The
user-level legacy cannot be force-removed across all machines (it lives
outside the repo), so the warning is the safety net.
setup_version bumps 4 → 5. The on-disk format, both locations, and
the overlay semantics all change — version 5 is the explicit migration
boundary and prevents ambiguity with legacy v4 files. migrate-setup.sh
refuses a downgrade.
The shared reader supports full JSONC: full-line // comments,
trailing // comments, and block /* */ comments (single- and
multi-line).
Note: This overrides the
@architectreview's recommended contract ("full-line//only; trailing/block fail closed"). The human chose full JSONC for consistency with common JSONC tooling. The reader therefore cannot be a regex stripper — it must be a real state-machine tokenizer that correctly handles//and/* */sequences inside string literals, escaped quotes/backslashes (\",\\), multi-line block comments, and nested structures. This is a meaningful complexity cost, accepted deliberately.
Trailing commas are allowed in objects and arrays (e.g. {"a":1,}).
The tokenizer strips them only outside strings and comments. This matches
common JSONC expectations and is consistent with decision 7's "full JSONC"
posture.
Migration is invoked both ways:
migrate-setup.shis the engine — scriptable, CI-friendly, idempotent (a no-op when already migrated). It handles the dual rename (project + user), validates before deleting, and refuses downgrade/conflicts./setupinvokesmigrate-setup.shautomatically on entry, so a user who runs/setupself-heals without a separate manual step.
/setup performs in-place field patching, not wholesale template
regeneration. It modifies only the specific owned fields, preserving every
user-authored comment and every unrelated field. Applying the same update
twice is byte-identical on the second pass.
Complexity cost: This requires the tokenizer to support round-trip comment-preserving serialization — replacing value spans in place and inserting missing leaves at the owning object's closing brace while leaving every other byte untouched. This is the dominant implementation risk (see Consequences).
A dependency-free PHP 8.5 boundary under .github/scripts/ exposes
PrismJsoncDocument (parse, file load, root access, source retention,
span-preserving withValues() patch, atomic writeAtomic()) and
PrismManifest (recursive resolve(), validateProject(),
validateUser()), plus a narrow CLI (prism_manifest.php) for shell
consumers (validate, decode, env0, get, values0, patch,
migrate-preview, migrate, check-secrets). The reader works before
vendor/autoload.php exists; no Composer dependency is added.
A missing user manifest is valid. Each of the following fails closed with a redacted diagnostic (no secret values in output): a missing project manifest, a malformed project or user manifest, a duplicate object key, an unsupported schema version, an unsafe symlink, excessive size (1 MiB), or excessive nesting (64 levels). There is no silent fall-through from a malformed higher-priority file.
env.*values in committedprism.jsoncremain empty. The staged-blob guard (check-setup-secrets.sh+pre-commit+ both CI jobs) enforces this at the new path.- User writes are atomic, mode
0600, and reject symlink targets. Project writes are mode0644. .envrcdoes notevalconfiguration. The CLI transports allowlisted environment names and values as NUL-delimited pairs, buffered and validated before the first byte is written.
/setup is human-invoked only. It may read, write, chmod, and remove
only ~/.config/opencode/prism.jsonc and the legacy
~/.config/opencode/setup.json (during migration), creating
~/.config/opencode/ when absent. No agent invokes /setup
autonomously; no other path under ~/.config/opencode/ is touched. This
is recorded as a narrow exception to AGENTS.md's "Do not modify files
outside the project directory" Hard Boundary.
- Commentable configuration — both manifests self-document via inline
comments; users no longer cross-reference
mcp.md/model-configuration.mdto understand each field. - Single shared reader — one dependency-free PHP tokenizer replaces
jqacross.envrc,resolve-identity.sh,setup-scaffold.sh,/setup,migrate-setup.sh,setup-write-user-config.sh,check-setup-secrets.sh, and the pre-commit guard. Nojqtoolchain dependency remains. - Top-level manifest identity —
prism.jsoncat the repo root is a first-class artifact alongsideAGENTS.md,CONTEXT.md, andopencode.jsonc. - Explicit migration boundary — schema v5 + dual rename makes the
cutover unambiguous;
migrate-setup.shrefuses downgrade. - Round-trip safety —
/setuppreserves user comments and unknown fields across rewrites; byte-idempotent on repeat invocation.
- Tokenizer complexity — the dominant risk. Full JSONC lexical edge
cases (comment markers in strings, escaped delimiters, multi-line block
comments, duplicate keys, source-span insertion) require a real
state-machine parser, not a regex stripper. Task 1 of the plan carries
extra review weight; the plan recommends a focused
@architectre-review after Task 2 nails the public API. - Round-trip serialization cost — in-place field patching with comment preservation is harder than wholesale regeneration. Replacing scalar spans is tractable; insertion into sparsely formatted or comment-heavy objects must remain deterministic and byte-idempotent.
- Dual-rename blast radius — 35+ active and historical references. Historical ADRs/plans/specs remain immutable; active source and living docs must reach zero accidental legacy reads.
- User-level legacy cannot be force-removed across all machines —
~/.config/opencode/setup.jsonlives outside the repo. Migration relies on/setupinvocation plus a deprecation warning when the old file is detected afterward. Machines that never run/setupretain a stale, ignored file. - One-way schema migration — rolling back requires manual schema
editing. The deleted
.opencode/setup.jsonis preserved in git history (recoverable if rollback is absolutely required).
{env:VAR}substitution inopencode.jsoncis unchanged — the exported variable names and values remain identical, so indirect consumers need no path changes.- The five model tiers (PRIMARY/PLANNER/DESIGN/JUDGE/UTILITY) are unchanged.
- CI workflow structure is unchanged; only the secret-guard step names and validated path change.
Rejected by the human. Full-line-only is the simplest contract and the hardest to misuse, but the human chose full JSONC for consistency with common JSONC tooling and to avoid claiming "JSONC" while rejecting widely supported constructs. The accepted complexity cost is a real tokenizer rather than a regex stripper.
Rejected. Simpler to implement (no round-trip serialization), but discards
user-authored comments on every /setup invocation — defeating a primary
motivation for the migration (decision 1's "users want to comment their
configuration"). In-place field patching preserves comments and unknown
fields.
Rejected. Inconsistent with the project file (two formats, two parsers) and forfeits comment support for user configuration. The human chose a single JSONC format and shared reader across both tiers.
Rejected. The human wants prism.jsonc as the single source of truth with
no project-level fallback. A fallback tier adds a "both exist" branch,
silent fall-through risk, and drift between two project files.
Superseded. With the user manifest renamed and field-by-field overlay adopted, identity resolution becomes a 2-tier resolved-view lookup (project
- user overlay) with
git configas the identity fallback when the resolved pair is incomplete. Git configuration is an identity fallback, not a third manifest tier.
adr/0029-unified-setup-json-config.md(superseded)adr/0032-mcp-server-onboarding.md(JSONC rejection superseded; empty-env.*invariant preserved)adr/0007-setup-token-strategy.md(scaffolding token substitution, unaffected)adr/0026-project-scaffolding.md(scaffold bookkeeping fields, relocated)adr/0030-design-primary-agent-and-tier.md(jq-fallback precedent, superseded by schema bump)docs/plans/2026-07-28-prism-jsonc-migration.md(implementation plan, 13 TDD tasks).opencode/docs/model-configuration.md.opencode/docs/mcp.mdCONTEXT.md(Prism manifest glossary terms + entity)