Status: Specification — v2 (post spec-pass reconciliation). No implementation yet. Goal: Re-architect OAC from an OpenCode-specific project into a tool-agnostic, npm-distributed system with a single neutral source of truth that generates per-tool outputs (OpenCode, Claude Code, Cursor, Windsurf).
This index is the authoritative shared context for all spec workstreams. Read it first. Where a spec document conflicts with this index, this index wins.
Single source of truth = a new tool-neutral /content/ directory.
OpenCode, Claude Code, Cursor, Windsurf are all generated build targets via
oac build --target <tool>. .opencode/ stops being source and becomes build
output. No tool is privileged — OpenCode is just another adapter.
No hardcoded models. Canonical model defaults to null = "use the tool's
default model." Content never bakes in a model. Supersedes PRs #311 and #324
(both rejected). Open: whether model should be authorable in /content/ at all,
or exist only in the IR as always-null (see 02 Open Questions).
⚠️ v2.2 — GAP: model: null destroys deliberate cost tiering. The shipped CC agents
are 5 sonnet + 2 haiku — and the two haiku agents are context-scout and
external-scout. That is not an arbitrary model choice; it is an intentional cost tier
(cheap/fast models for retrieval-shaped work). Flattening both to null silently discards
that intent with no replacement, and would push the scouts onto an expensive default.
Proposed resolution: add a neutral semantic tier — inference.tier: fast | balanced | deep
(default balanced) — expressing intent rather than a model name. Adapters map tier →
whatever that tool's fast/deep model is; tools without tiers drop it with a warning. This
preserves the optimization while keeping content model-agnostic, satisfying decision #2 in
spirit rather than merely in letter. Needs ratification — it adds a field to the IR.
Minimal-tests-first. Ship the smallest test proving the pipeline (golden snapshot
05. Priority is core functionality
working end to end.Incremental, each-stage-shippable. Avoid another stalled mega-PR (cf. #298, +19k lines, now conflicting). Every stage delivers standalone value.
Permissions = ordered rule list + scalar sugar (Option A). ← decided v2
The neutral capabilities model is an ordered list of {scope, decision} rules
per capability, with scalar sugar for the simple case. Rationale: the previously
specified flat model (edit: deny) is provably lossy — verified against real
agents, it cannot express coder-agent's deny-all-then-allowlist (which would break
its ability to run its own router.sh), loses per-agent security globs
(**/*.env*, **/*.key), and erases the deliberate distinction between
coder-agent (deny-by-default) and openagent (ask-by-default).
Consequence: safety globs live naturally in content — no separate guards hint
and no adapter-injected policy needed.
Context metadata stays MVI HTML-comment on disk. ← decided v2
296 context .md files use a single compact line:
<!-- Context: standards/code | Priority: critical | Version: 2.0 | Updated: … -->
(verified: 286 HTML-comment, 3 YAML ---, 7 neither). This is deliberate —
it is far more token-efficient than multi-line YAML and is read by the model on every
load. Do NOT migrate to YAML frontmatter. The IR parser normalizes this format
in-memory. The authored on-disk format and the IR shape do not have to match.
Two parser traps — both verified. This is the highest-risk parser requirement in the project:
gray-matter) finds no frontmatter
here and silently drops all priority data — and priority drives context ordering.openagents-repo/core-concepts/agents.md has a marker at line 232 and
categories.md at line 301 — both are prose about the format. A naive parser
assigns standards/code | critical as those files' real metadata.
The parser MUST only honor a leading window (line 1, or the first line after a
closing YAML ---) and treat every later marker as body text.Enum must not be strict: Priority: reference exists in real content
(core/workflows/lightweight-context-handoff-example.md) outside the documented
critical|high|medium|low set. A strict z.enum rejects a real file. Also, 4 markers
omit Version/Updated — so only Context and Priority may be required.
Verified distribution (v2.2 correction): high 112, critical 111, low 34,
medium 29, reference 1 — counted over the leading window only.
⚠️ *An earlier count of 116/113/34/31/1 was wrong: it grepped for the marker anywhere
in the file and swept in the line-232/301 prose-about-the-format markers — i.e. it
committed Trap 2 while attempting to verify Trap 2.* Any script counting or parsing
these markers must apply the leading-window rule, including the "first line after a
closing YAML ---" case (3 dual-format files carry their marker at line 11).
| Item | Correct | Previously claimed |
|---|---|---|
| Agents | 34 .md |
39 (= 34 + 5 0-category.json); one spec said 160 — false |
| Commands | 20 .md |
29 (= 20 + 9 .yaml eval templates) |
| Context | 296 .md (286 HTML-comment / 3 YAML / 7 neither) |
297 |
| Skills (OpenCode) | 6 across two dirs (v2.2 correction) | 2 — wrong |
| Skills (Claude Code plugin) | 12 (v2.2 correction) | ~11 — wrong |
⚠️ v2.2 — OpenCode skills live in TWO differently-named directories:
.opencode/skill/ (2: project-orchestration, task-management) and .opencode/skills/
(4: context-manager, context7, smart-router-skill, task-management) — with
task-management duplicated across both. The earlier "2" was wrong and propagated into
02 and 03. Which of the two task-management copies is authoritative is unresolved.
⚠️ v2.2 — 3 SYMLINKS in the context tree, previously unmentioned by every doc:
core/standards/code.md → code-quality.md, docs.md → documentation.md,
tests.md → test-coverage.md. They (a) break Windows checkouts — git materializes them
as text files containing the target path unless core.symlinks=true, which 04's Windows
section does not cover; (b) break build determinism; and (c) are the same three files as
01's duplicate-id finding — so the proposed "collapse to aliases[]" would delete them and
break the files referencing the alias paths directly. Needs an explicit decision.
Non-finding (corrected): an earlier claim of "committed .env files needing
rotation" is false. Verified: 0 tracked .env files; .env is gitignored
(.gitignore:9-13). No security incident. The only real point is build hygiene — a
filesystem-globbing build must not sweep local .env files into /content/.
Bun defeats the npm goal (Stage 0 blocker). bin/oac.js does
execFileSync('bun', …) → "Bun is required." 15 files use Bun-only APIs
(Bun.file/Bun.write across installer.ts, sha256.ts, ide-detect.ts;
import.meta.dir in bundled.ts). npm i -g without Bun fails outright —
defeating the entire reason for npm distribution and likely explaining Windows
install bugs (#304, #312). Fix is mechanical: node:fs/promises, node:crypto,
fileURLToPath(import.meta.url).
Dependency resolution is broken — 4 converging signals. add.ts never walks
dependencies[]; install.sh's recursive resolver swallows jq errors via
|| echo "" (unknown refs → zero deps, no error); issue #310's reporter
independently diagnosed "broken dependency chains from incomplete installation."
Likely consequence: /add-context installs none of the three standards it tells
agents to follow. Fixing this will surface currently-hidden breakage.
Seeding /content/ is a MERGE, not a copy. Drift is bidirectional — Claude
Code is ahead in places: ~11 skills vs OpenCode's 2 (disjoint sets);
hand-authored <example> blocks existing nowhere in .opencode/; and
session-start.sh (skill catalogue, first-run onboarding, context-discovery
bootstrap, injection defense) has no OpenCode equivalent — arguably the most
important runtime feature, living in a file currently slated for deletion.
Seeding from .opencode/ alone destroys all of it.
ClaudeAdapter.ts targets the wrong layout. It writes the project layout
(.claude/agents/*.md, .claude/config.json); the authoritative format is the
plugin layout (.claude-plugin/plugin.json, flat agents/, hooks/hooks.json,
bundled context/). As written it produces something CC cannot load as a plugin.
CapabilityMatrix.ts lies in two rows. agentModes: claude = full → should be
partial (plugin format has no per-agent mode:; every agents/ file is a
subagent, so role: primary flattens). agentCategories: claude → likely none
(survives only as plugin.json keywords).
Version mismatch: 5 numbers across 7 files (verified): VERSION/root package.json
= 0.7.1, packages/cli = 1.0.0, compatibility-layer = 0.1.0, evals/framework = 0.1.1,
marketplace = 1.0.0, CC plugin cache = 1.0.2. The cache is ahead of the
marketplace entry it's built from, which would suppress the /plugin update that
delivers any fix. Must be reconciled before any CC release.
bundled.ts breaks post-refactor. It identifies the package root by the absence
of registry.json — but registry.json will ship inside the package.
oac apply is already ~80% of oac build (loadAgents → adapter.fromOAC → write).
build is that pipeline promoted, re-rooted at /content, with OpenCode as a peer target.
The CLI is already safer than the bash scripts — sha256 manifest gating means user
edits survive updates, whereas update.sh blind-curl-overwrites everything. Migration
selling point, not just parity.
🔴 LIVE SECURITY GAP — coder-agent on Claude Code (exists in production today).
coder-agent.edit lists only denies with no * rule ⇒ implicit default allow ⇒ CC
grants Edit and all five security globs vanish (**/*.env*, **/*.key,
**/*.secret, node_modules/**, .git/**). Verified: the shipped plugin declares
tools: Read, Write, Edit, Glob, Grep. The live CC coder-agent can edit .env/.key/
.secret files that OpenCode's coder-agent is explicitly denied. This is a defect in
the current product, not a refactor risk. No adapter-side fix exists (CC cannot express
path-scoped denies per-agent). Open (03 Q3): should this BLOCK the build rather than warn?
coder-agent bash → OMIT Bash, fail-closed. Including it grants unrestricted shell
to an agent authored as "no shell except two exact commands" — a silent escalation from a
build step nobody inspects. Omitting it is a loud, visible, recoverable functional break.
The asymmetry decides: a wrong deny yields a broken agent; a wrong allow yields rm -rf in
a repo the owner believed sandboxed. Corroborated — the hand-authored plugin already ships
exactly this (no Bash).
WindsurfAdapter.ts:511-516 is broken, but fails CLOSED (corrected — an earlier claim
that it escalates to unrestricted shell was wrong). It does
const hasAllow = permObj.allow !== undefined — but OpenCode permissions are glob-keyed
({"*": "deny", "cmd*": "allow"}), so no key named allow ever exists and hasAllow is
always false. Every granular permission collapses to deny: over-restrictive, not a
privilege escalation. Real bug; needs replacing, not re-pointing.
The shipped CC agents hardcode model: sonnet — violates locked decision #2. Must
become null/omitted when /content/ is seeded from the CC side (see finding #3: seeding
is a merge, so CC-side content carries this in).
Unverified: settings.json {"model": "opusplan"} (#264, 40dd267) is plausibly a
no-op — model may not be a supported plugin-settings key. Worth an issue; flagged,
not asserted.
/content ← THE source of truth (neutral; context keeps MVI HTML-comment metadata)
/agents /skills /commands /context
registry.json ← component catalog
/packages
/core ← Zod IR schema + parser + registry loader (from compatibility-layer/{types,core})
/adapters ← opencode | claude | cursor | windsurf (from compatibility-layer/adapters)
/cli ← oac init|add|update|build|doctor (from packages/cli, absorbs install.sh)
/evals ← behavioral tests (unchanged; a free end-to-end gate — see 05)
To be deleted (only after the minimal golden test is green): install.sh, update.sh,
scripts/bridge/sync-to-claude.sh, plugins/claude-code/ (generated), .opencode/ as source.
⚠️ session-start.sh must be preserved into /content/ before plugins/claude-code/ is deleted.
packages/compatibility-layer/src/types.ts — Zod OpenAgentSchema (the IR).
⚠️ Known defects: ModelIdentifierSchema = z.union([z.string(), z.string()]) is a
no-op union (line 116) with no null default — cannot enforce decision #2; three
overlapping identity carriers (frontmatter + metadata + OpenAgentSchema.metadata +
sidecar) → collapse to one flat schema; category is conflated — domain
(core/development) in agent metadata vs distribution tier (essential/standard)
in registry.json → split into category + profiles.packages/compatibility-layer/src/core/ — TranslationEngine, AdapterRegistry,
AgentLoader, CapabilityMatrixpackages/compatibility-layer/src/adapters/ — Base, Claude, Cursor, Windsurf.
Warning templates already exist at BaseAdapter.ts:187-208 — reuse their exact output.packages/compatibility-layer/src/mappers/ — Context, Model, Permission, Tool.
⚠️ ContextMapper anchors .opencode/context as privileged base → must become /content/.packages/cli/src/** — init/add/update/doctor/status/list/apply + ide-detect + registry + installerregistry.json — 107KB component catalogEveryone must align to this. Revised in v2 for the Option A capabilities model.
capability: <scalar> # sugar → [{ scope: "*", decision: <scalar> }]
capability: [ { scope, decision }, … ] # full ordered form
delegate: { name: decision, … } # map sugar → [{ scope: name, decision }, … ]
decision ∈ allow | deny | ask. Simple agents use sugar and stay simple; the nested form
appears only when scoping actually matters. Map sugar is accepted for any capability
— which is OpenCode's exact on-disk shape, making the OpenCode importer a lossless desugar.
delegate is not a special case: it is simply the capability whose scopes are agent ids
rather than globs.
⚠️ Epistemic correction (v2.2). v2.1 called this "CONFIRMED via three independent
sources." That was an overclaim — all three are documents this project wrote about
itself, not primary evidence of OpenCode's resolver. Worse, the primary citation
(12-MASTER-SYNTHESIS.md:432) describes a JSON array permission format
([{ "deny": "bash(**)" }, …]) that does not exist in any agent — they all use YAML
maps (bash: { "*": "deny" }). It is aspirational planning prose, not a description of
reality. The conclusion is still probably right (first-match-wins is provably wrong
against the corpus, and the authored order only makes sense under last-match), but
primary verification against OpenCode's actual resolver is still REQUIRED before Stage 1.
In-repo sources (self-authored — corroborating, not probative):
docs/archive/planning/12-MASTER-SYNTHESIS.md:432 — *"The permission: field uses last-match-wins
evaluation (same as OpenCode's native system)"*.opencode/context/openagents-repo/standards/permission-patterns.md:11 — *"OpenCode v1.1.1+
uses permission: … Rules follow last-matching-wins evaluation order."*.opencode/context/openagents-repo/standards/agent-frontmatter.md:45 — "*": "ask" # Catch-all (last-match-wins)No further verification against OpenCode's resolver is required. Two agents reached this conclusion independently before the documentation was found.
⚠️ v2 correction — the earlier "first-match-wins" was wrong and security-regressing. Real agents author broad-first, specific-after:
coder-agent bash: { "*": deny, "…router.sh complete*": allow, … }
openagent bash: { "*": ask, "rm -rf /*": deny, "sudo *": deny, … }
Under first-match-wins, "*" matches everything first and every later rule is unreachable:
coder-agent's router.sh would be denied (breaking the exact behavior decision #5 exists
to protect) and openagent's sudo * would degrade deny → ask (a security regression).
Last-match-wins makes the authored content correct as written and maps 1:1 to OpenCode
key order. Most-specific-wins also produces correct results and agrees with last-match-wins
on all 34 agents, but requires defining a glob-specificity ranking.
Last-match-wins needs zero reordering transform and requires no migration of the 34 agents. First-match-wins would force order-reversal on both serialize and parse, plus an order-reversing migration of every agent file.
⚠️ Residual risk — order-as-semantics is an implicit contract. YAML mappings are unordered per spec, so OpenCode's key order is a convention its parser honors, not a guarantee. Two concrete failure modes the IR MUST validate against:
"8080") silently jump to the front under ECMAScript
integer-key ordering;Both are cheap to validate at parse time. See 02 Q2.
⚠️ v2 correction. An absent capability does not mean deny. OpenCode's permission
block is a restriction list: openagent declares no write: key at all yet relies on
write being allowed (verified). Therefore:
[] → tool defaultDefaulting write/edit/bash to deny (as v1 did) would silently break real agents.
/content/agents/code-reviewer.md---
id: code-reviewer
name: Code Reviewer # adapters slugify per tool
role: subagent # primary | subagent
category: development # domain (distribution tier lives in registry `profiles`)
description: Review code for security, correctness, and quality before commit.
tags: [review, security, quality]
capabilities: # INTENT, not any tool's syntax
read: allow # sugar
grep: allow
glob: allow
edit: deny
write: deny
bash: deny
delegate: { contextscout: allow }
inference:
temperature: 0.1
model: null # null ⇒ tool default (no hardcoded models)
context:
- { path: core/standards/security.md, priority: high }
dependencies: [subagent:contextscout]
examples:
- { context: "coder finished an auth service", user: "check it?", assistant: "Running code-review before commit." }
---
<system prompt / instructions — authored once>
/content/agents/coder-agent.md (why Option A exists)capabilities:
bash: # deny-all-then-allowlist: flat CANNOT express this
- { scope: "*", decision: deny }
- { scope: "bash …/router.sh complete*", decision: allow }
- { scope: "bash …/router.sh status*", decision: allow }
edit:
- { scope: "**/*.env*", decision: deny } # real security controls
- { scope: "**/*.key", decision: deny }
- { scope: "**/*.secret", decision: deny }
- { scope: "node_modules/**", decision: deny }
- { scope: ".git/**", decision: deny }
delegate: { contextscout: allow, externalscout: allow, TestEngineer: allow }
oac build --target opencode → .opencode/agent/subagents/code/reviewer.md---
name: CodeReviewer # id → PascalCase
description: Review code for security, correctness, and quality before commit.
mode: subagent # role → mode
temperature: 0.1 # supported → preserved
permission: # capabilities → OpenCode granular block
bash: { "*": "deny" }
edit: { "**/*": "deny" }
write: { "**/*": "deny" }
task: { contextscout: "allow" }
---
Metadata → agent-metadata.json sidecar; no model: line (null → OpenCode default).
OpenCode round-trips Option A exactly — same expressive power, reserialized.
oac build --target claude → agents/code-reviewer.md---
name: code-reviewer # id → kebab-case
description: |
Review code for security, correctness, and quality before commit.
Examples:
<example>Context: coder finished an auth service. user: "check it?" …</example>
tools: Read, Glob, Grep # capabilities allow-set → tools allowlist
---
context[] → bundled into plugin context/, injected via SessionStart hook; contributes
to .claude-plugin/plugin.json.
⚠️ 2 warnings (corrected in v2.1 — was "1"): (a) temperature dropped — CC has no
per-agent temperature; (b) delegate: {contextscout: allow} is map sugar for a scoped
rule, so dropping Task is a real loss and "never silent" demands a warning. The emitted
file is identical either way; only the count changes. This is an expected consequence of
Option A — under the old flat model, delegate carried no scope. coder-agent = 4 warnings.
✅ CC settings.json — question CLOSED, negative (v2.1). Plugins cannot ship
permissions at all: plugin settings.json supports only the agent and
subagentStatusLine keys, and our CC target is a plugin — anything the adapter wrote
would be ignored. Even via user settings it fails: rules are project-scoped (restoring
coder-agent's intent needs deny: ["Bash(*)"] project-wide, breaking every other agent),
and the agent's tools: allowlist gates the tool regardless.
Resolution: emit documentation, not configuration — a RECOMMENDED-PERMISSIONS.md with
an opt-in snippet, deny rules only (they over-apply in the safe direction; hoisting an
allow would re-create the escalation one layer up).
| File | Scope | Status |
|---|---|---|
01-feature-inventory.md |
Feature inventory; universal vs OpenCode-specific; ~150-item preservation checklist | ✅ v1 |
02-canonical-schema.md |
Neutral Zod IR for all 7 content types | ✅ v2 (Option A + MVI parser) |
03-adapter-specs.md |
Per-tool transforms, layouts, capability matrix, warnings | ✅ v2 (Option A) |
04-cli-build-distribution.md |
CLI commands, build pipeline, npm distribution, install.sh parity | ✅ v1 |
05-impact-migration-tests.md |
Impact, staged migration, minimal + deferred test spec | ✅ v1 |
## Open Questions for anything needing a human decision.