# opc-agents **Multi-target** agent manager for **opencode** + **Claude Code** + **Feynman**. Per-target parked sources (different formats), shared CLI surface. Companion to [`opc-skills`](https://gitea.taygun.net.tr/salvacybersec/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 │ │ ↓ emits 3 archives │ └───────────────┬────────────────────────┘ │ ┌──────────────────────────┼──────────────────────────┐ ▼ ▼ ▼ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ agents-opencode-│ │ agents-claude- │ │ agents-feynman- │ │ archive/ │ │ archive/ │ │ archive/ │ │ │ │ │ │ │ │ .md │ │ .md │ │ .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 ` succeeds only if `.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 --target T1,T2,... │ ├──► for each target T: │ src = PARKED_/.md │ if src missing: print "[T] not parked" and skip │ else: cp src → ACTIVE_/.md │ └──► reindex picks up new presence on next call disable --target T1,T2,... │ └──► rm ACTIVE_/.md (PARKED untouched) ``` ### INDEX schema Single index in `agents-opencode-archive/INDEX.json` tracks all 3 targets: ```json { "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.` 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: ```bash # ❌ 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 ```bash 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 .md (frontmatter: mode, permission, …) INDEX.json + INDEX.md (canonical index — also tracks claude+feynman parked/active) ~/Documents/personas/agents-claude-archive/ # parked, claude format .md (frontmatter: tools: Read, Glob, …) ~/Documents/personas/agents-feynman-archive/ # parked, feynman format .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 (default `personas/agents-opencode-archive`) - `OPC_AGENTS_CLAUDE_PARKED` — claude parked source (default `personas/agents-claude-archive`) - `OPC_AGENTS_FEYNMAN_PARKED` — feynman parked source (default `personas/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 (default `opencode,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] ` | enable single agent (copy parked → active) in selected targets | | `disable [--target T] ` | 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 ` / `disable-category ` | fzf multi-pick by **base persona** prefix (eg. `frodo`, `marshal`) | | `enable-variant ` / `disable-variant ` | bulk by **variant suffix** (eg. `salva`, `iran`, `russian-doctrine`) | | `enable-domain ` / `disable-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 1. Choose a category from the **union of parked agents across all 3 targets** (so a feynman-only agent like `researcher` is reachable via `pick`) 2. Multi-select agents — each row shows `[parked-in:oc,cl,fy]` indicators 3. **Target picker** appears: `all` / `opencode` / `claude` / `feynman` — `TAB` for multi-select; `ENTER` on highlighted `all` (default) sends to all three 4. cmd_enable copies the appropriate parked file per target; targets without that agent in their parked dir are skipped with `[t] not parked` message #### `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 `.md` file 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 with `opc-agents enable `. - `--keep-primary` on `disable-all` skips agents with `mode: primary` (opencode-format only — claude and feynman don't have a `mode:` field). - Common agent prefixes seen in personas: `frodo-`, `marshal-`, `sentinel-`, `bastion-`, `neo-`, `oracle-`, `warden-`, `polyglot-`, etc. ## Implementation notes - `fzf` placeholder `{}` 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 `.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 |