Files
opc-agents/README.md
salvacybersec 1f5980c101 docs: add Architecture section to README
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
2026-05-01 13:26:44 +03:00

15 KiB

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 (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] <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)
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 / feynmanTAB 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 <name>.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 <name>.
  • --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 <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