feat: runtime smoke test, gitea actions workflows, runtime-checks record
check / check (push) Successful in 49s

This commit is contained in:
haylan committed 2026-10-14 12:34:57 +02:00
1 parent d301ef5a62
commit c92fe4db73
15 files changed
+847 -137

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
name: check
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm run check
- run: npm test
- run: npm run build
+76
View File
@@ -0,0 +1,76 @@
name: nightly
# Upstream sync -> regenerate -> validate -> version -> publish -> commit back.
# Stops successfully when mattpocock/skills has not moved. Credentials only from secrets.
on:
schedule:
- cron: "30 3 * * *"
workflow_dispatch:
jobs:
sync:
if: gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- name: Sync upstream snapshot
id: sync
run: |
node scripts/sync-upstream.ts
if git diff --quiet -- upstream .opencode/state/upstream.json && git ls-files --error-unmatch upstream/UPSTREAM.json >/dev/null 2>&1 && [ -z "$(git status --porcelain -- upstream .opencode/state/upstream.json)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- name: Stop when nothing changed
if: steps.sync.outputs.changed == 'false'
run: echo "upstream unchanged; nothing to do"
- name: Regenerate OpenCode artifacts
if: steps.sync.outputs.changed == 'true'
run: node scripts/generate.ts
- name: Format, typecheck, test, build
if: steps.sync.outputs.changed == 'true'
run: |
npm run format
npm run check
npm test
npm run build
- name: Prepare changelog + version
if: steps.sync.outputs.changed == 'true'
run: node scripts/version.ts
- name: Commit generated changes back
if: steps.sync.outputs.changed == 'true'
run: |
SHA=$(node -p "require('./upstream/UPSTREAM.json').commit.slice(0,9)")
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@noreply"
git remote set-url origin "https://x-access-token:${{ secrets.PACKAGE_TOKEN }}@${GITEA_HOST#https://}"
git add upstream skills agents manifest.json docs/skill-mapping.md CHANGELOG.md .opencode/state package-lock.json
if git diff --cached --quiet; then
echo "no generated changes to commit"
else
git commit -m "sync: update mattpocock skills to ${SHA} [skip ci]"
git push origin main
fi
env:
GITEA_HOST: ${{ gitea.server_url }}
- name: Publish to Gitea npm registry, tag, link
if: steps.sync.outputs.changed == 'true'
run: |
SHA=$(git rev-parse HEAD)
SERVER_URL="$SERVER_URL" REPOSITORY="$REPOSITORY" SHA="$SHA" TOKEN="$TOKEN" node scripts/release.ts
env:
SERVER_URL: ${{ gitea.server_url }}
REPOSITORY: ${{ gitea.repository }}
TOKEN: ${{ secrets.PACKAGE_TOKEN }}
+23
View File
@@ -0,0 +1,23 @@
name: smoke
# Runtime smoke against a real opencode v2 server: plugin loads, skills register with correct
# autoinvoke flags, agents decode with permissions. No model calls, no tokens. Manual trigger only:
# it installs OpenCode via the official installer and is not required for publishing.
on:
workflow_dispatch:
jobs:
smoke:
if: gitea.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm ci
- run: npm run build
- name: Install OpenCode
run: curl -fsSL https://opencode.ai/install | bash
- name: Run smoke
run: node scripts/smoke.ts
+380 -38
View File
@@ -8,43 +8,385 @@
"pocock-reviewer": "subagent: read-only code-review axis executor (Standards/Spec), findings returned to caller" "pocock-reviewer": "subagent: read-only code-review axis executor (Standards/Spec), findings returned to caller"
}, },
"skills": [ "skills": [
{ "name": "ask-matt", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Pure router prose over all skills; no execution or isolation. Its routing logic also informs the pocock agent's system prompt." }, {
{ "name": "code-review", "bucket": "engineering", "classification": "agent+skill", "user_invoked": false, "artifacts": ["skill:verbatim", "agent:pocock-reviewer"], "depends_on": [], "status": "analyzed", "rationale": "Skill stays verbatim (aggregation protocol), but its core mechanic is two parallel isolated axis reviewers with large self-contained prompts -> dedicated read-only pocock-reviewer subagent hosts each axis. Caller (pocock or user) aggregates per the skill." }, "name": "ask-matt",
{ "name": "codebase-design", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Canonical shared-vocabulary reference consumed by tdd/improve-arch/setup-ts-deep-modules; DESIGN-IT-TWICE parallel subagents run via task tool from the loading agent." }, "bucket": "engineering",
{ "name": "diagnosing-bugs", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Six-phase procedural discipline run inline; no isolation or special permissions needed." }, "classification": "skill",
{ "name": "domain-modeling", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Interactive writing discipline + bundled format files (GLOSSARY/ADR); must run in the main conversation to challenge user wording - an isolated agent would break it." }, "user_invoked": true,
{ "name": "grill-with-docs", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["grilling", "domain-modeling"], "status": "analyzed", "rationale": "7-line composition wrapper; OpenCode skill tool loads the two named skills. Preserve verbatim; orchestrator verifies both load (known upstream wrapper failure mode)." }, "artifacts": ["skill:verbatim"],
{ "name": "implement", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["tdd", "code-review"], "status": "analyzed", "rationale": "Thin single-context per-ticket procedure composing tdd + code-review; the pocock-worker agent embodies its discipline for parallel builds." }, "depends_on": [],
{ "name": "implement-spec", "bucket": "engineering", "classification": "orchestrator", "user_invoked": true, "artifacts": ["skill:verbatim", "agent:pocock", "agent:pocock-worker"], "depends_on": ["tdd", "code-review"], "status": "analyzed", "rationale": "The repo's clearest orchestrator: task-graph frontier, background implementers in git worktrees, merger, fixer. OpenCode's task tool provides no worktrees, so the pocock agent supplies worktree creation, frontier scheduling from its own merge record, and merges; skill text stays verbatim." }, "status": "analyzed",
{ "name": "improve-codebase-architecture", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["codebase-design", "grilling", "domain-modeling"], "status": "analyzed", "rationale": "Interactive survey-and-grill; its exploration/design-it-twice subagents are internal delegation via task tool, not its identity." }, "rationale": "Pure router prose over all skills; no execution or isolation. Its routing logic also informs the pocock agent's system prompt."
{ "name": "pr", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Pure PR-body template/reference. Verbatim vendoring preserves required attribution (metadata.credits + CREDITS.md, Dex Horthy's show-me)." }, },
{ "name": "prototype", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Generation procedure with two branch docs (LOGIC/UI); user reacts to artifacts in main conversation." }, {
{ "name": "research", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "The 12-line skill IS a background-agent brief; OpenCode's built-in general subagent + background:true executes it as written. Dedicated pocock-researcher agent evaluated and REJECTED (D8): the brief is self-contained, a branded agent adds drift risk and no capability." }, "name": "code-review",
{ "name": "retro", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["writing-for-agents"], "status": "analyzed", "rationale": "Analysis over the current session's history - isolating it in a subagent would starve it of the session context it mines." }, "bucket": "engineering",
{ "name": "setup-matt-pocock-skills", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Interactive per-repo scaffolding + seed assets; it creates the docs/agents/* state that to-spec/to-tickets/triage/implement/implement-spec/code-review/wayfinder hard-depend on. Keep verbatim; README flags it as required first run." }, "classification": "agent+skill",
{ "name": "tdd", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": ["codebase-design"], "status": "analyzed", "rationale": "The reusable build discipline; consumed by implement and every pocock-worker via skill allow-list." }, "user_invoked": false,
{ "name": "to-spec", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Synthesis + template + publish procedure; explicitly no interview, needs main-conversation context (grilling output)." }, "artifacts": ["skill:verbatim", "agent:pocock-reviewer"],
{ "name": "to-tickets", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Decomposition with human approval loop; needs the spec conversation in context." }, "depends_on": [],
{ "name": "triage", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["grilling", "domain-modeling"], "status": "analyzed", "rationale": "Maintainer-driven interactive state machine; heavy human-in-the-loop design fits a skill. A dedicated triage agent was considered and rejected: every step waits on the maintainer." }, "status": "analyzed",
{ "name": "wayfinder", "bucket": "engineering", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["grilling", "domain-modeling", "research", "prototype"], "status": "analyzed", "rationale": "Stateful planning protocol over the tracker (map/fog/frontier); its research dispatches use the research skill + built-in subagents." }, "rationale": "Skill stays verbatim (aggregation protocol), but its core mechanic is two parallel isolated axis reviewers with large self-contained prompts -> dedicated read-only pocock-reviewer subagent hosts each axis. Caller (pocock or user) aggregates per the skill."
{ "name": "wizard", "bucket": "engineering", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Code generator with bundled template.sh asset; the generated artifact is a human-run script, no harness integration needed." }, },
{ "name": "grill-me", "bucket": "productivity", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": ["grilling"], "status": "analyzed", "rationale": "7-line stateless wrapper around grilling." }, {
{ "name": "grilling", "bucket": "productivity", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "The interview primitive the whole methodology builds on; 100% human interaction in the main conversation - converting to an agent would make it worse (isolated context can't grill the user). Fact-finding subagents dispatch via task tool." }, "name": "codebase-design",
{ "name": "handoff", "bucket": "productivity", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Document-generation instructions writing to OS temp dir." }, "bucket": "engineering",
{ "name": "teach", "bucket": "productivity", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Stateful multi-session tutor with bundled format files; workspace state lives in files, so no agent isolation needed. A tutor agent was considered and rejected: continuous human dialogue belongs in the primary conversation." }, "classification": "skill",
{ "name": "to-questionnaire", "bucket": "productivity", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Two structured interviews + document template." }, "user_invoked": false,
{ "name": "wait-what", "bucket": "productivity", "classification": "skill", "user_invoked": true, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "7-line corrective re-pitch instruction, usable mid-any-skill." }, "artifacts": ["skill:verbatim"],
{ "name": "writing-for-agents", "bucket": "productivity", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Pure theory reference; the most instructions-only skill in the repo." }, "depends_on": [],
{ "name": "git-guardrails-claude-code", "bucket": "misc", "classification": "plugin", "user_invoked": false, "artifacts": ["skill:verbatim", "plugin:guardrails"], "depends_on": [], "status": "analyzed", "rationale": "Only skill needing executable integration: a Claude Code PreToolUse hook. OpenCode equivalent = opt-in ctx.tool.hook('execute.before') on shell inside our plugin (D7), default off, porting the bundled block-dangerous-git.sh pattern list. Skill text stays vendored verbatim for reference/fidelity." }, "status": "analyzed",
{ "name": "migrate-to-shoehorn", "bucket": "misc", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Mechanical codemod recipe. Frozen misc bucket - vendored as-is." }, "rationale": "Canonical shared-vocabulary reference consumed by tdd/improve-arch/setup-ts-deep-modules; DESIGN-IT-TWICE parallel subagents run via task tool from the loading agent."
{ "name": "scaffold-exercises", "bucket": "misc", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Environment-bound: hard-depends on Matt's private ai-hero-cli, only works in his course repos. Vendored verbatim with caveat noted in mapping doc; no OpenCode artifact can fix an external private tool." }, },
{ "name": "setup-pre-commit", "bucket": "misc", "classification": "skill", "user_invoked": false, "artifacts": ["skill:verbatim"], "depends_on": [], "status": "analyzed", "rationale": "Setup recipe, npm ecosystem only. Frozen misc bucket - vendored as-is." }, {
{ "name": "chief-of-staff", "bucket": "in-progress", "classification": "orchestrator", "user_invoked": true, "artifacts": ["excluded"], "depends_on": [], "status": "analyzed", "rationale": "EXCLUDED per D6 (in-progress beta). Would map to a coordinator persona over background subagents; revisit only if upstream promotes it." }, "name": "diagnosing-bugs",
{ "name": "claude-handoff", "bucket": "in-progress", "classification": "not-convertible", "user_invoked": true, "artifacts": ["excluded"], "depends_on": ["handoff"], "status": "analyzed", "rationale": "EXCLUDED per D6 + harness-bound: hard-depends on Claude CLI --bg/agents launcher. OpenCode analogue would need its own background-session mechanism; handoff skill covers the portable half." }, "bucket": "engineering",
{ "name": "loop-me", "bucket": "in-progress", "classification": "skill", "user_invoked": true, "artifacts": ["excluded"], "depends_on": ["grilling"], "status": "analyzed", "rationale": "EXCLUDED per D6 (in-progress). Grilling + workflows/*.md workspace convention; converts trivially if promoted." }, "classification": "skill",
{ "name": "setup-ts-deep-modules", "bucket": "in-progress", "classification": "skill", "user_invoked": true, "artifacts": ["excluded"], "depends_on": ["codebase-design"], "status": "analyzed", "rationale": "EXCLUDED per D6 (in-progress). Dependency-cruiser setup with bundled config + prove-it-bites self-check; converts cleanly if promoted." }, "user_invoked": false,
{ "name": "writing-beats", "bucket": "in-progress", "classification": "skill", "user_invoked": true, "artifacts": ["excluded"], "depends_on": ["writing-fragments"], "status": "analyzed", "rationale": "EXCLUDED per D6 (in-progress)." }, "artifacts": ["skill:verbatim"],
{ "name": "writing-fragments", "bucket": "in-progress", "classification": "skill", "user_invoked": true, "artifacts": ["excluded"], "depends_on": ["grilling"], "status": "analyzed", "rationale": "EXCLUDED per D6 (in-progress)." }, "depends_on": [],
{ "name": "writing-shape", "bucket": "in-progress", "classification": "skill", "user_invoked": true, "artifacts": ["excluded"], "depends_on": ["writing-fragments"], "status": "analyzed", "rationale": "EXCLUDED per D6 (in-progress)." } "status": "analyzed",
"rationale": "Six-phase procedural discipline run inline; no isolation or special permissions needed."
},
{
"name": "domain-modeling",
"bucket": "engineering",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Interactive writing discipline + bundled format files (GLOSSARY/ADR); must run in the main conversation to challenge user wording - an isolated agent would break it."
},
{
"name": "grill-with-docs",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["grilling", "domain-modeling"],
"status": "analyzed",
"rationale": "7-line composition wrapper; OpenCode skill tool loads the two named skills. Preserve verbatim; orchestrator verifies both load (known upstream wrapper failure mode)."
},
{
"name": "implement",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["tdd", "code-review"],
"status": "analyzed",
"rationale": "Thin single-context per-ticket procedure composing tdd + code-review; the pocock-worker agent embodies its discipline for parallel builds."
},
{
"name": "implement-spec",
"bucket": "engineering",
"classification": "orchestrator",
"user_invoked": true,
"artifacts": ["skill:verbatim", "agent:pocock", "agent:pocock-worker"],
"depends_on": ["tdd", "code-review"],
"status": "analyzed",
"rationale": "The repo's clearest orchestrator: task-graph frontier, background implementers in git worktrees, merger, fixer. OpenCode's task tool provides no worktrees, so the pocock agent supplies worktree creation, frontier scheduling from its own merge record, and merges; skill text stays verbatim."
},
{
"name": "improve-codebase-architecture",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["codebase-design", "grilling", "domain-modeling"],
"status": "analyzed",
"rationale": "Interactive survey-and-grill; its exploration/design-it-twice subagents are internal delegation via task tool, not its identity."
},
{
"name": "pr",
"bucket": "engineering",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Pure PR-body template/reference. Verbatim vendoring preserves required attribution (metadata.credits + CREDITS.md, Dex Horthy's show-me)."
},
{
"name": "prototype",
"bucket": "engineering",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Generation procedure with two branch docs (LOGIC/UI); user reacts to artifacts in main conversation."
},
{
"name": "research",
"bucket": "engineering",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "The 12-line skill IS a background-agent brief; OpenCode's built-in general subagent + background:true executes it as written. Dedicated pocock-researcher agent evaluated and REJECTED (D8): the brief is self-contained, a branded agent adds drift risk and no capability."
},
{
"name": "retro",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["writing-for-agents"],
"status": "analyzed",
"rationale": "Analysis over the current session's history - isolating it in a subagent would starve it of the session context it mines."
},
{
"name": "setup-matt-pocock-skills",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Interactive per-repo scaffolding + seed assets; it creates the docs/agents/* state that to-spec/to-tickets/triage/implement/implement-spec/code-review/wayfinder hard-depend on. Keep verbatim; README flags it as required first run."
},
{
"name": "tdd",
"bucket": "engineering",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": ["codebase-design"],
"status": "analyzed",
"rationale": "The reusable build discipline; consumed by implement and every pocock-worker via skill allow-list."
},
{
"name": "to-spec",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Synthesis + template + publish procedure; explicitly no interview, needs main-conversation context (grilling output)."
},
{
"name": "to-tickets",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Decomposition with human approval loop; needs the spec conversation in context."
},
{
"name": "triage",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["grilling", "domain-modeling"],
"status": "analyzed",
"rationale": "Maintainer-driven interactive state machine; heavy human-in-the-loop design fits a skill. A dedicated triage agent was considered and rejected: every step waits on the maintainer."
},
{
"name": "wayfinder",
"bucket": "engineering",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["grilling", "domain-modeling", "research", "prototype"],
"status": "analyzed",
"rationale": "Stateful planning protocol over the tracker (map/fog/frontier); its research dispatches use the research skill + built-in subagents."
},
{
"name": "wizard",
"bucket": "engineering",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Code generator with bundled template.sh asset; the generated artifact is a human-run script, no harness integration needed."
},
{
"name": "grill-me",
"bucket": "productivity",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": ["grilling"],
"status": "analyzed",
"rationale": "7-line stateless wrapper around grilling."
},
{
"name": "grilling",
"bucket": "productivity",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "The interview primitive the whole methodology builds on; 100% human interaction in the main conversation - converting to an agent would make it worse (isolated context can't grill the user). Fact-finding subagents dispatch via task tool."
},
{
"name": "handoff",
"bucket": "productivity",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Document-generation instructions writing to OS temp dir."
},
{
"name": "teach",
"bucket": "productivity",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Stateful multi-session tutor with bundled format files; workspace state lives in files, so no agent isolation needed. A tutor agent was considered and rejected: continuous human dialogue belongs in the primary conversation."
},
{
"name": "to-questionnaire",
"bucket": "productivity",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Two structured interviews + document template."
},
{
"name": "wait-what",
"bucket": "productivity",
"classification": "skill",
"user_invoked": true,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "7-line corrective re-pitch instruction, usable mid-any-skill."
},
{
"name": "writing-for-agents",
"bucket": "productivity",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Pure theory reference; the most instructions-only skill in the repo."
},
{
"name": "git-guardrails-claude-code",
"bucket": "misc",
"classification": "plugin",
"user_invoked": false,
"artifacts": ["skill:verbatim", "plugin:guardrails"],
"depends_on": [],
"status": "analyzed",
"rationale": "Only skill needing executable integration: a Claude Code PreToolUse hook. OpenCode equivalent = opt-in ctx.tool.hook('execute.before') on shell inside our plugin (D7), default off, porting the bundled block-dangerous-git.sh pattern list. Skill text stays vendored verbatim for reference/fidelity."
},
{
"name": "migrate-to-shoehorn",
"bucket": "misc",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Mechanical codemod recipe. Frozen misc bucket - vendored as-is."
},
{
"name": "scaffold-exercises",
"bucket": "misc",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Environment-bound: hard-depends on Matt's private ai-hero-cli, only works in his course repos. Vendored verbatim with caveat noted in mapping doc; no OpenCode artifact can fix an external private tool."
},
{
"name": "setup-pre-commit",
"bucket": "misc",
"classification": "skill",
"user_invoked": false,
"artifacts": ["skill:verbatim"],
"depends_on": [],
"status": "analyzed",
"rationale": "Setup recipe, npm ecosystem only. Frozen misc bucket - vendored as-is."
},
{
"name": "chief-of-staff",
"bucket": "in-progress",
"classification": "orchestrator",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": [],
"status": "analyzed",
"rationale": "EXCLUDED per D6 (in-progress beta). Would map to a coordinator persona over background subagents; revisit only if upstream promotes it."
},
{
"name": "claude-handoff",
"bucket": "in-progress",
"classification": "not-convertible",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": ["handoff"],
"status": "analyzed",
"rationale": "EXCLUDED per D6 + harness-bound: hard-depends on Claude CLI --bg/agents launcher. OpenCode analogue would need its own background-session mechanism; handoff skill covers the portable half."
},
{
"name": "loop-me",
"bucket": "in-progress",
"classification": "skill",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": ["grilling"],
"status": "analyzed",
"rationale": "EXCLUDED per D6 (in-progress). Grilling + workflows/*.md workspace convention; converts trivially if promoted."
},
{
"name": "setup-ts-deep-modules",
"bucket": "in-progress",
"classification": "skill",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": ["codebase-design"],
"status": "analyzed",
"rationale": "EXCLUDED per D6 (in-progress). Dependency-cruiser setup with bundled config + prove-it-bites self-check; converts cleanly if promoted."
},
{
"name": "writing-beats",
"bucket": "in-progress",
"classification": "skill",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": ["writing-fragments"],
"status": "analyzed",
"rationale": "EXCLUDED per D6 (in-progress)."
},
{
"name": "writing-fragments",
"bucket": "in-progress",
"classification": "skill",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": ["grilling"],
"status": "analyzed",
"rationale": "EXCLUDED per D6 (in-progress)."
},
{
"name": "writing-shape",
"bucket": "in-progress",
"classification": "skill",
"user_invoked": true,
"artifacts": ["excluded"],
"depends_on": ["writing-fragments"],
"status": "analyzed",
"rationale": "EXCLUDED per D6 (in-progress)."
}
] ]
} }
+44 -32
View File
@@ -1,46 +1,58 @@
# Project status # Project status
Updated: 2026-10-14 (phase 0 — analysis) Updated: 2026-10-14 (phase 1 implementation, ~90% done)
## Current phase ## Current phase
Phase 0 (analysis) — COMPLETE. Phase 1 (plugin package) ready to start at TODO 1.1. Phase 1 (plugin package) — implementation complete and runtime-verified locally. Remaining:
first registry release (1.10) + Gitea secret provisioning, then Phase 2.
## Done ## Done
- Repo bootstrapped (was empty; Gitea remote `haylan/mattpocock-skills-opencode` configured). - Phase 0 analysis complete: architecture, 38-skill classification matrix, decisions D1–D8,
- Studied reference repos (cloned to temp, not vendored): verified V2 API surface (docs + `@opencode/plugin@2.0.29` typings + live runtime).
- `fastcrw-opencode-websearch` — V2 plugin shape, Gitea release script/workflows, test style - Package scaffolded: `@haylan/opencode-pocock` (name confirmed by owner), TS ESM, prettier,
(`node --test` on .ts), prettier/tsconfig conventions. `node --test` on .ts, MIT + NOTICE attributions (Matt Pocock, Dex Horthy, mcdays94, fastcrw).
- `mcdays94/pocock-agents` — proven orchestrator/worker agent pair (V1-era, tracks upstream v1.3 - `scripts/sync-upstream.ts`: pristine snapshot vendoring @49dd158 (v1.3.1, 38 skills), early exit
`d81f3a1`), drift-check script, Docker smoke test. on no-change, needs-review classification entries for new skills, previous_commit/last_diff in
- `mattpocock/skills` @ `49dd158` (2026-10-09, v1.3.1) — all 38 SKILL.md files read in full by a state for the changelog generator.
subagent: frontmatter census (only name/description/disable-model-invocation×23/argument-hint×4/ - `scripts/generate.ts`: skills/ tree (verbatim, minus agents/openai.yaml), manifest.json (31
metadata.credits×1), dependency graph, subagent census, 17 pending patch changesets. skills, autoinvoke from upstream DMI), docs/skill-mapping.md matrix between markers.
- Verified OpenCode V2 API surface against docs + `anomalyco/opencode@dev` source + installed - Plugin `src/index.ts` (id `haylan.pocock`): injects all manifest skills (frontmatter-stripped
`@opencode/plugin@2.0.29` typings (see AGENTS.md "Key V2 facts"). content, absolute package paths), opt-in `guardrails` tool hook (upstream pattern list ported),
- Wrote: AGENTS.md, state files (upstream/decisions D1–D8/classification.json), TODO.md, agent missing/stale warning via `.pocock-version` stamp. Fails loudly on bad options.
docs/architecture.md, docs/skill-mapping.md (31 vendored + 7 excluded skills classified). - `src/init.ts` bin: copies agents to project/global agents dir, never clobbers modified files
without --force, stamps version.
- Agents (V2-native frontmatter only): `pocock` (primary orchestrator, worktree dispatch loop),
`pocock-worker` (subagent, skill allow-list tdd/codebase-design/diagnosing-bugs, git/gh denies,
no subagents), `pocock-reviewer` (subagent, read-only code-review axis executor).
- Release pipeline: `scripts/version.ts` (changelog-driven minor bump, idempotent per upstream
SHA) + `scripts/release.ts` (Gitea npm registry publish via HTTP, tag, package link; adapted
from fastcrw) + CHANGELOG.md with 0.1.0 prepared.
- Workflows: check.yml (PR+push), nightly.yml (sync→generate→format/check/test/build→version→
commit-back→publish+tag+link, secrets-only), smoke.yml (dispatch-only, installs opencode).
- `scripts/smoke.ts` — PASS on opencode v2.0.29: plugin active, 31 skills registered (16 hidden),
agents decoded with full permission arrays. Findings: docs/runtime-checks.md.
- Test suite: 32 unit/integration tests green (parser, generator+real-snapshot regression, sync
diff, plugin fake-ctx, init, drift gates, release/version helpers).
## Known gaps / risks ## Known gaps / risks
- Unverified at runtime: exact `Skill.Info` field set accepted by `editor.add` (docs show - 1.10 not done: package never published to the Gitea registry yet; npm-name plugin entry
id/name/description/path/content + autoinvoke; confirm against installed `@opencode/plugin` (resolving through `exports`) unverified — local directory entry verified instead.
typings in Phase 1). Same for `permissions` array frontmatter in agent .md — verify with a real - `PACKAGE_TOKEN` Gitea secret must be provisioned by the repo admin before nightly can publish
`opencode serve` smoke. or commit back.
- Background subagents: V1 needed `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`; V2 status - Nightly runner network access to github.com unverified (fallback: Gitea mirror of upstream).
unknown — orchestrator must work in batch mode either way. - Windows-only smoke quirk: fixture temp dir deletion can lag (EPERM warning, harmless); CI is
- `request.body` overlays are accepted but not sent by the V2 runner yet — don't rely on Linux.
temperature tuning. - Background subagents in V2 (env-gated in V1) not verified; orchestrator text works either way.
- Nightly commit-back to main needs a Gitea token with repo write scope (same `PACKAGE_TOKEN` - `request.body` overlays accepted but not sent by the V2 runner — agents avoid them.
pattern as fastcrw, which used it for tags + package link).
## Next steps (exact) ## Next steps (exact)
1. Phase 1.1: scaffold package (package.json `@haylan/opencode-pocock` — confirm scope with owner, 1. Run `npm run format`, `npm run check`, `npm test`, `npm run build`; commit workflows/smoke/docs.
tsconfig, prettier, .gitignore, MIT LICENSE + upstream attribution, README stub). 2. Push to origin; ask owner to add `PACKAGE_TOKEN` secret (write:package + write:repository).
2. Phase 1.2: scripts/sync-upstream.ts + vendor upstream/ @49dd158 (strip non-skill files: keep 3. 1.10: `node scripts/release.ts` with SERVER_URL/REPOSITORY/SHA/TOKEN env (or dispatch a manual
skills/{engineering,productivity,misc}/** minus agents/openai.yaml; write UPSTREAM.json). release workflow) → verify `plugins: ["@haylan/opencode-pocock"]` installs from the registry in
3. Phase 1.6 note: agent templates = port pocock.md/pocock-worker.md from mcdays94/pocock-agents a clean fixture; record in docs/runtime-checks.md.
(temp clone path in status history) to V2-native frontmatter + add pocock-reviewer; update 4. Phase 2 per TODO.md (2.2 guardrails already implemented in 1.4 — verify mapping note; 2.4–2.7).
stack-skills table to be user-editable; worker skill allow-list from classification.json.
+7 -2
View File
@@ -53,12 +53,17 @@ safe intermediate work.
`ctx.agent.transform(editor)` **cannot add** agents — only list/get/update/remove/default. Hence: `ctx.agent.transform(editor)` **cannot add** agents — only list/get/update/remove/default. Hence:
skills ship via plugin injection, agents via `init` copy. skills ship via plugin injection, agents via `init` copy.
- `Agent.Info` (verified): `{ id, name, mode: subagent|primary|all, description?, model?, system?, - `Agent.Info` (verified): `{ id, name, mode: subagent|primary|all, description?, model?, system?,
hidden, color?, steps?, request{settings,headers,body}, permissions[{action,resource,effect}] }`. hidden, color?, steps?, request{settings,headers,body}, permissions[{action,resource,effect}] }`.
- Skills frontmatter honored: `name`, `description`, `slash`, `disable-model-invocation` / - Skills frontmatter honored: `name`, `description`, `slash`, `disable-model-invocation` /
`metadata.opencode/autoinvoke` (hidden from model, still loadable by id/slash). `metadata.opencode/autoinvoke` (hidden from model, still loadable by id/slash).
- Agent files: `.opencode/agents/<name>.md` (project), `~/.config/opencode/agents/` (global); - Agent files: `.opencode/agents/<name>.md` (project), `~/.config/opencode/agents/` (global);
frontmatter = agent config, body = system prompt. frontmatter = agent config, body = system prompt.
- Permission rules are ordered, last match wins; per-agent skill restrictions via - Permission rules are ordered, last match wins; per-agent skill restrictions via
`{action: "skill", resource: "<pattern>", effect: ...}`. Shell action is `shell` in V2. `{action: "skill", resource: "<pattern>", effect: ...}`. V2 actions include `shell`, `edit`,
`read`/`glob`/`grep`, `webfetch`, `skill`, `subagent`, `external_directory`; no rule → ask.
- Runtime-verified on v2.0.29 (see docs/runtime-checks.md): local plugin entries must be
DIRECTORIES (file entries ignored); `/api/skill` omits `autoinvoke` when true; injected skills
get no slash commands (file-based only); default rules already allow `external_directory /tmp/*`;
injected content must have frontmatter stripped; serve prints its Basic-auth password.
- Agent `steps` replaces `maxSteps`; `temperature`/`top_p` go under `request.body` (not yet sent by - Agent `steps` replaces `maxSteps`; `temperature`/`top_p` go under `request.body` (not yet sent by
the runner — avoid). the runner — avoid).
+18 -13
View File
@@ -17,7 +17,7 @@ upstream revision each release carries).
// opencode.json // opencode.json
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"plugins": ["@haylan/opencode-pocock"] "plugins": ["@haylan/opencode-pocock"],
} }
``` ```
@@ -43,18 +43,23 @@ Start OpenCode with the `pocock` agent selected and describe what you want to bu
opencode --agent pocock opencode --agent pocock
``` ```
| Agent | Role | | Agent | Role |
| --- | --- | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pocock` | Primary orchestrator. Routes with `ask-matt`, walks grill → spec → tickets → build → review → retro, proposes before each user-invoked step, runs `implement-spec` with one git worktree per ticket and parallel `pocock-worker` subagents. | | `pocock` | Primary orchestrator. Routes with `ask-matt`, walks grill → spec → tickets → build → review → retro, proposes before each user-invoked step, runs `implement-spec` with one git worktree per ticket and parallel `pocock-worker` subagents. |
| `pocock-worker` | Subagent. Builds one ticket test-first (`tdd` at the spec's seams) inside its worktree, merges the integration tip, reports. Never pushes. | | `pocock-worker` | Subagent. Builds one ticket test-first (`tdd` at the spec's seams) inside its worktree, merges the integration tip, reports. Never pushes. |
| `pocock-reviewer` | Subagent. Read-only executor of one `code-review` axis (Standards or Spec). | | `pocock-reviewer` | Subagent. Read-only executor of one `code-review` axis (Standards or Spec). |
23 of the 31 vendored skills are user-invoked upstream (`disable-model-invocation: true`); 23 of the 31 vendored skills are user-invoked upstream (`disable-model-invocation: true`);
OpenCode v2 honors that natively — they stay out of the model's auto-selection and are run by you OpenCode v2 honors that natively — they stay out of the model's auto-selection. Note that
as slash commands (`/to-spec`, `/retro`, ...). plugin-injected skills are not exposed as slash commands in v2 (only file-based skills are):
the `pocock` agent proposes each user-invoked step and loads it after your yes. If you want
`/to-spec` & co. directly, either keep a file-based install of Matt's skills (e.g.
`npx skills@latest add mattpocock/skills -a opencode`; id collisions then resolve to your
file-based copies) or, for a project-level npm install, add a skills source:
`"skills": ["./node_modules/@haylan/opencode-pocock/skills"]`.
Both agents may create worktrees under `/tmp/pocock-workers`. If you use subagents that write Worktrees live under `/tmp/pocock-workers`. Current OpenCode v2 builds allow `external_directory`
there (exploration notes), consider a global permission rule: for `/tmp/*` by default; if yours asks before a subagent writes there, add a global rule:
```json ```json
{ "permission": { "external_directory": { "/tmp/pocock-workers/*": "allow" } } } { "permission": { "external_directory": { "/tmp/pocock-workers/*": "allow" } } }
@@ -69,9 +74,9 @@ there (exploration notes), consider a global permission rule:
"package": "@haylan/opencode-pocock", "package": "@haylan/opencode-pocock",
// Ports skills/misc/git-guardrails-claude-code to OpenCode: blocks dangerous git // Ports skills/misc/git-guardrails-claude-code to OpenCode: blocks dangerous git
// commands (push --force, reset --hard, clean -f, branch -D, ...) before execution. // commands (push --force, reset --hard, clean -f, branch -D, ...) before execution.
"options": { "guardrails": true } "options": { "guardrails": true },
} },
] ],
} }
``` ```
+21 -18
View File
@@ -19,19 +19,19 @@ State: `.opencode/state/status.md`. Design: `docs/architecture.md`.
## Phase 1 — plugin package ## Phase 1 — plugin package
- [ ] 1.1 scaffold package (package.json name @haylan/opencode-pocock — CONFIRM scope with repo - [x] 1.1 scaffold package (`@haylan/opencode-pocock` confirmed by owner 2026-10-14)
owner, tsconfig, prettier, .gitignore, MIT LICENSE + upstream attribution, README stub) - [x] 1.2 scripts/sync-upstream.ts + vendor upstream/ @49dd158 + state update
- [ ] 1.2 scripts/sync-upstream.ts + vendor upstream/ @49dd158 + state update - [x] 1.3 scripts/generate.ts: manifest.json + skills/ output + unit tests (parser, manifest)
- [ ] 1.3 scripts/generate.ts: manifest.json + skills/ output + unit tests (parser, manifest) - [x] 1.4 src/index.ts plugin: skill injection (frontmatter-stripped content), options validation,
- [ ] 1.4 src/index.ts plugin: skill injection, options validation, agent-staleness warning + agent-staleness warning + fake-ctx integration tests (Skill.Info verified: cast needed for
fake-ctx integration tests (verify Skill.Info fields against installed @opencode/plugin typings!) effect-branded fields; runtime PASS)
- [ ] 1.5 bin init (copy agents, -g global, no silent overwrite) + tests - [x] 1.5 bin init (copy agents, -g global, no silent overwrite, .pocock-version stamp) + tests
- [ ] 1.6 agent-templates/: port pocock + pocock-worker from pocock-agents to V2-native frontmatter - [x] 1.6 agents/: pocock + pocock-worker + pocock-reviewer, V2-native frontmatter; drift tests
(permissions array, shell action, no legacy keys); generate agents/*.md; drift tests (upstream names, worker skill allow-list, reviewer read-only, no subagent recursion)
- [ ] 1.7 scripts/release.ts + version.ts adapted from fastcrw + unit tests - [x] 1.7 scripts/release.ts + version.ts adapted from fastcrw + unit tests
- [ ] 1.8 .gitea/workflows/: check.yml, nightly.yml, smoke (dispatch); smoke.ts against opencode serve - [x] 1.8 .gitea/workflows/: check.yml, nightly.yml, smoke.yml (dispatch); scripts/smoke.ts —
- [ ] 1.9 local runtime verification (serve + HTTP API: plugin ok, skills visible, autoinvoke flags, PASS on v2.0.29 (plugin active, 31 skills, 16 hidden, agents + permissions decoded)
agents after init, worker denies hold) → docs/runtime-checks.md - [x] 1.9 local runtime verification → docs/runtime-checks.md (findings folded into README/AGENTS)
- [ ] 1.10 manual release 0.1.0 to Gitea registry; verify install-from-registry by package name - [ ] 1.10 manual release 0.1.0 to Gitea registry; verify install-from-registry by package name
- [ ] ask repo admin to add Gitea secret PACKAGE_TOKEN (write:package + write:repository) - [ ] ask repo admin to add Gitea secret PACKAGE_TOKEN (write:package + write:repository)
@@ -51,12 +51,15 @@ State: `.opencode/state/status.md`. Design: `docs/architecture.md`.
- ~~Exact `Skill.Info`/editor.add field set~~ RESOLVED 2026-10-14 (`@opencode/plugin@2.0.29`): - ~~Exact `Skill.Info`/editor.add field set~~ RESOLVED 2026-10-14 (`@opencode/plugin@2.0.29`):
`{id, name, description?, autoinvoke?, path, content}`; no `slash` in Info. `{id, name, description?, autoinvoke?, path, content}`; no `slash` in Info.
- V2 background subagents: still env-gated? Affects orchestrator dispatch loop wording (1.6). - V2 background subagents: still env-gated? Affects orchestrator dispatch loop wording only
- Do plugin-injected skills get `/name` slash exposure (no `slash` field in Skill.Info)? Runtime (agent text covers both modes) — low priority, observe at 2.5.
check at 1.9; fallback = document `skills: ["<pkg-dir>/skills"]` config entry for file-based load. - ~~Slash exposure for injected skills~~ RESOLVED: none (runtime check 2026-10-14); README
- Package name/scope confirmation (1.1). documents file-based alternatives.
- Package name/scope: CONFIRMED `@haylan/opencode-pocock` (owner, 2026-10-14).
- Does nightly runner have network access to github.com for upstream fetch? (1.8 — fallback: mirror - Does nightly runner have network access to github.com for upstream fetch? (1.8 — fallback: mirror
upstream to Gitea and sync from the mirror.) upstream to Gitea and sync from the mirror.) Verify on first nightly run.
- Install-from-registry by package name (npm-name plugin entry resolving through `exports`) —
verify at 1.10; local dir entry verified.
## Done log ## Done log
+10 -10
View File
@@ -60,16 +60,16 @@ This is `ask-matt`'s main flow with your part added. Announce each phase as you
Route by `ask-matt`. The common ones: Route by `ask-matt`. The common ones:
| Situation | Start with | | Situation | Start with |
|---|---| | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Incoming bug reports or feature requests nobody on the team wrote | `triage`, then Build for `ready-for-agent` issues | | Incoming bug reports or feature requests nobody on the team wrote | `triage`, then Build for `ready-for-agent` issues |
| A hard bug, flaky test or performance regression | `diagnosing-bugs` | | A hard bug, flaky test or performance regression | `diagnosing-bugs` |
| A huge, foggy effort that won't fit one session | `wayfinder`; when the map clears, continue at step 3 | | A huge, foggy effort that won't fit one session | `wayfinder`; when the map clears, continue at step 3 |
| A codebase health pass | `improve-codebase-architecture`; a chosen candidate becomes an idea for step 1 | | A codebase health pass | `improve-codebase-architecture`; a chosen candidate becomes an idea for step 1 |
| A question that needs docs, source or specs read | `research` (dispatch it as a background `general` subagent, as the skill says) | | A question that needs docs, source or specs read | `research` (dispatch it as a background `general` subagent, as the skill says) |
| A step only a human can do (dashboards, credentials, CI secrets) | `wizard` | | A step only a human can do (dashboards, credentials, CI secrets) | `wizard` |
| A decision only someone else can make | `to-questionnaire` | | A decision only someone else can make | `to-questionnaire` |
| A finished spec with tickets | step 5 | | A finished spec with tickets | step 5 |
Don't triage tickets that `to-tickets` produced; they're agent-ready already. Don't triage tickets that `to-tickets` produced; they're agent-ready already.
+16 -16
View File
@@ -55,7 +55,7 @@ Generator scope (deliberately small):
- strip non-OpenCode harness files (`agents/openai.yaml`, Claude plugin metadata) from the output tree - 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, - parse each SKILL.md frontmatter → `manifest.json` entry: `{ id, name, description, autoinvoke,
slash, path, files[] }`; `autoinvoke=false` when upstream sets `disable-model-invocation: true` 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) - 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) - regenerate `docs/skill-mapping.md` from `classification.json` (docs never drift from data)
@@ -68,14 +68,14 @@ D6), deprecated 0. Full table with classification + rationale: `docs/skill-mappi
Verified against V2 (`@opencode/plugin` ^2.x, `Plugin.define`): Verified against V2 (`@opencode/plugin` ^2.x, `Plugin.define`):
| Requirement | V2 mechanism | | Requirement | V2 mechanism |
| --- | --- | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Stable identity | `Plugin.define({ id: "haylan.pocock", setup })` | | 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 | | 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) | | 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 }` | | 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 | | 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 | | 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. Not needed: custom tools, MCP, websearch, auth. Keep the plugin surface minimal.
@@ -83,12 +83,12 @@ Not needed: custom tools, MCP, websearch, auth. Keep the plugin surface minimal.
Hierarchy (D5) — two core agents, one likely addition, rest rejected unless the matrix proves value: Hierarchy (D5) — two core agents, one likely addition, rest rejected unless the matrix proves value:
| Agent | Mode | Role | Key V2 config | | 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` | 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-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-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 | | `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; 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 skills via `action: skill` permission patterns; primary vs subagent via `mode`; spawn rights — only
@@ -112,7 +112,7 @@ methodology depends on, e.g. grilling facts feeding `to-spec`).
- Diff old vs new snapshot → added/removed/changed skill list → written into the changelog entry and - 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 `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 (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 by determinism — unchanged inputs produce byte-identical outputs, so the git diff _is_ the
affected-set report). affected-set report).
- New upstream skills appear in the manifest automatically but land unclassified: - 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 `classification.json` defaults them to `skill` + `status: "needs-review"`, and the drift test fails
+46
View File
@@ -0,0 +1,46 @@
# Runtime checks — verified against real OpenCode v2
Environment: Windows 11, `opencode v2.0.29`, `@opencode/plugin@2.0.29`, 2026-10-14.
Method: `scripts/smoke.ts` boots `opencode serve` (private server, Basic auth user `opencode`,
password printed on stdout) in a temp fixture and asserts against the HTTP API. Result: **PASS**.
## Verified
- **Plugin loads and activates** from a local directory entry (`plugins: ["<abs path>/dist"]`);
`GET /api/plugin` shows `haylan.pocock` with `state.status: "active"`.
- A **file** entry (`file://.../dist/index.js`) is IGNORED — local plugin entries must be
directories with a root `index`/`server` file (confirms fastcrw research). npm-name entries
resolve through `exports` (to be re-verified after first registry publish, TODO 1.10).
- **Skill injection works**: all 31 manifest skills registered (`GET /api/skill`); the 16
user-invoked ones carry `autoinvoke: false`. The API **omits `autoinvoke` when true** (default).
- **DMI honored natively**: file-loaded skills with upstream `disable-model-invocation: true`
(e.g. a global `~/.agents/skills` install) show `autoinvoke: false` without our plugin.
- **ID collisions**: when the same skill id also exists as a file-based skill (global install), the
file-based entry is what `/api/skill` reports; plugin injection coexists without errors.
(`pr`, absent from the tester's global install, was served from our package path.)
- **Injected content must have frontmatter stripped** to match file-based loading
(`markdown.content`); the plugin does this since 2026-10-14.
- **Agent markdown with V2-native frontmatter decodes correctly** from `.opencode/agents/`:
`pocock` (primary), `pocock-worker` / `pocock-reviewer` (subagent), descriptions intact, and the
full `permissions` array survived decoding in order — verified on `pocock-worker`:
`skill * deny` + `tdd/codebase-design/diagnosing-bugs allow`, `subagent * deny`, and all
git/gh/glab shell denies present.
- **Default permission floor** (v2.0.29): agents start with `{action:"*",resource:"*",effect:"allow"}`
plus specific rules, among them `external_directory /tmp` and `/tmp/*` **allow** — the
`/tmp/pocock-workers` worktree root needs no extra user config on this build (README still
documents the rule for other setups/subagents).
- **Injected skills do NOT become slash commands** (`GET /api/command` shows none for them;
`Skill.Info` has no `slash` field). User-invoked steps are started via conversation with `pocock`
(propose → yes → load) or by any agent explicitly loading the skill. File-based installs (global
`~/.agents/skills`, or a `skills: [...]` directory source) keep slash behavior.
- **Server API**: prefix `/api/*`, Basic auth (`opencode` + printed password); SPA HTML answers
unknown non-/api paths with 200 — assert JSON, not status. `POST /api/location/reload` exists.
Plugin loading is lazy (appears seconds after boot); `/openapi.json` requires auth.
## Known environment quirks (tester machine, not product issues)
- Windows: `server.kill()` under `shell:true` only kills `cmd.exe`; smoke uses
`taskkill /PID <pid> /T /F`. Temp fixture deletion can lag behind handle release (EPERM warning);
harmless, retried on next run.
- The user's global `~/.agents/skills` already contains most Matt Pocock skills — smoke assertions
are written to tolerate id collisions (presence + flags, not source path).
+159
View File
@@ -0,0 +1,159 @@
// Runtime smoke test against a real `opencode serve` (v2). No model calls, no tokens spent.
//
// npm run build && node scripts/smoke.ts [--keep]
//
// Builds a temp fixture project whose opencode.json loads this repo's dist/ directory as a local
// plugin (local plugin entries must be DIRECTORIES with a root index file; file entries are
// ignored), installs the agents with our own init, boots a private server, and asserts over the
// HTTP API (Basic auth user `opencode`, password printed by serve):
// - plugin haylan.pocock reaches state.status "active"
// - every manifest skill id is registered and user-invoked ones have autoinvoke=false
// - agents pocock (primary), pocock-worker / pocock-reviewer (subagent) are discovered and the
// worker's skill deny + allow-list survived frontmatter decoding
// Env: OPENCODE_BIN (default "opencode"), SMOKE_PORT (default random).
import { spawn, spawnSync } from "node:child_process"
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync } from "node:fs"
import { tmpdir } from "node:os"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { runInit } from "../src/init.ts"
import { loadManifest, packageRoot } from "../src/manifest.ts"
const BIN = process.env.OPENCODE_BIN ?? "opencode"
const PORT = Number(process.env.SMOKE_PORT ?? 40000 + Math.floor(Math.random() * 20000))
const BASE = `http://127.0.0.1:${PORT}`
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(`smoke FAIL: ${message}`)
}
async function waitReady(timeoutMs = 90_000): Promise<void> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
try {
const res = await fetch(BASE + "/")
if (res.ok) return
} catch {
/* not up yet */
}
await new Promise((r) => setTimeout(r, 500))
}
throw new Error("smoke FAIL: server did not come up in time")
}
async function main() {
if (!existsSync(path.join(packageRoot, "dist", "index.js"))) {
throw new Error("smoke FAIL: dist/index.js missing - run `npm run build` first")
}
const manifest = loadManifest()
const fixture = mkdtempSync(path.join(tmpdir(), "pocock-smoke-"))
let server: ReturnType<typeof spawn> | undefined
let out = ""
try {
mkdirSync(path.join(fixture, ".opencode"), { recursive: true })
writeFileSync(
path.join(fixture, "opencode.json"),
JSON.stringify(
{ $schema: "https://opencode.ai/config.json", plugins: [path.join(packageRoot, "dist")] },
null,
2,
),
)
runInit({ argv: ["init"], cwd: fixture, pkgRoot: packageRoot })
server = spawn(BIN, ["serve", "--hostname", "127.0.0.1", "--port", String(PORT)], {
cwd: fixture,
stdio: ["ignore", "pipe", "pipe"],
shell: process.platform === "win32",
})
const collect = (chunk: Buffer) => (out += chunk.toString())
server.stdout?.on("data", collect)
server.stderr?.on("data", collect)
await waitReady()
const pw = /server password (\S+)/.exec(out)?.[1]
assert(pw, `server password not found in output:\n${out.slice(0, 400)}`)
const auth = { Authorization: "Basic " + Buffer.from(`opencode:${pw}`).toString("base64") }
const get = async (p: string) => {
const res = await fetch(BASE + "/api" + p, { headers: auth })
assert(res.ok, `GET /api${p} -> ${res.status}`)
return res.json() as Promise<{ data?: unknown } & Record<string, unknown>>
}
// Plugin loads (lazy: poll until it appears)
let plugin: { id: string; state?: { status?: string } } | undefined
const deadline = Date.now() + 60_000
while (!plugin && Date.now() < deadline) {
const body = (await get("/plugin")) as { data: { id: string; state?: { status: string } }[] }
plugin = body.data.find((p) => p.id === "haylan.pocock")
if (!plugin) await new Promise((r) => setTimeout(r, 1000))
}
assert(plugin, "plugin haylan.pocock never appeared in /api/plugin")
assert(plugin!.state?.status === "active", `plugin state: ${JSON.stringify(plugin!.state)}`)
console.log("ok plugin haylan.pocock active")
// Skills registered with correct autoinvoke flags (ids may also exist from global installs)
const skillBody = (await get("/skill")) as { data: { id: string; autoinvoke?: boolean }[] }
const byId = new Map(skillBody.data.map((s) => [s.id, s]))
for (const entry of manifest) {
const found = byId.get(entry.id)
assert(found, `skill '${entry.id}' not registered`)
// the API omits autoinvoke when it defaults to true
const actual = found!.autoinvoke ?? true
assert(
actual === entry.autoinvoke,
`skill '${entry.id}' autoinvoke=${actual}, want ${entry.autoinvoke}`,
)
}
console.log(
`ok ${manifest.length} skills registered (${manifest.filter((m) => !m.autoinvoke).length} hidden from model)`,
)
// Agents discovered with correct modes and the worker's permission decoding intact
const agentBody = (await get("/agent")) as {
data: {
id: string
mode: string
permissions: { action: string; resource: string; effect: string }[]
}[]
}
const agents = new Map(agentBody.data.map((a) => [a.id, a]))
assert(agents.get("pocock")?.mode === "primary", "pocock is not primary")
assert(agents.get("pocock-reviewer")?.mode === "subagent", "pocock-reviewer is not subagent")
const worker = agents.get("pocock-worker")
assert(worker?.mode === "subagent", "pocock-worker is not subagent")
const rules = worker!.permissions
const has = (action: string, resource: string, effect: string) =>
rules.some((r) => r.action === action && r.resource === resource && r.effect === effect)
assert(has("skill", "*", "deny"), "worker skill deny * missing")
assert(has("skill", "tdd", "allow"), "worker tdd allow missing")
assert(has("shell", "git push*", "deny"), "worker git push deny missing")
assert(has("subagent", "*", "deny"), "worker subagent deny missing")
console.log("ok agents pocock/pocock-worker/pocock-reviewer with permissions decoded")
console.log("smoke PASS")
} finally {
// shell:true on Windows means kill() only reaches cmd.exe; kill the whole tree.
if (server?.pid) {
if (process.platform === "win32")
spawnSync("taskkill", ["/PID", String(server.pid), "/T", "/F"])
else server.kill()
}
// Windows holds locks briefly after kill: retry, and never let cleanup mask the real error.
try {
if (process.argv.includes("--keep")) console.log(`fixture kept at ${fixture}`)
else rmSync(fixture, { recursive: true, force: true, maxRetries: 10, retryDelay: 300 })
} catch (cleanupErr) {
console.log(`warning: could not remove fixture ${fixture}: ${(cleanupErr as Error).message}`)
}
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main()
.then(() => process.exit(0))
.catch((err) => {
console.error(err.message)
process.exit(1)
})
}
+3 -1
View File
@@ -7,7 +7,9 @@ export function resolveOptions(options: Record<string, unknown> | undefined): Op
const opts = options ?? {} const opts = options ?? {}
const guardrails = opts.guardrails ?? false const guardrails = opts.guardrails ?? false
if (typeof guardrails !== "boolean") { if (typeof guardrails !== "boolean") {
throw new Error(`pocock: option guardrails must be a boolean, got ${JSON.stringify(guardrails)}`) throw new Error(
`pocock: option guardrails must be a boolean, got ${JSON.stringify(guardrails)}`,
)
} }
return { guardrails } return { guardrails }
} }
+8 -3
View File
@@ -27,7 +27,8 @@ export function runInit(opts: {
const global = args.has("-g") || args.has("--global") const global = args.has("-g") || args.has("--global")
const force = args.has("--force") const force = args.has("--force")
const unknown = opts.argv.filter((a) => !["-g", "--global", "--force", "init"].includes(a)) const unknown = opts.argv.filter((a) => !["-g", "--global", "--force", "init"].includes(a))
if (unknown.length) throw new Error(`opencode-pocock: unknown argument ${unknown[0]} (try: init [-g] [--force])`) if (unknown.length)
throw new Error(`opencode-pocock: unknown argument ${unknown[0]} (try: init [-g] [--force])`)
const root = opts.pkgRoot ?? packageRoot const root = opts.pkgRoot ?? packageRoot
const sourceDir = path.join(root, "agents") const sourceDir = path.join(root, "agents")
@@ -36,7 +37,9 @@ export function runInit(opts: {
mkdirSync(target, { recursive: true }) mkdirSync(target, { recursive: true })
const result: InitResult = { target, copied: [], skipped: [] } const result: InitResult = { target, copied: [], skipped: [] }
for (const file of readdirSync(sourceDir).filter((f) => f.endsWith(".md")).sort()) { for (const file of readdirSync(sourceDir)
.filter((f) => f.endsWith(".md"))
.sort()) {
const from = path.join(sourceDir, file) const from = path.join(sourceDir, file)
const to = path.join(target, file) const to = path.join(target, file)
const content = readFileSync(from, "utf8") const content = readFileSync(from, "utf8")
@@ -56,7 +59,9 @@ function main() {
const result = runInit({ argv: process.argv.slice(2), cwd: process.cwd() }) const result = runInit({ argv: process.argv.slice(2), cwd: process.cwd() })
for (const file of result.copied) console.log(`installed ${path.join(result.target, file)}`) for (const file of result.copied) console.log(`installed ${path.join(result.target, file)}`)
for (const file of result.skipped) { for (const file of result.skipped) {
console.log(`skipped ${file}: local copy differs from the package (re-run with --force to replace)`) console.log(
`skipped ${file}: local copy differs from the package (re-run with --force to replace)`,
)
} }
console.log( console.log(
`\nNext: make sure opencode.json loads the plugin:\n { "plugins": ["@haylan/opencode-pocock"] }\n` + `\nNext: make sure opencode.json loads the plugin:\n { "plugins": ["@haylan/opencode-pocock"] }\n` +
+16 -4
View File
@@ -23,7 +23,10 @@ test("runInit copies agents, stamps version, targets project or global dir", ()
assert.equal(project.target, path.join(cwd, ".opencode", "agents")) assert.equal(project.target, path.join(cwd, ".opencode", "agents"))
assert.deepEqual(project.copied, ["pocock-worker.md", "pocock.md"]) assert.deepEqual(project.copied, ["pocock-worker.md", "pocock.md"])
assert.deepEqual(project.skipped, []) assert.deepEqual(project.skipped, [])
assert.equal(readFileSync(path.join(project.target, "pocock.md"), "utf8"), "---\nmode: primary\n---\nv1\n") assert.equal(
readFileSync(path.join(project.target, "pocock.md"), "utf8"),
"---\nmode: primary\n---\nv1\n",
)
assert.equal(readFileSync(path.join(project.target, ".pocock-version"), "utf8"), "9.9.9\n") assert.equal(readFileSync(path.join(project.target, ".pocock-version"), "utf8"), "9.9.9\n")
const global = runInit({ argv: ["init", "-g"], cwd, pkgRoot, home }) const global = runInit({ argv: ["init", "-g"], cwd, pkgRoot, home })
@@ -41,7 +44,10 @@ test("runInit copies agents, stamps version, targets project or global dir", ()
assert.deepEqual(forced.skipped, []) assert.deepEqual(forced.skipped, [])
assert.match(readFileSync(path.join(project.target, "pocock.md"), "utf8"), /v1/) assert.match(readFileSync(path.join(project.target, "pocock.md"), "utf8"), /v1/)
assert.throws(() => runInit({ argv: ["init", "--wat"], cwd, pkgRoot, home }), /unknown argument --wat/) assert.throws(
() => runInit({ argv: ["init", "--wat"], cwd, pkgRoot, home }),
/unknown argument --wat/,
)
} finally { } finally {
for (const dir of [pkgRoot, home, cwd]) rmSync(dir, { recursive: true, force: true }) for (const dir of [pkgRoot, home, cwd]) rmSync(dir, { recursive: true, force: true })
} }
@@ -49,14 +55,20 @@ test("runInit copies agents, stamps version, targets project or global dir", ()
test("agentsTarget resolves global vs project locations", () => { test("agentsTarget resolves global vs project locations", () => {
assert.equal(agentsTarget(false, "/repo", "/home/u"), path.join("/repo", ".opencode", "agents")) assert.equal(agentsTarget(false, "/repo", "/home/u"), path.join("/repo", ".opencode", "agents"))
assert.equal(agentsTarget(true, "/repo", "/home/u"), path.join("/home/u", ".config", "opencode", "agents")) assert.equal(
agentsTarget(true, "/repo", "/home/u"),
path.join("/home/u", ".config", "opencode", "agents"),
)
}) })
test("runInit fails clearly when the package has no agents dir", () => { test("runInit fails clearly when the package has no agents dir", () => {
const empty = mkdtempSync(path.join(tmpdir(), "init-empty-")) const empty = mkdtempSync(path.join(tmpdir(), "init-empty-"))
try { try {
writeFileSync(path.join(empty, "package.json"), "{}") writeFileSync(path.join(empty, "package.json"), "{}")
assert.throws(() => runInit({ argv: ["init"], cwd: empty, pkgRoot: empty, home: empty }), /no agents\/ in package/) assert.throws(
() => runInit({ argv: ["init"], cwd: empty, pkgRoot: empty, home: empty }),
/no agents\/ in package/,
)
} finally { } finally {
rmSync(empty, { recursive: true, force: true }) rmSync(empty, { recursive: true, force: true })
} }