221 lines
17 KiB
Markdown
221 lines
17 KiB
Markdown
# 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:
|
||
|
||
```jsonc
|
||
// opencode.json
|
||
{ "plugins": ["@haylan/opencode-pocock"] }
|
||
```
|
||
|
||
```bash
|
||
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).
|