00-INDEX.md 24 KB

OAC Canonical Refactor — Master Spec Index

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.


Locked Decisions (do not relitigate)

  1. 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.

  2. 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 tierinference.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.

  1. Minimal-tests-first. Ship the smallest test proving the pipeline (golden snapshot

    • parse/manifest check on ONE worked agent). Write the full layered test spec now; implement later layers at the stages defined in 05. Priority is core functionality working end to end.
  2. Incremental, each-stage-shippable. Avoid another stalled mega-PR (cf. #298, +19k lines, now conflicting). Every stage delivers standalone value.

  3. 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.

  4. 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:

  • ⚠️ Trap 1 (silent data loss): a generic parser (gray-matter) finds no frontmatter here and silently drops all priority data — and priority drives context ordering.
  • ⚠️ Trap 2 (false positives): "grep the first marker anywhere" is also wrong. 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.
  • The 3 "YAML" files also carry an MVI marker at line 11 — they are dual-format, not YAML-only.

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).

Corrected Census (verified — earlier numbers were wrong)

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/.

Verified Critical Findings

  1. 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).

  2. 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.

  3. 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.

  4. 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.

  5. 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).

  6. 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.

  7. bundled.ts breaks post-refactor. It identifies the package root by the absence of registry.json — but registry.json will ship inside the package.

  8. 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.

  9. 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.

  10. 🔴 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?

  11. 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).

  12. 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.

  13. 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-opmodel may not be a supported plugin-settings key. Worth an issue; flagged, not asserted.

Target Repo Shape

/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.

Existing Assets to Build On (extend, do NOT reinvent)

  • 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 conflateddomain (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, CapabilityMatrix
  • packages/compatibility-layer/src/adapters/Base, Claude, Cursor, Windsurf. Warning templates already exist at BaseAdapter.ts:187-208reuse 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 + installer
  • registry.json — 107KB component catalog

Worked Example (the alignment reference)

Everyone must align to this. Revised in v2 for the Option A capabilities model.

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.

Precedence — last-match-wins ⚠️ STRONGLY INDICATED (v2.2) — NOT independently confirmed

⚠️ 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:

  • integer-like scopes (e.g. "8080") silently jump to the front under ECMAScript integer-key ordering;
  • duplicate scopes silently collapse.

Both are cheap to validate at parse time. See 02 Q2.

Default semantics — restriction list, not allowlist

⚠️ 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:

  • absent capability → []tool default
  • no rule matched → allow

Defaulting write/edit/bash to deny (as v1 did) would silently break real agents.

Neutral source — /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>

A scoping-heavy agent — /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 claudeagents/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).


Spec Documents

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

Conventions

  • Ground every claim in real files (cite paths). Verify counts before asserting them.
  • End each doc with ## Open Questions for anything needing a human decision.
  • Spec only — no implementation code.