diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml new file mode 100644 index 0000000..fc11426 --- /dev/null +++ b/.gitea/workflows/check.yml @@ -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 diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml new file mode 100644 index 0000000..946df1a --- /dev/null +++ b/.gitea/workflows/nightly.yml @@ -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 }} diff --git a/.gitea/workflows/smoke.yml b/.gitea/workflows/smoke.yml new file mode 100644 index 0000000..8ce9306 --- /dev/null +++ b/.gitea/workflows/smoke.yml @@ -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 diff --git a/.opencode/state/classification.json b/.opencode/state/classification.json index 4488157..b04d545 100644 --- a/.opencode/state/classification.json +++ b/.opencode/state/classification.json @@ -8,43 +8,385 @@ "pocock-reviewer": "subagent: read-only code-review axis executor (Standards/Spec), findings returned to caller" }, "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": "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." }, - { "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." }, - { "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)." } + { + "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": "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." + }, + { + "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." + }, + { + "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)." + } ] } diff --git a/.opencode/state/status.md b/.opencode/state/status.md index c7587ef..fcb9934 100644 --- a/.opencode/state/status.md +++ b/.opencode/state/status.md @@ -1,46 +1,58 @@ # Project status -Updated: 2026-10-14 (phase 0 — analysis) +Updated: 2026-10-14 (phase 1 implementation, ~90% done) ## 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 -- Repo bootstrapped (was empty; Gitea remote `haylan/mattpocock-skills-opencode` configured). -- Studied reference repos (cloned to temp, not vendored): - - `fastcrw-opencode-websearch` — V2 plugin shape, Gitea release script/workflows, test style - (`node --test` on .ts), prettier/tsconfig conventions. - - `mcdays94/pocock-agents` — proven orchestrator/worker agent pair (V1-era, tracks upstream v1.3 - `d81f3a1`), drift-check script, Docker smoke test. - - `mattpocock/skills` @ `49dd158` (2026-10-09, v1.3.1) — all 38 SKILL.md files read in full by a - subagent: frontmatter census (only name/description/disable-model-invocation×23/argument-hint×4/ - metadata.credits×1), dependency graph, subagent census, 17 pending patch changesets. -- Verified OpenCode V2 API surface against docs + `anomalyco/opencode@dev` source + installed - `@opencode/plugin@2.0.29` typings (see AGENTS.md "Key V2 facts"). -- Wrote: AGENTS.md, state files (upstream/decisions D1–D8/classification.json), TODO.md, - docs/architecture.md, docs/skill-mapping.md (31 vendored + 7 excluded skills classified). +- Phase 0 analysis complete: architecture, 38-skill classification matrix, decisions D1–D8, + verified V2 API surface (docs + `@opencode/plugin@2.0.29` typings + live runtime). +- Package scaffolded: `@haylan/opencode-pocock` (name confirmed by owner), TS ESM, prettier, + `node --test` on .ts, MIT + NOTICE attributions (Matt Pocock, Dex Horthy, mcdays94, fastcrw). +- `scripts/sync-upstream.ts`: pristine snapshot vendoring @49dd158 (v1.3.1, 38 skills), early exit + on no-change, needs-review classification entries for new skills, previous_commit/last_diff in + state for the changelog generator. +- `scripts/generate.ts`: skills/ tree (verbatim, minus agents/openai.yaml), manifest.json (31 + skills, autoinvoke from upstream DMI), docs/skill-mapping.md matrix between markers. +- Plugin `src/index.ts` (id `haylan.pocock`): injects all manifest skills (frontmatter-stripped + content, absolute package paths), opt-in `guardrails` tool hook (upstream pattern list ported), + agent missing/stale warning via `.pocock-version` stamp. Fails loudly on bad options. +- `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 -- Unverified at runtime: exact `Skill.Info` field set accepted by `editor.add` (docs show - id/name/description/path/content + autoinvoke; confirm against installed `@opencode/plugin` - typings in Phase 1). Same for `permissions` array frontmatter in agent .md — verify with a real - `opencode serve` smoke. -- Background subagents: V1 needed `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true`; V2 status - unknown — orchestrator must work in batch mode either way. -- `request.body` overlays are accepted but not sent by the V2 runner yet — don't rely on - temperature tuning. -- Nightly commit-back to main needs a Gitea token with repo write scope (same `PACKAGE_TOKEN` - pattern as fastcrw, which used it for tags + package link). +- 1.10 not done: package never published to the Gitea registry yet; npm-name plugin entry + (resolving through `exports`) unverified — local directory entry verified instead. +- `PACKAGE_TOKEN` Gitea secret must be provisioned by the repo admin before nightly can publish + or commit back. +- Nightly runner network access to github.com unverified (fallback: Gitea mirror of upstream). +- Windows-only smoke quirk: fixture temp dir deletion can lag (EPERM warning, harmless); CI is + Linux. +- Background subagents in V2 (env-gated in V1) not verified; orchestrator text works either way. +- `request.body` overlays accepted but not sent by the V2 runner — agents avoid them. ## Next steps (exact) -1. Phase 1.1: scaffold package (package.json `@haylan/opencode-pocock` — confirm scope with owner, - tsconfig, prettier, .gitignore, MIT LICENSE + upstream attribution, README stub). -2. Phase 1.2: scripts/sync-upstream.ts + vendor upstream/ @49dd158 (strip non-skill files: keep - skills/{engineering,productivity,misc}/** minus agents/openai.yaml; write UPSTREAM.json). -3. Phase 1.6 note: agent templates = port pocock.md/pocock-worker.md from mcdays94/pocock-agents - (temp clone path in status history) to V2-native frontmatter + add pocock-reviewer; update - stack-skills table to be user-editable; worker skill allow-list from classification.json. +1. Run `npm run format`, `npm run check`, `npm test`, `npm run build`; commit workflows/smoke/docs. +2. Push to origin; ask owner to add `PACKAGE_TOKEN` secret (write:package + write:repository). +3. 1.10: `node scripts/release.ts` with SERVER_URL/REPOSITORY/SHA/TOKEN env (or dispatch a manual + release workflow) → verify `plugins: ["@haylan/opencode-pocock"]` installs from the registry in + a clean fixture; record in docs/runtime-checks.md. +4. Phase 2 per TODO.md (2.2 guardrails already implemented in 1.4 — verify mapping note; 2.4–2.7). diff --git a/AGENTS.md b/AGENTS.md index af7aff4..c77d7a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,12 +53,17 @@ safe intermediate work. `ctx.agent.transform(editor)` **cannot add** agents — only list/get/update/remove/default. Hence: skills ship via plugin injection, agents via `init` copy. - `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` / `metadata.opencode/autoinvoke` (hidden from model, still loadable by id/slash). - Agent files: `.opencode/agents/.md` (project), `~/.config/opencode/agents/` (global); frontmatter = agent config, body = system prompt. - Permission rules are ordered, last match wins; per-agent skill restrictions via - `{action: "skill", resource: "", effect: ...}`. Shell action is `shell` in V2. + `{action: "skill", resource: "", 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 the runner — avoid). diff --git a/README.md b/README.md index 0179c91..abdb510 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ upstream revision each release carries). // opencode.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 ``` -| 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-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). | +| 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-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). | 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 -as slash commands (`/to-spec`, `/retro`, ...). +OpenCode v2 honors that natively — they stay out of the model's auto-selection. Note that +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 -there (exploration notes), consider a global permission rule: +Worktrees live under `/tmp/pocock-workers`. Current OpenCode v2 builds allow `external_directory` +for `/tmp/*` by default; if yours asks before a subagent writes there, add a global rule: ```json { "permission": { "external_directory": { "/tmp/pocock-workers/*": "allow" } } } @@ -69,9 +74,9 @@ there (exploration notes), consider a global permission rule: "package": "@haylan/opencode-pocock", // Ports skills/misc/git-guardrails-claude-code to OpenCode: blocks dangerous git // commands (push --force, reset --hard, clean -f, branch -D, ...) before execution. - "options": { "guardrails": true } - } - ] + "options": { "guardrails": true }, + }, + ], } ``` diff --git a/TODO.md b/TODO.md index 9b0e7f2..265c6ce 100644 --- a/TODO.md +++ b/TODO.md @@ -19,19 +19,19 @@ State: `.opencode/state/status.md`. Design: `docs/architecture.md`. ## Phase 1 — plugin package -- [ ] 1.1 scaffold package (package.json name @haylan/opencode-pocock — CONFIRM scope with repo - owner, tsconfig, prettier, .gitignore, MIT LICENSE + upstream attribution, README stub) -- [ ] 1.2 scripts/sync-upstream.ts + vendor upstream/ @49dd158 + state update -- [ ] 1.3 scripts/generate.ts: manifest.json + skills/ output + unit tests (parser, manifest) -- [ ] 1.4 src/index.ts plugin: skill injection, options validation, agent-staleness warning + - fake-ctx integration tests (verify Skill.Info fields against installed @opencode/plugin typings!) -- [ ] 1.5 bin init (copy agents, -g global, no silent overwrite) + tests -- [ ] 1.6 agent-templates/: port pocock + pocock-worker from pocock-agents to V2-native frontmatter - (permissions array, shell action, no legacy keys); generate agents/*.md; drift tests -- [ ] 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 -- [ ] 1.9 local runtime verification (serve + HTTP API: plugin ok, skills visible, autoinvoke flags, - agents after init, worker denies hold) → docs/runtime-checks.md +- [x] 1.1 scaffold package (`@haylan/opencode-pocock` confirmed by owner 2026-10-14) +- [x] 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) +- [x] 1.4 src/index.ts plugin: skill injection (frontmatter-stripped content), options validation, + agent-staleness warning + fake-ctx integration tests (Skill.Info verified: cast needed for + effect-branded fields; runtime PASS) +- [x] 1.5 bin init (copy agents, -g global, no silent overwrite, .pocock-version stamp) + tests +- [x] 1.6 agents/: pocock + pocock-worker + pocock-reviewer, V2-native frontmatter; drift tests + (upstream names, worker skill allow-list, reviewer read-only, no subagent recursion) +- [x] 1.7 scripts/release.ts + version.ts adapted from fastcrw + unit tests +- [x] 1.8 .gitea/workflows/: check.yml, nightly.yml, smoke.yml (dispatch); scripts/smoke.ts — + PASS on v2.0.29 (plugin active, 31 skills, 16 hidden, agents + permissions decoded) +- [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 - [ ] 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`): `{id, name, description?, autoinvoke?, path, content}`; no `slash` in Info. -- V2 background subagents: still env-gated? Affects orchestrator dispatch loop wording (1.6). -- Do plugin-injected skills get `/name` slash exposure (no `slash` field in Skill.Info)? Runtime - check at 1.9; fallback = document `skills: ["/skills"]` config entry for file-based load. -- Package name/scope confirmation (1.1). +- V2 background subagents: still env-gated? Affects orchestrator dispatch loop wording only + (agent text covers both modes) — low priority, observe at 2.5. +- ~~Slash exposure for injected skills~~ RESOLVED: none (runtime check 2026-10-14); README + 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 - 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 diff --git a/agents/pocock.md b/agents/pocock.md index ed8653e..5323e8e 100644 --- a/agents/pocock.md +++ b/agents/pocock.md @@ -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: -| Situation | Start with | -|---|---| -| 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 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 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 decision only someone else can make | `to-questionnaire` | -| A finished spec with tickets | step 5 | +| Situation | Start with | +| ----------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| 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 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 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 decision only someone else can make | `to-questionnaire` | +| A finished spec with tickets | step 5 | Don't triage tickets that `to-tickets` produced; they're agent-ready already. diff --git a/docs/architecture.md b/docs/architecture.md index 412f407..6ef5f9b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -55,7 +55,7 @@ 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` +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) @@ -68,14 +68,14 @@ D6), deprecated 0. Full table with classification + rationale: `docs/skill-mappi 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 | +| 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. @@ -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: -| 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 | +| 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 @@ -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 `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 + 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 diff --git a/docs/runtime-checks.md b/docs/runtime-checks.md new file mode 100644 index 0000000..0ced176 --- /dev/null +++ b/docs/runtime-checks.md @@ -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: ["/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 /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). diff --git a/scripts/smoke.ts b/scripts/smoke.ts new file mode 100644 index 0000000..ceea203 --- /dev/null +++ b/scripts/smoke.ts @@ -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 { + 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 | 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> + } + + // 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) + }) +} diff --git a/src/guardrails.ts b/src/guardrails.ts index c709de1..2b77c0e 100644 --- a/src/guardrails.ts +++ b/src/guardrails.ts @@ -7,7 +7,9 @@ export function resolveOptions(options: Record | undefined): Op const opts = options ?? {} const guardrails = opts.guardrails ?? false 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 } } diff --git a/src/init.ts b/src/init.ts index 184636d..cc20abb 100644 --- a/src/init.ts +++ b/src/init.ts @@ -27,7 +27,8 @@ export function runInit(opts: { const global = args.has("-g") || args.has("--global") const force = args.has("--force") 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 sourceDir = path.join(root, "agents") @@ -36,7 +37,9 @@ export function runInit(opts: { mkdirSync(target, { recursive: true }) 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 to = path.join(target, file) const content = readFileSync(from, "utf8") @@ -56,7 +59,9 @@ function main() { 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.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( `\nNext: make sure opencode.json loads the plugin:\n { "plugins": ["@haylan/opencode-pocock"] }\n` + diff --git a/test/init.test.ts b/test/init.test.ts index b442899..ae636b5 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -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.deepEqual(project.copied, ["pocock-worker.md", "pocock.md"]) 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") 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.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 { 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", () => { 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", () => { const empty = mkdtempSync(path.join(tmpdir(), "init-empty-")) try { 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 { rmSync(empty, { recursive: true, force: true }) }