ASCII diagrams + prose covering:
- Three-source storage model (one persona, three formatted parked archives,
three lazy mirrors) and why it differs from opc-skills' one-source model
- Data flow for enable/disable with the "[T] not parked" graceful-skip path
- INDEX.json schema tracking parked.{oc,cl,fy} + active[] across 3 targets
- Interactive pick / disable-pick flows: union-of-parked for pick (so a
feynman-only agent like researcher is reachable), union-of-actives for
disable-pick, target-picker UX
- --keep-primary semantics across the 3 frontmatter formats
- The fzf {} shell-escape gotcha and the correct quoting pattern
opc-agents
Multi-target agent manager for opencode + Claude Code + Feynman. Per-target parked sources (different formats), shared CLI surface. Companion to opc-skills.
Why
opencode injects every agent's frontmatter into the calling agent's tool registry. With 100+ agents the registry alone burns ~280K tokens of context before the user types a prompt. Park the ones you don't need this week.
opencode, Claude Code, and Feynman use three different agent frontmatter formats:
| target | tools field | extra fields |
|---|---|---|
| opencode | permission: { edit, bash, webfetch, … } |
mode: (primary/subagent), temperature: |
| claude | tools: Read, Glob, Grep, … (PascalCase) |
color: |
| feynman | tools: read, write, grep, … (lowercase) |
thinking:, output:, defaultProgress:, inheritProjectContext: |
opc-agents keeps a separate parked source per target and copies the appropriate format for each --target selection. The upstream personas/build.py pipeline can produce all three formats from one persona definition, so a --target all enable can deliver the same persona to all three platforms in their native format.
Architecture
Storage model — one persona, three formatted parked sources, three lazy mirrors
Unlike opc-skills (one parked source, three identical mirrors), opc-agents has three parked sources because the on-disk frontmatter format differs per target. The upstream personas/build.py pipeline emits all three from one persona spec.
┌────────────────────────────────────────┐
│ personas/ (upstream repo) │
│ build.py --install <variant> │
│ ↓ emits 3 archives │
└───────────────┬────────────────────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ agents-opencode-│ │ agents-claude- │ │ agents-feynman- │
│ archive/ │ │ archive/ │ │ archive/ │
│ │ │ │ │ │
│ <name>.md │ │ <name>.md │ │ <name>.md │
│ description │ │ description │ │ description │
│ mode: primary │ │ tools: Read, │ │ tools: read, │
│ | subagent │ │ Glob, Grep │ │ write, edit │
│ permission:{} │ │ color: yellow │ │ thinking: high │
│ temperature │ │ │ │ output: foo.md │
│ │ │ │ │ defaultProgress │
│ INDEX.json ─────┼──────┴─────────────────┴───────┘ (canonical) │
│ INDEX.md ──────┼─ tracks parked.{oc,cl,fy} + active[] for all 3 │
└────────┬────────┘ ┌────────┬────────┐ ┌─────────┬────────┘
│ cp │ │ cp │ │ │ cp
▼ │ ▼ │ │ ▼
┌─────────────────┐ │ ┌─────────────────┐ │ ┌──────────────────┐
│ ~/.config/ │ │ │ ~/.claude/ │ │ │ ~/.feynman/ │
│ opencode/agents │ │ │ agents/ │ │ │ agent/agents/ │
└─────────────────┘ │ └─────────────────┘ │ └──────────────────┘
│ │
no cross-target conversion: opc-agents only ships
bytes; format selection happens upstream in build.py
Why 3 separate parked dirs (not 1)
opc-skills can use one parked dir because a SKILL.md is platform-neutral markdown. An agent file's frontmatter is platform-specific: opencode parses permission:, claude parses PascalCase tools:, feynman parses lowercase tools: + thinking:. Putting them in one dir would require runtime format conversion — instead, opc-agents reads from the matching format for each --target.
Practical implication: enable --target all <name> succeeds only if <name>.md exists in all three parked archives. If only agents-feynman-archive/researcher.md exists, --target all researcher enables to feynman and prints [opencode] not parked / [claude] not parked.
Data flow — enable / disable
enable <name> --target T1,T2,...
│
├──► for each target T:
│ src = PARKED_<T>/<name>.md
│ if src missing: print "[T] not parked" and skip
│ else: cp src → ACTIVE_<T>/<name>.md
│
└──► reindex picks up new presence on next call
disable <name> --target T1,T2,...
│
└──► rm ACTIVE_<T>/<name>.md (PARKED untouched)
INDEX schema
Single index in agents-opencode-archive/INDEX.json tracks all 3 targets:
{
"name": "frodo-russia",
"persona": "frodo",
"variant": "russia",
"mode": "subagent",
"domain": "intelligence",
"description": "Strategic Intelligence Analyst — Russia desk…",
"parked": { "opencode": true, "claude": true, "feynman": true },
"active": ["opencode", "claude"]
}
parked.<target> reflects whether that target's archive currently contains the agent (the build pipeline can produce a persona for some targets and skip others). active[] tracks where it's currently enabled.
Interactive flows
pick: disable-pick:
───── ────────────
① fzf: pick category ① fzf: pick category
(base persona prefix from (from union-of-actives across
union of all 3 parked dirs) opencode + claude + feynman)
② fzf --multi: pick agents ② fzf --multi: pick agents
each row annotated each row annotated
[parked-in:oc,cl,fy] [active-in:oc,cl,fy]
preview: first parked file preview: first active file
③ fzf: pick target(s) ③ fzf: pick target(s) to remove from
┌───────────────────────────┐ same picker
│ all (default) │ ENTER on "all"
│ opencode (N active) │ = remove from every target
│ claude (N active) │ where it's currently active
│ feynman (N active) │
└───────────────────────────┘
[TAB multi-select supported]
④ cp per-target ④ rm per-target
missing-in-parked → skip missing-in-active → skip
with [T] not parked with [T] not active
pick uses union-of-parked for the category list so a feynman-only agent (e.g. researcher) is reachable without manually switching --target.
--keep-primary semantics
disable-all --keep-primary skips agents with mode: primary — but only opencode-format files have that field. Claude and feynman agents have no mode:, so they're never primary by definition and --keep-primary is effectively a no-op for those targets.
fzf {} quoting (gotcha)
fzf shell-escapes {} substitutions automatically. Wrapping {} in extra outer quotes embeds those quote literals into the path:
# ❌ becomes: sed -n '1,40p' ".../agents-archive/'name'.md"
--preview="sed -n '1,40p' \"$PARKED/{}.md\" 2>/dev/null"
# ✅ becomes: sed -n '1,40p' ".../agents-archive"/'name'.md
# (bash concatenates adjacent quoted/unquoted strings cleanly)
--preview="sed -n '1,40p' \"$PARKED\"/{}.md 2>/dev/null"
All preview lines in bin/opc-agents follow the second pattern.
Install
ln -s ~/Documents/opc-agents/bin/opc-agents ~/.local/bin/opc-agents
Ensure ~/.local/bin is on your PATH.
Layout it expects
~/Documents/personas/agents-opencode-archive/ # parked, opencode format
<name>.md (frontmatter: mode, permission, …)
INDEX.json + INDEX.md (canonical index — also tracks claude+feynman parked/active)
~/Documents/personas/agents-claude-archive/ # parked, claude format
<name>.md (frontmatter: tools: Read, Glob, …)
~/Documents/personas/agents-feynman-archive/ # parked, feynman format
<name>.md (frontmatter: tools: read,write,… thinking, output, defaultProgress)
~/.config/opencode/agents/ # active, opencode target
~/.claude/agents/ # active, claude target
~/.feynman/agent/agents/ # active, feynman target
(~/Documents/opencode-agents-parked is symlinked to personas/agents-opencode-archive for backward compat.)
Override via env:
OPC_AGENTS_PARKED— opencode parked source (defaultpersonas/agents-opencode-archive)OPC_AGENTS_CLAUDE_PARKED— claude parked source (defaultpersonas/agents-claude-archive)OPC_AGENTS_FEYNMAN_PARKED— feynman parked source (defaultpersonas/agents-feynman-archive)OPC_AGENTS_ACTIVE— opencode active dir (default~/.config/opencode/agents)OPC_AGENTS_CLAUDE_ACTIVE— claude active dir (default~/.claude/agents)OPC_AGENTS_FEYNMAN_ACTIVE— feynman active dir (default~/.feynman/agent/agents)OPC_AGENTS_TARGETS— comma-separated default targets (defaultopencode,claude,feynman)
Targets
enable, disable, and disable-all accept --target / -t (opencode, claude, feynman, all, or comma-separated subset). Default is opencode,claude,feynman. both is preserved as a legacy alias for opencode,claude (no feynman).
An agent missing in a target's parked source is skipped with a clear message — there is no automatic format conversion at the opc-agents layer. Cross-format conversion happens upstream in personas/build.py.
Commands
Inspection
status |
counts of active vs parked across all 3 targets + opencode primary/subagent breakdown |
list {active|parked|all} |
list agent names |
categories / cats |
prefix-based base-persona counts (PARKED) |
variants / vars |
suffix-based variant counts (PARKED) — eg. salva, iran |
Single-agent operations
enable [--target T] <name> |
enable single agent (copy parked → active) in selected targets |
disable [--target T] <name> |
disable single agent (remove from active; keep parked) |
disable-all [--target T] [-y|--yes] [--keep-primary] |
disable every active agent (asks for confirmation) |
Bulk by axis
enable-category <prefix> / disable-category <prefix> |
fzf multi-pick by base persona prefix (eg. frodo, marshal) |
enable-variant <suffix> / disable-variant <suffix> |
bulk by variant suffix (eg. salva, iran, russian-doctrine) |
enable-domain <domain> / disable-domain <domain> |
bulk by Domain: value in description (live scan) |
Interactive / search
pick |
fzf: union-parked category → multi-select agents → target picker → enable |
disable-pick |
fzf: union-of-actives → category → multi-select → target picker → disable |
search [query] / disable-search |
fzf fuzzy search across name + mode + domain + variant + description |
reindex |
rebuild INDEX.json / INDEX.md (extracts persona/variant/domain from frontmatter; tracks all 3 targets) |
fzf is required for the interactive pickers; jq for the search variants; python3 for reindex.
pick flow
- Choose a category from the union of parked agents across all 3 targets (so a feynman-only agent like
researcheris reachable viapick) - Multi-select agents — each row shows
[parked-in:oc,cl,fy]indicators - Target picker appears:
all/opencode/claude/feynman—TABfor multi-select;ENTERon highlightedall(default) sends to all three - cmd_enable copies the appropriate parked file per target; targets without that agent in their parked dir are skipped with
[t] not parkedmessage
disable-pick flow
Lists the union of all active agents across the three targets, with each entry annotated [oc,cl,fy] showing where it's currently active. After multi-select, the target picker decides which target(s) to remove from.
Notes
- Each agent is a single
<name>.mdfile with YAML frontmatter; format depends on target (see table above). disable*commands never delete data — they remove the active copy and keep (or restore) the parked copy. Re-enable withopc-agents enable <name>.--keep-primaryondisable-allskips agents withmode: primary(opencode-format only — claude and feynman don't have amode:field).- Common agent prefixes seen in personas:
frodo-,marshal-,sentinel-,bastion-,neo-,oracle-,warden-,polyglot-, etc.
Implementation notes
fzfplaceholder{}is shell-escaped automatically — preview commands use"$PARKED"/{}.md(variable quoted,{}unquoted) so bash concatenation produces a clean path.
Differences from opc-skills
opc-skills |
opc-agents |
|
|---|---|---|
| Unit | directory containing SKILL.md |
single <name>.md file |
| Frontmatter fields used | name, description, domain, subdomain, tags |
description, mode, tools, permission |
| Format per target | identical (folder + SKILL.md) | different per target (oc / claude / feynman) |
| Parked source-of-truth | one (skills-archive) |
three (one per target format) |
| Shared refs sync | yes (_platform-mapping.md) |
n/a |
disable-all flags |
-y |
-y, --keep-primary |
| Status breakdown | active/parked | + opencode primary/subagent counts |