From 1f5980c1010bf35355d199a70b704b172c60ff12 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Fri, 1 May 2026 13:26:44 +0300 Subject: [PATCH] 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 --- README.md | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/README.md b/README.md index 19b7bd4..b07506c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,137 @@ opencode, Claude Code, and Feynman use **three different agent frontmatter forma 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