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
This commit is contained in:
131
README.md
131
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.
|
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:
|
||||||
|
|
||||||
|
```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.<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:
|
||||||
|
|
||||||
|
```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
|
## Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Reference in New Issue
Block a user