Files
mattpocock-skills-opencode/docs/architecture.md
T
2026-10-14 12:34:57 +02:00

17 KiB
Raw Permalink Blame History

Architecture — Matt Pocock Skills → OpenCode v2

Phase-0 analysis, 2026-10-14. Sources: mattpocock/skills@49dd158 (v1.3.1), fastcrw-opencode-websearch (working V2 plugin + Gitea release pipeline), mcdays94/pocock-agents (proven V1 orchestrator/worker pair, tracks upstream v1.3), OpenCode v2 docs + anomalyco/opencode@dev source (verified via Context7). Per-skill classification: docs/skill-mapping.md.

1. Proposed architecture

One npm package, three delivery channels, one generator pipeline:

mattpocock/skills (upstream, git)
        │  scripts/sync-upstream.ts (nightly)
        ▼
upstream/                 pristine snapshot (skills/ subtree + UPSTREAM.json) — never hand-edited
        │  scripts/generate.ts   (inputs: upstream/ + .opencode/state/classification.json + agent-templates/ + overlay/)
        ▼
skills/ + manifest.json   OpenCode skill tree (verbatim content, stripped of other-harness files)
agents/*.md               V2-native agent files (orchestrator, worker, ...)
        │
        ▼
package @haylan/opencode-pocock (name provisional)
  ├── dist/index.js       plugin: injects skills via ctx.skill.transform; optional guardrails hook;
  │                       warns when agents are missing/stale
  ├── bin init            copies agents/*.md → .opencode/agents/ (or ~/.config/opencode/agents/ with -g)
  └── files: dist, skills, agents, manifest

Why this split (decision D2): V2 SkillEditor.add() exists, AgentEditor.add() does not. Skills can be injected at runtime by the plugin with zero user file management; agents must exist as markdown files in a discovered directory, so a tiny init copy step is unavoidable. The plugin detects missing/version-mismatched agents and prints the one-line fix instead of failing or silently writing into the user's repo.

User-facing install:

// opencode.json
{ "plugins": ["@haylan/opencode-pocock"] }
npx @haylan/opencode-pocock init   # once per project (or -g for global agents)

Fidelity rule (D1): skill text ships verbatim. V2 natively honors disable-model-invocation (user-invoked skills stay hidden from auto-selection but loadable via /name or by id), skill name/description, and supporting-file paths relative to SKILL.md (we keep each skill's whole directory, so DEEPENING.md, template.sh, etc. resolve). OpenCode-specific wiring lives only in generated agents. If a skill ever needs a textual deviation, it goes through a tracked overlay/ patch input — never a hand-edit of upstream/ or skills/.

Generator scope (deliberately small):

  • strip non-OpenCode harness files (agents/openai.yaml, Claude plugin metadata) from the output tree
  • parse each SKILL.md frontmatter → manifest.json entry: { id, name, description, autoinvoke, slash, path, files[] }; autoinvoke=false when upstream sets disable-model-invocation: true
  • render agents/*.md from templates + classification.json (skill allow/deny lists, version stamp)
  • regenerate docs/skill-mapping.md from classification.json (docs never drift from data)

2. Skill inventory (summary)

38 active skills at 49dd158: engineering 20, productivity 7, misc 4, in-progress 7 (excluded per D6), deprecated 0. Full table with classification + rationale: docs/skill-mapping.md.

3. Plugin API requirements

Verified against V2 (@opencode/plugin ^2.x, Plugin.define):

Requirement V2 mechanism
Stable identity Plugin.define({ id: "haylan.pocock", setup })
Inject all skills await ctx.skill.transform(editor => editor.add({ id, name, description, path, content, autoinvoke, slash })) per manifest entry; path = absolute SKILL.md location inside the installed package so supporting-file references resolve
Options ctx.options — validate and fail loudly (fastcrw pattern)
Dangerous-git guardrails (D7) await ctx.tool.hook("execute.before", ...) on shell tool; opt-in { guardrails: true }
Agent staleness hint await ctx.agent.list() in setup; one-line console warning if pocock/pocock-worker missing or stamped version ≠ package version
Packaging type: module, exports["."], dependency @opencode/plugin; installed via bun by OpenCode at startup

Not needed: custom tools, MCP, websearch, auth. Keep the plugin surface minimal.

4. Agent architecture

Hierarchy (D5) — two core agents, one likely addition, rest rejected unless the matrix proves value:

Agent Mode Role Key V2 config
pocock primary Orchestrator: routes via ask-matt, walks grill→spec→tickets→build→review→retro, gates user-invoked skills behind explicit proposals, runs the implement-spec dispatch loop (integration branch + one worktree per ticket + frontier scheduling from its own merge record) permissions: edit/shell allow; shell deny for destructive git on the main checkout; skill allow *; task allow
pocock-worker subagent Builds one ticket test-first inside its prepared worktree, merges integration tip into its branch, reports; never pushes/stashes/worktrees/PRs; asks nothing permissions: skill deny * + allow-list (tdd, codebase-design, diagnosing-bugs, stack skills); shell deny push/stash/reset --hard/clean/worktree/switch, gh pr/gh issue close; edit allow
pocock-reviewer (candidate) subagent Read-only code-review fan-out over the integration branch; findings go back to the orchestrator, fixes run in a fresh worker permissions: edit deny; shell allow git read commands only; skill allow code-review only
pocock-researcher (likely reject) subagent Exploration notes Built-in explore/general subagents already do this; a branded one adds drift risk, no capability

Design requirements coverage (per mission): purpose/scope/IO contract in each agent's system body; skills via action: skill permission patterns; primary vs subagent via mode; spawn rights — only pocock gets task; model — left to user default (pocock-agents pinned opus; we ship without a model line and document how to pin, since provider availability varies); context/duration — phases 1–4 stay in one window (~150k guidance from ask-matt PHASE-BOUNDARIES), workers get bounded steps only if testing shows runaway loops; git/worktrees — orchestrator-owned /tmp/pocock-workers convention (documented, external_directory permission guidance in README); failure modes — worker reports instead of repairing bad worktrees, orchestrator never merges failed workers; validation — smoke + drift tests below.

Explicitly rejected: one giant agent containing all skills; a 12-agent hierarchy mirroring phase names (phases are conversation steps, not roles — splitting them shreds the shared context the methodology depends on, e.g. grilling facts feeding to-spec).

5. Synchronization strategy

  • upstream/ is vendored by script only: shallow-clone/fetch mattpocock/skills, resolve HEAD, copy the skills/ subtree, write upstream/UPSTREAM.json { repo, commit, commit_date, version } and mirror it to .opencode/state/upstream.json.
  • Diff old vs new snapshot → added/removed/changed skill list → written into the changelog entry and status.md; changed skills are matched against classification.json to flag affected agents (regeneration is full but cheap and deterministic; "regenerate only what is necessary" is satisfied by determinism — unchanged inputs produce byte-identical outputs, so the git diff is the affected-set report).
  • New upstream skills appear in the manifest automatically but land unclassified: classification.json defaults them to skill + status: "needs-review", and the drift test fails until a human confirms the classification. Upstream renames/removals fail the drift test when a generated agent still references the old name (pocock-agents check-skills.sh pattern, ported to TS, run against the vendored snapshot — no network in tests).
  • Locally maintained artifacts (agent-templates/, overlay/, src/) are never touched by sync.

6. Gitea Actions design

.gitea/workflows/check.yml — on PR + push to main: npm ci → npm run check (prettier + tsc) → npm test → npm run build. (fastcrw pattern.)

.gitea/workflows/nightly.yml — schedule: cron nightly + workflow_dispatch, if: gitea.ref == 'refs/heads/main', node 24:

  1. checkout, npm ci
  2. node scripts/sync-upstream.ts — no change → log + exit 0 (success)
  3. node scripts/generate.ts
  4. npm run format → npm run check → npm test (unit + integration + drift/regression) → npm run build — any failure aborts before publish; drift-test failure for unclassified new skills aborts too (human review required; publish waits for the next green run)
  5. node scripts/version.ts — prepend ## [<minor-bump>] - <date> + upstream sync entry to CHANGELOG.md unless this exact upstream SHA was already released (state check → idempotent)
  6. node scripts/release.ts — fastcrw release script adapted: publish packument+tarball to ${SERVER_URL}/api/packages/${owner}/npm with Bearer ${PACKAGE_TOKEN}, skip if version exists, ensure tag v<version> at gitea.sha, link package to repo
  7. commit-back: sync: update mattpocock skills to <short-sha> [skip ci] (upstream/, skills/, agents/, docs/skill-mapping.md, state, CHANGELOG) pushed to main via token-embedded URL; job fails loudly if push is rejected (no force-push, ever)

Secrets: only secrets.PACKAGE_TOKEN (write:package + write:repository). Nothing else, nothing committed. Release/tag = publish step 6; no separate release job.

Smoke workflow (workflow_dispatch only): scripts/smoke.ts boots opencode serve in a fixture project whose config points at the local package path, polls GET /api/plugin (status ok), GET /api/skill (contains tdd, to-spec hidden-from-model flag), GET /api/agent (contains pocock, pocock-worker). No model calls, no tokens spent. Optionally a model-driven acceptance run (pocock-agents Docker pattern) stays manual-only.

7. Testing strategy

node --test directly on .ts (Node ≥ 22.18 type stripping; fastcrw precedent — zero test deps).

  • Unit: frontmatter parser (name/description/disable-model-invocation/slash extraction incl. quoted YAML values); manifest generation (fixture skill tree → expected manifest); classifier (classification.json schema: every snapshot skill classified exactly once, enum values valid, in-progress/deprecated excluded); agent generation (frontmatter keys ⊆ V2 allowlist — fails on any legacy key like prompt/permission/maxSteps; permission arrays well-formed; worker skill allow-list ∩ user-invoked skills = ∅); version bump calc; release helpers (latestVersion, publishBody shasum/integrity, isPublished, ensureTag with fake fetch — fastcrw's tests already prove this shape).
  • Integration: plugin setup against a fake ctx (fastcrw's fake-editor pattern) → every manifest skill added once, ids unique, path points at an existing file inside the package, user-invoked skills get autoinvoke: false; bad options throw and register nothing; init bin copies agents to a temp dir, -g to a fake home, refuses to overwrite modified files without --force; guardrails hook denies git push --force, allows git status; drift/regression suite (agents reference only skills present in the snapshot; every agent-referenced skill is in the manifest).
  • Smoke: dispatch-only workflow above; representative execution (one worker ticket build) stays manual/local, documented in docs/runtime-checks.md style.

8. Versioning

Semver; CHANGELOG is the source of truth (D3/D4). Package version in package.json stays 0.0.0-dev; release stamps the staged copy. Upstream tracking is orthogonal: .opencode/state/upstream.json + changelog entries naming mattpocock/skills@<short-sha> (from → to) + skill add/remove/change list. Upstream content change ⇒ minor bump (automated); generator/ plugin/agent behavior change ⇒ human-written changelog entry decides the bump; major never automatic.

9. Phased implementation plan

Phase 1 — plugin package (quality gate: install → load → skills work → publish → nightly green)

1.1 scaffold: package.json (@haylan/opencode-pocock, type module, exports, bin, files), tsconfig, prettier, .gitignore, LICENSE (MIT — matches upstream; include upstream NOTICE/attribution), README 1.2 vendor snapshot: sync-upstream.ts + first upstream/ commit + state files 1.3 generator: manifest + skills tree + tests 1.4 plugin: src/index.ts (skill injection, options validation, agent-staleness warning) + fake-ctx tests 1.5 init bin + tests 1.6 agents v1: hand-port pocock + pocock-worker to V2-native frontmatter as agent-templates/, generate agents/*.md, drift tests 1.7 release.ts + version.ts (adapted from fastcrw) + unit tests 1.8 workflows: check.yml, nightly.yml (secrets documented for the repo admin), smoke.ts + dispatch workflow 1.9 local runtime verification: opencode serve against a fixture project — skills visible, /tdd loads, agents appear after init, worker permission denies hold (opencode debug-style checks or HTTP API); record results in docs/runtime-checks.md 1.10 first manual release 0.1.0 to Gitea registry; verify plugins: ["@haylan/opencode-pocock"] installs from the registry (the one thing fastcrw left unverified — do it here)

Phase 2 — classification-driven conversion (quality gate: every skill classified, conversions tested, rejections documented, sync doesn't break agents)

2.1 per-skill deep analysis (Step 1–3 of the conversion process) → fill classification.json + docs/skill-mapping.md rationale/dependencies 2.2 implement plugin classification: guardrails hook (D7) + tests 2.3 evaluate pocock-reviewer; implement or document rejection 2.4 wire orchestrator flow to V2 realities (background subagents? batch fallback; worktree permissions guidance; slash-command UX for user-invoked skills) 2.5 regression suite per converted skill; orchestration smoke (manual, token-spending, documented) 2.6 docs: README usage guide, skill-mapping complete, runtime-checks updated 2.7 let nightly run a full week; fix fallout; declare stable

10. Persistent TODO

Lives in TODO.md at the repo root (that file is the checklist; this section is the pointer).