salvacybersec cd937efc01 docs: add Architecture section to README
ASCII diagrams + prose covering:
- Storage model: one parked source-of-truth fanned out to 3 lazy mirrors
- Data flow for enable/disable (incremental INDEX updates, no global reindex)
- INDEX.json schema with per-target active[] array
- Interactive pick / disable-pick flows incl. target-picker UX
- The fzf {} shell-escape gotcha and the correct quoting pattern
2026-05-01 13:26:36 +03:00
2026-04-18 19:07:00 +03:00

opc-skills

Multi-target skill manager for opencode + Claude Code + Feynman. Single source-of-truth (parked archive in the personas repo), per-target lazy activation. Default operations target all three platforms; an interactive target-picker prompts when picking via fzf.

Why

opencode (≥1.14) auto-scans ~/.claude/skills/ as well as its own ~/.config/opencode/skills/ — so a 1000-skill Claude Code archive ends up bloating the opencode prompt. Feynman has its own ~/.feynman/agent/skills/. opc-skills inverts this: keep all three target dirs empty by default, copy individual skills into them on demand.

For full agent / skill / persona generation across multiple AI platforms, see the upstream personas repo.

Architecture

Storage model — one source-of-truth, three lazy mirrors

                ┌─────────────────────────────────────────┐
                │  ~/Documents/personas/skills-archive/   │
                │  (single source-of-truth, 1072+ skills) │
                │                                         │
                │   <skill>/SKILL.md   ← frontmatter:     │
                │                        name, desc,      │
                │                        domain, sub,     │
                │                        tags             │
                │   INDEX.json   ← per-target active map  │
                │   INDEX.md     ← human-readable table   │
                └─────────────────┬───────────────────────┘
                                  │
                                  │  cp -r  (lazy, on demand,
                                  │           per --target)
                                  ▼
        ┌─────────────────────────┼─────────────────────────┐
        ▼                         ▼                         ▼
┌───────────────┐         ┌───────────────┐         ┌───────────────┐
│  opencode     │         │  claude       │         │  feynman      │
│  ~/.config/   │         │  ~/.claude/   │         │  ~/.feynman/  │
│  opencode/    │         │  skills/      │         │  agent/       │
│  skills/      │         │               │         │  skills/      │
└───────────────┘         └───────────────┘         └───────────────┘

The parked archive is the only dir that's authoritative. The three target dirs are mutable mirrors — enable copies in, disable removes from. No skill is ever deleted from parked by opc-skills.

Data flow — enable / disable

  enable <name> --target T1,T2,...
        │
        ├──► for each target T:
        │      cp -r  PARKED/<name>  →  ACTIVE_T/<name>
        │      sync shared refs (e.g. _platform-mapping.md)
        │      INDEX.json: append T to record's `active[]` array
        │
        └──► no global reindex required (incremental update)

  disable <name> --target T1,T2,...
        │
        ├──► for each target T:
        │      rm -rf  ACTIVE_T/<name>
        │      INDEX.json: remove T from `active[]`
        │
        └──► PARKED copy stays intact

INDEX schema

Each entry is a deduped record across all 3 targets:

{
  "folder":      "performing-network-traffic-analysis-with-tshark",
  "name":        "performing-network-traffic-analysis-with-tshark",
  "description": "Automate network traffic analysis using tshark…",
  "domain":      "cybersecurity",
  "subdomain":   "network-security",
  "tags":        ["network-analysis", "pcap", "tshark"],
  "active":      ["claude", "feynman"],   // per-target presence
  "status":      "active"                  // "active" if any target, else "parked"
}

reindex rebuilds this from filesystem state across all 4 dirs (parked + 3 actives). enable/disable mutate active[] and status incrementally so bulk axis-ops (enable-domain, enable-tag) stay accurate without rescanning.

Interactive flows (pick / disable-pick)

  pick:                                disable-pick:
  ─────                                ────────────
  ① fzf: pick category                 ① fzf: pick category
     (verb-prefix: performing,            (from union-of-actives across
      detecting, analyzing…)              opencode + claude + feynman)
                                       
  ② fzf --multi: pick skills           ② fzf --multi: pick skills
     preview: PARKED/<skill>/SKILL.md     preview: same
     [TAB toggles, ENTER confirms]        rows annotated [oc,cl,fy]
                                       
  ③ fzf: pick target(s)                ③ fzf: pick target(s) to remove from
     ┌───────────────────────────┐        same picker
     │ all       (default)       │        ENTER on highlighted "all"
     │ opencode  (N active)      │        = remove from every target
     │ claude    (N active)      │        where it's currently active
     │ feynman   (N active)      │
     └───────────────────────────┘
     [TAB multi-select supported]
                                       
  ④ cp -r per-target                   ④ rm -rf per-target

The target picker leverages an fzf quirk: --multi returns the highlighted line as the sole selection if the user never pressed TAB. Putting all first means a default ENTER falls through to all three targets.

fzf {} quoting (gotcha)

fzf shell-escapes {} substitutions automatically (wraps in single quotes). Wrapping {} again in outer quotes embeds those quote literals into the path:

# ❌  becomes:  sed -n '1,40p' ".../skills-archive/'name'/SKILL.md"
--preview="sed -n '1,40p' \"$PARKED/{}/SKILL.md\" 2>/dev/null"

# ✅  becomes:  sed -n '1,40p' ".../skills-archive"/'name'/SKILL.md
#              (bash concatenates adjacent quoted/unquoted strings cleanly)
--preview="sed -n '1,40p' \"$PARKED\"/{}/SKILL.md 2>/dev/null"

All preview lines in bin/opc-skills follow the second pattern.

Install

ln -s ~/Documents/opc-skills/bin/opc-skills ~/.local/bin/opc-skills

Ensure ~/.local/bin is on your PATH.

Layout it expects

~/Documents/personas/skills-archive/    # full catalog — single source-of-truth
  <skill-name>/SKILL.md
  INDEX.json
  INDEX.md

~/.config/opencode/skills/              # currently-enabled (opencode target)
  <skill-name>/SKILL.md

~/.claude/skills/                       # currently-enabled (claude target)
  <skill-name>/SKILL.md

~/.feynman/agent/skills/                # currently-enabled (feynman target)
  <skill-name>/SKILL.md

(~/Documents/opencode-skills-parked is symlinked to personas/skills-archive for backward-compat.)

Override via env:

  • OPC_PARKED — parked catalog root (default ~/Documents/personas/skills-archive)
  • OPC_ACTIVE — opencode active dir (default ~/.config/opencode/skills)
  • OPC_CLAUDE_ACTIVE — claude active dir (default ~/.claude/skills)
  • OPC_FEYNMAN_ACTIVE — feynman active dir (default ~/.feynman/agent/skills)
  • OPC_TARGETS — comma-separated default targets (default opencode,claude,feynman)

Targets

enable, disable, and disable-all accept --target / -t:

flag meaning
--target opencode only ~/.config/opencode/skills/
--target claude only ~/.claude/skills/
--target feynman only ~/.feynman/agent/skills/
--target all all three
--target both legacy opencode,claude (no feynman)
--target opencode,feynman comma-separated subset

When omitted, the default is taken from OPC_TARGETS (default opencode,claude,feynman).

Commands

Inspection

opc-skills status                     # parked + per-target active counts (3 targets)
opc-skills list {active|parked|all}   # skill folder names
opc-skills categories                 # verb-prefix counts (performing, detecting, ...)
opc-skills domains                    # frontmatter `domain:` distribution
opc-skills subdomains                 # frontmatter `subdomain:` distribution
opc-skills tags                       # frontmatter `tags:` distribution

Single-skill operations

opc-skills enable  [--target T] <folder>           # copy parked → active in selected targets
opc-skills disable [--target T] <folder>           # remove from active in selected targets
opc-skills disable-all [--target T] [-y|--yes]     # disable every active skill across targets

Bulk by axis

opc-skills enable-category   <prefix>      # fzf multi-pick within a verb-prefix
opc-skills disable-category  <prefix>
opc-skills enable-domain     <domain>      # bulk by frontmatter domain (eg. cybersecurity)
opc-skills disable-domain    <domain>
opc-skills enable-subdomain  <subdomain>   # bulk by subdomain (eg. malware-analysis)
opc-skills disable-subdomain <subdomain>
opc-skills enable-tag  [--all] <tag>...    # bulk by tags (default: any; --all: intersection)
opc-skills disable-tag [--all] <tag>...
opc-skills pick                       # fzf: category → multi-select skills → target picker → enable
opc-skills disable-pick               # fzf: union-of-actives → category → multi-select → target picker → disable
opc-skills search [query]             # fzf fuzzy over name+description (enables on selection)
opc-skills disable-search [query]     # fzf over ACTIVE only (disables on selection)
opc-skills reindex                    # rebuild INDEX.json / INDEX.md

pick flow

  1. Choose a verb-prefix category (performing, detecting, …)
  2. fzf shows skills in that category — TAB toggles, ENTER confirms
  3. Target picker appears: all / opencode / claude / feynmanTAB for multi-select; ENTER on highlighted all (default) sends to all three
  4. Selected skills are enabled in the chosen target(s)

disable-pick flow

Now multi-target aware: lists the union of all active skills across the three targets, with each entry annotated [oc,cl,fy] showing where it's currently active. After multi-select, the same target picker decides which target(s) to remove from.

fzf is required for the interactive commands; jq for the bulk-by-axis commands; python3 (with optional PyYAML) for reindex.

INDEX schema

reindex parses each SKILL.md frontmatter and emits records with: folder, name, description, domain, subdomain, tags[], active[] (per-target list — opencode/claude/feynman), status. enable / disable perform an incremental status update on INDEX.json so enable-domain / disable-tag etc. stay accurate without a full reindex.

Shared reference files

Some skills reference sibling files via ../<name>.md (e.g. the 20 Feynman research skills use ../_platform-mapping.md for cross-platform tool mapping). opc-skills enable auto-syncs any such files from PARKED → ACTIVE in all three targets so the relative references keep resolving.

Currently synced:

  • _platform-mapping.md — Feynman-skills cross-platform subagent/scheduling/persistence mapping

If you add new shared-reference files at the PARKED root, extend sync_shared_refs_to_targets() in bin/opc-skills.

Implementation notes

  • fzf placeholder {} is shell-escaped automatically — preview commands use "$PARKED"/{}/SKILL.md (variable quoted, {} unquoted) so bash concatenation produces a clean path. Wrapping {} in extra quotes ("$PARKED/{}/SKILL.md") embeds fzf's escape literals into the path and breaks preview.

Known interaction

The personas repo's build.py --install opencode currently wipes and repopulates ~/.config/opencode/skills/ as part of installation, which destroys the active set curated here. Use opc-skills after any personas install, or skip the skills half of build.py installs.

build.py --install opencode and build.py --install claude-skills both emit _platform-mapping.md at the skills-root alongside the 20 Feynman skills (deep-research, literature-review, paper-code-audit, peer-review, paper-writing, replication, source-comparison, summarize, alpha-research, eli5, autoresearch, docker, modal-compute, runpod-compute, session-log, session-search, jobs, watch, preview, contributing).

Description
opencode skills enable/disable manager (CLI tool)
Readme 89 KiB
Languages
Shell 100%