diff --git a/README.md b/README.md index abaaaad..19b7bd4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ # opc-agents -**Multi-target** agent manager for **opencode** + **Claude Code**. Per-target parked sources (different formats), shared CLI surface. Companion to [`opc-skills`](https://gitea.taygun.net.tr/salvacybersec/opc-skills). +**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 and Claude Code use **different agent frontmatter formats** (opencode: `permission: { … }`, claude: `tools: Read, Glob, …`), so opc-agents keeps a separate parked source per target. +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. ## Install @@ -21,13 +29,17 @@ Ensure `~/.local/bin` is on your `PATH`. ``` ~/Documents/personas/agents-opencode-archive/ # parked, opencode format .md (frontmatter: mode, permission, …) - INDEX.json + INDEX.md (canonical index — also tracks claude side) + 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.) @@ -35,13 +47,17 @@ Ensure `~/.local/bin` is on your `PATH`. 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_TARGETS` — comma-separated default targets (default `opencode,claude`) +- `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`, `both`/`all`). Default is `opencode,claude`. An agent missing in a target's parked source is skipped with a clear message — there is **no automatic format conversion**. +`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 @@ -49,7 +65,7 @@ Override via env: | | | |---|---| -| `status` | counts of active vs parked + primary/subagent breakdown | +| `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` | @@ -58,9 +74,9 @@ Override via env: | | | |---|---| -| `enable ` | enable single agent (copy parked → active) | -| `disable ` | disable single agent (remove from active; keep parked) | -| `disable-all [-y\|--yes] [--keep-primary]` | disable every active agent (asks for confirmation) | +| `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 @@ -74,25 +90,43 @@ Override via env: | | | |---|---| -| `pick` / `disable-pick` | fzf: pick category → multi-select | +| `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) | +| `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 (typically `description`, `mode`, `temperature`, …). +- 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` (those are loaded as top-level personas in opencode). +- `--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` | `description`, `mode` | +| 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 | + primary/subagent counts | +| Status breakdown | active/parked | + opencode primary/subagent counts | diff --git a/bin/opc-agents b/bin/opc-agents index cf078b0..b494e3d 100755 --- a/bin/opc-agents +++ b/bin/opc-agents @@ -1,35 +1,40 @@ #!/usr/bin/env bash -# opc-agents — multi-target agent enable/disable manager (opencode + claude) +# opc-agents — multi-target agent enable/disable manager (opencode + claude + feynman) # # Parked sources (per-target — formats differ): # opencode → ~/Documents/personas/agents-opencode-archive (permission: { ... }) # claude → ~/Documents/personas/agents-claude-archive (tools: Read, Glob, ...) +# feynman → ~/Documents/personas/agents-feynman-archive (tools: lowercase, thinking, output) # # Active targets: # opencode → ~/.config/opencode/agents # claude → ~/.claude/agents +# feynman → ~/.feynman/agent/agents # -# Default targets: opencode,claude (override via OPC_AGENTS_TARGETS or --target) +# Default targets: opencode,claude,feynman (override via OPC_AGENTS_TARGETS or --target) set -euo pipefail # Backward compat: OPC_AGENTS_PARKED still works for opencode (legacy) PARKED_OPENCODE="${OPC_AGENTS_PARKED:-$HOME/Documents/personas/agents-opencode-archive}" PARKED_CLAUDE="${OPC_AGENTS_CLAUDE_PARKED:-$HOME/Documents/personas/agents-claude-archive}" +PARKED_FEYNMAN="${OPC_AGENTS_FEYNMAN_PARKED:-$HOME/Documents/personas/agents-feynman-archive}" ACTIVE_OPENCODE="${OPC_AGENTS_ACTIVE:-$HOME/.config/opencode/agents}" ACTIVE_CLAUDE="${OPC_AGENTS_CLAUDE_ACTIVE:-$HOME/.claude/agents}" +ACTIVE_FEYNMAN="${OPC_AGENTS_FEYNMAN_ACTIVE:-$HOME/.feynman/agent/agents}" -# Index lives under opencode-archive (canonical) but tracks BOTH targets +# Index lives under opencode-archive (canonical) but tracks ALL targets INDEX_JSON="$PARKED_OPENCODE/INDEX.json" INDEX_MD="$PARKED_OPENCODE/INDEX.md" -DEFAULT_TARGETS="${OPC_AGENTS_TARGETS:-opencode,claude}" +DEFAULT_TARGETS="${OPC_AGENTS_TARGETS:-opencode,claude,feynman}" # resolve target → parked dir / active dir target_parked() { case "$1" in opencode) printf '%s\n' "$PARKED_OPENCODE" ;; claude) printf '%s\n' "$PARKED_CLAUDE" ;; + feynman) printf '%s\n' "$PARKED_FEYNMAN" ;; *) echo "unknown target: $1" >&2; return 1 ;; esac } @@ -37,13 +42,17 @@ target_active() { case "$1" in opencode) printf '%s\n' "$ACTIVE_OPENCODE" ;; claude) printf '%s\n' "$ACTIVE_CLAUDE" ;; + feynman) printf '%s\n' "$ACTIVE_FEYNMAN" ;; *) echo "unknown target: $1" >&2; return 1 ;; esac } resolve_targets() { local raw="${1:-$DEFAULT_TARGETS}" - case "$raw" in both|all) raw="opencode,claude" ;; esac + case "$raw" in + all) raw="opencode,claude,feynman" ;; + both) raw="opencode,claude" ;; + esac printf '%s\n' "$raw" | tr ',' '\n' | awk 'NF' | awk '!seen[$0]++' } @@ -68,7 +77,8 @@ parse_target_flag() { require_dirs() { [ -d "$PARKED_OPENCODE" ] || mkdir -p "$PARKED_OPENCODE" [ -d "$PARKED_CLAUDE" ] || mkdir -p "$PARKED_CLAUDE" - mkdir -p "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" + [ -d "$PARKED_FEYNMAN" ] || mkdir -p "$PARKED_FEYNMAN" + mkdir -p "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$ACTIVE_FEYNMAN" } # legacy aliases for backward compat @@ -89,11 +99,12 @@ cmd_status() { echo "Parked sources (per-target — different formats):" printf " %-10s %5d (%s)\n" "opencode" "$(agent_names_in "$PARKED_OPENCODE" | wc -l)" "$PARKED_OPENCODE" printf " %-10s %5d (%s)\n" "claude" "$(agent_names_in "$PARKED_CLAUDE" | wc -l)" "$PARKED_CLAUDE" + printf " %-10s %5d (%s)\n" "feynman" "$(agent_names_in "$PARKED_FEYNMAN" | wc -l)" "$PARKED_FEYNMAN" echo "" echo "Active targets:" local t base n - for t in opencode claude; do + for t in opencode claude feynman; do base=$(target_active "$t") n=$(agent_names_in "$base" | wc -l) printf " %-10s %5d (%s)\n" "$t" "$n" "$base" @@ -110,7 +121,7 @@ cmd_status() { echo "opencode mode breakdown:" printf " primary : %5d\n" "$primary" printf " subagent : %5d\n" "$subagent" - [ "$other" -gt 0 ] && printf " (other) : %5d\n" "$other" + [ "$other" -gt 0 ] && printf " (other) : %5d\n" "$other" || true fi } @@ -240,7 +251,7 @@ cmd_enable() { set -- "${REMAINING_ARGS[@]}" require_dirs local name="${1:-}" - [ -n "$name" ] || { echo "usage: opc-agents enable [--target opencode|claude|both] " >&2; exit 2; } + [ -n "$name" ] || { echo "usage: opc-agents enable [--target opencode|claude|feynman|all] " >&2; exit 2; } name="${name%.md}" local t pdir adir src dst IFS=',' read -ra tgts <<< "$PARSED_TARGETS" @@ -268,7 +279,7 @@ cmd_disable() { set -- "${REMAINING_ARGS[@]}" require_dirs local name="${1:-}" - [ -n "$name" ] || { echo "usage: opc-agents disable [--target opencode|claude|both] " >&2; exit 2; } + [ -n "$name" ] || { echo "usage: opc-agents disable [--target opencode|claude|feynman|all] " >&2; exit 2; } name="${name%.md}" local t adir src IFS=',' read -ra tgts <<< "$PARSED_TARGETS" @@ -387,7 +398,7 @@ cmd_enable_category() { selection=$(printf '%s\n' "${matches[@]}" | fzf --multi --height=70% \ --prompt="enable-category $prefix > " \ --header="TAB: toggle | ENTER: confirm | ESC: cancel" \ - --preview="sed -n '1,40p' \"$PARKED/{}.md\" 2>/dev/null" \ + --preview="sed -n '1,40p' \"$PARKED\"/{}.md 2>/dev/null" \ --preview-window=right:60%:wrap) [ -n "$selection" ] || { echo "cancelled"; exit 0; } local n @@ -411,7 +422,7 @@ cmd_disable_category() { selection=$(printf '%s\n' "${matches[@]}" | fzf --multi --height=70% \ --prompt="disable-category $prefix > " \ --header="TAB: toggle | ENTER: confirm | ESC: cancel" \ - --preview="sed -n '1,40p' \"$ACTIVE/{}.md\" 2>/dev/null" \ + --preview="sed -n '1,40p' \"$ACTIVE\"/{}.md 2>/dev/null" \ --preview-window=right:60%:wrap) [ -n "$selection" ] || { echo "cancelled"; exit 0; } local n @@ -421,57 +432,143 @@ cmd_disable_category() { done <<< "$selection" } -# interactive pick: parked category → multi-select → enable +# interactive target picker — returns comma-separated targets on stdout, exit 1 on cancel. +pick_targets() { + local action="${1:-enable}" + ensure_fzf + local oc cl fy + oc=$(agent_names_in "$ACTIVE_OPENCODE" | wc -l) + cl=$(agent_names_in "$ACTIVE_CLAUDE" | wc -l) + fy=$(agent_names_in "$ACTIVE_FEYNMAN" | wc -l) + local sel + sel=$(printf '%s\n' \ + "all → opencode + claude + feynman" \ + "opencode ($oc active)" \ + "claude ($cl active)" \ + "feynman ($fy active)" \ + | fzf --multi --height=30% \ + --prompt="$action target > " \ + --header="TAB: multi-select | ENTER: confirm (default highlighted = all)" \ + | awk '{print $1}') + [ -n "$sel" ] || return 1 + if grep -qx all <<< "$sel"; then + echo "opencode,claude,feynman" + else + echo "$sel" | paste -sd ',' + fi +} + +# Helper: union of parked agent names across all 3 targets (deduped) +parked_union() { + { agent_names_in "$PARKED_OPENCODE" + agent_names_in "$PARKED_CLAUDE" + agent_names_in "$PARKED_FEYNMAN"; } | sort -u +} + +# Helper: union of active agent names with target indicators "name\t[targets]" +active_union_with_targets() { + { + for t in opencode claude feynman; do + while IFS= read -r n; do + [ -z "$n" ] && continue + printf '%s\t%s\n' "$n" "$t" + done < <(agent_names_in "$(target_active "$t")") + done + } | awk -F'\t' ' + { agents[$1] = (agents[$1] ? agents[$1]"," : "") $2 } + END { for (a in agents) printf "%s\t[%s]\n", a, agents[a] } + ' | sort +} + +# Resolve a parked source path for an agent name (first target where parked file exists) +parked_path_for() { + local name="$1" t pdir + for t in opencode claude feynman; do + pdir=$(target_parked "$t") + [ -f "$pdir/$name.md" ] && { echo "$pdir/$name.md"; return 0; } + done + return 1 +} + +# interactive pick: union-parked category → multi-select → pick target(s) cmd_pick() { require_dirs ensure_fzf - local category - category=$(cmd_categories | fzf --prompt="category > " --height=50% \ - --header="Pick a category (first column = count)" \ + # Categories from union of parked across all 3 targets + local cats category + cats=$(parked_union | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn) + category=$(printf '%s\n' "$cats" | fzf --prompt="category > " --height=50% \ + --header="Pick a category (first column = count) • parked = union of all 3 targets" \ | awk '{print $2}') [ -n "$category" ] || { echo "cancelled"; exit 0; } + + # Build {name}\t{parked-in:[oc,cl,fy]} lines + local tmp; tmp=$(mktemp) + { + for t in opencode claude feynman; do + while IFS= read -r n; do + [ -z "$n" ] && continue + printf '%s\t%s\n' "$n" "$t" + done < <(agent_names_in "$(target_parked "$t")") + done + } | awk -F'\t' ' + { p[$1] = (p[$1] ? p[$1]"," : "") $2 } + END { for (a in p) printf "%s\t[%s]\n", a, p[a] } + ' | awk -F'\t' -v p="$category" '$1 ~ "^"p"(-|$)"' | sort > "$tmp" + local selection - selection=$(agent_names_in "$PARKED" \ - | awk -v p="$category" '$0 ~ "^" p "(-|$)"' \ - | fzf --multi --height=80% \ - --prompt="$category > " \ - --header="TAB: toggle | ENTER: enable selected" \ - --preview="sed -n '1,40p' \"$PARKED/{}.md\" 2>/dev/null" \ - --preview-window=right:60%:wrap) + selection=$(fzf --multi --height=80% \ + --prompt="$category > " \ + --header="TAB: toggle | ENTER: confirm → pick target" \ + --delimiter='\t' --with-nth=1,2 \ + --preview="for d in \"$PARKED_OPENCODE\" \"$PARKED_CLAUDE\" \"$PARKED_FEYNMAN\"; do [ -f \"\$d\"/{1}.md ] && { echo \"=== \$d ===\"; sed -n '1,40p' \"\$d\"/{1}.md; break; }; done 2>/dev/null" \ + --preview-window=right:60%:wrap < "$tmp") + rm -f "$tmp" [ -n "$selection" ] || { echo "cancelled"; exit 0; } + + local targets + targets=$(pick_targets "enable") || { echo "cancelled (no target)"; exit 0; } + local n - while IFS= read -r n; do + while IFS=$'\t' read -r n _; do [ -z "$n" ] && continue - cmd_enable "$n" || true + cmd_enable --target "$targets" "$n" || true done <<< "$selection" } -# interactive pick: ACTIVE category → multi-select → disable +# interactive disable-pick: union of actives across all 3 targets → pick target(s) to remove from cmd_disable_pick() { require_dirs ensure_fzf - local active_count - active_count=$(agent_names_in "$ACTIVE" | wc -l) - [ "$active_count" -gt 0 ] || { echo "no active agents"; exit 0; } + local tmp; tmp=$(mktemp) + active_union_with_targets > "$tmp" + [ -s "$tmp" ] || { rm -f "$tmp"; echo "no active agents across any target"; exit 0; } + local cats category - cats=$(agent_names_in "$ACTIVE" | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn) + cats=$(awk -F'\t' '{print $1}' "$tmp" | awk -F'-' '{print $1}' | sort | uniq -c | sort -rn) category=$(printf '%s\n' "$cats" | fzf --prompt="active-category > " --height=50% \ - --header="Pick a category of ACTIVE agents to disable" \ + --header="Pick category from ACTIVE agents (any target)" \ | awk '{print $2}') - [ -n "$category" ] || { echo "cancelled"; exit 0; } + [ -n "$category" ] || { rm -f "$tmp"; echo "cancelled"; exit 0; } + local selection - selection=$(agent_names_in "$ACTIVE" \ - | awk -v p="$category" '$0 ~ "^" p "(-|$)"' \ + selection=$(awk -F'\t' -v p="$category" '$1 ~ "^"p"(-|$)"' "$tmp" \ | fzf --multi --height=80% \ --prompt="disable $category > " \ - --header="TAB: toggle | ENTER: disable selected" \ - --preview="sed -n '1,40p' \"$ACTIVE/{}.md\" 2>/dev/null" \ + --header="TAB: toggle | ENTER: pick target(s) to disable from" \ + --delimiter='\t' --with-nth=1,2 \ + --preview="for d in \"$ACTIVE_OPENCODE\" \"$ACTIVE_CLAUDE\" \"$ACTIVE_FEYNMAN\"; do [ -f \"\$d\"/{1}.md ] && { echo \"=== \$d ===\"; sed -n '1,40p' \"\$d\"/{1}.md; break; }; done 2>/dev/null" \ --preview-window=right:60%:wrap) + rm -f "$tmp" [ -n "$selection" ] || { echo "cancelled"; exit 0; } + + local targets + targets=$(pick_targets "disable") || { echo "cancelled (no target)"; exit 0; } + local n - while IFS= read -r n; do + while IFS=$'\t' read -r n _; do [ -z "$n" ] && continue - cmd_disable "$n" || true + cmd_disable --target "$targets" "$n" || true done <<< "$selection" } @@ -490,14 +587,14 @@ cmd_search() { --delimiter='\t' --with-nth=1,2,3,4,5 \ --prompt="search > " \ --header="TAB: toggle | ENTER: enable selected" \ - --preview="sed -n '1,40p' \"$PARKED/{1}.md\" 2>/dev/null || echo '(not parked — maybe already active)'" \ + --preview="sed -n '1,40p' \"$PARKED\"/{1}.md 2>/dev/null || echo '(not parked — maybe already active)'" \ --preview-window=right:60%:wrap < "$tmp") else selection=$(fzf --multi --height=80% \ --delimiter='\t' --with-nth=1,2,3,4,5 \ --prompt="search > " \ --header="TAB: toggle | ENTER: enable selected" \ - --preview="sed -n '1,40p' \"$PARKED/{1}.md\" 2>/dev/null || echo '(not parked — maybe already active)'" \ + --preview="sed -n '1,40p' \"$PARKED\"/{1}.md 2>/dev/null || echo '(not parked — maybe already active)'" \ --preview-window=right:60%:wrap < "$tmp") fi rm -f "$tmp" @@ -525,14 +622,14 @@ cmd_disable_search() { --delimiter='\t' --with-nth=1,2,3,4,5 \ --prompt="disable-search > " \ --header="TAB: toggle | ENTER: disable selected" \ - --preview="sed -n '1,40p' \"$ACTIVE/{1}.md\" 2>/dev/null" \ + --preview="sed -n '1,40p' \"$ACTIVE\"/{1}.md 2>/dev/null" \ --preview-window=right:60%:wrap < "$tmp") else selection=$(fzf --multi --height=80% \ --delimiter='\t' --with-nth=1,2,3,4,5 \ --prompt="disable-search > " \ --header="TAB: toggle | ENTER: disable selected" \ - --preview="sed -n '1,40p' \"$ACTIVE/{1}.md\" 2>/dev/null" \ + --preview="sed -n '1,40p' \"$ACTIVE\"/{1}.md 2>/dev/null" \ --preview-window=right:60%:wrap < "$tmp") fi rm -f "$tmp" @@ -547,16 +644,18 @@ cmd_disable_search() { # rebuild INDEX.json + INDEX.md by scanning parked + active cmd_reindex() { require_dirs - python3 - "$PARKED_OPENCODE" "$PARKED_CLAUDE" "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$INDEX_JSON" "$INDEX_MD" <<'PY' + python3 - "$PARKED_OPENCODE" "$PARKED_CLAUDE" "$PARKED_FEYNMAN" "$ACTIVE_OPENCODE" "$ACTIVE_CLAUDE" "$ACTIVE_FEYNMAN" "$INDEX_JSON" "$INDEX_MD" <<'PY' import sys, re, json from pathlib import Path parked_oc = Path(sys.argv[1]) parked_cl = Path(sys.argv[2]) -active_oc = Path(sys.argv[3]) -active_cl = Path(sys.argv[4]) -out_json = Path(sys.argv[5]) -out_md = Path(sys.argv[6]) +parked_fy = Path(sys.argv[3]) +active_oc = Path(sys.argv[4]) +active_cl = Path(sys.argv[5]) +active_fy = Path(sys.argv[6]) +out_json = Path(sys.argv[7]) +out_md = Path(sys.argv[8]) def read_agent(p: Path): name = p.stem @@ -590,7 +689,7 @@ def upsert(rec): n = rec["name"] cur = agents.get(n) if cur is None: - rec.setdefault("parked", {"opencode": False, "claude": False}) + rec.setdefault("parked", {"opencode": False, "claude": False, "feynman": False}) rec.setdefault("active", []) agents[n] = rec else: @@ -619,17 +718,21 @@ def scan_active(base: Path, target: str): scan_parked(parked_oc, "opencode") scan_parked(parked_cl, "claude") +scan_parked(parked_fy, "feynman") scan_active(active_oc, "opencode") scan_active(active_cl, "claude") +scan_active(active_fy, "feynman") items = sorted(agents.values(), key=lambda x: x["name"]) out_json.write_text(json.dumps(items, ensure_ascii=False, indent=2)) n_oc_park = sum(1 for i in items if i["parked"]["opencode"]) n_cl_park = sum(1 for i in items if i["parked"]["claude"]) +n_fy_park = sum(1 for i in items if i["parked"]["feynman"]) in_oc = sum(1 for i in items if "opencode" in i["active"]) in_cl = sum(1 for i in items if "claude" in i["active"]) -both_park = sum(1 for i in items if i["parked"]["opencode"] and i["parked"]["claude"]) +in_fy = sum(1 for i in items if "feynman" in i["active"]) +all_park = sum(1 for i in items if all(i["parked"].get(t) for t in ("opencode","claude","feynman"))) prim = sum(1 for i in items if i["mode"] == "primary") sub = sum(1 for i in items if i["mode"] == "subagent") @@ -640,19 +743,21 @@ top_doms = ", ".join(f"{k}:{v}" for k,v in dom_c.most_common(8)) or "-" top_vars = ", ".join(f"{k}:{v}" for k,v in var_c.most_common(8)) or "-" lines = [ - "# Opencode + Claude Agents — Index", + "# Opencode + Claude + Feynman Agents — Index", "", f"**{len(items)} unique agents.**", - f"Parked sources — opencode: {n_oc_park}, claude: {n_cl_park}, both: {both_park}.", - f"Active — opencode: {in_oc}, claude: {in_cl}.", + f"Parked sources — opencode: {n_oc_park}, claude: {n_cl_park}, feynman: {n_fy_park}, all-three: {all_park}.", + f"Active — opencode: {in_oc}, claude: {in_cl}, feynman: {in_fy}.", f"Modes — primary: {prim}, subagent: {sub}.", f"Top domains — {top_doms}.", f"Top variants — {top_vars}.", "", f"Parked opencode: `{parked_oc}`", f"Parked claude : `{parked_cl}`", + f"Parked feynman : `{parked_fy}`", f"Active opencode: `{active_oc}`", f"Active claude : `{active_cl}`", + f"Active feynman : `{active_fy}`", "", "| # | Parked | Active | Mode | Persona | Variant | Domain | Name | Description |", "|---|--------|--------|------|---------|---------|--------|------|-------------|", @@ -660,7 +765,7 @@ lines = [ for i, it in enumerate(items, 1): d = it["description"].replace("\n", " ").replace("|", "\\|") if len(d) > 140: d = d[:137] + "..." - pk = ",".join(t for t in ("opencode","claude") if it["parked"][t]) or "-" + pk = ",".join(t for t in ("opencode","claude","feynman") if it["parked"][t]) or "-" ac = ",".join(it.get("active", [])) or "-" lines.append( f"| {i} | {pk} | {ac} | {it['mode']} | {it['persona']} | " @@ -668,17 +773,17 @@ for i, it in enumerate(items, 1): ) out_md.write_text("\n".join(lines) + "\n") print(f"reindexed: {len(items)} agents " - f"(parked oc={n_oc_park} cl={n_cl_park} both={both_park}, " - f"active oc={in_oc} cl={in_cl}, primary={prim}, subagent={sub})") + f"(parked oc={n_oc_park} cl={n_cl_park} fy={n_fy_park} all3={all_park}, " + f"active oc={in_oc} cl={in_cl} fy={in_fy}, primary={prim}, subagent={sub})") PY } usage() { cat <<'USG' -opc-agents — multi-target agent manager (opencode + claude) +opc-agents — multi-target agent manager (opencode + claude + feynman) -Targets: opencode, claude (or "both"/"all"; default = opencode,claude) -Use --target / -t with enable/disable/enable-all/disable-all to scope the operation. +Targets: opencode, claude, feynman ("all"=all three, "both"=opencode+claude legacy) +Default = opencode,claude,feynman. Use --target / -t to scope ops. status per-target counts (parked + active) + mode breakdown list {active|parked|all} list agent names (active = opencode set) @@ -703,18 +808,24 @@ Use --target / -t with enable/disable/enable-all/disable-all to scope the operat reindex rebuild INDEX.json / INDEX.md (per-target parked + active) Notes: - - Agent format differs between targets: opencode uses `permission: {...}`, - claude uses `tools: Read, Glob, ...`. Each target reads from its own parked dir. + - Agent format differs between targets: + opencode → `permission: {...}` + `mode:` field + claude → `tools: Read, Glob, ...` (PascalCase) + feynman → `tools: read, write, ...` (lowercase) + `thinking`/`output` fields + Each target reads from its own parked dir; cross-target enable copies as-is. - disable* never deletes parked sources. - --keep-primary skips agents with `mode: primary` (opencode-format only). - - Single source-of-truth: ~/Documents/personas/agents-{opencode,claude}-archive + - Single source-of-truth: ~/Documents/personas/agents-{opencode,claude,feynman}-archive Environment: - OPC_AGENTS_PARKED opencode parked (default: personas/agents-opencode-archive) - OPC_AGENTS_CLAUDE_PARKED claude parked (default: personas/agents-claude-archive) - OPC_AGENTS_ACTIVE opencode active (default: ~/.config/opencode/agents) - OPC_AGENTS_CLAUDE_ACTIVE claude active (default: ~/.claude/agents) - OPC_AGENTS_TARGETS default targets when --target omitted (default: opencode,claude) + OPC_AGENTS_PARKED opencode parked (default: personas/agents-opencode-archive) + OPC_AGENTS_CLAUDE_PARKED claude parked (default: personas/agents-claude-archive) + OPC_AGENTS_FEYNMAN_PARKED feynman parked (default: personas/agents-feynman-archive) + OPC_AGENTS_ACTIVE opencode active (default: ~/.config/opencode/agents) + OPC_AGENTS_CLAUDE_ACTIVE claude active (default: ~/.claude/agents) + OPC_AGENTS_FEYNMAN_ACTIVE feynman active (default: ~/.feynman/agent/agents) + OPC_AGENTS_TARGETS default targets when --target omitted + (default: opencode,claude,feynman) USG }