diff --git a/.gitignore b/.gitignore
index fb4964c..dffca3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,6 @@ tmp/
# OpenCode
.opencode/
-!.opencode/skills/
+!.opencode/skills/*.md
.context/
.nx/
\ No newline at end of file
diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc
index 813f127..ced7969 100644
--- a/.markdownlint-cli2.jsonc
+++ b/.markdownlint-cli2.jsonc
@@ -21,9 +21,7 @@
"MD041": false,
// MD024/no-duplicate-heading - Multiple headings with the same content
- "MD024": {
- "siblings_only": true
- },
+ "MD024": false,
// MD045/no-alt-text - Images should have alt text
"MD045": false,
@@ -53,7 +51,13 @@
"MD060": false
},
// Only lint specific directories - focus on core documentation
- "globs": ["docs/**/*.md", "README.md", "CONTRIBUTING.md", "SECURITY.md"],
+ "globs": [
+ "docs/**/*.md",
+ "README.md",
+ "CONTRIBUTING.md",
+ "SECURITY.md",
+ "AGENTS.md"
+ ],
// Fix mode disabled by default
"fix": false
}
diff --git a/.opencode/skills/brainstorming/SKILL.md b/.opencode/skills/brainstorming/SKILL.md
new file mode 100644
index 0000000..cee39e7
--- /dev/null
+++ b/.opencode/skills/brainstorming/SKILL.md
@@ -0,0 +1,180 @@
+---
+name: dispatching-parallel-agents
+description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
+---
+
+# Dispatching Parallel Agents
+
+## Overview
+
+When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating
+them sequentially wastes time. Each investigation is independent and can happen in parallel.
+
+**Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently.
+
+## When to Use
+
+```mermaid
+graph TD
+ A["Multiple failures?"] -->|yes| B["Are they independent?"]
+ B -->|no - related| C["Single agent investigates all"]
+ B -->|yes| D["Can they work in parallel?"]
+ D -->|yes| E["Parallel dispatch"]
+ D -->|no - shared state| F["Sequential agents"]
+```
+
+**Use when:**
+
+- 3+ test files failing with different root causes
+- Multiple subsystems broken independently
+- Each problem can be understood without context from others
+- No shared state between investigations
+
+**Don't use when:**
+
+- Failures are related (fix one might fix others)
+- Need to understand full system state
+- Agents would interfere with each other
+
+## The Pattern
+
+### 1. Identify Independent Domains
+
+Group failures by what's broken:
+
+- File A tests: Tool approval flow
+- File B tests: Batch completion behavior
+- File C tests: Abort functionality
+
+Each domain is independent - fixing tool approval doesn't affect abort tests.
+
+### 2. Create Focused Agent Tasks
+
+Each agent gets:
+
+- **Specific scope:** One test file or subsystem
+- **Clear goal:** Make these tests pass
+- **Constraints:** Don't change other code
+- **Expected output:** Summary of what you found and fixed
+
+### 3. Dispatch in Parallel
+
+```typescript
+// In Claude Code / AI environment
+Task("Fix agent-tool-abort.test.ts failures");
+Task("Fix batch-completion-behavior.test.ts failures");
+Task("Fix tool-approval-race-conditions.test.ts failures");
+// All three run concurrently
+```
+
+### 4. Review and Integrate
+
+When agents return:
+
+- Read each summary
+- Verify fixes don't conflict
+- Run full test suite
+- Integrate all changes
+
+## Agent Prompt Structure
+
+Good agent prompts are:
+
+1. **Focused** - One clear problem domain
+2. **Self-contained** - All context needed to understand the problem
+3. **Specific about output** - What should the agent return?
+
+```markdown
+Fix the 3 failing tests in src/agents/agent-tool-abort.test.ts:
+
+1. "should abort tool with partial output capture" - expects 'interrupted at' in message
+2. "should handle mixed completed and aborted tools" - fast tool aborted instead of completed
+3. "should properly track pendingToolCount" - expects 3 results but gets 0
+
+These are timing/race condition issues. Your task:
+
+1. Read the test file and understand what each test verifies
+2. Identify root cause - timing issues or actual bugs?
+3. Fix by:
+ - Replacing arbitrary timeouts with event-based waiting
+ - Fixing bugs in abort implementation if found
+ - Adjusting test expectations if testing changed behavior
+
+Do NOT just increase timeouts - find the real issue.
+
+Return: Summary of what you found and what you fixed.
+```
+
+## Common Mistakes
+
+**❌ Too broad:** "Fix all the tests" - agent gets lost **✅ Specific:** "Fix agent-tool-abort.test.ts" - focused scope
+
+**❌ No context:** "Fix the race condition" - agent doesn't know where **✅ Context:** Paste the error messages and test
+names
+
+**❌ No constraints:** Agent might refactor everything **✅ Constraints:** "Do NOT change production code" or "Fix tests
+only"
+
+**❌ Vague output:** "Fix it" - you don't know what changed **✅ Specific:** "Return summary of root cause and changes"
+
+## When NOT to Use
+
+**Related failures:** Fixing one might fix others - investigate together first **Need full context:** Understanding
+requires seeing entire system **Exploratory debugging:** You don't know what's broken yet **Shared state:** Agents would
+interfere (editing same files, using same resources)
+
+## Real Example from Session
+
+**Scenario:** 6 test failures across 3 files after major refactoring
+
+**Failures:**
+
+- agent-tool-abort.test.ts: 3 failures (timing issues)
+- batch-completion-behavior.test.ts: 2 failures (tools not executing)
+- tool-approval-race-conditions.test.ts: 1 failure (execution count = 0)
+
+**Decision:** Independent domains - abort logic separate from batch completion separate from race conditions
+
+**Dispatch:**
+
+```
+Agent 1 → Fix agent-tool-abort.test.ts
+Agent 2 → Fix batch-completion-behavior.test.ts
+Agent 3 → Fix tool-approval-race-conditions.test.ts
+```
+
+**Results:**
+
+- Agent 1: Replaced timeouts with event-based waiting
+- Agent 2: Fixed event structure bug (threadId in wrong place)
+- Agent 3: Added wait for async tool execution to complete
+
+**Integration:** All fixes independent, no conflicts, full suite green
+
+**Time saved:** 3 problems solved in parallel vs sequentially
+
+## Key Benefits
+
+1. **Parallelization** - Multiple investigations happen simultaneously
+2. **Focus** - Each agent has narrow scope, less context to track
+3. **Independence** - Agents don't interfere with each other
+4. **Speed** - 3 problems solved in time of 1
+
+## Verification
+
+After agents return:
+
+1. **Review each summary** - Understand what changed
+2. **Check for conflicts** - Did agents edit same code?
+3. **Run full suite** - Verify all fixes work together
+4. **Spot check** - Agents can make systematic errors
+
+## Real-World Impact
+
+From debugging session (2025-10-03):
+
+- 6 failures across 3 files
+- 3 agents dispatched in parallel
+- All investigations completed concurrently
+- All fixes integrated successfully
+- Zero conflicts between agent changes
diff --git a/.opencode/skills/disaptching-parallel-agents/SKILL.md b/.opencode/skills/disaptching-parallel-agents/SKILL.md
new file mode 100644
index 0000000..81a357b
--- /dev/null
+++ b/.opencode/skills/disaptching-parallel-agents/SKILL.md
@@ -0,0 +1,189 @@
+---
+name: dispatching-parallel-agents
+description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies
+---
+
+# Dispatching Parallel Agents
+
+## Overview
+
+When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating
+them sequentially wastes time. Each investigation is independent and can happen in parallel.
+
+**Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently.
+
+## When to Use
+
+```dot
+digraph when_to_use {
+ "Multiple failures?" [shape=diamond];
+ "Are they independent?" [shape=diamond];
+ "Single agent investigates all" [shape=box];
+ "One agent per problem domain" [shape=box];
+ "Can they work in parallel?" [shape=diamond];
+ "Sequential agents" [shape=box];
+ "Parallel dispatch" [shape=box];
+
+ "Multiple failures?" -> "Are they independent?" [label="yes"];
+ "Are they independent?" -> "Single agent investigates all" [label="no - related"];
+ "Are they independent?" -> "Can they work in parallel?" [label="yes"];
+ "Can they work in parallel?" -> "Parallel dispatch" [label="yes"];
+ "Can they work in parallel?" -> "Sequential agents" [label="no - shared state"];
+}
+```
+
+**Use when:**
+
+- 3+ test files failing with different root causes
+- Multiple subsystems broken independently
+- Each problem can be understood without context from others
+- No shared state between investigations
+
+**Don't use when:**
+
+- Failures are related (fix one might fix others)
+- Need to understand full system state
+- Agents would interfere with each other
+
+## The Pattern
+
+### 1. Identify Independent Domains
+
+Group failures by what's broken:
+
+- File A tests: Tool approval flow
+- File B tests: Batch completion behavior
+- File C tests: Abort functionality
+
+Each domain is independent - fixing tool approval doesn't affect abort tests.
+
+### 2. Create Focused Agent Tasks
+
+Each agent gets:
+
+- **Specific scope:** One test file or subsystem
+- **Clear goal:** Make these tests pass
+- **Constraints:** Don't change other code
+- **Expected output:** Summary of what you found and fixed
+
+### 3. Dispatch in Parallel
+
+```typescript
+// In Claude Code / AI environment
+Task("Fix agent-tool-abort.test.ts failures");
+Task("Fix batch-completion-behavior.test.ts failures");
+Task("Fix tool-approval-race-conditions.test.ts failures");
+// All three run concurrently
+```
+
+### 4. Review and Integrate
+
+When agents return:
+
+- Read each summary
+- Verify fixes don't conflict
+- Run full test suite
+- Integrate all changes
+
+## Agent Prompt Structure
+
+Good agent prompts are:
+
+1. **Focused** - One clear problem domain
+2. **Self-contained** - All context needed to understand the problem
+3. **Specific about output** - What should the agent return?
+
+```markdown
+Fix the 3 failing tests in src/agents/agent-tool-abort.test.ts:
+
+1. "should abort tool with partial output capture" - expects 'interrupted at' in message
+2. "should handle mixed completed and aborted tools" - fast tool aborted instead of completed
+3. "should properly track pendingToolCount" - expects 3 results but gets 0
+
+These are timing/race condition issues. Your task:
+
+1. Read the test file and understand what each test verifies
+2. Identify root cause - timing issues or actual bugs?
+3. Fix by:
+ - Replacing arbitrary timeouts with event-based waiting
+ - Fixing bugs in abort implementation if found
+ - Adjusting test expectations if testing changed behavior
+
+Do NOT just increase timeouts - find the real issue.
+
+Return: Summary of what you found and what you fixed.
+```
+
+## Common Mistakes
+
+**❌ Too broad:** "Fix all the tests" - agent gets lost **✅ Specific:** "Fix agent-tool-abort.test.ts" - focused scope
+
+**❌ No context:** "Fix the race condition" - agent doesn't know where **✅ Context:** Paste the error messages and test
+names
+
+**❌ No constraints:** Agent might refactor everything **✅ Constraints:** "Do NOT change production code" or "Fix tests
+only"
+
+**❌ Vague output:** "Fix it" - you don't know what changed **✅ Specific:** "Return summary of root cause and changes"
+
+## When NOT to Use
+
+**Related failures:** Fixing one might fix others - investigate together first **Need full context:** Understanding
+requires seeing entire system **Exploratory debugging:** You don't know what's broken yet **Shared state:** Agents would
+interfere (editing same files, using same resources)
+
+## Real Example from Session
+
+**Scenario:** 6 test failures across 3 files after major refactoring
+
+**Failures:**
+
+- agent-tool-abort.test.ts: 3 failures (timing issues)
+- batch-completion-behavior.test.ts: 2 failures (tools not executing)
+- tool-approval-race-conditions.test.ts: 1 failure (execution count = 0)
+
+**Decision:** Independent domains - abort logic separate from batch completion separate from race conditions
+
+**Dispatch:**
+
+```
+Agent 1 → Fix agent-tool-abort.test.ts
+Agent 2 → Fix batch-completion-behavior.test.ts
+Agent 3 → Fix tool-approval-race-conditions.test.ts
+```
+
+**Results:**
+
+- Agent 1: Replaced timeouts with event-based waiting
+- Agent 2: Fixed event structure bug (threadId in wrong place)
+- Agent 3: Added wait for async tool execution to complete
+
+**Integration:** All fixes independent, no conflicts, full suite green
+
+**Time saved:** 3 problems solved in parallel vs sequentially
+
+## Key Benefits
+
+1. **Parallelization** - Multiple investigations happen simultaneously
+2. **Focus** - Each agent has narrow scope, less context to track
+3. **Independence** - Agents don't interfere with each other
+4. **Speed** - 3 problems solved in time of 1
+
+## Verification
+
+After agents return:
+
+1. **Review each summary** - Understand what changed
+2. **Check for conflicts** - Did agents edit same code?
+3. **Run full suite** - Verify all fixes work together
+4. **Spot check** - Agents can make systematic errors
+
+## Real-World Impact
+
+From debugging session (2025-10-03):
+
+- 6 failures across 3 files
+- 3 agents dispatched in parallel
+- All investigations completed concurrently
+- All fixes integrated successfully
+- Zero conflicts between agent changes
diff --git a/.opencode/skills/executing-plans/SKILL.md b/.opencode/skills/executing-plans/SKILL.md
new file mode 100644
index 0000000..dac0902
--- /dev/null
+++ b/.opencode/skills/executing-plans/SKILL.md
@@ -0,0 +1,96 @@
+---
+name: executing-plans
+description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
+---
+
+# Executing Plans
+
+## Overview
+
+Load plan, review critically, execute tasks in batches, report for review between batches.
+
+**Core principle:** Batch execution with checkpoints for architect review.
+
+**Announce at start:** "I'm using the executing-plans skill to implement this plan."
+
+## The Process
+
+### Step 1: Load and Review Plan
+
+1. Read plan file
+2. Review critically - identify any questions or concerns about the plan
+3. If concerns: Raise them with your human partner before starting
+4. If no concerns: Create TodoWrite and proceed
+
+### Step 2: Execute Batch
+
+**Default: First 3 tasks**
+
+For each task:
+
+1. Mark as in_progress
+2. Follow each step exactly (plan has bite-sized steps)
+3. Run verifications as specified
+4. Mark as completed
+
+### Step 3: Report
+
+When batch complete:
+
+- Show what was implemented
+- Show verification output
+- Say: "Ready for feedback."
+
+### Step 4: Continue
+
+Based on feedback:
+
+- Apply changes if needed
+- Execute next batch
+- Repeat until complete
+
+### Step 5: Complete Development
+
+After all tasks complete and verified:
+
+- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
+- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch
+- Follow that skill to verify tests, present options, execute choice
+
+## When to Stop and Ask for Help
+
+**STOP executing immediately when:**
+
+- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
+- Plan has critical gaps preventing starting
+- You don't understand an instruction
+- Verification fails repeatedly
+
+**Ask for clarification rather than guessing.**
+
+## When to Revisit Earlier Steps
+
+**Return to Review (Step 1) when:**
+
+- Partner updates the plan based on your feedback
+- Fundamental approach needs rethinking
+
+**Don't force through blockers** - stop and ask.
+
+## Remember
+
+- Review plan critically first
+- Follow plan steps exactly
+- Don't skip verifications
+- Reference skills when plan says to
+- Between batches: just report and wait
+- Stop when blocked, don't guess
+- Never start implementation on main/master branch without explicit user consent
+
+## Integration
+
+**Required workflow skills:**
+
+- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
+- **superpowers:writing-plans** - Creates the plan this skill executes
+- **superpowers:finishing-a-development-branch** - Complete development after all tasks
diff --git a/.opencode/skills/pr-iteration/SKILL.md b/.opencode/skills/pr-iteration/SKILL.md
new file mode 100644
index 0000000..7d57354
--- /dev/null
+++ b/.opencode/skills/pr-iteration/SKILL.md
@@ -0,0 +1,447 @@
+---
+name: pr-iteration
+description:
+ Guide the agent through iterative PR/MR refinement to ensure all CI checks, tests, linting, and validation passes
+ before considering the work complete. Never declare success until all automated checks pass.
+license: MIT
+compatibility: opencode
+metadata:
+ category: workflow
+ tool: git
+---
+
+## What I Do
+
+I ensure that all Pull Requests (PRs) or Merge Requests (MRs) pass **every automated check** before being declared
+complete. I guide the agent through iterative refinement until CI is green.
+
+## Core Principles
+
+1. **Iterate until all checks pass** - Never declare "done" while any check is failing
+2. **Run checks locally first** - Fix issues locally before pushing to avoid CI noise
+3. **Address cascading failures systematically** - Fix one category at a time (lint → typecheck → test → build)
+4. **Commit incremental fixes** - Save progress as you fix issues, don't batch all fixes into one commit
+5. **Verify after each fix** - Re-run the failed check to confirm it's resolved
+
+## When to Use Me
+
+- Creating a new PR/MR from scratch
+- Addressing CI failures on an existing PR/MR
+- Refactoring code that affects multiple packages
+- Adding features that require cross-package changes
+- Merging dependent PRs in sequence
+
+## Iteration Workflow
+
+### Phase 1: Pre-Flight Checks (Before Creating PR)
+
+```bash
+# 1. Install dependencies
+bun install
+
+# 2. Run linting
+bun run lint
+
+# 3. Run type checking
+bun run typecheck
+
+# 4. Run tests
+bun run test
+
+# 5. Run build
+bun run build
+```
+
+**If any check fails → FIX IT before proceeding**
+
+### Phase 2: PR Creation & Initial Validation
+
+After creating PR/MR:
+
+```bash
+# 1. Verify all checks pass locally
+bun run lint && bun run typecheck && bun run test && bun run build
+
+# 2. Push and wait for CI
+
+# 3. Monitor CI results
+```
+
+### Phase 3: Iterative Fix Loop
+
+**While any check is failing:**
+
+```
+CHECK → FAIL → FIX → COMMIT → PUSH → RE-CHECK → (REPEAT UNTIL PASS)
+```
+
+**Priority order for fixing failures:**
+
+1. **Linting errors** (formatting, style) - Usually quickest to fix
+2. **Type errors** - May require interface changes
+3. **Test failures** - Logic bugs or test updates needed
+4. **Build failures** - Often the most complex
+
+### Phase 4: Final Verification
+
+```bash
+# Run full check suite one more time
+bun run lint
+bun run typecheck
+bun run test
+bun run build
+```
+
+**Only declare PR ready when ALL checks pass.**
+
+## Common Failure Patterns & Fixes
+
+### Pattern 1: Linting Failures
+
+**Symptom**: CI fails on `bun run lint` or biome/eslint errors
+
+**Fix Process**:
+
+```bash
+# Run auto-fix first
+bun run lint --fix
+# or
+biome check --write .
+
+# If manual fixes needed, address each error
+# Re-run lint to verify
+bun run lint
+```
+
+**Iterate until**: `bun run lint` exits 0
+
+### Pattern 2: Type Errors
+
+**Symptom**: `bun run typecheck` or `tsc` fails
+
+**Fix Process**:
+
+```bash
+# Run type check
+bun run typecheck
+
+# Fix each error:
+# - Add missing types
+# - Fix interface mismatches
+# - Update imports
+
+# Re-run typecheck
+bun run typecheck
+```
+
+**Iterate until**: `bun run typecheck` exits 0
+
+### Pattern 3: Test Failures
+
+**Symptom**: Tests fail locally or in CI
+
+**Fix Process**:
+
+```bash
+# Run failing test
+bun test path/to/failing-test.ts
+
+# Debug and fix
+# - Check test expectations
+# - Verify mock setup
+# - Review logic changes
+
+# Re-run test
+bun test path/to/failing-test.ts
+
+# Run full test suite
+bun run test
+```
+
+**Iterate until**: All tests pass
+
+### Pattern 4: Build Failures
+
+**Symptom**: `bun run build` or `nx build` fails
+
+**Fix Process**:
+
+```bash
+# Run build with verbose output
+bunx nx run-many --target=build --all --verbose
+
+# Identify failing package
+# Check error message
+
+# Common causes:
+# - Missing dependencies in package.json
+# - Import errors
+# - TypeScript compilation errors
+
+# Fix issue
+# Re-run build
+bun run build
+```
+
+**Iterate until**: Build succeeds
+
+### Pattern 5: Cross-Package Failures
+
+**Symptom**: Changes in one package break another
+
+**Fix Process**:
+
+```bash
+# Identify affected packages
+bunx nx affected:graph
+
+# Build dependency graph first
+bunx nx run-many --target=build --projects=dependency-package
+
+# Then build dependent
+bunx nx run-many --target=build --projects=dependent-package
+
+# Run tests on affected packages
+bunx nx affected:test
+```
+
+**Iterate until**: All affected packages build and test successfully
+
+## CI/CD Integration
+
+### GitHub Actions Workflow
+
+```yaml
+name: Validate PR
+on: [pull_request]
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v1
+
+ - name: Install
+ run: bun install
+
+ - name: Lint
+ run: bun run lint
+
+ - name: Type Check
+ run: bun run typecheck
+
+ - name: Test
+ run: bun run test
+
+ - name: Build
+ run: bun run build
+```
+
+### Required Status Checks
+
+Configure branch protection to require:
+
+- ✅ Lint passing
+- ✅ Type check passing
+- ✅ Tests passing
+- ✅ Build passing
+
+**No PR should be mergeable until all checks pass.**
+
+## Debugging CI Failures
+
+### Step 1: Replicate Locally
+
+```bash
+# Run the exact command that failed in CI
+# (Check your workflow file for the command)
+biome ci --reporter=github --diagnostic-level=error . --verbose
+
+# Compare results
+```
+
+### Step 2: Check Environment Differences
+
+Common differences:
+
+- **Node/Bun version** - Match CI version exactly
+- **Operating system** - CI runs Linux; develop on Linux/macOS/WSL
+- **Clean state** - CI starts fresh; you may have cached files
+
+```bash
+# Clean and reinstall
+rm -rf node_modules bun.lockb
+bun install
+
+# Re-run checks
+```
+
+### Step 3: Fix and Verify
+
+```bash
+# Fix the issue
+
+# Verify locally
+bun run lint && bun run typecheck && bun run test && bun run build
+
+# Commit and push
+git add .
+git commit -m "fix: resolve CI failures"
+git push
+```
+
+### Step 4: Monitor CI
+
+- Watch CI logs for new failures
+- Repeat fix loop if needed
+
+## Multi-PR Dependency Management
+
+When merging PRs with dependencies (e.g., PR B depends on PR A):
+
+### Step 1: Merge Base PR First
+
+```bash
+# Merge PR A
+git checkout main
+git merge feat/pr-a
+
+# Verify CI passes on main
+```
+
+### Step 2: Rebase Dependent PR
+
+```bash
+# PR B branch
+git checkout feat/pr-b
+git rebase main
+
+# Re-run all checks
+bun run lint && bun run typecheck && bun run test && bun run build
+
+# Push rebased branch
+git push --force-with-lease
+```
+
+### Step 3: Iterate Until Green
+
+If checks fail after rebase:
+
+```
+FAIL → ANALYZE (conflicts? dependency changes?) → FIX → PUSH → RE-CHECK
+```
+
+**Dependencies may introduce breaking changes requiring fixes in dependent PR.**
+
+## Commit Strategy During Iteration
+
+### Incremental Commits
+
+Save progress as you fix issues:
+
+```bash
+# Fix lint errors
+git add .
+git commit -m "style: fix linting errors"
+
+# Fix type errors
+git add .
+git commit -m "fix: resolve type errors"
+
+# Fix tests
+git add .
+git commit -m "test: update failing tests"
+```
+
+### Squash Before Merge (Optional)
+
+If you prefer clean history:
+
+```bash
+# After all checks pass
+git rebase -i main
+# Squash fix commits
+```
+
+## Emergency Recovery
+
+### CI is completely broken
+
+```bash
+# 1. Check if it's a configuration issue
+cat package.json | grep -A 5 '"scripts"'
+
+# 2. Verify tools are installed
+bunx biome --version
+bunx tsc --version
+
+# 3. Check for environment issues
+echo $NODE_VERSION
+echo $BUN_VERSION
+
+# 4. Clean slate
+rm -rf node_modules bun.lockb
+bun install
+
+# 5. Re-run checks
+```
+
+### Infinite fix loop
+
+If you keep fixing and CI keeps failing:
+
+1. **Take a break** - Step away, review with fresh eyes
+2. **Check CI logs carefully** - Read the full error message
+3. **Run exact CI command locally** - Don't assume your command is equivalent
+4. **Ask for help** - Sometimes a second pair of eyes helps
+
+## Success Criteria
+
+A PR is **ONLY** ready when:
+
+- ✅ `bun run lint` passes (0 errors, 0 warnings)
+- ✅ `bun run typecheck` passes (0 type errors)
+- ✅ `bun run test` passes (all tests green)
+- ✅ `bun run build` passes (all packages build)
+- ✅ CI pipeline is green
+- ✅ No merge conflicts with target branch
+
+**If any check fails → CONTINUE ITERATING**
+
+## Remember
+
+✅ **DO**:
+
+- Run checks locally before pushing
+- Fix one category at a time
+- Commit incremental fixes
+- Verify after each fix
+- Monitor CI after every push
+- Rebase dependent PRs after base PR merges
+
+❌ **NEVER**:
+
+- Declare PR ready with failing checks
+- Ignore CI failures
+- Batch all fixes into one giant commit
+- Skip local verification and rely on CI
+- Merge with "will fix later" mentality
+
+## Quick Reference
+
+```bash
+# Full validation suite
+bun run lint && bun run typecheck && bun run test && bun run build
+
+# Fix linting
+bun run lint --fix
+
+# Fix types
+bun run typecheck # then fix errors manually
+
+# Run specific test
+bun test path/to/test.ts
+
+# Build specific package
+bunx nx run package-name:build
+```
diff --git a/.opencode/skills/recieving-code-review/SKILL.md b/.opencode/skills/recieving-code-review/SKILL.md
new file mode 100644
index 0000000..979d633
--- /dev/null
+++ b/.opencode/skills/recieving-code-review/SKILL.md
@@ -0,0 +1,230 @@
+---
+name: receiving-code-review
+description:
+ Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or
+ technically questionable - requires technical rigor and verification, not performative agreement or blind
+ implementation
+---
+
+# Code Review Reception
+
+## Overview
+
+Code review requires technical evaluation, not emotional performance.
+
+**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort.
+
+## The Response Pattern
+
+```
+WHEN receiving code review feedback:
+
+1. READ: Complete feedback without reacting
+2. UNDERSTAND: Restate requirement in own words (or ask)
+3. VERIFY: Check against codebase reality
+4. EVALUATE: Technically sound for THIS codebase?
+5. RESPOND: Technical acknowledgment or reasoned pushback
+6. IMPLEMENT: One item at a time, test each
+```
+
+## Forbidden Responses
+
+**NEVER:**
+
+- "You're absolutely right!" (explicit AGENT.md violation)
+- "Great point!" / "Excellent feedback!" (performative)
+- "Let me implement that now" (before verification)
+
+**INSTEAD:**
+
+- Restate the technical requirement
+- Ask clarifying questions
+- Push back with technical reasoning if wrong
+- Just start working (actions > words)
+
+## Handling Unclear Feedback
+
+```
+IF any item is unclear:
+ STOP - do not implement anything yet
+ ASK for clarification on unclear items
+
+WHY: Items may be related. Partial understanding = wrong implementation.
+```
+
+**Example:**
+
+```
+your human partner: "Fix 1-6"
+You understand 1,2,3,6. Unclear on 4,5.
+
+❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later
+✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding."
+```
+
+## Source-Specific Handling
+
+### From your human partner
+
+- **Trusted** - implement after understanding
+- **Still ask** if scope unclear
+- **No performative agreement**
+- **Skip to action** or technical acknowledgment
+
+### From External Reviewers
+
+```
+BEFORE implementing:
+ 1. Check: Technically correct for THIS codebase?
+ 2. Check: Breaks existing functionality?
+ 3. Check: Reason for current implementation?
+ 4. Check: Works on all platforms/versions?
+ 5. Check: Does reviewer understand full context?
+
+IF suggestion seems wrong:
+ Push back with technical reasoning
+
+IF can't easily verify:
+ Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?"
+
+IF conflicts with your human partner's prior decisions:
+ Stop and discuss with your human partner first
+```
+
+**your human partner's rule:** "External feedback - be skeptical, but check carefully"
+
+## YAGNI Check for "Professional" Features
+
+```
+IF reviewer suggests "implementing properly":
+ grep codebase for actual usage
+
+ IF unused: "This endpoint isn't called. Remove it (YAGNI)?"
+ IF used: Then implement properly
+```
+
+**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it."
+
+## Implementation Order
+
+```
+FOR multi-item feedback:
+ 1. Clarify anything unclear FIRST
+ 2. Then implement in this order:
+ - Blocking issues (breaks, security)
+ - Simple fixes (typos, imports)
+ - Complex fixes (refactoring, logic)
+ 3. Test each fix individually
+ 4. Verify no regressions
+```
+
+## When To Push Back
+
+Push back when:
+
+- Suggestion breaks existing functionality
+- Reviewer lacks full context
+- Violates YAGNI (unused feature)
+- Technically incorrect for this stack
+- Legacy/compatibility reasons exist
+- Conflicts with your human partner's architectural decisions
+
+**How to push back:**
+
+- Use technical reasoning, not defensiveness
+- Ask specific questions
+- Reference working tests/code
+- Involve your human partner if architectural
+
+**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K"
+
+## Acknowledging Correct Feedback
+
+When feedback IS correct:
+
+```
+✅ "Fixed. [Brief description of what changed]"
+✅ "Good catch - [specific issue]. Fixed in [location]."
+✅ [Just fix it and show in the code]
+
+❌ "You're absolutely right!"
+❌ "Great point!"
+❌ "Thanks for catching that!"
+❌ "Thanks for [anything]"
+❌ ANY gratitude expression
+```
+
+**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback.
+
+**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead.
+
+## Gracefully Correcting Your Pushback
+
+If you pushed back and were wrong:
+
+```
+✅ "You were right - I checked [X] and it does [Y]. Implementing now."
+✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing."
+
+❌ Long apology
+❌ Defending why you pushed back
+❌ Over-explaining
+```
+
+State the correction factually and move on.
+
+## Common Mistakes
+
+| Mistake | Fix |
+| ---------------------------- | ----------------------------------- |
+| Performative agreement | State requirement or just act |
+| Blind implementation | Verify against codebase first |
+| Batch without testing | One at a time, test each |
+| Assuming reviewer is right | Check if breaks things |
+| Avoiding pushback | Technical correctness > comfort |
+| Partial implementation | Clarify all items first |
+| Can't verify, proceed anyway | State limitation, ask for direction |
+
+## Real Examples
+
+**Performative Agreement (Bad):**
+
+```
+Reviewer: "Remove legacy code"
+❌ "You're absolutely right! Let me remove that..."
+```
+
+**Technical Verification (Good):**
+
+```
+Reviewer: "Remove legacy code"
+✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?"
+```
+
+**YAGNI (Good):**
+
+```
+Reviewer: "Implement proper metrics tracking with database, date filters, CSV export"
+✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?"
+```
+
+**Unclear Item (Good):**
+
+```
+your human partner: "Fix items 1-6"
+You understand 1,2,3,6. Unclear on 4,5.
+✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing."
+```
+
+## GitHub Thread Replies
+
+When replying to inline review comments on GitHub, reply in the comment thread
+(`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment.
+
+## The Bottom Line
+
+**External feedback = suggestions to evaluate, not orders to follow.**
+
+Verify. Question. Then implement.
+
+No performative agreement. Technical rigor always.
diff --git a/.opencode/skills/requesting-code-review/SKILL.md b/.opencode/skills/requesting-code-review/SKILL.md
new file mode 100644
index 0000000..d32c9b7
--- /dev/null
+++ b/.opencode/skills/requesting-code-review/SKILL.md
@@ -0,0 +1,115 @@
+---
+name: requesting-code-review
+description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements
+---
+
+# Requesting Code Review
+
+Dispatch @code-reviewer subagent to catch issues before they cascade.
+
+**Core principle:** Review early, review often.
+
+## When to Request Review
+
+**Mandatory:**
+
+- After each task in subagent-driven development
+- After completing major feature
+- Before merge to main
+
+**Optional but valuable:**
+
+- When stuck (fresh perspective)
+- Before refactoring (baseline check)
+- After fixing complex bug
+
+## How to Request
+
+**1. Get git SHAs:**
+
+```bash
+BASE_SHA=$(git rev-parse HEAD~1) # or origin/main
+HEAD_SHA=$(git rev-parse HEAD)
+```
+
+**2. Dispatch code-reviewer subagent:**
+
+Use Task tool with code-reviewer type, fill template at `code-reviewer.md`
+
+**Placeholders:**
+
+- `{WHAT_WAS_IMPLEMENTED}` - What you just built
+- `{PLAN_OR_REQUIREMENTS}` - What it should do
+- `{BASE_SHA}` - Starting commit
+- `{HEAD_SHA}` - Ending commit
+- `{DESCRIPTION}` - Brief summary
+
+**3. Act on feedback:**
+
+- Fix Critical issues immediately
+- Fix Important issues before proceeding
+- Note Minor issues for later
+- Push back if reviewer is wrong (with reasoning)
+
+## Example
+
+```
+[Just completed Task 2: Add verification function]
+
+You: Let me request code review before proceeding.
+
+BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}')
+HEAD_SHA=$(git rev-parse HEAD)
+
+[Dispatch code-reviewer subagent]
+ WHAT_WAS_IMPLEMENTED: Verification and repair functions for conversation index
+ PLAN_OR_REQUIREMENTS: Task 2 from docs/plans/deployment-plan.md
+ BASE_SHA: a7981ec
+ HEAD_SHA: 3df7661
+ DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types
+
+[Subagent returns]:
+ Strengths: Clean architecture, real tests
+ Issues:
+ Important: Missing progress indicators
+ Minor: Magic number (100) for reporting interval
+ Assessment: Ready to proceed
+
+You: [Fix progress indicators]
+[Continue to Task 3]
+```
+
+## Integration with Workflows
+
+**Subagent-Driven Development:**
+
+- Review after EACH task
+- Catch issues before they compound
+- Fix before moving to next task
+
+**Executing Plans:**
+
+- Review after each batch (3 tasks)
+- Get feedback, apply, continue
+
+**Ad-Hoc Development:**
+
+- Review before merge
+- Review when stuck
+
+## Red Flags
+
+**Never:**
+
+- Skip review because "it's simple"
+- Ignore Critical issues
+- Proceed with unfixed Important issues
+- Argue with valid technical feedback
+
+**If reviewer wrong:**
+
+- Push back with technical reasoning
+- Show code/tests that prove it works
+- Request clarification
+
+See template at: requesting-code-review/code-reviewer.md
diff --git a/.opencode/skills/requesting-code-review/code-reviewer.md b/.opencode/skills/requesting-code-review/code-reviewer.md
new file mode 100644
index 0000000..68d4eba
--- /dev/null
+++ b/.opencode/skills/requesting-code-review/code-reviewer.md
@@ -0,0 +1,159 @@
+# Code Review Agent
+
+You are reviewing code changes for production readiness.
+
+**Your task:**
+
+1. Review {WHAT_WAS_IMPLEMENTED}
+2. Compare against {PLAN_OR_REQUIREMENTS}
+3. Check code quality, architecture, testing
+4. Categorize issues by severity
+5. Assess production readiness
+
+## What Was Implemented
+
+{DESCRIPTION}
+
+## Requirements/Plan
+
+{PLAN_REFERENCE}
+
+## Git Range to Review
+
+**Base:** {BASE_SHA} **Head:** {HEAD_SHA}
+
+```bash
+git diff --stat {BASE_SHA}..{HEAD_SHA}
+git diff {BASE_SHA}..{HEAD_SHA}
+```
+
+## Review Checklist
+
+**Code Quality:**
+
+- Clean separation of concerns?
+- Proper error handling?
+- Type safety (if applicable)?
+- DRY principle followed?
+- Edge cases handled?
+
+**Architecture:**
+
+- Sound design decisions?
+- Scalability considerations?
+- Performance implications?
+- Security concerns?
+
+**Testing:**
+
+- Tests actually test logic (not mocks)?
+- Edge cases covered?
+- Integration tests where needed?
+- All tests passing?
+
+**Requirements:**
+
+- All plan requirements met?
+- Implementation matches spec?
+- No scope creep?
+- Breaking changes documented?
+
+**Production Readiness:**
+
+- Migration strategy (if schema changes)?
+- Backward compatibility considered?
+- Documentation complete?
+- No obvious bugs?
+
+## Output Format
+
+### Strengths
+
+[What's well done? Be specific.]
+
+### Issues
+
+#### Critical (Must Fix)
+
+[Bugs, security issues, data loss risks, broken functionality]
+
+#### Important (Should Fix)
+
+[Architecture problems, missing features, poor error handling, test gaps]
+
+#### Minor (Nice to Have)
+
+[Code style, optimization opportunities, documentation improvements]
+
+**For each issue:**
+
+- File:line reference
+- What's wrong
+- Why it matters
+- How to fix (if not obvious)
+
+### Recommendations
+
+[Improvements for code quality, architecture, or process]
+
+### Assessment
+
+**Ready to merge?** [Yes/No/With fixes]
+
+**Reasoning:** [Technical assessment in 1-2 sentences]
+
+## Critical Rules
+
+**DO:**
+
+- Categorize by actual severity (not everything is Critical)
+- Be specific (file:line, not vague)
+- Explain WHY issues matter
+- Acknowledge strengths
+- Give clear verdict
+
+**DON'T:**
+
+- Say "looks good" without checking
+- Mark nitpicks as Critical
+- Give feedback on code you didn't review
+- Be vague ("improve error handling")
+- Avoid giving a clear verdict
+
+## Example Output
+
+```
+### Strengths
+- Clean database schema with proper migrations (db.ts:15-42)
+- Comprehensive test coverage (18 tests, all edge cases)
+- Good error handling with fallbacks (summarizer.ts:85-92)
+
+### Issues
+
+#### Important
+1. **Missing help text in CLI wrapper**
+ - File: index-conversations:1-31
+ - Issue: No --help flag, users won't discover --concurrency
+ - Fix: Add --help case with usage examples
+
+2. **Date validation missing**
+ - File: search.ts:25-27
+ - Issue: Invalid dates silently return no results
+ - Fix: Validate ISO format, throw error with example
+
+#### Minor
+1. **Progress indicators**
+ - File: indexer.ts:130
+ - Issue: No "X of Y" counter for long operations
+ - Impact: Users don't know how long to wait
+
+### Recommendations
+- Add progress reporting for user experience
+- Consider config file for excluded projects (portability)
+
+### Assessment
+
+**Ready to merge: With fixes**
+
+**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality.
+```
diff --git a/.opencode/skills/subagent-driven-development/SKILL.md b/.opencode/skills/subagent-driven-development/SKILL.md
new file mode 100644
index 0000000..7b7a750
--- /dev/null
+++ b/.opencode/skills/subagent-driven-development/SKILL.md
@@ -0,0 +1,251 @@
+---
+name: subagent-driven-development
+description: Use when executing implementation plans with independent tasks in the current session
+---
+
+# Subagent-Driven Development
+
+Execute plan by dispatching fresh subagent per task, with two-stage review after each: spec compliance review first,
+then code quality review.
+
+**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration
+
+## When to Use
+
+```mermaid
+flowchart LR
+ have_plan{"Have implementation plan?"}
+ tasks_independent{"Tasks mostly independent?"}
+ stay_session{"Stay in this session?"}
+ subagent["subagent-driven-development"]
+ executing["executing-plans"]
+ manual["Manual execution or brainstorm first"]
+
+ have_plan -->|yes| tasks_independent
+ have_plan -->|no| manual
+ tasks_independent -->|yes| stay_session
+ tasks_independent -->|"no - tightly coupled"| manual
+ stay_session -->|yes| subagent
+ stay_session -->|"no - parallel session"| executing
+```
+
+**vs. Executing Plans (parallel session):**
+
+- Same session (no context switch)
+- Fresh subagent per task (no context pollution)
+- Two-stage review after each task: spec compliance first, then code quality
+- Faster iteration (no human-in-loop between tasks)
+
+## The Process
+
+```mermaid
+flowchart TB
+ subgraph cluster_per_task["Per Task"]
+ d_impl["Dispatch implementer subagent (./implementer-prompt.md)"]
+ q_impl{"Implementer subagent asks questions?"}
+ ans["Answer questions, provide context"]
+ impl["Implementer subagent implements, tests, commits, self-reviews"]
+ d_spec["Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)"]
+ spec_ok{"Spec reviewer subagent confirms code matches spec?"}
+ fix_spec["Implementer subagent fixes spec gaps"]
+ d_quality["Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)"]
+ quality_ok{"Code quality reviewer subagent approves?"}
+ fix_quality["Implementer subagent fixes quality issues"]
+ mark["Mark task complete in TodoWrite"]
+ end
+
+ read_plan["Read plan, extract all tasks with full text, note context, create TodoWrite"]
+ more_tasks{"More tasks remain?"}
+ final_review["Dispatch final code reviewer subagent for entire implementation"]
+ finish["Use superpowers:finishing-a-development-branch"]
+
+ read_plan --> d_impl
+ d_impl --> q_impl
+ q_impl -->|yes| ans
+ ans --> d_impl
+ q_impl -->|no| impl
+ impl --> d_spec
+ d_spec --> spec_ok
+ spec_ok -->|no| fix_spec
+ fix_spec --> d_spec
+ spec_ok -->|yes| d_quality
+ d_quality --> quality_ok
+ quality_ok -->|no| fix_quality
+ fix_quality --> d_quality
+ quality_ok -->|yes| mark
+ mark --> more_tasks
+ more_tasks -->|yes| d_impl
+ more_tasks -->|no| final_review
+ final_review --> finish
+```
+
+## Prompt Templates
+
+- `./implementer-prompt.md` - Dispatch implementer subagent
+- `./spec-reviewer-prompt.md` - Dispatch spec compliance reviewer subagent
+- `./code-quality-reviewer-prompt.md` - Dispatch code quality reviewer subagent
+
+## Example Workflow
+
+```
+You: I'm using Subagent-Driven Development to execute this plan.
+
+[Read plan file once: docs/plans/feature-plan.md]
+[Extract all 5 tasks with full text and context]
+[Create TodoWrite with all tasks]
+
+Task 1: Hook installation script
+
+[Get Task 1 text and context (already extracted)]
+[Dispatch implementation subagent with full task text + context]
+
+Implementer: "Before I begin - should the hook be installed at user or system level?"
+
+You: "User level (~/.config/superpowers/hooks/)"
+
+Implementer: "Got it. Implementing now..."
+[Later] Implementer:
+ - Implemented install-hook command
+ - Added tests, 5/5 passing
+ - Self-review: Found I missed --force flag, added it
+ - Committed
+
+[Dispatch spec compliance reviewer]
+Spec reviewer: ✅ Spec compliant - all requirements met, nothing extra
+
+[Get git SHAs, dispatch code quality reviewer]
+Code reviewer: Strengths: Good test coverage, clean. Issues: None. Approved.
+
+[Mark Task 1 complete]
+
+Task 2: Recovery modes
+
+[Get Task 2 text and context (already extracted)]
+[Dispatch implementation subagent with full task text + context]
+
+Implementer: [No questions, proceeds]
+Implementer:
+ - Added verify/repair modes
+ - 8/8 tests passing
+ - Self-review: All good
+ - Committed
+
+[Dispatch spec compliance reviewer]
+Spec reviewer: ❌ Issues:
+ - Missing: Progress reporting (spec says "report every 100 items")
+ - Extra: Added --json flag (not requested)
+
+[Implementer fixes issues]
+Implementer: Removed --json flag, added progress reporting
+
+[Spec reviewer reviews again]
+Spec reviewer: ✅ Spec compliant now
+
+[Dispatch code quality reviewer]
+Code reviewer: Strengths: Solid. Issues (Important): Magic number (100)
+
+[Implementer fixes]
+Implementer: Extracted PROGRESS_INTERVAL constant
+
+[Code reviewer reviews again]
+Code reviewer: ✅ Approved
+
+[Mark Task 2 complete]
+
+...
+
+[After all tasks]
+[Dispatch final code-reviewer]
+Final reviewer: All requirements met, ready to merge
+
+Done!
+```
+
+## Advantages
+
+**vs. Manual execution:**
+
+- Subagents follow TDD naturally
+- Fresh context per task (no confusion)
+- Parallel-safe (subagents don't interfere)
+- Subagent can ask questions (before AND during work)
+
+**vs. Executing Plans:**
+
+- Same session (no handoff)
+- Continuous progress (no waiting)
+- Review checkpoints automatic
+
+**Efficiency gains:**
+
+- No file reading overhead (controller provides full text)
+- Controller curates exactly what context is needed
+- Subagent gets complete information upfront
+- Questions surfaced before work begins (not after)
+
+**Quality gates:**
+
+- Self-review catches issues before handoff
+- Two-stage review: spec compliance, then code quality
+- Review loops ensure fixes actually work
+- Spec compliance prevents over/under-building
+- Code quality ensures implementation is well-built
+
+**Cost:**
+
+- More subagent invocations (implementer + 2 reviewers per task)
+- Controller does more prep work (extracting all tasks upfront)
+- Review loops add iterations
+- But catches issues early (cheaper than debugging later)
+
+## Red Flags
+
+**Never:**
+
+- Start implementation on main/master branch without explicit user consent
+- Skip reviews (spec compliance OR code quality)
+- Proceed with unfixed issues
+- Dispatch multiple implementation subagents in parallel (conflicts)
+- Make subagent read plan file (provide full text instead)
+- Skip scene-setting context (subagent needs to understand where task fits)
+- Ignore subagent questions (answer before letting them proceed)
+- Accept "close enough" on spec compliance (spec reviewer found issues = not done)
+- Skip review loops (reviewer found issues = implementer fixes = review again)
+- Let implementer self-review replace actual review (both are needed)
+- **Start code quality review before spec compliance is ✅** (wrong order)
+- Move to next task while either review has open issues
+
+**If subagent asks questions:**
+
+- Answer clearly and completely
+- Provide additional context if needed
+- Don't rush them into implementation
+
+**If reviewer finds issues:**
+
+- Implementer (same subagent) fixes them
+- Reviewer reviews again
+- Repeat until approved
+- Don't skip the re-review
+
+**If subagent fails task:**
+
+- Dispatch fix subagent with specific instructions
+- Don't try to fix manually (context pollution)
+
+## Integration
+
+**Required workflow skills:**
+
+- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
+- **superpowers:writing-plans** - Creates the plan this skill executes
+- **superpowers:requesting-code-review** - Code review template for reviewer subagents
+- **superpowers:finishing-a-development-branch** - Complete development after all tasks
+
+**Subagents should use:**
+
+- **superpowers:test-driven-development** - Subagents follow TDD for each task
+
+**Alternative workflow:**
+
+- **superpowers:executing-plans** - Use for parallel session instead of same-session execution
diff --git a/.opencode/skills/subagent-driven-development/code-qualitty-reviewer-prompt.md b/.opencode/skills/subagent-driven-development/code-qualitty-reviewer-prompt.md
new file mode 100644
index 0000000..0a04745
--- /dev/null
+++ b/.opencode/skills/subagent-driven-development/code-qualitty-reviewer-prompt.md
@@ -0,0 +1,20 @@
+# Code Quality Reviewer Prompt Template
+
+Use this template when dispatching a code quality reviewer subagent.
+
+**Purpose:** Verify implementation is well-built (clean, tested, maintainable)
+
+**Only dispatch after spec compliance review passes.**
+
+```
+Task tool (code-reviewer):
+ Use template at requesting-code-review/code-reviewer.md
+
+ WHAT_WAS_IMPLEMENTED: [from implementer's report]
+ PLAN_OR_REQUIREMENTS: Task N from [plan-file]
+ BASE_SHA: [commit before task]
+ HEAD_SHA: [current commit]
+ DESCRIPTION: [task summary]
+```
+
+**Code reviewer returns:** Strengths, Issues (Critical/Important/Minor), Assessment
diff --git a/.opencode/skills/subagent-driven-development/implementer-prompt.md b/.opencode/skills/subagent-driven-development/implementer-prompt.md
new file mode 100644
index 0000000..db5404b
--- /dev/null
+++ b/.opencode/skills/subagent-driven-development/implementer-prompt.md
@@ -0,0 +1,78 @@
+# Implementer Subagent Prompt Template
+
+Use this template when dispatching an implementer subagent.
+
+```
+Task tool (general-purpose):
+ description: "Implement Task N: [task name]"
+ prompt: |
+ You are implementing Task N: [task name]
+
+ ## Task Description
+
+ [FULL TEXT of task from plan - paste it here, don't make subagent read file]
+
+ ## Context
+
+ [Scene-setting: where this fits, dependencies, architectural context]
+
+ ## Before You Begin
+
+ If you have questions about:
+ - The requirements or acceptance criteria
+ - The approach or implementation strategy
+ - Dependencies or assumptions
+ - Anything unclear in the task description
+
+ **Ask them now.** Raise any concerns before starting work.
+
+ ## Your Job
+
+ Once you're clear on requirements:
+ 1. Implement exactly what the task specifies
+ 2. Write tests (following TDD if task says to)
+ 3. Verify implementation works
+ 4. Commit your work
+ 5. Self-review (see below)
+ 6. Report back
+
+ Work from: [directory]
+
+ **While you work:** If you encounter something unexpected or unclear, **ask questions**.
+ It's always OK to pause and clarify. Don't guess or make assumptions.
+
+ ## Before Reporting Back: Self-Review
+
+ Review your work with fresh eyes. Ask yourself:
+
+ **Completeness:**
+ - Did I fully implement everything in the spec?
+ - Did I miss any requirements?
+ - Are there edge cases I didn't handle?
+
+ **Quality:**
+ - Is this my best work?
+ - Are names clear and accurate (match what things do, not how they work)?
+ - Is the code clean and maintainable?
+
+ **Discipline:**
+ - Did I avoid overbuilding (YAGNI)?
+ - Did I only build what was requested?
+ - Did I follow existing patterns in the codebase?
+
+ **Testing:**
+ - Do tests actually verify behavior (not just mock behavior)?
+ - Did I follow TDD if required?
+ - Are tests comprehensive?
+
+ If you find issues during self-review, fix them now before reporting.
+
+ ## Report Format
+
+ When done, report:
+ - What you implemented
+ - What you tested and test results
+ - Files changed
+ - Self-review findings (if any)
+ - Any issues or concerns
+```
diff --git a/.opencode/skills/subagent-driven-development/spec-reviewer-prompt.md b/.opencode/skills/subagent-driven-development/spec-reviewer-prompt.md
new file mode 100644
index 0000000..ab5ddb8
--- /dev/null
+++ b/.opencode/skills/subagent-driven-development/spec-reviewer-prompt.md
@@ -0,0 +1,61 @@
+# Spec Compliance Reviewer Prompt Template
+
+Use this template when dispatching a spec compliance reviewer subagent.
+
+**Purpose:** Verify implementer built what was requested (nothing more, nothing less)
+
+```
+Task tool (general-purpose):
+ description: "Review spec compliance for Task N"
+ prompt: |
+ You are reviewing whether an implementation matches its specification.
+
+ ## What Was Requested
+
+ [FULL TEXT of task requirements]
+
+ ## What Implementer Claims They Built
+
+ [From implementer's report]
+
+ ## CRITICAL: Do Not Trust the Report
+
+ The implementer finished suspiciously quickly. Their report may be incomplete,
+ inaccurate, or optimistic. You MUST verify everything independently.
+
+ **DO NOT:**
+ - Take their word for what they implemented
+ - Trust their claims about completeness
+ - Accept their interpretation of requirements
+
+ **DO:**
+ - Read the actual code they wrote
+ - Compare actual implementation to requirements line by line
+ - Check for missing pieces they claimed to implement
+ - Look for extra features they didn't mention
+
+ ## Your Job
+
+ Read the implementation code and verify:
+
+ **Missing requirements:**
+ - Did they implement everything that was requested?
+ - Are there requirements they skipped or missed?
+ - Did they claim something works but didn't actually implement it?
+
+ **Extra/unneeded work:**
+ - Did they build things that weren't requested?
+ - Did they over-engineer or add unnecessary features?
+ - Did they add "nice to haves" that weren't in spec?
+
+ **Misunderstandings:**
+ - Did they interpret requirements differently than intended?
+ - Did they solve the wrong problem?
+ - Did they implement the right feature but wrong way?
+
+ **Verify by reading code, not by trusting report.**
+
+ Report:
+ - ✅ Spec compliant (if everything matches after code inspection)
+ - ❌ Issues found: [list specifically what's missing or extra, with file:line references]
+```
diff --git a/.opencode/skills/test-driven-development/SKILL.md b/.opencode/skills/test-driven-development/SKILL.md
new file mode 100644
index 0000000..65f9473
--- /dev/null
+++ b/.opencode/skills/test-driven-development/SKILL.md
@@ -0,0 +1,394 @@
+---
+name: test-driven-development
+description: Use when implementing any feature or bugfix, before writing implementation code
+---
+
+# Test-Driven Development (TDD)
+
+## Overview
+
+Write the test first. Watch it fail. Write minimal code to pass.
+
+**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing.
+
+**Violating the letter of the rules is violating the spirit of the rules.**
+
+## When to Use
+
+**Always:**
+
+- New features
+- Bug fixes
+- Refactoring
+- Behavior changes
+
+**Exceptions (ask your human partner):**
+
+- Throwaway prototypes
+- Generated code
+- Configuration files
+
+Thinking "skip TDD just this once"? Stop. That's rationalization.
+
+## The Iron Law
+
+```
+NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
+```
+
+Write code before the test? Delete it. Start over.
+
+**No exceptions:**
+
+- Don't keep it as "reference"
+- Don't "adapt" it while writing tests
+- Don't look at it
+- Delete means delete
+
+Implement fresh from tests. Period.
+
+## Red-Green-Refactor
+
+```mermaid
+flowchart LR
+ red["RED
Write failing test"]:::red
+ verify_red{"Verify fails
correctly"}:::diamond
+ green["GREEN
Minimal code"]:::green
+ verify_green{"Verify passes
All green"}:::diamond
+ refactor["REFACTOR
Clean up"]:::blue
+ next([Next])
+
+ red --> verify_red
+ verify_red -- yes --> green
+ verify_red -- "wrong
failure" --> red
+ green --> verify_green
+ verify_green -- yes --> refactor
+ verify_green -- no --> green
+ refactor --> verify_green
+ verify_green --> next
+
+ classDef red fill:#ffcccc,stroke:#333,stroke-width:1px;
+ classDef green fill:#ccffcc,stroke:#333,stroke-width:1px;
+ classDef blue fill:#ccccff,stroke:#333,stroke-width:1px;
+ classDef diamond fill:#fff,stroke:#333,stroke-width:1px;
+```
+
+### RED - Write Failing Test
+
+Write one minimal test showing what should happen.
+
+#### Good
+
+```typescript
+test("retries failed operations 3 times", async () => {
+ let attempts = 0;
+ const operation = () => {
+ attempts++;
+ if (attempts < 3) throw new Error("fail");
+ return "success";
+ };
+
+ const result = await retryOperation(operation);
+
+ expect(result).toBe("success");
+ expect(attempts).toBe(3);
+});
+```
+
+Clear name, tests real behavior, one thing
+
+#### Bad
+
+```typescript
+test("retry works", async () => {
+ const mock = jest
+ .fn()
+ .mockRejectedValueOnce(new Error())
+ .mockRejectedValueOnce(new Error())
+ .mockResolvedValueOnce("success");
+ await retryOperation(mock);
+ expect(mock).toHaveBeenCalledTimes(3);
+});
+```
+
+Vague name, tests mock not code
+
+**Requirements:**
+
+- One behavior
+- Clear name
+- Real code (no mocks unless unavoidable)
+
+### Verify RED - Watch It Fail
+
+**MANDATORY. Never skip.**
+
+```bash
+npm test path/to/test.test.ts
+```
+
+Confirm:
+
+- Test fails (not errors)
+- Failure message is expected
+- Fails because feature missing (not typos)
+
+**Test passes?** You're testing existing behavior. Fix test.
+
+**Test errors?** Fix error, re-run until it fails correctly.
+
+### GREEN - Minimal Code
+
+Write simplest code to pass the test.
+
+#### Good
+
+```typescript
+async function retryOperation(fn: () => Promise): Promise {
+ for (let i = 0; i < 3; i++) {
+ try {
+ return await fn();
+ } catch (e) {
+ if (i === 2) throw e;
+ }
+ }
+ throw new Error("unreachable");
+}
+```
+
+Just enough to pass
+
+#### Bad
+
+```typescript
+async function retryOperation(
+ fn: () => Promise,
+ options?: {
+ maxRetries?: number;
+ backoff?: "linear" | "exponential";
+ onRetry?: (attempt: number) => void;
+ },
+): Promise {
+ // YAGNI
+}
+```
+
+Over-engineered
+
+Don't add features, refactor other code, or "improve" beyond the test.
+
+### Verify GREEN - Watch It Pass
+
+**MANDATORY.**
+
+```bash
+npm test path/to/test.test.ts
+```
+
+Confirm:
+
+- Test passes
+- Other tests still pass
+- Output pristine (no errors, warnings)
+
+**Test fails?** Fix code, not test.
+
+**Other tests fail?** Fix now.
+
+### REFACTOR - Clean Up
+
+After green only:
+
+- Remove duplication
+- Improve names
+- Extract helpers
+
+Keep tests green. Don't add behavior.
+
+### Repeat
+
+Next failing test for next feature.
+
+## Good Tests
+
+| Quality | Good | Bad |
+| ---------------- | ----------------------------------- | --------------------------------------------------- |
+| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` |
+| **Clear** | Name describes behavior | `test('test1')` |
+| **Shows intent** | Demonstrates desired API | Obscures what code should do |
+
+## Why Order Matters
+
+**"I'll write tests after to verify it works"**
+
+Tests written after code pass immediately. Passing immediately proves nothing:
+
+- Might test wrong thing
+- Might test implementation, not behavior
+- Might miss edge cases you forgot
+- You never saw it catch the bug
+
+Test-first forces you to see the test fail, proving it actually tests something.
+
+**"I already manually tested all the edge cases"**
+
+Manual testing is ad-hoc. You think you tested everything but:
+
+- No record of what you tested
+- Can't re-run when code changes
+- Easy to forget cases under pressure
+- "It worked when I tried it" ≠ comprehensive
+
+Automated tests are systematic. They run the same way every time.
+
+**"Deleting X hours of work is wasteful"**
+
+Sunk cost fallacy. The time is already gone. Your choice now:
+
+- Delete and rewrite with TDD (X more hours, high confidence)
+- Keep it and add tests after (30 min, low confidence, likely bugs)
+
+The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
+
+**"TDD is dogmatic, being pragmatic means adapting"**
+
+TDD IS pragmatic:
+
+- Finds bugs before commit (faster than debugging after)
+- Prevents regressions (tests catch breaks immediately)
+- Documents behavior (tests show how to use code)
+- Enables refactoring (change freely, tests catch breaks)
+
+"Pragmatic" shortcuts = debugging in production = slower.
+
+**"Tests after achieve the same goals - it's spirit not ritual"**
+
+No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
+
+Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge
+cases, not discovered ones.
+
+Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
+
+30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
+
+## Common Rationalizations
+
+| Excuse | Reality |
+| -------------------------------------- | ----------------------------------------------------------------------- |
+| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
+| "I'll test after" | Tests passing immediately prove nothing. |
+| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
+| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
+| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
+| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
+| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
+| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
+| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
+| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
+| "Existing code has no tests" | You're improving it. Add tests for existing code. |
+
+## Red Flags - STOP and Start Over
+
+- Code before test
+- Test after implementation
+- Test passes immediately
+- Can't explain why test failed
+- Tests added "later"
+- Rationalizing "just this once"
+- "I already manually tested it"
+- "Tests after achieve the same purpose"
+- "It's about spirit not ritual"
+- "Keep as reference" or "adapt existing code"
+- "Already spent X hours, deleting is wasteful"
+- "TDD is dogmatic, I'm being pragmatic"
+- "This is different because..."
+
+**All of these mean: Delete code. Start over with TDD.**
+
+## Example: Bug Fix
+
+**Bug:** Empty email accepted
+
+**RED**
+
+```typescript
+test("rejects empty email", async () => {
+ const result = await submitForm({ email: "" });
+ expect(result.error).toBe("Email required");
+});
+```
+
+**Verify RED**
+
+```bash
+$ npm test
+FAIL: expected 'Email required', got undefined
+```
+
+**GREEN**
+
+```typescript
+function submitForm(data: FormData) {
+ if (!data.email?.trim()) {
+ return { error: "Email required" };
+ }
+ // ...
+}
+```
+
+**Verify GREEN**
+
+```bash
+$ npm test
+PASS
+```
+
+**REFACTOR** Extract validation for multiple fields if needed.
+
+## Verification Checklist
+
+Before marking work complete:
+
+- [ ] Every new function/method has a test
+- [ ] Watched each test fail before implementing
+- [ ] Each test failed for expected reason (feature missing, not typo)
+- [ ] Wrote minimal code to pass each test
+- [ ] All tests pass
+- [ ] Output pristine (no errors, warnings)
+- [ ] Tests use real code (mocks only if unavoidable)
+- [ ] Edge cases and errors covered
+
+Can't check all boxes? You skipped TDD. Start over.
+
+## When Stuck
+
+| Problem | Solution |
+| ---------------------- | -------------------------------------------------------------------- |
+| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
+| Test too complicated | Design too complicated. Simplify interface. |
+| Must mock everything | Code too coupled. Use dependency injection. |
+| Test setup huge | Extract helpers. Still complex? Simplify design. |
+
+## Debugging Integration
+
+Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
+
+Never fix bugs without a test.
+
+## Testing Anti-Patterns
+
+When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls:
+
+- Testing mock behavior instead of real behavior
+- Adding test-only methods to production classes
+- Mocking without understanding dependencies
+
+## Final Rule
+
+```
+Production code → test exists and failed first
+Otherwise → not TDD
+```
+
+No exceptions without your human partner's permission.
diff --git a/.opencode/skills/test-driven-development/testing-anti-patterns.md b/.opencode/skills/test-driven-development/testing-anti-patterns.md
new file mode 100644
index 0000000..1520cea
--- /dev/null
+++ b/.opencode/skills/test-driven-development/testing-anti-patterns.md
@@ -0,0 +1,319 @@
+# Testing Anti-Patterns
+
+**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production
+code.
+
+## Overview
+
+Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested.
+
+**Core principle:** Test what the code does, not what the mocks do.
+
+**Following strict TDD prevents these anti-patterns.**
+
+## The Iron Laws
+
+```
+1. NEVER test mock behavior
+2. NEVER add test-only methods to production classes
+3. NEVER mock without understanding dependencies
+```
+
+## Anti-Pattern 1: Testing Mock Behavior
+
+**The violation:**
+
+```typescript
+// ❌ BAD: Testing that the mock exists
+test('renders sidebar', () => {
+ render();
+ expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument();
+});
+```
+
+**Why this is wrong:**
+
+- You're verifying the mock works, not that the component works
+- Test passes when mock is present, fails when it's not
+- Tells you nothing about real behavior
+
+**your human partner's correction:** "Are we testing the behavior of a mock?"
+
+**The fix:**
+
+```typescript
+// ✅ GOOD: Test real component or don't mock it
+test('renders sidebar', () => {
+ render(); // Don't mock sidebar
+ expect(screen.getByRole('navigation')).toBeInTheDocument();
+});
+
+// OR if sidebar must be mocked for isolation:
+// Don't assert on the mock - test Page's behavior with sidebar present
+```
+
+### Gate Function
+
+```
+BEFORE asserting on any mock element:
+ Ask: "Am I testing real component behavior or just mock existence?"
+
+ IF testing mock existence:
+ STOP - Delete the assertion or unmock the component
+
+ Test real behavior instead
+```
+
+## Anti-Pattern 2: Test-Only Methods in Production
+
+**The violation:**
+
+```typescript
+// ❌ BAD: destroy() only used in tests
+class Session {
+ async destroy() {
+ // Looks like production API!
+ await this._workspaceManager?.destroyWorkspace(this.id);
+ // ... cleanup
+ }
+}
+
+// In tests
+afterEach(() => session.destroy());
+```
+
+**Why this is wrong:**
+
+- Production class polluted with test-only code
+- Dangerous if accidentally called in production
+- Violates YAGNI and separation of concerns
+- Confuses object lifecycle with entity lifecycle
+
+**The fix:**
+
+```typescript
+// ✅ GOOD: Test utilities handle test cleanup
+// Session has no destroy() - it's stateless in production
+
+// In test-utils/
+export async function cleanupSession(session: Session) {
+ const workspace = session.getWorkspaceInfo();
+ if (workspace) {
+ await workspaceManager.destroyWorkspace(workspace.id);
+ }
+}
+
+// In tests
+afterEach(() => cleanupSession(session));
+```
+
+### Gate Function
+
+```
+BEFORE adding any method to production class:
+ Ask: "Is this only used by tests?"
+
+ IF yes:
+ STOP - Don't add it
+ Put it in test utilities instead
+
+ Ask: "Does this class own this resource's lifecycle?"
+
+ IF no:
+ STOP - Wrong class for this method
+```
+
+## Anti-Pattern 3: Mocking Without Understanding
+
+**The violation:**
+
+```typescript
+// ❌ BAD: Mock breaks test logic
+test("detects duplicate server", () => {
+ // Mock prevents config write that test depends on!
+ vi.mock("ToolCatalog", () => ({
+ discoverAndCacheTools: vi.fn().mockResolvedValue(undefined),
+ }));
+
+ await addServer(config);
+ await addServer(config); // Should throw - but won't!
+});
+```
+
+**Why this is wrong:**
+
+- Mocked method had side effect test depended on (writing config)
+- Over-mocking to "be safe" breaks actual behavior
+- Test passes for wrong reason or fails mysteriously
+
+**The fix:**
+
+```typescript
+// ✅ GOOD: Mock at correct level
+test("detects duplicate server", () => {
+ // Mock the slow part, preserve behavior test needs
+ vi.mock("MCPServerManager"); // Just mock slow server startup
+
+ await addServer(config); // Config written
+ await addServer(config); // Duplicate detected ✓
+});
+```
+
+### Gate Function
+
+```
+BEFORE mocking any method:
+ STOP - Don't mock yet
+
+ 1. Ask: "What side effects does the real method have?"
+ 2. Ask: "Does this test depend on any of those side effects?"
+ 3. Ask: "Do I fully understand what this test needs?"
+
+ IF depends on side effects:
+ Mock at lower level (the actual slow/external operation)
+ OR use test doubles that preserve necessary behavior
+ NOT the high-level method the test depends on
+
+ IF unsure what test depends on:
+ Run test with real implementation FIRST
+ Observe what actually needs to happen
+ THEN add minimal mocking at the right level
+
+ Red flags:
+ - "I'll mock this to be safe"
+ - "This might be slow, better mock it"
+ - Mocking without understanding the dependency chain
+```
+
+## Anti-Pattern 4: Incomplete Mocks
+
+**The violation:**
+
+```typescript
+// ❌ BAD: Partial mock - only fields you think you need
+const mockResponse = {
+ status: "success",
+ data: { userId: "123", name: "Alice" },
+ // Missing: metadata that downstream code uses
+};
+
+// Later: breaks when code accesses response.metadata.requestId
+```
+
+**Why this is wrong:**
+
+- **Partial mocks hide structural assumptions** - You only mocked fields you know about
+- **Downstream code may depend on fields you didn't include** - Silent failures
+- **Tests pass but integration fails** - Mock incomplete, real API complete
+- **False confidence** - Test proves nothing about real behavior
+
+**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses.
+
+**The fix:**
+
+```typescript
+// ✅ GOOD: Mirror real API completeness
+const mockResponse = {
+ status: "success",
+ data: { userId: "123", name: "Alice" },
+ metadata: { requestId: "req-789", timestamp: 1234567890 },
+ // All fields real API returns
+};
+```
+
+### Gate Function
+
+```
+BEFORE creating mock responses:
+ Check: "What fields does the real API response contain?"
+
+ Actions:
+ 1. Examine actual API response from docs/examples
+ 2. Include ALL fields system might consume downstream
+ 3. Verify mock matches real response schema completely
+
+ Critical:
+ If you're creating a mock, you must understand the ENTIRE structure
+ Partial mocks fail silently when code depends on omitted fields
+
+ If uncertain: Include all documented fields
+```
+
+## Anti-Pattern 5: Integration Tests as Afterthought
+
+**The violation:**
+
+```
+✅ Implementation complete
+❌ No tests written
+"Ready for testing"
+```
+
+**Why this is wrong:**
+
+- Testing is part of implementation, not optional follow-up
+- TDD would have caught this
+- Can't claim complete without tests
+
+**The fix:**
+
+```
+TDD cycle:
+1. Write failing test
+2. Implement to pass
+3. Refactor
+4. THEN claim complete
+```
+
+## When Mocks Become Too Complex
+
+**Warning signs:**
+
+- Mock setup longer than test logic
+- Mocking everything to make test pass
+- Mocks missing methods real components have
+- Test breaks when mock changes
+
+**your human partner's question:** "Do we need to be using a mock here?"
+
+**Consider:** Integration tests with real components often simpler than complex mocks
+
+## TDD Prevents These Anti-Patterns
+
+**Why TDD helps:**
+
+1. **Write test first** → Forces you to think about what you're actually testing
+2. **Watch it fail** → Confirms test tests real behavior, not mocks
+3. **Minimal implementation** → No test-only methods creep in
+4. **Real dependencies** → You see what the test actually needs before mocking
+
+**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code
+first.
+
+## Quick Reference
+
+| Anti-Pattern | Fix |
+| ------------------------------- | --------------------------------------------- |
+| Assert on mock elements | Test real component or unmock it |
+| Test-only methods in production | Move to test utilities |
+| Mock without understanding | Understand dependencies first, mock minimally |
+| Incomplete mocks | Mirror real API completely |
+| Tests as afterthought | TDD - tests first |
+| Over-complex mocks | Consider integration tests |
+
+## Red Flags
+
+- Assertion checks for `*-mock` test IDs
+- Methods only called in test files
+- Mock setup is >50% of test
+- Test fails when you remove mock
+- Can't explain why mock is needed
+- Mocking "just to be safe"
+
+## The Bottom Line
+
+**Mocks are tools to isolate, not things to test.**
+
+If TDD reveals you're testing mock behavior, you've gone wrong.
+
+Fix: Test real behavior or question why you're mocking at all.
diff --git a/.opencode/skills/writing-plans/SKILL.md b/.opencode/skills/writing-plans/SKILL.md
new file mode 100644
index 0000000..896397d
--- /dev/null
+++ b/.opencode/skills/writing-plans/SKILL.md
@@ -0,0 +1,125 @@
+---
+name: writing-plans
+description: Use when you have a spec or requirements for a multi-step task, before touching code
+---
+
+# Writing Plans
+
+## Overview
+
+Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste.
+Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check,
+how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
+
+Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know
+good test design very well.
+
+**Announce at start:** "I'm using the writing-plans skill to create the implementation plan."
+
+**Context:** This should be run in a dedicated worktree (created by brainstorming skill).
+
+**Save plans to:** `docs/plans/YYYY-MM-DD-.md`
+
+## Bite-Sized Task Granularity
+
+**Each step is one action (2-5 minutes):**
+
+- "Write the failing test" - step
+- "Run it to make sure it fails" - step
+- "Implement the minimal code to make the test pass" - step
+- "Run the tests and make sure they pass" - step
+- "Commit" - step
+
+## Plan Document Header
+
+**Every plan MUST start with this header:**
+
+```markdown
+# [Feature Name] Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** [One sentence describing what this builds]
+
+**Architecture:** [2-3 sentences about approach]
+
+**Tech Stack:** [Key technologies/libraries]
+
+---
+```
+
+## Task Structure
+
+````markdown
+### Task N: [Component Name]
+
+**Files:**
+
+- Create: `exact/path/to/file.ts`
+- Modify: `exact/path/to/existing.ts:123-145`
+- Test: `tests/exact/path/to/test.test.ts`
+
+**Step 1: Write the failing test**
+
+```typescript
+describe('Component Name', () => {
+ it('should have specific behavior', () => {
+ const result = functionName(input);
+ expect(result).toBe(expected);
+ });
+});
+```
+
+**Step 2: Run test to verify it fails**
+
+Run: `bun test tests/path/test.test.ts` Expected: FAIL with "functionName is not defined"
+
+**Step 3: Write minimal implementation**
+
+```typescript
+export function functionName(input: InputType): OutputType {
+ return expected;
+}
+```
+
+**Step 4: Run test to verify it passes**
+
+Run: `bun test tests/path/test.test.ts` Expected: PASS
+
+**Step 5: Commit**
+
+```bash
+git add tests/path/test.test.ts src/path/file.ts
+git commit -m "feat: add specific feature"
+```
+
+```
+
+## Remember
+- Exact file paths always
+- Complete code in plan (not "add validation")
+- Exact commands with expected output
+- Reference relevant skills with @ syntax
+- DRY, YAGNI, TDD, frequent commits
+
+## Execution Handoff
+
+After saving the plan, offer execution choice:
+
+**"Plan complete and saved to `docs/plans/.md`. Two execution options:**
+
+**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
+
+**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
+
+**Which approach?"**
+
+**If Subagent-Driven chosen:**
+- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development
+- Stay in this session
+- Fresh subagent per task + code review
+
+**If Parallel Session chosen:**
+- Guide them to open new session in worktree
+- **REQUIRED SUB-SKILL:** New session uses superpowers:executing-plans
+```
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..07a364c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,454 @@
+# AGENTS.md
+
+This document contains important rules and guidelines for AI agents working on this codebase.
+
+## Arrow Functions Only
+
+**Use arrow functions exclusively. Never use named function declarations or classes.**
+
+### Good
+
+```typescript
+// Single function per module - arrow function
+export const calculateTotal = (items: Item[]): number => {
+ return items.reduce((sum, item) => sum + item.price, 0);
+};
+
+// Async arrow function
+export const fetchData = async (id: string): Promise => {
+ const response = await api.get(`/data/${id}`);
+ return response.data;
+};
+```
+
+### Bad
+
+```typescript
+// NEVER use named function declarations
+function calculateTotal(items: Item[]): number {
+ return items.reduce((sum, item) => sum + item.price, 0);
+}
+
+// NEVER use classes
+class DataFetcher {
+ async fetch(id: string): Promise {
+ return api.get(`/data/${id}`);
+ }
+}
+```
+
+### Rationale
+
+- Arrow functions have lexical `this` binding (no binding issues)
+- Consistent syntax for both sync and async functions
+- Better type inference in TypeScript
+- No function hoisting surprises
+- Uniform codebase style
+
+## Code Organization Rules
+
+### 1. One Function Per Module Maximum
+
+- Each `.ts` file MUST export at most ONE function or class
+- Each module should have a single, well-defined responsibility
+- This promotes modularity, testability, and code reuse
+
+**BAD**:
+
+```typescript
+// math-utils.ts
+export function add(a: number, b: number): number { ... }
+export function subtract(a: number, b: number): number { ... }
+export function multiply(a: number, b: number): number { ... }
+```
+
+**GOOD**:
+
+```typescript
+// add.ts
+export function add(a: number, b: number): number { ... }
+
+// subtract.ts
+export function subtract(a: number, b: number): number { ... }
+
+// index.ts (barrel)
+export { add } from './add';
+export { subtract } from './subtract';
+```
+
+### Exceptions
+
+Private helper functions that are only used by the main exported function may be defined in the same file, but they must
+not be exported:
+
+```typescript
+// validators/is-valid-email.ts
+
+// Private helper - not exported
+const hasValidDomain = (email: string): boolean => {
+ return email.includes("@") && email.split("@")[1].includes(".");
+};
+
+// Single exported function
+export const isValidEmail = (email: string): boolean => {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) && hasValidDomain(email);
+};
+```
+
+### File Naming
+
+- Use kebab-case for file names: `calculate-total.ts`, `format-currency.ts`
+- Match the file name to the exported function name
+
+### Rationale
+
+- Maximum code discoverability
+- Easy to locate specific functions
+- Clear module boundaries
+- Simplifies code review (one concern per file)
+- Better tree-shaking for bundlers
+- Easier testing (isolated units)
+
+### 2. Barrel Module Pattern
+
+- Use barrel exports (`index.ts`) to expose public API of a directory
+- Each subdirectory should have an `index.ts` that re-exports its children
+- Consumers should import from the barrel, not individual files
+
+**BAD**:
+
+```typescript
+// Importing from specific files
+import { add } from "./utils/math/add";
+import { subtract } from "./utils/math/subtract";
+```
+
+**GOOD**:
+
+```typescript
+// foo/index.ts
+export { add } from "./add";
+export { subtract } from "./subtract";
+export { multiply } from "./multiply";
+
+// consumer.ts
+import { add, subtract } from "./foo"; // Not './foo/add'
+```
+
+### Import Conventions
+
+- Always import from barrel exports when available
+- Only import from specific files when the item is not exported from the barrel
+- Avoid deep relative imports (e.g., `../../../foo/bar/baz`)
+
+### Structure
+
+```
+src/
+├── utils/
+│ ├── calculate-total.ts
+│ ├── format-currency.ts
+│ ├── retry.ts
+│ └── index.ts # Barrel: re-exports all utils
+```
+
+### Barrel File Pattern
+
+```typescript
+// utils/index.ts
+export { calculateTotal } from "./calculate-total";
+export { formatCurrency } from "./format-currency";
+export { withRetry } from "./retry";
+
+// Re-export types separately
+export type { Item, Currency } from "./types";
+```
+
+### Multiple Barrel Levels
+
+For nested structures, create barrels at each level:
+
+```
+src/
+├── scripts/
+│ ├── mirror-package/
+│ │ ├── parse-tag.ts
+│ │ ├── validate-mirror-url.ts
+│ │ └── index.ts # Level 2 barrel
+│ ├── check-repo-settings/
+│ │ ├── checks.ts
+│ │ └── index.ts # Level 2 barrel
+│ └── index.ts # Level 1 barrel
+```
+
+```typescript
+// scripts/mirror-package/index.ts
+export { detectChanges } from "./detect-changes";
+export { parseTag } from "./parse-tag";
+export { validateMirrorUrl } from "./validate-mirror-url";
+export type { PackageInfo, MirrorUrl } from "./types";
+```
+
+```typescript
+// scripts/index.ts
+export { detectChanges, parseTag, validateMirrorUrl } from "./mirror-package";
+export { checkRepoSettings } from "./check-repo-settings";
+```
+
+### Rationale
+
+- Clean import statements throughout the codebase
+- Encapsulation of internal module structure
+- Easy to refactor (change internal organization without affecting imports)
+- Clear public API surface for each directory
+- Reduces import complexity
+
+### 3. Test Collocation
+
+**Tests must be collocated with source files. Never use a separate `tests/` directory.**
+
+- Test files MUST be collocated with the source files they test
+- Each source file `foo.ts` should have its own `foo.test.ts` next to it
+- NEVER create consolidated test files for an entire directory
+
+**BAD**:
+
+```
+foo/
+ baa-0.ts
+ baa-1.ts
+ foo.test.ts <-- WRONG: consolidated test file
+```
+
+**GOOD**:
+
+```
+foo/
+ baa-0.ts
+ baa-0.test.ts <-- CORRECT: test next to source
+ baa-1.ts
+ baa-1.test.ts <-- CORRECT: test next to source
+```
+
+### Naming Convention
+
+- Source file: `{module-name}.ts`
+- Test file: `{module-name}.test.ts`
+
+### Example
+
+```typescript
+// utils/retry.ts
+export const withRetry = async (fn: () => Promise, maxRetries: number = 3): Promise => {
+ // implementation
+};
+```
+
+```typescript
+// utils/retry.test.ts
+import { withRetry } from "./retry";
+
+describe("withRetry", () => {
+ it("should retry on failure", async () => {
+ // test implementation
+ });
+});
+```
+
+### Rationale
+
+- Tests are always adjacent to the code they test
+- Easy to find and maintain tests
+- Refactoring is safer (tests move with source)
+- No complex import path management
+- Encourages testing discipline
+
+## Test-Driven Development (TDD)
+
+**Write tests BEFORE implementing the functionality. Follow the Red-Green-Refactor cycle.**
+
+### The TDD Cycle
+
+```
+1. RED → Write a failing test (describe behavior, assert expectations)
+2. GREEN → Write minimal code to make the test pass
+3. REFACTOR → Clean up the code while keeping tests green
+```
+
+Repeat this cycle for every new behavior or edge case.
+
+### Example Workflow
+
+**Step 1: Write the failing test first**
+
+```typescript
+// utils/calculate-discount.test.ts
+import { calculateDiscount } from "./calculate-discount";
+
+describe("calculateDiscount", () => {
+ it("should apply 10% discount to amounts over $100", () => {
+ // Arrange
+ const amount = 150;
+ const discountRate = 0.1;
+
+ // Act
+ const result = calculateDiscount(amount, discountRate);
+
+ // Assert
+ expect(result).toBe(15);
+ });
+
+ it("should return 0 discount for amounts at or below $100", () => {
+ expect(calculateDiscount(100, 0.1)).toBe(0);
+ expect(calculateDiscount(50, 0.1)).toBe(0);
+ });
+});
+```
+
+Run tests: `bun test calculate-discount.test.ts` Expected: Tests FAIL (function doesn't exist yet) → RED
+
+**Step 2: Implement minimal code to pass**
+
+```typescript
+// utils/calculate-discount.ts
+export const calculateDiscount = (amount: number, rate: number): number => {
+ if (amount <= 100) return 0;
+ return amount * rate;
+};
+```
+
+Run tests: `bun test calculate-discount.test.ts` Expected: Tests PASS → GREEN
+
+**Step 3: Refactor (if needed)**
+
+No refactoring needed for this simple function. Move to next test case or feature.
+
+### Iterative Development
+
+Continue adding test cases one at a time:
+
+```typescript
+// Additional test case (RED)
+it("should handle zero discount rate", () => {
+ expect(calculateDiscount(200, 0)).toBe(0);
+});
+
+// Update implementation (GREEN)
+export const calculateDiscount = (amount: number, rate: number): number => {
+ if (amount <= 100 || rate <= 0) return 0;
+ return amount * rate;
+};
+
+// Add edge case test (RED)
+it("should handle negative amounts gracefully", () => {
+ expect(calculateDiscount(-50, 0.1)).toBe(0);
+});
+
+// Update implementation (GREEN)
+export const calculateDiscount = (amount: number, rate: number): number => {
+ if (amount <= 100 || rate <= 0 || amount < 0) return 0;
+ return amount * rate;
+};
+
+// Refactor: Simplify the logic
+export const calculateDiscount = (amount: number, rate: number): number => {
+ if (amount <= 100 || rate <= 0) return 0;
+ return amount * rate;
+};
+```
+
+### Rules
+
+1. **Never write production code without a failing test first**
+2. **Write only enough test code to fail** (start with the assertion you want)
+3. **Write only enough production code to pass** (no speculative features)
+4. **Tests must fail for the right reason** (not compilation errors)
+5. **Keep tests fast** (< 100ms per test ideally)
+6. **One test per behavior** (edge cases, happy path, error cases)
+
+### Test Structure (AAA Pattern)
+
+```typescript
+it("should validate email format", () => {
+ // Arrange: Set up inputs
+ const invalidEmail = "not-an-email";
+
+ // Act: Execute the function
+ const result = isValidEmail(invalidEmail);
+
+ // Assert: Verify the outcome
+ expect(result).toBe(false);
+});
+```
+
+### Benefits of TDD
+
+- **Design pressure**: Forces you to think about API design before coding
+- **Confidence**: Every line of production code is covered by a test
+- **Documentation**: Tests serve as executable documentation
+- **Refactoring safety**: Change implementation with confidence
+- **Debugging time**: Catch issues immediately, not in production
+- **Scope control**: Prevents over-engineering and gold-plating
+
+### Anti-Patterns
+
+```typescript
+// BAD: Writing implementation before tests
+export const complexLogic = () => {
+ /* 50 lines of code */
+};
+// Now try to figure out how to test this...
+
+// BAD: Writing tests after the fact
+// Tests become "happy path" verification rather than design tool
+
+// BAD: Testing implementation details instead of behavior
+it("should call the database", () => {
+ const spy = jest.spyOn(db, "query");
+ getUsers();
+ expect(spy).toHaveBeenCalled(); // Testing HOW, not WHAT
+});
+
+// GOOD: Testing behavior
+it("should return all active users", async () => {
+ const users = await getUsers({ status: "active" });
+ expect(users).toHaveLength(3);
+ expect(users.every((u) => u.status === "active")).toBe(true);
+});
+```
+
+## Testing Requirements
+
+- Every public function must have corresponding test coverage
+- Test files must follow the naming convention: `{source-file}.test.ts`
+- Use descriptive test names that explain the behavior being tested
+- Test both success and error cases
+
+## Directory Structure Example
+
+```
+libs/
+ workflows/
+ src/
+ utils/
+ index.ts
+ string-utils.ts
+ string-utils.test.ts
+ math-utils.ts
+ math-utils.test.ts
+ date-utils.ts
+ date-utils.test.ts
+```
+
+## Quick Reference
+
+| Convention | Rule |
+| ------------------ | ----------------------------------------------------- |
+| Functions | Arrow functions only (`const fn = () => {}`) |
+| Classes | Never use classes |
+| Named functions | Never use `function` keyword |
+| Tests | Collocated: `{module}.test.ts` next to `{module}.ts` |
+| TDD | Write tests BEFORE code. Red → Green → Refactor cycle |
+| Functions per file | Maximum 1 exported function per module |
+| Exports | Use barrel files (`index.ts`) for clean imports |
+| File naming | Kebab-case matching function name |
diff --git a/bun.lock b/bun.lock
index c88bfbb..4b87ce5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -97,6 +97,19 @@
"typescript": "5.6.2",
},
},
+ "packages/opencode-skills": {
+ "name": "@pantheon-org/opencode-skills",
+ "version": "0.1.0",
+ "dependencies": {
+ "yaml": "^2.3.4",
+ "zod": "^3.22.4",
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.3.13",
+ "@types/bun": "latest",
+ "@types/glob": "^8.1.0",
+ },
+ },
"packages/opencode-warcraft-notifications-plugin": {
"name": "@pantheon-org/opencode-warcraft-notifications-plugin",
"version": "0.1.0",
@@ -121,8 +134,12 @@
},
},
"tools/executors": {
- "name": "@pantheon-org/tools",
- "version": "1.0.0",
+ "name": "@pantheon-org/executors",
+ "version": "0.1.0",
+ "dependencies": {
+ "@pantheon-org/opencode-skills": "workspace:*",
+ "glob": "^10.3.10",
+ },
},
},
"packages": {
@@ -660,6 +677,8 @@
"@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.15.0", "", { "os": "win32", "cpu": "x64" }, "sha512-HZsfne0s/tGOcJK9ZdTGxsNU2P/dH0Shf0jqrPvsC6wX0Wk+6AyhSpHFLQCnLOuFQiHHU0ePfM8iYsoJb5hHpQ=="],
+ "@pantheon-org/executors": ["@pantheon-org/executors@workspace:tools/executors"],
+
"@pantheon-org/opencode-agent-loader-plugin": ["@pantheon-org/opencode-agent-loader-plugin@workspace:packages/opencode-agent-loader-plugin"],
"@pantheon-org/opencode-config": ["@pantheon-org/opencode-config@workspace:packages/opencode-config"],
@@ -668,9 +687,9 @@
"@pantheon-org/opencode-notification": ["@pantheon-org/opencode-notification@workspace:packages/opencode-notification"],
- "@pantheon-org/opencode-warcraft-notifications-plugin": ["@pantheon-org/opencode-warcraft-notifications-plugin@workspace:packages/opencode-warcraft-notifications-plugin"],
+ "@pantheon-org/opencode-skills": ["@pantheon-org/opencode-skills@workspace:packages/opencode-skills"],
- "@pantheon-org/tools": ["@pantheon-org/tools@workspace:tools/executors"],
+ "@pantheon-org/opencode-warcraft-notifications-plugin": ["@pantheon-org/opencode-warcraft-notifications-plugin@workspace:packages/opencode-warcraft-notifications-plugin"],
"@phenomnomnominal/tsquery": ["@phenomnomnominal/tsquery@5.0.1", "", { "dependencies": { "esquery": "^1.4.0" }, "peerDependencies": { "typescript": "^3 || ^4 || ^5" } }, "sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA=="],
@@ -804,6 +823,8 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+ "@types/glob": ["@types/glob@8.1.0", "", { "dependencies": { "@types/minimatch": "^5.1.2", "@types/node": "*" } }, "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w=="],
+
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
@@ -816,6 +837,8 @@
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
+ "@types/minimatch": ["@types/minimatch@5.1.2", "", {}, "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA=="],
+
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
@@ -1276,7 +1299,7 @@
"get-them-args": ["get-them-args@1.3.2", "", {}, "sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw=="],
- "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
+ "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -1386,7 +1409,7 @@
"istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
- "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
+ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="],
@@ -1508,7 +1531,7 @@
"lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
- "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+ "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
@@ -1688,7 +1711,7 @@
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
- "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
+ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="],
@@ -1986,6 +2009,8 @@
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -2020,8 +2045,6 @@
"@jest/reporters/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
- "@jest/reporters/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
-
"@jest/source-map/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@jest/transform/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
@@ -2030,8 +2053,6 @@
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
- "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
"@opencode-ai/plugin/@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.133", "", {}, "sha512-kM+VJJ09SU51aruQ78DSy+6CjNc4wMytvGBrZ1IIJ8etUIdGA59wrnIOSxBVs4u/Gb9pjjgsF8sWp59UdLWP9w=="],
"@opencode-ai/plugin/zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
@@ -2054,6 +2075,10 @@
"@pantheon-org/opencode-notification/typescript": ["typescript@5.6.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw=="],
+ "@pantheon-org/opencode-skills/@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
+
+ "@pantheon-org/opencode-skills/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
"@pantheon-org/opencode-warcraft-notifications-plugin/bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"@swc-node/sourcemap-support/source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
@@ -2074,10 +2099,6 @@
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
- "cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
-
- "cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cosmiconfig/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
@@ -2102,7 +2123,7 @@
"front-matter/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
- "glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
+ "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
@@ -2112,20 +2133,14 @@
"istanbul-lib-source-maps/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
- "jest-config/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
-
"jest-runner/source-map-support": ["source-map-support@0.5.13", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w=="],
- "jest-runtime/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
-
"jest-runtime/strip-bom": ["strip-bom@4.0.0", "", {}, "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w=="],
"jest-util/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
- "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
-
"markdownlint-cli2/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -2140,8 +2155,6 @@
"parse-json/lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
- "path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
-
"postcss-load-config/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
@@ -2160,6 +2173,8 @@
"svgicons2svgfont/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
+ "svgicons2svgfont/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
+
"tcp-port-used/debug": ["debug@4.3.1", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ=="],
"test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -2180,6 +2195,8 @@
"write-file-atomic/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -2194,12 +2211,6 @@
"@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
- "@jest/reporters/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-
- "@jest/reporters/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
- "@jest/reporters/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"@oxc-resolver/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@pantheon-org/opencode-config/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
@@ -2234,42 +2245,32 @@
"@pantheon-org/opencode-notification/tsup/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
+ "@pantheon-org/opencode-skills/@types/bun/bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
+
"@swc-node/sourcemap-support/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@yarnpkg/parsers/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
- "cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-
- "cacache/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
- "cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"front-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
- "jest-config/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-
- "jest-config/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
- "jest-config/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"jest-runner/source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
- "jest-runtime/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
-
- "jest-runtime/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
-
- "jest-runtime/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
-
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-sized/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+ "svgicons2svgfont/glob/jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="],
+
+ "svgicons2svgfont/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="],
+
+ "svgicons2svgfont/glob/path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="],
+
"tcp-port-used/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"test-exclude/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -2320,8 +2321,6 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
- "@jest/reporters/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
"@pantheon-org/opencode-config/tsup/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.23.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ=="],
"@pantheon-org/opencode-config/tsup/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.23.1", "", { "os": "android", "cpu": "arm" }, "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ=="],
@@ -2418,9 +2417,7 @@
"@pantheon-org/opencode-notification/tsup/postcss-load-config/lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
- "jest-config/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
-
- "jest-runtime/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+ "svgicons2svgfont/glob/path-scurry/lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
}
diff --git a/packages/opencode-skills/.github/.release-please-manifest.json b/packages/opencode-skills/.github/.release-please-manifest.json
new file mode 100644
index 0000000..466df71
--- /dev/null
+++ b/packages/opencode-skills/.github/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.1.0"
+}
diff --git a/packages/opencode-skills/.github/actions/setup-bun/action.yml b/packages/opencode-skills/.github/actions/setup-bun/action.yml
new file mode 100644
index 0000000..1d53f5c
--- /dev/null
+++ b/packages/opencode-skills/.github/actions/setup-bun/action.yml
@@ -0,0 +1,37 @@
+name: 'Setup Bun with Caching'
+description: 'Sets up Bun with dependency caching for faster workflow runs'
+
+inputs:
+ bun-version:
+ description: 'Bun version to install'
+ required: false
+ default: 'latest'
+ frozen-lockfile:
+ description: 'Use frozen lockfile for installation'
+ required: false
+ default: 'true'
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
+ with:
+ bun-version: ${{ inputs.bun-version }}
+
+ - name: Cache dependencies
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+ with:
+ path: ~/.bun/install/cache
+ key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-bun-
+
+ - name: Install dependencies
+ shell: bash
+ run: |
+ if [ "${{ inputs.frozen-lockfile }}" = "true" ]; then
+ bun install --frozen-lockfile
+ else
+ bun install
+ fi
diff --git a/packages/opencode-skills/.github/actions/setup-node-npm/action.yml b/packages/opencode-skills/.github/actions/setup-node-npm/action.yml
new file mode 100644
index 0000000..ad809fa
--- /dev/null
+++ b/packages/opencode-skills/.github/actions/setup-node-npm/action.yml
@@ -0,0 +1,21 @@
+name: 'Setup Node.js for NPM'
+description: 'Sets up Node.js configured for npm registry publishing'
+
+inputs:
+ node-version:
+ description: 'Node.js version to install'
+ required: false
+ default: '20'
+ registry-url:
+ description: 'NPM registry URL'
+ required: false
+ default: 'https://registry.npmjs.org'
+
+runs:
+ using: 'composite'
+ steps:
+ - name: Setup Node.js for npm
+ uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
+ with:
+ node-version: ${{ inputs.node-version }}
+ registry-url: ${{ inputs.registry-url }}
diff --git a/packages/opencode-skills/.github/dependabot.yml b/packages/opencode-skills/.github/dependabot.yml
new file mode 100644
index 0000000..db9a033
--- /dev/null
+++ b/packages/opencode-skills/.github/dependabot.yml
@@ -0,0 +1,51 @@
+version: 2
+updates:
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
+ day: 'monday'
+ time: '09:00'
+ commit-message:
+ prefix: 'chore'
+ include: 'scope'
+ labels:
+ - 'dependencies'
+ - 'github-actions'
+ - 'security'
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: 'npm'
+ directory: '/'
+ schedule:
+ interval: 'weekly'
+ day: 'tuesday'
+ time: '09:00'
+ commit-message:
+ prefix: 'chore'
+ include: 'scope'
+ labels:
+ - 'dependencies'
+ - 'npm'
+ open-pull-requests-limit: 5
+ ignore:
+ - dependency-name: '*'
+ update-types: ['version-update:semver-major']
+
+ - package-ecosystem: 'npm'
+ directory: '/pages'
+ schedule:
+ interval: 'weekly'
+ day: 'tuesday'
+ time: '09:00'
+ commit-message:
+ prefix: 'chore'
+ include: 'scope'
+ labels:
+ - 'dependencies'
+ - 'npm'
+ - 'documentation'
+ open-pull-requests-limit: 5
+ ignore:
+ - dependency-name: '*'
+ update-types: ['version-update:semver-major']
diff --git a/packages/opencode-skills/.github/release-please-config.json b/packages/opencode-skills/.github/release-please-config.json
new file mode 100644
index 0000000..000e924
--- /dev/null
+++ b/packages/opencode-skills/.github/release-please-config.json
@@ -0,0 +1,35 @@
+{
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
+ "packages": {
+ ".": {
+ "release-type": "node",
+ "package-name": "@pantheon-org/opencode-skills",
+ "include-component-in-tag": false,
+ "changelog-sections": [
+ { "type": "feat", "section": "Features", "hidden": false },
+ { "type": "fix", "section": "Bug Fixes", "hidden": false },
+ {
+ "type": "perf",
+ "section": "Performance Improvements",
+ "hidden": false
+ },
+ { "type": "revert", "section": "Reverts", "hidden": false },
+ { "type": "docs", "section": "Documentation", "hidden": false },
+ { "type": "style", "section": "Code Style", "hidden": true },
+ { "type": "chore", "section": "Miscellaneous", "hidden": true },
+ { "type": "refactor", "section": "Code Refactoring", "hidden": false },
+ { "type": "test", "section": "Tests", "hidden": true },
+ { "type": "build", "section": "Build System", "hidden": true },
+ { "type": "ci", "section": "Continuous Integration", "hidden": true }
+ ],
+ "bump-minor-pre-major": true,
+ "bump-patch-for-minor-pre-major": false,
+ "draft": false,
+ "prerelease": false
+ }
+ },
+ "bootstrap-sha": "",
+ "skip-github-release": false,
+ "draft": false,
+ "prerelease": false
+}
diff --git a/packages/opencode-skills/.github/workflows/chores-pages.yml b/packages/opencode-skills/.github/workflows/chores-pages.yml
new file mode 100644
index 0000000..63fc3e6
--- /dev/null
+++ b/packages/opencode-skills/.github/workflows/chores-pages.yml
@@ -0,0 +1,78 @@
+name: Chores — GitHub Pages Configuration
+
+on:
+ schedule:
+ - cron: '0 0 * * *' # daily at 00:00 UTC
+ workflow_dispatch: {}
+
+permissions:
+ contents: read
+ pages: read
+ issues: write
+
+jobs:
+ pages-check:
+ name: Check GitHub Pages Configuration
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+
+ - name: Install jq (if missing)
+ run: |
+ sudo apt-get update -y
+ sudo apt-get install -y jq
+
+ - name: Check Pages configuration
+ id: pages_check
+ continue-on-error: true
+ run: |
+ set +e
+
+ # Get Pages info from GitHub API
+ PAGES_INFO=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
+ "https://api.github.com/repos/${{ github.repository }}/pages")
+
+ # Check if Pages is enabled
+ if echo "$PAGES_INFO" | jq -e '.html_url' > /dev/null 2>&1; then
+ echo "✅ GitHub Pages is configured"
+ echo "exit_code=0" >> $GITHUB_OUTPUT
+ exit 0
+ else
+ echo "❌ GitHub Pages is not configured or not accessible"
+ echo "exit_code=1" >> $GITHUB_OUTPUT
+ exit 1
+ fi
+
+ - name: Create GitHub issue on failure
+ if: steps.pages_check.outputs.exit_code != '0'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { owner, repo } = context.repo;
+ const label = 'chores/pages-check';
+ const title = `[chores] GitHub Pages misconfigured for ${owner}/${repo}`;
+ const body = `The scheduled GitHub Pages configuration check failed for **${owner}/${repo}**.\n\n` +
+ `Please verify the Pages source is configured correctly in repository settings.\n\n` +
+ `Workflow run: ${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
+
+ // Check for existing open issues with the label
+ const existing = await github.rest.issues.listForRepo({ owner, repo, state: 'open', labels: label });
+ if (existing.data && existing.data.length > 0) {
+ console.log('An open issue for Pages check already exists; skipping creation.');
+ } else {
+ // Create the issue
+ await github.rest.issues.create({ owner, repo, title, body, labels: [label] });
+ console.log('Created issue:', title);
+ }
+
+ - name: Final status
+ run: |
+ if [ "${{ steps.pages_check.outputs.exit_code }}" = "0" ]; then
+ echo "Pages check: OK"
+ else
+ echo "Pages check: FAILURE — issue created or already exists"
+ exit 1
+ fi
diff --git a/packages/opencode-skills/.github/workflows/deploy-docs.yml b/packages/opencode-skills/.github/workflows/deploy-docs.yml
new file mode 100644
index 0000000..3578b85
--- /dev/null
+++ b/packages/opencode-skills/.github/workflows/deploy-docs.yml
@@ -0,0 +1,97 @@
+name: Deploy Documentation
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'docs/**'
+ release:
+ types: [published]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ name: Build Documentation
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ with:
+ fetch-depth: 0
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2
+ with:
+ bun-version: 'latest'
+
+ - name: Clone docs-builder
+ run: |
+ git clone --depth 1 https://github.com/pantheon-org/opencode-docs-builder.git docs-builder
+ echo "✅ Docs builder cloned"
+
+ - name: Install docs-builder dependencies
+ working-directory: ./docs-builder
+ run: bun install --frozen-lockfile
+
+ - name: Install Playwright browsers
+ working-directory: ./docs-builder
+ run: |
+ bunx playwright install --with-deps chromium
+ echo "✅ Playwright browsers installed"
+
+ - name: Copy plugin documentation
+ run: |
+ cp -r ./docs/* ./docs-builder/src/content/docs/ || mkdir -p ./docs-builder/src/content/docs/
+ echo "✅ Plugin docs copied to docs-builder"
+
+ - name: Build documentation with Astro action
+ uses: withastro/action@v3
+ with:
+ path: ./docs-builder
+ package-manager: bun@latest
+
+ - name: Verify internal links
+ working-directory: ./docs-builder
+ run: |
+ bun run verify
+ echo "✅ All internal links verified"
+
+ deploy:
+ name: Deploy to GitHub Pages
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+
+ - name: Summary
+ run: |
+ echo "## 🎉 Documentation Deployment Complete!" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Deployment Details" >> $GITHUB_STEP_SUMMARY
+ echo "- **URL**: ${{ steps.deployment.outputs.page_url }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Commit**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "- **Trigger**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Build Process" >> $GITHUB_STEP_SUMMARY
+ echo "1. 📥 Clone docs-builder from \`pantheon-org/opencode-docs-builder\`" >> $GITHUB_STEP_SUMMARY
+ echo "2. 📄 Copy plugin docs from \`./docs/\`" >> $GITHUB_STEP_SUMMARY
+ echo "3. 🔄 Transform to Astro content" >> $GITHUB_STEP_SUMMARY
+ echo "4. 🏗️ Build Astro site" >> $GITHUB_STEP_SUMMARY
+ echo "5. 🔗 Fix and verify internal links" >> $GITHUB_STEP_SUMMARY
+ echo "6. 🚀 Deploy to GitHub Pages via Actions" >> $GITHUB_STEP_SUMMARY
diff --git a/packages/opencode-skills/.github/workflows/publish-on-tag.yml b/packages/opencode-skills/.github/workflows/publish-on-tag.yml
new file mode 100644
index 0000000..f092a04
--- /dev/null
+++ b/packages/opencode-skills/.github/workflows/publish-on-tag.yml
@@ -0,0 +1,204 @@
+name: Publish on Tag (Manual Release)
+
+on:
+ push:
+ tags:
+ - 'v*'
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Tag to publish (e.g., v1.2.3)'
+ required: true
+ type: string
+
+permissions:
+ contents: write
+ id-token: write
+ pages: write
+
+jobs:
+ # Extract version from tag
+ prepare:
+ name: Prepare Release
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ tag: ${{ steps.version.outputs.tag }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ with:
+ fetch-depth: 0
+
+ - name: Get version from tag
+ id: version
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ TAG="${{ github.event.inputs.tag }}"
+ else
+ TAG=${GITHUB_REF#refs/tags/}
+ fi
+
+ # Strip 'v' prefix (v2.0.0 -> 2.0.0)
+ VERSION=${TAG#v}
+ # Also handle Release Please format (opencode-skills-plugin-v2.0.0 -> 2.0.0)
+ VERSION=${VERSION##*-v}
+
+ echo "tag=$TAG" >> $GITHUB_OUTPUT
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "📦 Publishing version: $VERSION from tag: $TAG"
+
+ - name: Verify package.json matches tag
+ run: |
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
+ TAG_VERSION="${{ steps.version.outputs.version }}"
+
+ if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
+ echo "❌ Version mismatch!"
+ echo " package.json: $PACKAGE_VERSION"
+ echo " tag: $TAG_VERSION"
+ exit 1
+ fi
+
+ echo "✅ Version verified: $PACKAGE_VERSION"
+
+ # Publish to npm
+ publish-npm:
+ name: Publish to npm
+ needs: prepare
+ uses: ./.github/workflows/reusable/reusable-npm-publish.yml
+ with:
+ version: ${{ needs.prepare.outputs.version }}
+ npm-scope: 'pantheon-org'
+ project-name: 'opencode-skills-plugin'
+ secrets:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ # Deploy documentation
+ publish-docs:
+ name: Deploy Documentation
+ runs-on: ubuntu-latest
+ needs: publish-npm
+ if: success()
+ uses: ./.github/workflows/reusable/reusable-deploy-docs.yml
+ with:
+ commit-sha: ${{ github.sha }}
+ secrets:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ # Create GitHub Release
+ create-release:
+ name: Create GitHub Release
+ runs-on: ubuntu-latest
+ needs: [prepare, publish-npm, publish-docs]
+ if: success()
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ with:
+ fetch-depth: 0
+
+ - name: Get commits for changelog
+ id: changelog
+ run: |
+ TAG="${{ needs.prepare.outputs.tag }}"
+
+ # Get previous tag
+ PREV_TAG=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || echo "")
+
+ if [ -z "$PREV_TAG" ]; then
+ COMMITS=$(git log --oneline --no-merges | head -20)
+ else
+ COMMITS=$(git log ${PREV_TAG}..${TAG} --oneline --no-merges)
+ fi
+
+ # Save to file
+ echo "$COMMITS" > commits.txt
+
+ echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT
+
+ - name: Create GitHub Release
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
+ with:
+ script: |
+ const fs = require('fs');
+ const tag = '${{ needs.prepare.outputs.tag }}';
+ const version = '${{ needs.prepare.outputs.version }}';
+ const prevTag = '${{ steps.changelog.outputs.prev_tag }}';
+
+ // Read commits
+ const commits = fs.readFileSync('commits.txt', 'utf8')
+ .split('\n')
+ .filter(line => line.trim())
+ .map(line => `- ${line}`)
+ .join('\n');
+
+ const npmPackageName = require('./package.json').name;
+ const npmUrl = `https://www.npmjs.com/package/${npmPackageName}/v/${version}`;
+ const docsUrl = `https://github.com/${{ github.repository }}/tree/docs`;
+
+ const changelogHeader = prevTag
+ ? `## Changes Since ${prevTag}`
+ : `## Initial Release`;
+
+ const body = `# Release ${tag}
+
+ ${changelogHeader}
+
+ ${commits}
+
+ ## 📦 Package Information
+
+ - **npm Package**: [\`${npmPackageName}@${version}\`](${npmUrl})
+ - **Installation**: \`npm install ${npmPackageName}@${version}\`
+ - **Documentation**: [View Docs](${docsUrl})
+
+ ## 🔧 Build Information
+
+ - **Node.js**: 20.x
+ - **Runtime**: Bun
+ - **Build Status**: ✅ All tests passed
+ - **Coverage**: ✅ Full coverage
+
+ ---
+
+ *This release was automatically created by the Publish on Tag workflow.*
+ `;
+
+ await github.rest.repos.createRelease({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag_name: tag,
+ name: `Release ${tag}`,
+ body: body,
+ draft: false,
+ prerelease: false
+ });
+
+ console.log(`✅ Created release for ${tag}`);
+
+ # Summary job
+ summary:
+ name: Publish Summary
+ runs-on: ubuntu-latest
+ needs: [prepare, publish-npm, publish-docs, create-release]
+ if: always()
+
+ steps:
+ - name: Display summary
+ run: |
+ echo "🎉 Release Pipeline Complete!"
+ echo ""
+ echo "📦 Version: ${{ needs.prepare.outputs.version }}"
+ echo "🏷️ Tag: ${{ needs.prepare.outputs.tag }}"
+ echo ""
+ echo "✅ npm: ${{ needs.publish-npm.result }}"
+ echo "✅ Docs: ${{ needs.publish-docs.result }}"
+ echo "✅ Release: ${{ needs.create-release.result }}"
+ echo ""
+ echo "🔗 Links:"
+ echo " 📦 npm: https://www.npmjs.com/package/@pantheon-org/opencode-skills-plugin/v/${{ needs.prepare.outputs.version }}"
+ echo " 📚 Docs: https://github.com/${{ github.repository }}/tree/docs"
+ echo " 🏷️ Release: https://github.com/${{ github.repository }}/releases/tag/${{ needs.prepare.outputs.tag }}"
diff --git a/packages/opencode-skills/.github/workflows/release-and-publish.yml b/packages/opencode-skills/.github/workflows/release-and-publish.yml
new file mode 100644
index 0000000..5f469b7
--- /dev/null
+++ b/packages/opencode-skills/.github/workflows/release-and-publish.yml
@@ -0,0 +1,86 @@
+name: Release & Publish (Automated)
+
+on:
+ push:
+ branches:
+ - main
+ paths-ignore:
+ - 'docs/**'
+ - '*.md'
+ - '.github/workflows/chores-*.yml'
+ - '.github/workflows/deploy-docs.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+concurrency:
+ group: release-please-${{ github.ref }}
+ cancel-in-progress: false
+
+jobs:
+ release-please:
+ name: Release Please
+ runs-on: ubuntu-latest
+
+ outputs:
+ release_created: ${{ steps.release.outputs.release_created }}
+ tag_name: ${{ steps.release.outputs.tag_name }}
+ version: ${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}
+ pr: ${{ steps.release.outputs.pr }}
+
+ steps:
+ - name: Run Release Please
+ id: release
+ uses: googleapis/release-please-action@7987652d64b4581673a76e33ad5e98e3dd56832f # v4.1.3
+ with:
+ token: ${{ secrets.WORKFLOW_PAT || secrets.GITHUB_TOKEN }}
+ config-file: .github/release-please-config.json
+ manifest-file: .github/.release-please-manifest.json
+
+ - name: Summary - Release Created
+ if: steps.release.outputs.release_created == 'true'
+ run: |
+ echo "## 🎉 Release Created" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Version**: ${{ steps.release.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Release URL**: ${{ steps.release.outputs.html_url }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Upload URL**: ${{ steps.release.outputs.upload_url }}" >> $GITHUB_STEP_SUMMARY
+
+ - name: Summary - Release PR
+ if: steps.release.outputs.pr != ''
+ run: |
+ echo "## 📝 Release PR Updated" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Release Please has updated the release PR" >> $GITHUB_STEP_SUMMARY
+ echo "- **PR Number**: ${{ steps.release.outputs.prs_created }}" >> $GITHUB_STEP_SUMMARY
+
+ - name: Summary - No Changes
+ if: steps.release.outputs.release_created != 'true' && steps.release.outputs.pr == ''
+ run: |
+ echo "## ℹ️ No Release Changes" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "No conventional commits found that require a release." >> $GITHUB_STEP_SUMMARY
+
+ publish:
+ name: Publish to NPM
+ needs: release-please
+ if: needs.release-please.outputs.release_created == 'true'
+ uses: ./.github/workflows/reusable/reusable-npm-publish.yml
+ with:
+ version: ${{ needs.release-please.outputs.version }}
+ npm-scope: 'pantheon-org'
+ project-name: 'opencode-skills-plugin'
+ secrets:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ publish-docs:
+ name: Deploy Documentation
+ needs: [release-please, publish]
+ if: needs.release-please.outputs.release_created == 'true' && success()
+ uses: ./.github/workflows/reusable/reusable-deploy-docs.yml
+ with:
+ commit-sha: ${{ github.sha }}
+ secrets:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/packages/opencode-skills/.github/workflows/reusable/reusable-deploy-docs.yml b/packages/opencode-skills/.github/workflows/reusable/reusable-deploy-docs.yml
new file mode 100644
index 0000000..3cd09d8
--- /dev/null
+++ b/packages/opencode-skills/.github/workflows/reusable/reusable-deploy-docs.yml
@@ -0,0 +1,68 @@
+name: Reusable Deploy Documentation
+
+on:
+ workflow_call:
+ inputs:
+ commit-sha:
+ description: 'Git commit SHA for deployment message'
+ required: false
+ type: string
+ default: ''
+ secrets:
+ GITHUB_TOKEN:
+ description: 'GitHub token for deployment'
+ required: true
+
+jobs:
+ deploy-docs:
+ name: Deploy Documentation
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ with:
+ fetch-depth: 0
+
+ - name: Setup Bun with caching
+ uses: ./.github/actions/setup-bun
+
+ - name: Clone docs-builder
+ run: |
+ git clone --depth 1 https://github.com/pantheon-org/opencode-docs-builder.git docs-builder
+ echo "✅ Docs builder cloned"
+
+ - name: Install docs-builder dependencies
+ run: |
+ cd docs-builder
+ bun install
+
+ - name: Copy plugin documentation
+ run: |
+ cp -r ./docs/* ./docs-builder/src/content/docs/ || mkdir -p ./docs-builder/src/content/docs/
+ echo "✅ Plugin docs copied to docs-builder"
+
+ - name: Build documentation
+ run: |
+ cd docs-builder
+ bun run build
+
+ - name: Deploy to docs branch
+ uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_branch: docs
+ publish_dir: ./docs-builder/dist
+ force_orphan: false
+ enable_jekyll: false
+ commit_message: 'docs: Deploy documentation from ${{ inputs.commit-sha || github.sha }}'
+ user_name: 'github-actions[bot]'
+ user_email: 'github-actions[bot]@users.noreply.github.com'
+
+ - name: Summary
+ run: |
+ echo "## ✅ Documentation Deployed" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Branch**: docs" >> $GITHUB_STEP_SUMMARY
+ echo "- **Commit**: ${{ inputs.commit-sha || github.sha }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **URL**: https://github.com/${{ github.repository }}/tree/docs" >> $GITHUB_STEP_SUMMARY
diff --git a/packages/opencode-skills/.github/workflows/reusable/reusable-npm-publish.yml b/packages/opencode-skills/.github/workflows/reusable/reusable-npm-publish.yml
new file mode 100644
index 0000000..9df9761
--- /dev/null
+++ b/packages/opencode-skills/.github/workflows/reusable/reusable-npm-publish.yml
@@ -0,0 +1,171 @@
+name: Reusable NPM Publish Pipeline
+
+on:
+ workflow_call:
+ inputs:
+ version:
+ description: 'Version to publish (e.g., 1.2.3 without v prefix)'
+ required: true
+ type: string
+ npm-scope:
+ description: 'NPM organization scope (e.g., pantheon-org)'
+ required: true
+ type: string
+ project-name:
+ description: 'Project name for display purposes'
+ required: true
+ type: string
+ secrets:
+ NPM_TOKEN:
+ description: 'NPM authentication token'
+ required: true
+ outputs:
+ published:
+ description: 'Whether package was published (true/false)'
+ value: ${{ jobs.publish-npm.outputs.published }}
+ package-url:
+ description: 'URL to published package on npm'
+ value: ${{ jobs.publish-npm.outputs.package-url }}
+
+jobs:
+ publish-npm:
+ name: Publish to npm
+ runs-on: ubuntu-latest
+
+ outputs:
+ published: ${{ steps.publish.outputs.published }}
+ package-url: ${{ steps.publish.outputs.package-url }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
+ with:
+ fetch-depth: 0
+
+ - name: Setup Bun with caching
+ uses: ./.github/actions/setup-bun
+
+ - name: Setup Node.js for npm
+ uses: ./.github/actions/setup-node-npm
+
+ - name: Run full validation
+ run: |
+ echo "🔍 Running linter..."
+ bun run lint
+
+ echo "📝 Type checking..."
+ bun run type-check
+
+ echo "🧪 Running tests..."
+ bun run test:coverage
+
+ echo "🏗️ Building project..."
+ bun run build
+
+ - name: Check if already published
+ id: check-npm
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: |
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
+ VERSION="${{ inputs.version }}"
+
+ echo "🔍 Checking if $PACKAGE_NAME@$VERSION exists on npm..."
+
+ # Try to check if version exists (capture both stdout and stderr)
+ NPM_CHECK=$(npm view "$PACKAGE_NAME@$VERSION" version 2>&1 || true)
+
+ if echo "$NPM_CHECK" | grep -q "E404"; then
+ echo "✅ Version $VERSION not yet published (404 - not found)"
+ echo "published=false" >> $GITHUB_OUTPUT
+ elif echo "$NPM_CHECK" | grep -q "$VERSION"; then
+ echo "⚠️ Version $VERSION already published to npm"
+ echo "published=true" >> $GITHUB_OUTPUT
+ else
+ echo "⚠️ Could not determine publication status, assuming not published"
+ echo "published=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Publish to npm
+ id: publish
+ if: steps.check-npm.outputs.published == 'false'
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: |
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
+ VERSION="${{ inputs.version }}"
+
+ echo "🚀 Publishing $PACKAGE_NAME@$VERSION to npm..."
+
+ if [ -z "$NODE_AUTH_TOKEN" ]; then
+ echo "❌ Error: NPM_TOKEN secret is not set!"
+ echo "Please configure npm authentication:"
+ echo " Option 1 (Recommended): Set up npm provenance with OIDC"
+ echo " Option 2: Add NPM_TOKEN secret with granular access token"
+ echo "See: https://docs.npmjs.com/generating-provenance-statements"
+ exit 1
+ fi
+
+ # Attempt to publish with detailed error handling
+ if npm publish --access public --provenance; then
+ echo "✅ Successfully published $PACKAGE_NAME@$VERSION to npm"
+ echo "published=true" >> $GITHUB_OUTPUT
+ echo "package-url=https://www.npmjs.com/package/$PACKAGE_NAME/v/$VERSION" >> $GITHUB_OUTPUT
+ else
+ NPM_EXIT_CODE=$?
+ echo "❌ npm publish failed with exit code: $NPM_EXIT_CODE"
+ echo ""
+ echo "Common causes:"
+ echo " - Expired or invalid NPM_TOKEN"
+ echo " - Token lacks publish permissions for @${{ inputs.npm-scope }} organization"
+ echo " - Package name already published with this version"
+ echo ""
+ echo "To fix: Update NPM_TOKEN with a granular access token that has"
+ echo " 'Read and write' permissions for this package"
+ exit $NPM_EXIT_CODE
+ fi
+
+ - name: Verify npm publication
+ if: steps.check-npm.outputs.published == 'false'
+ run: |
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
+ VERSION="${{ inputs.version }}"
+
+ echo "🔍 Verifying npm publication..."
+
+ for i in {1..6}; do
+ echo "Attempt $i/6..."
+ if npm view "$PACKAGE_NAME@$VERSION" version >/dev/null 2>&1; then
+ echo "✅ Package verified on npm: $PACKAGE_NAME@$VERSION"
+ exit 0
+ fi
+
+ if [ $i -lt 6 ]; then
+ echo "Waiting 15 seconds..."
+ sleep 15
+ fi
+ done
+
+ echo "❌ Failed to verify package (may still be propagating)"
+ exit 1
+
+ - name: Summary
+ if: always()
+ run: |
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
+
+ if [ "${{ steps.check-npm.outputs.published }}" = "true" ]; then
+ echo "## ℹ️ Already Published" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Version ${{ inputs.version }} was already published to npm" >> $GITHUB_STEP_SUMMARY
+ elif [ "${{ steps.publish.outputs.published }}" = "true" ]; then
+ echo "## ✅ Published to NPM" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Package**: $PACKAGE_NAME" >> $GITHUB_STEP_SUMMARY
+ echo "- **Version**: ${{ inputs.version }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **URL**: ${{ steps.publish.outputs.package-url }}" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "## ❌ Publication Failed" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Check the logs above for details" >> $GITHUB_STEP_SUMMARY
+ fi
diff --git a/packages/opencode-skills/LICENSE b/packages/opencode-skills/LICENSE
new file mode 100644
index 0000000..f334655
--- /dev/null
+++ b/packages/opencode-skills/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 pantheon-org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/opencode-skills/README.md b/packages/opencode-skills/README.md
new file mode 100644
index 0000000..d06cc5d
--- /dev/null
+++ b/packages/opencode-skills/README.md
@@ -0,0 +1,598 @@
+# @pantheon-org/opencode-skills
+
+TypeScript-based skill injection plugin for OpenCode with smart pattern matching and auto-injection capabilities.
+
+> **Note**: This plugin is part of the `pantheon-org/opencode-plugins` monorepo. All development and contributions
+> should be made in the main repository at: **https://github.com/pantheon-org/opencode-plugins**
+>
+> If you're viewing this as a mirror repository, it is read-only. Submit issues, PRs, and contributions to the main
+> monorepo.
+
+
+
+
+## Overview
+
+This plugin provides a seamless way to inject reusable knowledge and guidance (skills) into OpenCode chat sessions.
+Skills are automatically detected and injected based on user intent using smart pattern matching.
+
+**Key Features:**
+
+- **Type-safe skill definitions** - Skills defined in TypeScript with full type safety
+- **Smart pattern matching** - Intent detection, negation handling, and keyword matching
+- **Auto-injection** - Skills seamlessly injected via `chat.message` hook
+- **Zero file system side effects** - No SKILL.md files, everything in TypeScript
+- **Highly configurable** - Customize pattern matching behavior per your needs
+- **Comprehensive testing** - Full test coverage for pattern matching logic
+
+## Installation
+
+```bash
+bun add @pantheon-org/opencode-skills
+```
+
+## Quick Start
+
+### 1. Define Your Skills
+
+```typescript
+import { defineSkill, createSkillsPlugin } from '@pantheon-org/opencode-skills';
+
+// Define a skill
+const mySkill = defineSkill({
+ name: 'typescript-tdd',
+ description: 'TypeScript development with TDD',
+ keywords: ['tdd', 'test-driven', 'testing'],
+ content: `
+# TypeScript TDD Development
+
+Follow these guidelines...
+ `,
+});
+
+// Create skill registry
+const skills = {
+ 'typescript-tdd': mySkill,
+};
+
+// Create plugin
+export const MySkillsPlugin = createSkillsPlugin(skills);
+```
+
+### 2. Add to OpenCode Configuration
+
+In your `opencode.json`:
+
+```json
+{
+ "plugin": ["file:///path/to/your/plugin.ts"]
+}
+```
+
+### 3. Use Skills Naturally
+
+Simply mention the skill in your message:
+
+```
+User: "Let's use typescript-tdd for this component"
+```
+
+The plugin automatically detects intent and injects the skill content into the chat context.
+
+## How It Works
+
+### Pattern Matching
+
+The plugin uses three strategies to detect when a skill should be injected:
+
+#### 1. Word Boundary Matching
+
+Exact skill name with word boundaries:
+
+```typescript
+'use typescript-tdd approach'; // ✅ Matches
+'typescript-tdd-extended'; // ❌ Won't match (different skill)
+```
+
+#### 2. Intent Detection
+
+Intent keywords that signal user wants to use the skill:
+
+- `use`, `apply`, `follow`, `implement`, `load`, `get`, `show`, `with`
+
+```typescript
+'apply typescript-tdd principles'; // ✅ Matches
+'follow the typescript-tdd guide'; // ✅ Matches
+'typescript-tdd approach'; // ✅ Matches
+```
+
+#### 3. Negation Detection
+
+Prevents injection when user explicitly avoids a skill:
+
+- `don't`, `do not`, `avoid`, `skip`, `ignore`, `without`, `except`, `excluding`
+
+```typescript
+"don't use typescript-tdd"; // ❌ Won't inject
+'implement without typescript-tdd'; // ❌ Won't inject
+'avoid typescript-tdd patterns'; // ❌ Won't inject
+```
+
+### Keyword Enhancement
+
+Add optional keywords to improve detection:
+
+```typescript
+const mySkill = defineSkill({
+ name: 'typescript-tdd',
+ description: 'TypeScript TDD',
+ keywords: ['TDD', 'test-driven', 'bun'], // Additional matching keywords
+ content: '...',
+});
+```
+
+Now these also trigger injection:
+
+```typescript
+'write tests using TDD'; // ✅ Matches via keyword
+'use test-driven development'; // ✅ Matches via keyword
+```
+
+## API Reference
+
+### `createSkillsPlugin(skills, config?)`
+
+Creates an OpenCode plugin with the provided skills.
+
+**Parameters:**
+
+- `skills` - Record of skill name to Skill object
+- `config` (optional) - Plugin configuration options
+
+**Returns:** Plugin function for OpenCode
+
+**Example:**
+
+```typescript
+import { createSkillsPlugin } from '@pantheon-org/opencode-skills';
+
+export const MyPlugin = createSkillsPlugin(
+ {
+ 'my-skill': mySkill,
+ },
+ {
+ debug: true,
+ autoInject: true,
+ },
+);
+```
+
+### `defineSkill(skill)`
+
+Creates a skill object with defaults.
+
+**Parameters:**
+
+- `skill.name` (required) - Unique skill identifier (kebab-case)
+- `skill.description` (required) - Brief description
+- `skill.content` (required) - Full skill content (markdown)
+- `skill.keywords` (optional) - Additional keywords for pattern matching
+- `skill.version` (optional) - Skill version (default: '1.0.0')
+- `skill.category` (optional) - Skill category for organization
+- `skill.dependencies` (optional) - Other skill names this depends on
+
+**Returns:** Complete Skill object
+
+**Example:**
+
+```typescript
+import { defineSkill } from '@pantheon-org/opencode-skills';
+
+const skill = defineSkill({
+ name: 'react-patterns',
+ description: 'Modern React patterns',
+ keywords: ['react', 'hooks', 'components'],
+ category: 'development',
+ content: `
+# React Patterns
+
+Best practices for React development...
+ `,
+});
+```
+
+### Configuration Options
+
+```typescript
+interface SkillsPluginConfig {
+ // Enable/disable auto-injection (default: true)
+ autoInject?: boolean;
+
+ // Enable debug logging (default: false)
+ debug?: boolean;
+
+ // Pattern matching configuration
+ patternMatching?: {
+ // Word boundary matching (default: true)
+ wordBoundary?: boolean;
+
+ // Intent detection (default: true)
+ intentDetection?: boolean;
+
+ // Negation detection (default: true)
+ negationDetection?: boolean;
+
+ // Custom intent keywords (adds to defaults)
+ customIntentKeywords?: string[];
+
+ // Custom negation keywords (adds to defaults)
+ customNegationKeywords?: string[];
+ };
+
+ // BM25 relevance scoring configuration
+ bm25?: {
+ // Enable BM25 ranking (default: false)
+ enabled?: boolean;
+
+ // Term frequency saturation (default: 1.5, range: 1.2-2.0)
+ k1?: number;
+
+ // Length normalization (default: 0.75, range: 0-1)
+ b?: number;
+
+ // Minimum score threshold (default: 0.0)
+ threshold?: number;
+
+ // Max skills to inject per message (default: 3)
+ maxSkills?: number;
+ };
+}
+```
+
+**Example with custom configuration:**
+
+```typescript
+export const MyPlugin = createSkillsPlugin(skills, {
+ debug: true,
+ autoInject: true,
+ patternMatching: {
+ customIntentKeywords: ['leverage', 'adopt'],
+ customNegationKeywords: ['exclude'],
+ },
+});
+```
+
+## Example Skills
+
+The package includes example skills for reference:
+
+```typescript
+import { exampleSkills } from '@pantheon-org/opencode-skills/examples';
+
+// Available examples:
+// - typescript-tdd: TypeScript development with TDD
+// - plain-english: Writing for non-technical stakeholders
+// - react-patterns: Modern React component patterns
+
+export const MyPlugin = createSkillsPlugin(exampleSkills);
+```
+
+## Advanced Usage
+
+### BM25 Relevance Ranking
+
+Enable BM25 (Best Matching 25) probabilistic ranking for more sophisticated skill selection based on relevance scoring:
+
+```typescript
+export const MyPlugin = createSkillsPlugin(skills, {
+ bm25: {
+ enabled: true, // Enable BM25 ranking
+ k1: 1.5, // Term frequency saturation (default: 1.5)
+ b: 0.75, // Length normalization (default: 0.75)
+ threshold: 0.5, // Minimum score for injection (default: 0.0)
+ maxSkills: 3, // Max skills per message (default: 3)
+ },
+});
+```
+
+**How BM25 Works:**
+
+BM25 ranks skills by relevance to the user's message using:
+
+- **Term Frequency (TF)**: How often query terms appear in skill content
+- **Inverse Document Frequency (IDF)**: How unique/rare terms are across skills
+- **Length Normalization**: Adjusts for varying skill content lengths
+
+**BM25 vs Pattern Matching:**
+
+| Feature | Pattern Matching | BM25 Ranking |
+| -------------------- | ------------------------- | ---------------------- |
+| **Detection** | Exact name + intent words | Relevance scoring |
+| **Ranking** | No ranking | Scores all skills |
+| **Multiple Skills** | All matches injected | Top N by relevance |
+| **Content Analysis** | Limited | Full content analysis |
+| **False Positives** | Lower risk | Configurable threshold |
+| **Best For** | Explicit mentions | Semantic relevance |
+
+**Hybrid Mode:**
+
+When BM25 is enabled, the plugin uses a hybrid approach:
+
+1. BM25 ranks all skills by relevance
+2. Top N candidates are selected (based on `maxSkills`)
+3. Pattern matching filters out negated skills
+4. Remaining skills are injected
+
+**Example:**
+
+```typescript
+// User message: "help me write tests for React components"
+// BM25 will rank:
+// 1. typescript-tdd (high relevance: "tests", "write")
+// 2. react-patterns (high relevance: "react", "components")
+// 3. plain-english (low relevance)
+// Result: Injects typescript-tdd and react-patterns
+```
+
+**Configuration Tips:**
+
+- **k1 (1.2-2.0)**: Higher values = more weight on term frequency
+- **b (0-1)**: Higher values = more length normalization (0 = no normalization)
+- **threshold**: Set higher to reduce false positives
+- **maxSkills**: Balance between context size and relevance
+
+### Custom Pattern Matching
+
+Disable certain pattern matching features:
+
+```typescript
+export const MyPlugin = createSkillsPlugin(skills, {
+ patternMatching: {
+ wordBoundary: true,
+ intentDetection: true,
+ negationDetection: false, // Allow injection even with negation
+ },
+});
+```
+
+### Debug Mode
+
+Enable debug logging to see when skills are injected:
+
+```typescript
+export const MyPlugin = createSkillsPlugin(skills, {
+ debug: true,
+});
+```
+
+Console output:
+
+```
+[opencode-skills] Plugin loaded with 3 skills
+[opencode-skills] Skills: typescript-tdd, plain-english, react-patterns
+[opencode-skills] Auto-injected skill "typescript-tdd" in session abc123
+[opencode-skills] Matched pattern: intent-before:use
+```
+
+### Manual Skill Detection
+
+Use the pattern matching utilities directly:
+
+```typescript
+import { hasIntentToUse, findMatchingSkills } from '@pantheon-org/opencode-skills';
+
+// Check if content matches a skill
+const result = hasIntentToUse('use typescript-tdd approach', 'typescript-tdd');
+console.log(result.matches); // true
+console.log(result.matchedPattern); // 'intent-before:use'
+console.log(result.hasNegation); // false
+
+// Find all matching skills
+const matches = findMatchingSkills('use typescript-tdd and plain-english', [
+ 'typescript-tdd',
+ 'plain-english',
+ 'react-patterns',
+]);
+console.log(matches); // ['typescript-tdd', 'plain-english']
+```
+
+### Manual BM25 Ranking
+
+Use BM25 utilities directly for custom ranking logic:
+
+```typescript
+import { buildBM25Index, rankSkillsByBM25, getTopSkillsByBM25 } from '@pantheon-org/opencode-skills';
+
+// Build BM25 index from skills
+const skillsMap = new Map([
+ ['typescript-tdd', 'TypeScript development with TDD...'],
+ ['plain-english', 'Writing for non-technical stakeholders...'],
+ ['react-patterns', 'Modern React patterns...'],
+]);
+
+const index = buildBM25Index(skillsMap);
+
+// Rank all skills by relevance
+const query = 'help me write React tests';
+const ranked = rankSkillsByBM25(query, ['typescript-tdd', 'plain-english', 'react-patterns'], index);
+console.log(ranked);
+// [
+// ['typescript-tdd', 2.45],
+// ['react-patterns', 1.87],
+// ['plain-english', 0.23]
+// ]
+
+// Get top N skills
+const topSkills = getTopSkillsByBM25(query, ['typescript-tdd', 'plain-english', 'react-patterns'], index, 2);
+console.log(topSkills); // ['typescript-tdd', 'react-patterns']
+
+// With custom configuration
+const customRanked = rankSkillsByBM25(query, skillNames, index, {
+ k1: 2.0, // Higher term frequency weight
+ b: 0.5, // Lower length normalization
+ threshold: 1.0, // Only skills scoring above 1.0
+});
+```
+
+## Best Practices
+
+### 1. Skill Naming
+
+Use kebab-case for skill names:
+
+```typescript
+✅ 'typescript-tdd'
+✅ 'plain-english'
+✅ 'react-patterns'
+
+❌ 'TypeScript_TDD'
+❌ 'Plain English'
+```
+
+### 2. Skill Content
+
+Structure skill content with clear headings and examples:
+
+```typescript
+const skill = defineSkill({
+ name: 'my-skill',
+ description: 'Brief one-liner',
+ content: `
+# Main Title
+
+## Overview
+Brief introduction
+
+## Guidelines
+1. First guideline
+2. Second guideline
+
+## Examples
+\`\`\`typescript
+// Code example
+\`\`\`
+
+## Best Practices
+- Practice 1
+- Practice 2
+ `,
+});
+```
+
+### 3. Keywords
+
+Add keywords that users naturally use:
+
+```typescript
+const skill = defineSkill({
+ name: 'typescript-tdd',
+ description: 'TypeScript TDD',
+ keywords: [
+ 'tdd', // Abbreviation
+ 'test-driven', // Alternative phrasing
+ 'testing', // Related concept
+ 'bun', // Related tool
+ ],
+ content: '...',
+});
+```
+
+### 4. Avoid Over-Injection
+
+Be specific with skill names to avoid unwanted matches:
+
+```typescript
+// Too generic - might match too often
+❌ 'typescript'
+❌ 'development'
+
+// Specific enough to avoid false positives
+✅ 'typescript-tdd'
+✅ 'typescript-strict-mode'
+```
+
+## Comparison with Other Approaches
+
+### vs File-Based Skills (SKILL.md)
+
+| Feature | opencode-skills | File-Based |
+| ------------------- | -------------------- | ----------------- |
+| **Type Safety** | ✅ Full TypeScript | ❌ No types |
+| **No File I/O** | ✅ TypeScript only | ❌ Read files |
+| **Version Control** | ✅ Git-friendly | ⚠️ Separate files |
+| **IDE Support** | ✅ Full autocomplete | ❌ Limited |
+| **Testing** | ✅ Easy to test | ⚠️ Harder |
+| **Auto-Injection** | ✅ Built-in | ⚠️ Manual tool |
+
+### vs Custom Tool Approach
+
+| Feature | opencode-skills | Custom Tool |
+| -------------------- | ------------------ | -------------------- |
+| **User Experience** | ✅ Automatic | ❌ Manual invocation |
+| **Intent Detection** | ✅ Smart matching | ❌ Exact match |
+| **Configuration** | ✅ Highly flexible | ⚠️ Limited |
+| **Setup Complexity** | ✅ Simple API | ⚠️ More boilerplate |
+
+## Troubleshooting
+
+### Skills Not Injecting
+
+1. **Check skill name matches** - Ensure you're using the exact skill name
+2. **Enable debug mode** - See what patterns are matching
+3. **Check negation** - Make sure you're not using negation keywords
+4. **Verify configuration** - Ensure `autoInject: true` (default)
+
+```typescript
+// Debug
+export const MyPlugin = createSkillsPlugin(skills, { debug: true });
+```
+
+### False Positives
+
+If skills inject too often:
+
+1. **Use more specific names** - Avoid generic terms
+2. **Disable word boundary** - Require intent keywords
+3. **Add negation detection** - Already enabled by default
+
+```typescript
+export const MyPlugin = createSkillsPlugin(skills, {
+ patternMatching: {
+ wordBoundary: false, // Require intent keywords
+ },
+});
+```
+
+## Development
+
+### Running Tests
+
+```bash
+bun test
+```
+
+### Type Checking
+
+```bash
+bun run type-check
+```
+
+### Linting
+
+```bash
+bun run lint
+```
+
+## License
+
+MIT
+
+## Contributing
+
+Contributions welcome! Please read our [Contributing Guide](./docs/development.md) for details.
+
+## Related
+
+- [OpenCode Documentation](https://opencode.ai/docs)
+- [OpenCode Plugin Development Guide](https://opencode.ai/docs/plugins)
+- [TypeScript Handbook](https://www.typescriptlang.org/docs/)
diff --git a/packages/opencode-skills/docs/README.md b/packages/opencode-skills/docs/README.md
new file mode 100644
index 0000000..6f76692
--- /dev/null
+++ b/packages/opencode-skills/docs/README.md
@@ -0,0 +1,39 @@
+# Skills
+
+TypeScript-based skill injection plugin for OpenCode
+
+## Installation
+
+\`\`\`bash bun add @pantheon-org/opencode-skills \`\`\`
+
+## Usage
+
+Add the plugin to your `opencode.json`:
+
+\`\`\`json { "plugin": ["@pantheon-org/opencode-skills"] } \`\`\`
+
+## Features
+
+- Feature 1
+- Feature 2
+- Feature 3
+
+## Configuration
+
+Describe plugin configuration options here.
+
+## API
+
+Document your plugin's API here.
+
+## Development
+
+See the [Development Guide](./development.md) for information on contributing to this plugin.
+
+## Troubleshooting
+
+See the [Troubleshooting Guide](./troubleshooting.md) for common issues and solutions.
+
+## License
+
+MIT
diff --git a/packages/opencode-skills/docs/api.md b/packages/opencode-skills/docs/api.md
new file mode 100644
index 0000000..5e375c6
--- /dev/null
+++ b/packages/opencode-skills/docs/api.md
@@ -0,0 +1,27 @@
+# API Documentation
+
+This document describes the API provided by the Skills plugin.
+
+## Plugin Export
+
+The plugin exports a main function that conforms to the OpenCode Plugin interface.
+
+\`\`\`typescript import { Plugin } from '@opencode-ai/plugin'
+
+export const SkillsPlugin: Plugin = async (ctx) => { return { // Plugin implementation } } \`\`\`
+
+## Tools
+
+Document any tools your plugin provides.
+
+## Hooks
+
+Document any hooks your plugin implements.
+
+## Events
+
+Document any events your plugin handles.
+
+## Types
+
+Document any types your plugin exports.
diff --git a/packages/opencode-skills/docs/development.md b/packages/opencode-skills/docs/development.md
new file mode 100644
index 0000000..246e812
--- /dev/null
+++ b/packages/opencode-skills/docs/development.md
@@ -0,0 +1,182 @@
+# Development Guide
+
+This guide covers how to develop and contribute to the Skills plugin.
+
+## Setup
+
+\`\`\`bash
+
+# Clone the repository
+
+git clone https://github.com/pantheon-org/opencode-skills.git cd opencode-skills
+
+# Install dependencies
+
+bun install
+
+# Run tests
+
+bun test
+
+# Build the plugin
+
+bun run build \`\`\`
+
+## Project Structure
+
+\`\`\` opencode-skills/ ├── src/ # Plugin source code ├── docs/ # Documentation source ├── pages/ # Documentation site
+builder └── dist/ # Build output \`\`\`
+
+## Testing
+
+\`\`\`bash
+
+# Run all tests
+
+bun test
+
+# Run tests in watch mode
+
+bun test --watch
+
+# Run tests with coverage
+
+bun test --coverage \`\`\`
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch
+3. Make your changes
+4. Write tests
+5. Submit a pull request
+
+## Release Process
+
+This plugin uses **Release Please** for automated releases based on
+[Conventional Commits](https://www.conventionalcommits.org/).
+
+### Automated Releases
+
+Release Please automatically:
+
+- Creates/updates release PRs when conventional commits are pushed to `main`
+- Bumps version numbers based on commit types
+- Generates CHANGELOG.md from commit messages
+- Publishes to npm when release PR is merged
+- Deploys documentation to GitHub Pages
+
+### Commit Message Format
+
+Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
+
+\`\`\`bash
+
+# New features (bumps minor version)
+
+feat: add new notification sound
+
+# Bug fixes (bumps patch version)
+
+fix: resolve connection timeout issue
+
+# Breaking changes (bumps major version)
+
+feat!: redesign plugin API
+
+# or
+
+feat: redesign plugin API
+
+BREAKING CHANGE: The plugin now requires initialization before use
+
+# Documentation
+
+docs: update installation instructions
+
+# Other types (no version bump)
+
+chore: update dependencies style: fix code formatting refactor: simplify notification logic test: add unit tests for
+parser ci: update GitHub Actions workflow \`\`\`
+
+## Release Workflow
+
+1. **Make changes** using conventional commits: \`\`\`bash git add . git commit -m "feat: add support for custom themes"
+ git push origin feature-branch \`\`\`
+
+2. **Create PR** and merge to `main`:
+ - Release Please detects conventional commits
+ - Creates/updates a release PR automatically
+ - PR includes version bump and CHANGELOG updates
+
+3. **Review and merge release PR**:
+ - Release Please PR is created as `chore(main): release X.Y.Z`
+ - Review the version bump and changelog
+ - Merge the PR to trigger the release
+
+4. **Automated release**:
+ - `release-please.yml` workflow triggers
+ - Runs full validation (lint, type-check, tests, build)
+ - Publishes package to npm
+ - Deploys documentation to GitHub Pages
+ - Creates GitHub release with changelog
+
+### Manual Release (Fallback)
+
+If you need to manually trigger a release:
+
+\`\`\`bash
+
+# Create a version tag
+
+git tag v1.0.0 git push origin v1.0.0 \`\`\`
+
+The `2-publish.yml` workflow will handle npm publishing and docs deployment.
+
+## GitHub Workflows
+
+The plugin includes several automated workflows:
+
+- **1-validate.yml**: Validates PRs (lint, test, build, security)
+- **release-please.yml**: Automated release management (primary)
+- **2-publish.yml**: Manual tag-based releases (fallback)
+- **deploy-docs.yml**: Standalone documentation deployment
+- **chores-pages.yml**: Daily GitHub Pages health check
+
+### Version Bumping Rules
+
+Release Please follows semantic versioning:
+
+| Commit Type | Version Bump | Example |
+| ------------------------------ | ------------- | ------------- |
+| `feat:` | Minor (0.X.0) | 1.2.0 → 1.3.0 |
+| `fix:` | Patch (0.0.X) | 1.2.0 → 1.2.1 |
+| `feat!:` or `BREAKING CHANGE:` | Major (X.0.0) | 1.2.0 → 2.0.0 |
+| `docs:`, `chore:`, etc. | None | No release |
+
+### Configuration Files
+
+Release Please configuration:
+
+- `.github/release-please-config.json`: Release Please settings
+- `.github/.release-please-manifest.json`: Current version tracking
+
+### Troubleshooting Releases
+
+**Release PR not created:**
+
+- Ensure commits follow conventional commit format
+- Check `.github/release-please-config.json` for correct configuration
+- Verify `release-please.yml` workflow is enabled
+
+**npm publish failed:**
+
+- Verify `NPM_TOKEN` secret is configured in repository settings
+- Token must have "Read and write" permissions for the package
+- Check package name is available on npm
+
+**Documentation not deploying:**
+
+- Ensure `pages/` directory has valid Astro configuration
+- Check GitHub Pages is enabled in repository settings
+- Verify docs build succeeds locally: `cd pages && bun run build`
diff --git a/packages/opencode-skills/docs/troubleshooting.md b/packages/opencode-skills/docs/troubleshooting.md
new file mode 100644
index 0000000..d258132
--- /dev/null
+++ b/packages/opencode-skills/docs/troubleshooting.md
@@ -0,0 +1,37 @@
+# Troubleshooting
+
+Common issues and their solutions.
+
+## Installation Issues
+
+### Issue: Plugin not loading
+
+**Solution:** Ensure the plugin is listed in your `opencode.json` file.
+
+## Usage Issues
+
+### Issue: Feature not working
+
+**Solution:** Check your configuration and ensure all required options are set.
+
+## Performance Issues
+
+### Issue: Plugin is slow
+
+**Solution:** Review your configuration and consider optimizing any custom implementations.
+
+## Getting Help
+
+If you encounter an issue not listed here:
+
+1. Check the [GitHub Issues](https://github.com/pantheon-org/opencode-skills/issues)
+2. Search for similar issues
+3. Create a new issue if needed
+
+Include the following information:
+
+- OpenCode version
+- Plugin version
+- Operating system
+- Configuration file
+- Error messages
diff --git a/packages/opencode-skills/docs/user-guide.md b/packages/opencode-skills/docs/user-guide.md
new file mode 100644
index 0000000..18a6b24
--- /dev/null
+++ b/packages/opencode-skills/docs/user-guide.md
@@ -0,0 +1,25 @@
+# User Guide
+
+This guide covers how to use the Skills plugin.
+
+## Getting Started
+
+1. Install the plugin
+2. Configure it in your `opencode.json`
+3. Start using the features
+
+## Examples
+
+### Basic Example
+
+\`\`\`typescript // Add usage examples here \`\`\`
+
+## Configuration Options
+
+Document all configuration options here.
+
+## Best Practices
+
+- Best practice 1
+- Best practice 2
+- Best practice 3
diff --git a/packages/opencode-skills/index.ts b/packages/opencode-skills/index.ts
new file mode 100644
index 0000000..e2a9f12
--- /dev/null
+++ b/packages/opencode-skills/index.ts
@@ -0,0 +1,2 @@
+export type { MatchResult, Skill, SkillsPluginConfig } from './src';
+export { createSkillsPlugin, defineSkill, findMatchingSkills, hasIntentToUse } from './src';
diff --git a/packages/opencode-skills/package.json b/packages/opencode-skills/package.json
new file mode 100644
index 0000000..b19f902
--- /dev/null
+++ b/packages/opencode-skills/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@pantheon-org/opencode-skills",
+ "version": "0.1.0",
+ "description": "TypeScript-based skill injection plugin for OpenCode with smart pattern matching",
+ "keywords": ["opencode", "plugin", "typescript", "skills", "tdd", "development", "knowledge-base"],
+ "homepage": "https://github.com/pantheon-org/opencode-skills#readme",
+ "bugs": {
+ "url": "https://github.com/pantheon-org/opencode-skills/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/pantheon-org/opencode-skills.git"
+ },
+ "license": "MIT",
+ "type": "module",
+ "main": "index.ts",
+ "files": ["index.ts", "src/", "README.md", "LICENSE"],
+ "scripts": {
+ "test": "bun test src",
+ "test:coverage": "bun test --coverage src/",
+ "test:watch": "bun test --watch src/",
+ "test:verbose": "bun test src/ --reporter tap",
+ "type-check": "bun tsc --noEmit --project tsconfig.test.json",
+ "lint": "eslint src/",
+ "lint:md": "markdownlint-cli2",
+ "lint:md:fix": "markdownlint-cli2 --fix",
+ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,mjs,cjs,css}\"",
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,mjs,cjs,css}\"",
+ "clean": "rm -rf dist/ .bun-cache/ coverage/",
+ "build": "bun tsc",
+ "dev": "bun tsc --watch",
+ "verify:package": "npm pack --dry-run",
+ "prepublishOnly": "bun run build && bun test && bun run verify:package"
+ },
+ "dependencies": {
+ "yaml": "^2.3.4",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.3.13",
+ "@types/bun": "latest",
+ "@types/glob": "^8.1.0"
+ },
+ "engines": {
+ "node": ">=20.0.0",
+ "bun": ">=1.0.0"
+ }
+}
diff --git a/packages/opencode-skills/project.json b/packages/opencode-skills/project.json
new file mode 100644
index 0000000..96183e4
--- /dev/null
+++ b/packages/opencode-skills/project.json
@@ -0,0 +1,80 @@
+{
+ "name": "opencode-skills",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "packages/opencode-skills/src",
+ "projectType": "library",
+ "targets": {
+ "build": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": [
+ {
+ "command": "bunx tsup src/index.ts --format esm,cjs --dts --out-dir dist"
+ }
+ ],
+ "cwd": "packages/opencode-skills",
+ "parallel": false
+ }
+ },
+ "pack": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": [
+ {
+ "command": "bunx npm pack --prefix packages/opencode-skills"
+ }
+ ],
+ "parallel": false
+ }
+ },
+ "check-mirror-exists": {
+ "executor": "@pantheon-org/tools:check-mirror-exists"
+ },
+ "lint": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "biome check --write .",
+ "cwd": "packages/opencode-skills"
+ },
+ "cache": true,
+ "inputs": ["default", "{workspaceRoot}/biome.json", "{projectRoot}/biome.json"]
+ },
+ "format": {
+ "executor": "nx:run-commands",
+ "options": {
+ "command": "biome format --write .",
+ "cwd": "packages/opencode-skills"
+ },
+ "cache": true,
+ "inputs": ["default", "{workspaceRoot}/biome.json", "{projectRoot}/biome.json"]
+ },
+ "type-check": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": [
+ {
+ "command": "bunx tsc --noEmit"
+ }
+ ],
+ "cwd": "packages/opencode-skills",
+ "parallel": false
+ }
+ },
+ "dev-proxy": {
+ "executor": "@pantheon-org/tools:dev-proxy",
+ "options": {}
+ },
+ "test": {
+ "executor": "nx:run-commands",
+ "options": {
+ "commands": [
+ {
+ "command": "bun test"
+ }
+ ],
+ "cwd": "packages/opencode-skills",
+ "parallel": false
+ }
+ }
+ }
+}
diff --git a/packages/opencode-skills/src/bm25/build-index.test.ts b/packages/opencode-skills/src/bm25/build-index.test.ts
new file mode 100644
index 0000000..18fc831
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/build-index.test.ts
@@ -0,0 +1,87 @@
+/**
+ * Build BM25 Index Tests
+ *
+ * Tests for BM25 index construction and precomputation.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { buildBM25Index } from './build-index';
+
+describe('buildBM25Index', () => {
+ it('should build index from skills map', () => {
+ const skills = new Map([
+ ['skill-one', 'Content for skill one'],
+ ['skill-two', 'Content for skill two'],
+ ]);
+ const index = buildBM25Index(skills);
+ expect(index.documents).toHaveLength(2);
+ expect(index.totalDocs).toBe(2);
+ });
+
+ it('should include skill name in document', () => {
+ const skills = new Map([['typescript-tdd', 'TypeScript development']]);
+ const index = buildBM25Index(skills);
+ const doc = index.documents[0];
+ expect(doc).toContain('typescript-tdd');
+ expect(doc).toContain('typescript');
+ expect(doc).toContain('development');
+ });
+
+ it('should calculate average document length', () => {
+ const skills = new Map([
+ ['short', 'Short content'],
+ ['long', 'This is a much longer content with many words for testing'],
+ ]);
+ const index = buildBM25Index(skills);
+ expect(index.avgDocLength).toBeGreaterThan(0);
+ // Note: skill name is included in document, so word counts are different
+ // 'short' + 'short' + 'content' = 3 words
+ // 'long' + 'this' + 'is' + 'a' + 'much' + 'longer' + 'content' + 'with' + 'many' + 'words' + 'for' + 'testing' = 12 words
+ // Average = (3 + 12) / 2 = 7.5
+ expect(index.avgDocLength).toBeCloseTo(7.5, 1);
+ });
+
+ it('should build IDF cache for all terms', () => {
+ const skills = new Map([
+ ['skill-a', 'common unique'],
+ ['skill-b', 'common rare'],
+ ]);
+ const index = buildBM25Index(skills);
+ // Should have cached IDF for: skill-a, skill-b, common, unique, rare
+ expect(index.idfCache.size).toBeGreaterThan(0);
+ expect(index.idfCache.has('common')).toBe(true);
+ expect(index.idfCache.has('unique')).toBe(true);
+ });
+
+ it('should handle empty skills map', () => {
+ const skills = new Map();
+ // Currently returns NaN for avgDocLength when empty, doesn't throw
+ const index = buildBM25Index(skills);
+ expect(index.documents).toHaveLength(0);
+ expect(index.totalDocs).toBe(0);
+ expect(Number.isNaN(index.avgDocLength)).toBe(true);
+ });
+
+ it('should handle skills with same content', () => {
+ const skills = new Map([
+ ['skill-a', 'Same content'],
+ ['skill-b', 'Same content'],
+ ]);
+ const index = buildBM25Index(skills);
+ expect(index.documents).toHaveLength(2);
+ // IDF for 'content' should be low since it appears in all docs
+ const contentIdf = index.idfCache.get('content');
+ expect(contentIdf).toBeLessThan(Math.log(2)); // Less than max possible IDF
+ });
+
+ it('should handle skills with no overlap', () => {
+ const skills = new Map([
+ ['skill-a', 'Alpha Beta Gamma'],
+ ['skill-b', 'Delta Epsilon Zeta'],
+ ]);
+ const index = buildBM25Index(skills);
+ expect(index.totalDocs).toBe(2);
+ // No shared terms except skill names
+ expect(index.idfCache.size).toBe(8); // 2 skill names + 6 content words
+ });
+});
diff --git a/packages/opencode-skills/src/bm25/build-index.ts b/packages/opencode-skills/src/bm25/build-index.ts
new file mode 100644
index 0000000..a93b55c
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/build-index.ts
@@ -0,0 +1,63 @@
+/**
+ * Build BM25 Index
+ *
+ * Precompute document statistics and IDF values for efficient BM25 scoring.
+ */
+
+import { inverseDocumentFrequency } from './inverse-document-frequency';
+import { tokenize } from './tokenize';
+import type { BM25Index } from './types';
+
+/**
+ * Build BM25 index from skill content
+ *
+ * @param skills - Map of skill name to skill content
+ * @returns Precomputed BM25 index with tokenized documents and IDF cache
+ *
+ * @example
+ * ```typescript
+ * const skills = new Map([
+ * ['typescript-tdd', 'TypeScript development with test-driven development'],
+ * ['bun-runtime', 'Bun runtime for fast JavaScript execution'],
+ * ]);
+ *
+ * const index = buildBM25Index(skills);
+ * console.log(index.totalDocs); // => 2
+ * ```
+ */
+export const buildBM25Index = (skills: Map): BM25Index => {
+ const documents: string[][] = [];
+ const skillNames: string[] = [];
+
+ // Tokenize all skill content
+ for (const [name, content] of skills.entries()) {
+ // Combine skill name, description, and content for indexing
+ const combinedText = `${name} ${content}`;
+ documents.push(tokenize(combinedText));
+ skillNames.push(name);
+ }
+
+ // Calculate average document length
+ const totalLength = documents.reduce((sum, doc) => sum + doc.length, 0);
+ const avgDocLength = totalLength / documents.length;
+
+ // Precompute IDF for all unique terms
+ const allTerms = new Set();
+ for (const doc of documents) {
+ for (const term of doc) {
+ allTerms.add(term);
+ }
+ }
+
+ const idfCache = new Map();
+ for (const term of allTerms) {
+ idfCache.set(term, inverseDocumentFrequency(term, documents, documents.length));
+ }
+
+ return {
+ documents,
+ avgDocLength,
+ totalDocs: documents.length,
+ idfCache,
+ };
+};
diff --git a/packages/opencode-skills/src/bm25/calculate-score.test.ts b/packages/opencode-skills/src/bm25/calculate-score.test.ts
new file mode 100644
index 0000000..a22c5eb
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/calculate-score.test.ts
@@ -0,0 +1,75 @@
+/**
+ * Calculate BM25 Score Tests
+ *
+ * Tests for BM25 relevance scoring algorithm.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { buildBM25Index } from './build-index';
+import { calculateBM25Score } from './calculate-score';
+import type { BM25Index } from './types';
+
+describe('calculateBM25Score', () => {
+ const createTestIndex = (): BM25Index => {
+ const skills = new Map([
+ ['typescript-tdd', 'TypeScript development with test-driven development approach'],
+ ['react-hooks', 'React hooks for state management in functional components'],
+ ['node-api', 'Building RESTful APIs with Node.js and Express framework'],
+ ]);
+ return buildBM25Index(skills);
+ };
+
+ it('should return positive score for matching query', () => {
+ const index = createTestIndex();
+ const score = calculateBM25Score('typescript development', 0, index);
+ expect(score).toBeGreaterThan(0);
+ });
+
+ it('should return 0 for non-matching query', () => {
+ const index = createTestIndex();
+ const score = calculateBM25Score('python machine learning', 0, index);
+ expect(score).toBe(0);
+ });
+
+ it('should return higher score for better matching document', () => {
+ const index = createTestIndex();
+ const score0 = calculateBM25Score('react hooks state', 0, index); // typescript-tdd
+ const score1 = calculateBM25Score('react hooks state', 1, index); // react-hooks
+ expect(score1).toBeGreaterThan(score0);
+ });
+
+ it('should return 0 for out of bounds index', () => {
+ const index = createTestIndex();
+ expect(calculateBM25Score('test', -1, index)).toBe(0);
+ expect(calculateBM25Score('test', 100, index)).toBe(0);
+ });
+
+ it('should respect custom k1 parameter', () => {
+ const index = createTestIndex();
+ const scoreLowK1 = calculateBM25Score('typescript', 0, index, { k1: 0.5 });
+ const scoreHighK1 = calculateBM25Score('typescript', 0, index, { k1: 2.0 });
+ // Higher k1 should allow more term frequency influence
+ expect(scoreHighK1).not.toBe(scoreLowK1);
+ });
+
+ it('should respect custom b parameter', () => {
+ const index = createTestIndex();
+ const scoreLowB = calculateBM25Score('typescript development', 0, index, { b: 0.0 });
+ const scoreHighB = calculateBM25Score('typescript development', 0, index, { b: 1.0 });
+ // Different b values should produce different scores
+ expect(scoreLowB).not.toBe(scoreHighB);
+ });
+
+ it('should handle multi-term queries', () => {
+ const index = createTestIndex();
+ const scoreSingle = calculateBM25Score('typescript', 0, index);
+ const scoreMultiple = calculateBM25Score('typescript development test', 0, index);
+ expect(scoreMultiple).toBeGreaterThan(scoreSingle);
+ });
+
+ it('should handle query with special characters', () => {
+ const index = createTestIndex();
+ const score = calculateBM25Score('TypeScript, Development!', 0, index);
+ expect(score).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/opencode-skills/src/bm25/calculate-score.ts b/packages/opencode-skills/src/bm25/calculate-score.ts
new file mode 100644
index 0000000..1d8b5d2
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/calculate-score.ts
@@ -0,0 +1,69 @@
+/**
+ * Calculate BM25 Score
+ *
+ * Calculate BM25 relevance score for a query against a document using the
+ * Best Matching 25 probabilistic ranking function.
+ */
+
+import { termFrequency } from './term-frequency';
+import { tokenize } from './tokenize';
+import type { BM25Config, BM25Index } from './types';
+import { DEFAULT_BM25_CONFIG } from './types';
+
+/**
+ * Calculate BM25 score for a query against a document
+ *
+ * Formula: BM25(D, Q) = Σ IDF(qi) * (f(qi, D) * (k1 + 1)) / (f(qi, D) + k1 * (1 - b + b * |D| / avgdl))
+ *
+ * Where:
+ * - D: Document (skill content)
+ * - Q: Query (user message)
+ * - qi: Query term i
+ * - f(qi, D): Term frequency of qi in D
+ * - |D|: Document length
+ * - avgdl: Average document length
+ * - k1: Term frequency saturation parameter (typically 1.2-2.0)
+ * - b: Length normalization parameter (typically 0.75)
+ * - IDF(qi): Inverse document frequency of qi
+ *
+ * @param query - User message/query
+ * @param docIndex - Document index in the BM25 index
+ * @param index - Precomputed BM25 index
+ * @param config - BM25 configuration parameters
+ * @returns BM25 relevance score
+ */
+export const calculateBM25Score = (
+ query: string,
+ docIndex: number,
+ index: BM25Index,
+ config: BM25Config = {},
+): number => {
+ const { k1, b } = { ...DEFAULT_BM25_CONFIG, ...config };
+
+ // Handle out of bounds
+ if (docIndex < 0 || docIndex >= index.documents.length) {
+ return 0;
+ }
+
+ const queryTerms = tokenize(query);
+ const document = index.documents[docIndex];
+ const docLength = document.length;
+
+ let score = 0;
+
+ for (const term of queryTerms) {
+ // Get IDF from cache, default to 0 if term not in corpus
+ const idf = index.idfCache.get(term) || 0;
+
+ // Calculate term frequency in document
+ const tf = termFrequency(term, document);
+
+ // BM25 formula
+ const numerator = tf * (k1 + 1);
+ const denominator = tf + k1 * (1 - b + (b * docLength) / index.avgDocLength);
+
+ score += idf * (numerator / denominator);
+ }
+
+ return score;
+};
diff --git a/packages/opencode-skills/src/bm25/get-top-skills.test.ts b/packages/opencode-skills/src/bm25/get-top-skills.test.ts
new file mode 100644
index 0000000..5c4524a
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/get-top-skills.test.ts
@@ -0,0 +1,94 @@
+/**
+ * Get Top Skills by BM25 Tests
+ *
+ * Tests for retrieving top N skills by BM25 relevance scoring.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { buildBM25Index } from './build-index';
+import { getTopSkillsByBM25 } from './get-top-skills';
+import type { BM25Index } from './types';
+
+describe('getTopSkillsByBM25', () => {
+ const createTestIndex = (): BM25Index => {
+ const skills = new Map([
+ ['typescript-tdd', 'TypeScript development with test-driven development approach'],
+ ['react-hooks', 'React hooks for state management in functional components'],
+ ['node-api', 'Building RESTful APIs with Node.js and Express framework'],
+ ['python-ml', 'Machine learning with Python and TensorFlow'],
+ ]);
+ return buildBM25Index(skills);
+ };
+
+ it('should return top N skills by relevance', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks', 'node-api', 'python-ml'];
+ const topSkills = getTopSkillsByBM25('typescript development', skillNames, index, 2);
+
+ expect(topSkills).toHaveLength(2);
+ expect(topSkills[0]).toBe('typescript-tdd');
+ });
+
+ it('should default to returning top 3 skills', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks', 'node-api', 'python-ml'];
+ const topSkills = getTopSkillsByBM25('development', skillNames, index);
+
+ expect(topSkills.length).toBeLessThanOrEqual(3);
+ });
+
+ it('should return empty array when no skills match', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks'];
+ // Use high threshold to filter out low-relevance matches
+ const topSkills = getTopSkillsByBM25('machine learning tensorflow', skillNames, index, 3, { threshold: 1.0 });
+
+ // All scores should be below threshold and filtered out
+ expect(topSkills).toHaveLength(0);
+ });
+
+ it('should return skills in descending order of relevance', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks', 'node-api'];
+ const topSkills = getTopSkillsByBM25('react state hooks', skillNames, index, 3);
+
+ // react-hooks should be most relevant
+ expect(topSkills[0]).toBe('react-hooks');
+ });
+
+ it('should handle empty skill names array', () => {
+ const index = createTestIndex();
+ const topSkills = getTopSkillsByBM25('test', [], index, 3);
+
+ expect(topSkills).toHaveLength(0);
+ });
+
+ it('should handle topN larger than available skills', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks'];
+ const topSkills = getTopSkillsByBM25('development', skillNames, index, 10);
+
+ expect(topSkills.length).toBeLessThanOrEqual(2);
+ });
+
+ it('should apply threshold from config', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks', 'node-api'];
+ // With high threshold, only very relevant matches return
+ const topSkills = getTopSkillsByBM25('typescript', skillNames, index, 3, { threshold: 5.0 });
+
+ // Should only return highly relevant matches
+ expect(topSkills.length).toBeGreaterThanOrEqual(0);
+ });
+
+ it('should handle query with special characters', () => {
+ const index = createTestIndex();
+ const skillNames = ['typescript-tdd', 'react-hooks'];
+ const topSkills = getTopSkillsByBM25('TypeScript, TDD!', skillNames, index, 2);
+
+ expect(topSkills.length).toBeGreaterThanOrEqual(0);
+ if (topSkills.length > 0) {
+ expect(topSkills[0]).toBe('typescript-tdd');
+ }
+ });
+});
diff --git a/packages/opencode-skills/src/bm25/get-top-skills.ts b/packages/opencode-skills/src/bm25/get-top-skills.ts
new file mode 100644
index 0000000..4eb8f02
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/get-top-skills.ts
@@ -0,0 +1,40 @@
+/**
+ * Get Top Skills by BM25
+ *
+ * Get the top N most relevant skills for a query using BM25 scoring.
+ */
+
+import { rankSkillsByBM25 } from './rank-skills';
+import type { BM25Config, BM25Index } from './types';
+
+/**
+ * Get top N skills by BM25 relevance
+ *
+ * @param query - User message/query
+ * @param skillNames - Array of skill names
+ * @param index - Precomputed BM25 index
+ * @param topN - Number of top results to return
+ * @param config - BM25 configuration
+ * @returns Array of top N skill names by relevance
+ *
+ * @example
+ * ```typescript
+ * const topSkills = getTopSkillsByBM25(
+ * 'How do I write TypeScript tests?',
+ * ['typescript-tdd', 'bun-runtime', 'react-testing'],
+ * index,
+ * 2
+ * );
+ * // => ['typescript-tdd', 'react-testing']
+ * ```
+ */
+export const getTopSkillsByBM25 = (
+ query: string,
+ skillNames: string[],
+ index: BM25Index,
+ topN: number = 3,
+ config: BM25Config = {},
+): string[] => {
+ const ranked = rankSkillsByBM25(query, skillNames, index, config);
+ return ranked.slice(0, topN).map(([name]) => name);
+};
diff --git a/packages/opencode-skills/src/bm25/index.ts b/packages/opencode-skills/src/bm25/index.ts
new file mode 100644
index 0000000..5b0f6dc
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/index.ts
@@ -0,0 +1,24 @@
+/**
+ * BM25 (Best Matching 25) Probabilistic Ranking Function
+ *
+ * BM25 is a bag-of-words retrieval function that ranks documents based on
+ * term frequency, inverse document frequency, and document length normalization.
+ *
+ * This module provides efficient BM25 scoring for skill relevance ranking.
+ *
+ * @see https://en.wikipedia.org/wiki/Okapi_BM25
+ */
+
+// Core functions
+export { buildBM25Index } from './build-index';
+export { calculateBM25Score } from './calculate-score';
+export { getTopSkillsByBM25 } from './get-top-skills';
+// Utility functions
+export { inverseDocumentFrequency } from './inverse-document-frequency';
+export { rankSkillsByBM25 } from './rank-skills';
+export { termFrequency } from './term-frequency';
+export { tokenize } from './tokenize';
+
+// Type definitions
+export type { BM25Config, BM25Index } from './types';
+export { DEFAULT_BM25_CONFIG } from './types';
diff --git a/packages/opencode-skills/src/bm25/inverse-document-frequency.test.ts b/packages/opencode-skills/src/bm25/inverse-document-frequency.test.ts
new file mode 100644
index 0000000..cd00a39
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/inverse-document-frequency.test.ts
@@ -0,0 +1,64 @@
+/**
+ * Inverse Document Frequency Tests
+ *
+ * Tests for IDF calculation using BM25 formula.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { inverseDocumentFrequency } from './inverse-document-frequency';
+
+describe('inverseDocumentFrequency', () => {
+ it('should calculate IDF for term appearing in all documents', () => {
+ const docs = [
+ ['common', 'word'],
+ ['common', 'phrase'],
+ ['common', 'text'],
+ ];
+ // When term is in all docs: log((3-3+0.5)/(3+0.5)+1) = log(0.5/3.5+1) = log(1.143)
+ const idf = inverseDocumentFrequency('common', docs, 3);
+ expect(idf).toBeCloseTo(Math.log(1.142857), 5);
+ });
+
+ it('should calculate IDF for term in half the documents', () => {
+ const docs = [
+ ['rare', 'word'],
+ ['common', 'phrase'],
+ ['common', 'text'],
+ ];
+ // When term is in 1 doc out of 3: log((3-1+0.5)/(1+0.5)+1) = log(2.5/1.5+1) = log(2.667)
+ const idf = inverseDocumentFrequency('rare', docs, 3);
+ expect(idf).toBeCloseTo(Math.log(2.666667), 5);
+ });
+
+ it('should calculate IDF for term in no documents', () => {
+ const docs = [
+ ['some', 'words'],
+ ['other', 'phrase'],
+ ['more', 'text'],
+ ];
+ // When term is in 0 docs: log((3-0+0.5)/(0+0.5)+1) = log(3.5/0.5+1) = log(8)
+ const idf = inverseDocumentFrequency('missing', docs, 3);
+ expect(idf).toBeCloseTo(Math.log(8), 5);
+ });
+
+ it('should return higher IDF for rarer terms', () => {
+ const docs = [['common'], ['common'], ['common', 'rare']];
+ const commonIdf = inverseDocumentFrequency('common', docs, 3);
+ const rareIdf = inverseDocumentFrequency('rare', docs, 3);
+ expect(rareIdf).toBeGreaterThan(commonIdf);
+ });
+
+ it('should handle single document corpus', () => {
+ const docs = [['only', 'document']];
+ // When term is in the doc: log((1-1+0.5)/(1+0.5)+1) = log(0.5/1.5+1)
+ const idf = inverseDocumentFrequency('only', docs, 1);
+ expect(idf).toBeCloseTo(Math.log(4 / 3), 5);
+ });
+
+ it('should handle empty documents array', () => {
+ // Edge case: totalDocs=0, docsWithTerm=0
+ // log((0-0+0.5)/(0+0.5)+1) = log(1+1) = log(2)
+ const idf = inverseDocumentFrequency('test', [], 0);
+ expect(idf).toBeCloseTo(Math.log(2), 5);
+ });
+});
diff --git a/packages/opencode-skills/src/bm25/inverse-document-frequency.ts b/packages/opencode-skills/src/bm25/inverse-document-frequency.ts
new file mode 100644
index 0000000..66fe7ef
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/inverse-document-frequency.ts
@@ -0,0 +1,24 @@
+/**
+ * Inverse Document Frequency Calculation
+ *
+ * Calculate IDF using the BM25 formula to measure term importance.
+ */
+
+/**
+ * Calculate inverse document frequency
+ *
+ * IDF(t) = ln((N - df(t) + 0.5) / (df(t) + 0.5) + 1)
+ *
+ * Where:
+ * - N: Total number of documents
+ * - df(t): Number of documents containing term t
+ *
+ * @param term - Term to calculate IDF for
+ * @param documents - All tokenized documents
+ * @param totalDocs - Total number of documents
+ * @returns IDF score for the term
+ */
+export const inverseDocumentFrequency = (term: string, documents: string[][], totalDocs: number): number => {
+ const docsWithTerm = documents.filter((doc) => doc.includes(term)).length;
+ return Math.log((totalDocs - docsWithTerm + 0.5) / (docsWithTerm + 0.5) + 1);
+};
diff --git a/packages/opencode-skills/src/bm25/rank-skills.ts b/packages/opencode-skills/src/bm25/rank-skills.ts
new file mode 100644
index 0000000..6517329
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/rank-skills.ts
@@ -0,0 +1,53 @@
+/**
+ * Rank Skills by BM25
+ *
+ * Rank all skills by their BM25 relevance scores for a given query.
+ */
+
+import { calculateBM25Score } from './calculate-score';
+import type { BM25Config, BM25Index } from './types';
+import { DEFAULT_BM25_CONFIG } from './types';
+
+/**
+ * Rank skills by BM25 relevance to a query
+ *
+ * @param query - User message/query
+ * @param skillNames - Array of skill names (must match index order)
+ * @param index - Precomputed BM25 index
+ * @param config - BM25 configuration
+ * @returns Array of [skillName, score] tuples sorted by relevance (descending)
+ *
+ * @example
+ * ```typescript
+ * const ranked = rankSkillsByBM25(
+ * 'How do I write TypeScript tests?',
+ * ['typescript-tdd', 'bun-runtime'],
+ * index
+ * );
+ * // => [['typescript-tdd', 12.5], ['bun-runtime', 3.2]]
+ * ```
+ */
+export const rankSkillsByBM25 = (
+ query: string,
+ skillNames: string[],
+ index: BM25Index,
+ config: BM25Config = {},
+): Array<[string, number]> => {
+ const { threshold } = { ...DEFAULT_BM25_CONFIG, ...config };
+
+ const scores: Array<[string, number]> = [];
+
+ for (let i = 0; i < skillNames.length; i++) {
+ const score = calculateBM25Score(query, i, index, config);
+
+ // Only include scores above threshold
+ if (score >= threshold) {
+ scores.push([skillNames[i], score]);
+ }
+ }
+
+ // Sort by score descending
+ scores.sort((a, b) => b[1] - a[1]);
+
+ return scores;
+};
diff --git a/packages/opencode-skills/src/bm25/term-frequency.test.ts b/packages/opencode-skills/src/bm25/term-frequency.test.ts
new file mode 100644
index 0000000..1e056ba
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/term-frequency.test.ts
@@ -0,0 +1,40 @@
+/**
+ * Term Frequency Tests
+ *
+ * Tests for term frequency calculation in documents.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { termFrequency } from './term-frequency';
+
+describe('termFrequency', () => {
+ it('should count single occurrence', () => {
+ const doc = ['the', 'quick', 'brown', 'fox'];
+ expect(termFrequency('quick', doc)).toBe(1);
+ });
+
+ it('should count multiple occurrences', () => {
+ const doc = ['the', 'quick', 'quick', 'brown', 'fox'];
+ expect(termFrequency('quick', doc)).toBe(2);
+ });
+
+ it('should return 0 for term not in document', () => {
+ const doc = ['the', 'quick', 'brown', 'fox'];
+ expect(termFrequency('lazy', doc)).toBe(0);
+ });
+
+ it('should handle empty document', () => {
+ expect(termFrequency('test', [])).toBe(0);
+ });
+
+ it('should be case sensitive', () => {
+ const doc = ['Hello', 'hello', 'HELLO'];
+ expect(termFrequency('hello', doc)).toBe(1);
+ expect(termFrequency('Hello', doc)).toBe(1);
+ });
+
+ it('should handle all same terms', () => {
+ const doc = ['test', 'test', 'test', 'test'];
+ expect(termFrequency('test', doc)).toBe(4);
+ });
+});
diff --git a/packages/opencode-skills/src/bm25/term-frequency.ts b/packages/opencode-skills/src/bm25/term-frequency.ts
new file mode 100644
index 0000000..267cd3e
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/term-frequency.ts
@@ -0,0 +1,22 @@
+/**
+ * Term Frequency Calculation
+ *
+ * Calculate how many times a term appears in a document.
+ */
+
+/**
+ * Calculate term frequency in a document
+ *
+ * @param term - Term to count
+ * @param document - Tokenized document
+ * @returns Number of occurrences of the term
+ *
+ * @example
+ * ```typescript
+ * termFrequency('test', ['test', 'driven', 'test'])
+ * // => 2
+ * ```
+ */
+export const termFrequency = (term: string, document: string[]): number => {
+ return document.filter((t) => t === term).length;
+};
diff --git a/packages/opencode-skills/src/bm25/tokenize.test.ts b/packages/opencode-skills/src/bm25/tokenize.test.ts
new file mode 100644
index 0000000..69d5756
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/tokenize.test.ts
@@ -0,0 +1,50 @@
+/**
+ * Tokenize Tests
+ *
+ * Tests for text tokenization functionality.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { tokenize } from './tokenize';
+
+describe('tokenize', () => {
+ it('should convert text to lowercase', () => {
+ const result = tokenize('Hello World');
+ expect(result).toEqual(['hello', 'world']);
+ });
+
+ it('should remove punctuation except hyphens', () => {
+ const result = tokenize('Hello, World! How are you?');
+ expect(result).toEqual(['hello', 'world', 'how', 'are', 'you']);
+ });
+
+ it('should preserve hyphens in skill names', () => {
+ const result = tokenize('typescript-tdd skill-name');
+ expect(result).toEqual(['typescript-tdd', 'skill-name']);
+ });
+
+ it('should handle multiple spaces', () => {
+ const result = tokenize('Hello World');
+ expect(result).toEqual(['hello', 'world']);
+ });
+
+ it('should handle empty string', () => {
+ const result = tokenize('');
+ expect(result).toEqual([]);
+ });
+
+ it('should handle string with only punctuation', () => {
+ const result = tokenize('!!!???...');
+ expect(result).toEqual([]);
+ });
+
+ it('should handle numbers', () => {
+ const result = tokenize('Version 1.2.3');
+ expect(result).toEqual(['version', '1', '2', '3']);
+ });
+
+ it('should handle camelCase', () => {
+ const result = tokenize('camelCaseText');
+ expect(result).toEqual(['camelcasetext']);
+ });
+});
diff --git a/packages/opencode-skills/src/bm25/tokenize.ts b/packages/opencode-skills/src/bm25/tokenize.ts
new file mode 100644
index 0000000..f5c6d0d
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/tokenize.ts
@@ -0,0 +1,28 @@
+/**
+ * Tokenize Text
+ *
+ * Tokenize text into lowercase words, removing punctuation while preserving hyphens.
+ */
+
+/**
+ * Tokenize text into lowercase words, removing punctuation
+ *
+ * @param text - Input text to tokenize
+ * @returns Array of lowercase tokens
+ *
+ * @example
+ * ```typescript
+ * tokenize('Hello World!')
+ * // => ['hello', 'world']
+ *
+ * tokenize('typescript-tdd')
+ * // => ['typescript-tdd']
+ * ```
+ */
+export const tokenize = (text: string): string[] => {
+ return text
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, ' ') // Keep hyphens for skill names
+ .split(/\s+/)
+ .filter((token) => token.length > 0);
+};
diff --git a/packages/opencode-skills/src/bm25/types.ts b/packages/opencode-skills/src/bm25/types.ts
new file mode 100644
index 0000000..cd98259
--- /dev/null
+++ b/packages/opencode-skills/src/bm25/types.ts
@@ -0,0 +1,46 @@
+/**
+ * BM25 Type Definitions
+ *
+ * Type definitions for BM25 scoring system.
+ */
+
+/**
+ * BM25 configuration parameters
+ */
+export interface BM25Config {
+ /** Term frequency saturation parameter (default: 1.5) */
+ k1?: number;
+ /** Length normalization parameter (default: 0.75) */
+ b?: number;
+ /** Minimum score threshold for injection (default: 0.0) */
+ threshold?: number;
+ /** Enable BM25 scoring (default: false) */
+ enabled?: boolean;
+ /** Maximum number of skills to inject (default: 3) */
+ maxSkills?: number;
+}
+
+/**
+ * Precomputed document statistics for BM25 scoring
+ */
+export interface BM25Index {
+ /** Tokenized documents */
+ documents: string[][];
+ /** Average document length */
+ avgDocLength: number;
+ /** Total number of documents */
+ totalDocs: number;
+ /** IDF cache for terms */
+ idfCache: Map;
+}
+
+/**
+ * Default BM25 configuration
+ */
+export const DEFAULT_BM25_CONFIG: Required = {
+ k1: 1.5,
+ b: 0.75,
+ threshold: 0.0,
+ enabled: false,
+ maxSkills: 3,
+};
diff --git a/packages/opencode-skills/src/create-skills-plugin.ts b/packages/opencode-skills/src/create-skills-plugin.ts
new file mode 100644
index 0000000..0ae12fd
--- /dev/null
+++ b/packages/opencode-skills/src/create-skills-plugin.ts
@@ -0,0 +1,232 @@
+/**
+ * Create Skills Plugin Factory
+ *
+ * Factory function to create an OpenCode plugin with skill injection capabilities.
+ * Provides automatic skill injection into chat context using smart pattern matching
+ * and BM25-based relevance ranking.
+ */
+
+import type { Plugin } from '@opencode-ai/plugin';
+
+import { type BM25Index, buildBM25Index, getTopSkillsByBM25 } from './bm25/index';
+import { hasIntentToUse } from './pattern-matching/index';
+import type { Skill, SkillsPluginConfig } from './types';
+
+/**
+ * Default configuration for the skills plugin
+ */
+const DEFAULT_CONFIG: SkillsPluginConfig = {
+ autoInject: true,
+ debug: false,
+ patternMatching: {
+ wordBoundary: true,
+ intentDetection: true,
+ negationDetection: true,
+ },
+ bm25: {
+ enabled: false,
+ k1: 1.5,
+ b: 0.75,
+ threshold: 0.0,
+ maxSkills: 3,
+ },
+};
+
+/**
+ * Create the OpenCode skills plugin with the given skills and configuration
+ *
+ * @param skills - Record of skill name to Skill object
+ * @param userConfig - Optional configuration overrides
+ * @returns Plugin function for OpenCode
+ *
+ * @example
+ * ```typescript
+ * const skills = {
+ * 'typescript-tdd': {
+ * name: 'typescript-tdd',
+ * description: 'TypeScript development with TDD',
+ * content: '# TypeScript TDD\n\n...',
+ * },
+ * };
+ *
+ * export const MyPlugin = createSkillsPlugin(skills, { debug: true });
+ * ```
+ */
+export const createSkillsPlugin = (
+ skills: Record,
+ userConfig: Partial = {},
+): Plugin => {
+ const config: SkillsPluginConfig = {
+ ...DEFAULT_CONFIG,
+ ...userConfig,
+ patternMatching: {
+ ...DEFAULT_CONFIG.patternMatching,
+ ...userConfig.patternMatching,
+ },
+ bm25: {
+ ...DEFAULT_CONFIG.bm25,
+ ...userConfig.bm25,
+ },
+ };
+
+ // biome-ignore lint: Plugin hook requires complex async logic for skills injection
+ return async (ctx) => {
+ const skillNames = Object.keys(skills);
+ const skillKeywords = new Map();
+
+ // Build keyword map for each skill
+ for (const [name, skill] of Object.entries(skills)) {
+ if (skill.keywords && skill.keywords.length > 0) {
+ skillKeywords.set(name, skill.keywords);
+ }
+ }
+
+ // Build BM25 index if enabled
+ let bm25Index: BM25Index | null = null;
+ if (config.bm25?.enabled) {
+ const skillsMap = new Map();
+ for (const [name, skill] of Object.entries(skills)) {
+ // Combine all skill content for BM25 indexing
+ const content =
+ skill.content || [skill.whatIDo, skill.whenToUseMe, skill.instructions].filter(Boolean).join(' ');
+ skillsMap.set(name, `${skill.description} ${content}`);
+ }
+ bm25Index = buildBM25Index(skillsMap);
+
+ if (config.debug) {
+ console.log('[opencode-skills] BM25 index built with', bm25Index.totalDocs, 'skills');
+ }
+ }
+
+ if (config.debug) {
+ console.log(`[opencode-skills] Plugin loaded with ${skillNames.length} skills`);
+ console.log('[opencode-skills] Skills:', skillNames.join(', '));
+ console.log('[opencode-skills] Config:', config);
+ }
+
+ return {
+ // Auto-inject skills via chat message hook
+ // biome-ignore lint: Chat message hook requires multiple conditions for skills injection
+ 'chat.message': async ({ sessionID }, { parts }) => {
+ // Skip if auto-inject is disabled
+ if (!config.autoInject) {
+ return;
+ }
+
+ // Extract text content from message parts
+ const textContent = parts
+ .filter((p) => p.type === 'text')
+ .map((p) => ('text' in p ? p.text : ''))
+ .join('\n');
+
+ // Skip empty messages
+ if (!textContent.trim()) {
+ return;
+ }
+
+ // Track which skills have been injected to avoid duplicates
+ const injectedSkills = new Set();
+
+ // Determine which skills to inject based on mode
+ const skillsToInject: string[] = [];
+
+ if (config.bm25?.enabled && bm25Index) {
+ // BM25 mode: Rank skills by relevance
+ const maxSkills = config.bm25.maxSkills ?? 3;
+ const candidates = getTopSkillsByBM25(textContent, skillNames, bm25Index, maxSkills, config.bm25);
+
+ if (config.debug) {
+ console.log(`[opencode-skills] BM25 top ${maxSkills} candidates:`, candidates);
+ }
+
+ // Filter candidates through pattern matching for negation detection
+ for (const name of candidates) {
+ const keywords = skillKeywords.get(name) || [];
+ const result = hasIntentToUse(textContent, name, keywords, config.patternMatching);
+
+ // Only inject if not negated
+ if (!result.hasNegation) {
+ skillsToInject.push(name);
+ } else if (config.debug) {
+ console.log(`[opencode-skills] Skipping skill "${name}" due to negation`);
+ }
+ }
+ } else {
+ // Pattern matching mode: Check each skill
+ for (const name of skillNames) {
+ const keywords = skillKeywords.get(name) || [];
+ const result = hasIntentToUse(textContent, name, keywords, config.patternMatching);
+
+ if (result.matches) {
+ skillsToInject.push(name);
+ }
+ }
+ }
+
+ // Inject selected skills
+ for (const name of skillsToInject) {
+ // Skip if already injected
+ if (injectedSkills.has(name)) {
+ continue;
+ }
+
+ const skill = skills[name];
+ if (!skill) continue;
+
+ // Create skill content from structured fields or legacy content
+ const content =
+ skill.content ||
+ [
+ `## What I do`,
+ skill.whatIDo || '',
+ '',
+ `## When to use me`,
+ skill.whenToUseMe || '',
+ '',
+ `## Instructions`,
+ skill.instructions || '',
+ '',
+ `## Checklist`,
+ ...(skill.checklist?.map((item) => `- [ ] ${item}`) || []),
+ ].join('\n');
+
+ // Create skill content
+ const skillContent = [
+ '',
+ '',
+ ``,
+ `# ${skill.description}`,
+ '',
+ content,
+ '',
+ ].join('\n');
+
+ // Inject via session prompt (noReply pattern from malhashemi example)
+ try {
+ await ctx.client.session.prompt({
+ path: { id: sessionID },
+ body: {
+ noReply: true,
+ parts: [{ type: 'text', text: skillContent }],
+ },
+ });
+
+ injectedSkills.add(name);
+
+ // Log injection for debugging/observability
+ if (config.debug) {
+ console.log(`[opencode-skills] Auto-injected skill "${name}" in session ${sessionID}`);
+ } else {
+ // Always log injection (non-debug mode shows minimal info)
+ console.log(`[opencode-skills] Injected skill: ${name}`);
+ }
+ } catch (error) {
+ if (config.debug) {
+ console.error(`[opencode-skills] Failed to inject skill "${name}":`, error);
+ }
+ }
+ }
+ },
+ };
+ };
+};
diff --git a/packages/opencode-skills/src/define-skill.test.ts b/packages/opencode-skills/src/define-skill.test.ts
new file mode 100644
index 0000000..4482ee2
--- /dev/null
+++ b/packages/opencode-skills/src/define-skill.test.ts
@@ -0,0 +1,85 @@
+/**
+ * Define Skill Tests
+ *
+ * Tests for the skill definition functionality.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { defineSkill } from './define-skill';
+
+describe('defineSkill', () => {
+ it('should create skill with structured content', () => {
+ const skill = defineSkill(
+ {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill',
+ whenToUseMe: 'Use when testing',
+ instructions: 'Follow these steps',
+ checklist: ['Item 1', 'Item 2'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ metadata: {
+ category: 'testing',
+ },
+ },
+ { validate: false },
+ );
+
+ expect(skill.name).toBe('test-skill');
+ expect(skill.description).toBe('A test skill');
+ expect(skill.whatIDo).toBe('Core capabilities of the skill');
+ expect(skill.whenToUseMe).toBe('Use when testing');
+ expect(skill.instructions).toBe('Follow these steps');
+ expect(skill.checklist).toEqual(['Item 1', 'Item 2']);
+ expect(skill.license).toBe('MIT');
+ expect(skill.compatibility).toBe('opencode');
+ expect(skill.metadata?.category).toBe('testing');
+ expect(skill.version).toBe('1.0.0');
+ expect(skill.updatedAt).toBeDefined();
+ });
+
+ it('should auto-add version and updatedAt', () => {
+ const skill = defineSkill(
+ {
+ name: 'test-skill',
+ description: 'A test skill',
+ content: 'Legacy content',
+ },
+ { validate: false },
+ );
+
+ expect(skill.version).toBe('1.0.0');
+ expect(skill.updatedAt).toBeDefined();
+ expect(new Date(skill.updatedAt!).getTime()).toBeLessThanOrEqual(Date.now());
+ });
+
+ it('should support legacy content field', () => {
+ const skill = defineSkill(
+ {
+ name: 'legacy-skill',
+ description: 'Legacy skill',
+ content: 'This is legacy content',
+ },
+ { validate: false },
+ );
+
+ expect(skill.content).toBe('This is legacy content');
+ });
+
+ it('should support optional fields', () => {
+ const skill = defineSkill(
+ {
+ name: 'optional-skill',
+ description: 'Skill with optional fields',
+ content: 'Content',
+ keywords: ['keyword1', 'keyword2'],
+ dependencies: ['dependency1'],
+ },
+ { validate: false },
+ );
+
+ expect(skill.keywords).toEqual(['keyword1', 'keyword2']);
+ expect(skill.dependencies).toEqual(['dependency1']);
+ });
+});
diff --git a/packages/opencode-skills/src/define-skill.ts b/packages/opencode-skills/src/define-skill.ts
new file mode 100644
index 0000000..8aa25ed
--- /dev/null
+++ b/packages/opencode-skills/src/define-skill.ts
@@ -0,0 +1,67 @@
+/**
+ * Define Skill Helper
+ *
+ * Helper function to create skill objects with validation and structured content migration.
+ */
+
+import type { Skill } from './types';
+import { formatValidationResult, validateSkill } from './validation/index';
+
+/**
+ * Helper function to create a skill object with validation
+ *
+ * @param skill - Partial skill object (name and description required)
+ * @param options - Optional validation options
+ * @returns Complete Skill object
+ *
+ * @example
+ * ```typescript
+ * const mySkill = defineSkill({
+ * name: 'typescript-tdd',
+ * description: 'TypeScript development with TDD',
+ * whatIDo: 'I help you write TypeScript with TDD practices',
+ * whenToUseMe: 'Use me when starting a new TypeScript project',
+ * instructions: 'Follow test-first development',
+ * });
+ * ```
+ */
+export const defineSkill = (
+ skill: Pick & Partial>,
+ options?: { strict?: boolean; validate?: boolean },
+): Skill => {
+ const fullSkill: Skill = {
+ version: '1.0.0',
+ updatedAt: new Date().toISOString(),
+ ...skill,
+ };
+
+ // Migrate legacy content to structured format if present
+ if (fullSkill.content && !fullSkill.whatIDo) {
+ console.warn(
+ `[opencode-skills] Skill "${skill.name}" uses deprecated "content" field. Consider migrating to structured content.`,
+ );
+ }
+
+ // Validate if requested (default: true in dev, false in prod)
+ const shouldValidate = options?.validate ?? process.env.NODE_ENV !== 'production';
+
+ if (shouldValidate) {
+ const result = validateSkill(fullSkill, options?.strict);
+
+ // Log formatted results
+ if (
+ result.errors.length > 0 ||
+ result.warnings.length > 0 ||
+ (process.env.DEBUG && result.suggestions.length > 0)
+ ) {
+ console.log(formatValidationResult(result, skill.name));
+ }
+
+ // Throw in strict mode
+ if (options?.strict && !result.valid) {
+ throw new Error(`Skill "${skill.name}" has validation errors. See output above.`);
+ }
+ }
+
+ return fullSkill;
+};
diff --git a/packages/opencode-skills/src/examples.ts b/packages/opencode-skills/src/examples.ts
new file mode 100644
index 0000000..155dc5f
--- /dev/null
+++ b/packages/opencode-skills/src/examples.ts
@@ -0,0 +1,304 @@
+/**
+ * Example skills for demonstration and testing
+ */
+
+import { defineSkill } from './index';
+import type { Skill } from './types';
+
+/**
+ * TypeScript TDD Development Skill
+ *
+ * Based on the Bun and TypeScript development standards used in this project.
+ * Promotes test-driven development, single-function modules, and barrel exports.
+ */
+export const typescriptTddSkill: Skill = defineSkill({
+ name: 'typescript-tdd',
+ description: 'TypeScript development with TDD, single-function modules, and barrel exports',
+ keywords: ['tdd', 'test-driven', 'testing', 'bun', 'typescript', 'ts'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ metadata: {
+ category: 'development',
+ },
+ whatIDo:
+ 'I help you write TypeScript code using test-driven development practices, with single-function modules and proper barrel exports',
+ whenToUseMe:
+ 'Use when writing TypeScript code with Bun, following TDD practices, or need guidance on module organization and testing patterns',
+ instructions: `## Core Principles
+
+1. **One Function Per Module** - Each file exports a single primary function
+2. **Test Collocation** - Place tests next to implementation with \`.test.ts\` suffix
+3. **Barrel Modules** - Use \`index.ts\` for clean public APIs
+4. **Type Safety** - Use strict mode, avoid \`any\`, explicit return types
+
+## File Structure
+
+\`\`\`
+src/
+├── index.ts # Barrel module
+├── utils/
+│ ├── index.ts # Utils barrel
+│ ├── validate.ts # Single function
+│ └── validate.test.ts # Test collocated
+\`\`\`
+
+## TDD Workflow
+
+1. Write a failing test
+2. Implement minimal code to pass
+3. Refactor with confidence
+
+## Testing with Bun
+
+\`\`\`typescript
+import { describe, it, expect } from 'bun:test';
+import { myFunction } from './my-function';
+
+describe('myFunction', () => {
+ it('should handle basic case', () => {
+ expect(myFunction('input')).toBe('expected');
+ });
+});
+\`\`\`
+
+## TypeScript Standards
+
+- Use \`strict: true\` mode
+- Explicit return types for functions
+- Prefer \`type\` for unions, \`interface\` for objects
+- No \`any\` type - use \`unknown\` instead
+
+## Module Patterns
+
+**Single Function Export:**
+\`\`\`typescript
+// utils/validate.ts
+export const validate = (input: string): boolean => {
+ return /^[a-z]+$/.test(input);
+};
+\`\`\`
+
+**Barrel Module:**
+\`\`\`typescript
+// utils/index.ts
+export { validate } from './validate';
+export { format } from './format';
+\`\`\``,
+ checklist: [
+ 'Write tests before implementation',
+ 'Use single-function modules',
+ 'Place tests collocated with implementation',
+ 'Use barrel modules for clean exports',
+ 'Enable strict TypeScript mode',
+ ],
+});
+
+/**
+ * Plain English Communication Skill
+ *
+ * Guidelines for writing technical content in plain English for non-technical stakeholders.
+ */
+export const plainEnglishSkill: Skill = defineSkill({
+ name: 'plain-english',
+ description: 'Write technical content in plain English for non-technical stakeholders',
+ keywords: ['plain-english', 'communication', 'documentation', 'stakeholders', 'business'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ metadata: {
+ category: 'communication',
+ },
+ whatIDo:
+ 'I help you write technical content that non-technical stakeholders can understand, using clear language and simple structure',
+ whenToUseMe:
+ 'Use when communicating technical information to executives, business managers, or non-technical team members',
+ instructions: `## Core Principles
+
+1. **Use Simple Words** - Prefer "use" over "utilize", "help" over "facilitate"
+2. **Short Sentences** - Keep sentences under 25 words when possible
+3. **Active Voice** - "We updated the system" not "The system was updated"
+4. **Define Jargon** - If you must use technical terms, explain them
+
+## Structure
+
+### Executive Summary
+Start with the key takeaway in 2-3 sentences. What decision needs to be made?
+
+### Problem Statement
+What problem are we solving? Why does it matter to the business?
+
+### Solution Overview
+High-level approach without technical details.
+
+### Impact
+- **Benefits**: What improves?
+- **Risks**: What could go wrong?
+- **Timeline**: When will this happen?
+- **Resources**: What do we need?
+
+## Writing Tips
+
+**Avoid:**
+ - Technical jargon without explanation
+ - Passive voice
+ - Long, complex sentences
+ - Acronyms without definitions
+
+ **Use:**
+ - Clear, direct language
+ - Concrete examples
+ - Bullet points for lists
+ - Visual aids (diagrams, charts)
+
+## Example Transformation
+
+**Before (Technical):**
+> The implementation of the new caching layer will leverage Redis to optimize database query performance, reducing latency by approximately 40% in the 95th percentile.
+
+**After (Plain English):**
+> We're adding a new system that remembers frequently-used data. This will make the app faster for most users—pages will load 40% quicker.
+
+## Audience-Specific Guidelines
+
+**For Executives:**
+- Focus on business impact and ROI
+- Use numbers and metrics
+- Keep it brief (1-2 pages max)
+
+**For Business Managers:**
+- Explain process changes
+- Include timelines and dependencies
+- Address operational impacts
+
+**For Compliance/Legal:**
+- Be precise about security and privacy
+- Document controls and safeguards
+- Use clear, unambiguous language`,
+ checklist: [
+ 'Start with an executive summary',
+ 'Use simple words and short sentences',
+ 'Write in active voice',
+ 'Define technical jargon',
+ 'Include specific metrics and timelines',
+ ],
+});
+
+/**
+ * React Component Patterns Skill
+ */
+export const reactPatternsSkill: Skill = defineSkill({
+ name: 'react-patterns',
+ description: 'Modern React component patterns and best practices',
+ keywords: ['react', 'components', 'hooks', 'jsx', 'tsx'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ metadata: {
+ category: 'development',
+ },
+ whatIDo: 'I help you write modern React components using best practices, hooks, and proven design patterns',
+ whenToUseMe: 'Use when building React applications, creating new components, or refactoring existing React code',
+ instructions: `## Component Structure
+
+\`\`\`typescript
+// Prefer arrow function components with hooks
+export const MyComponent = ({ name, onAction }: Props) => {
+ const [state, setState] = useState(initialValue);
+
+ useEffect(() => {
+ // Side effects
+ }, [dependencies]);
+
+ return (
+
+ {/* JSX */}
+
+ );
+};
+\`\`\`
+
+## Patterns
+
+### 1. Container/Presenter Pattern
+Separate logic from presentation:
+
+\`\`\`typescript
+// Container (logic)
+function UserProfileContainer() {
+ const { data, loading } = useUser();
+ return ;
+}
+
+// Presenter (UI)
+function UserProfile({ data, loading }: Props) {
+ if (loading) return ;
+ return {data.name}
;
+}
+\`\`\`
+
+### 2. Custom Hooks
+Extract reusable logic:
+
+\`\`\`typescript
+function useDebounce(value: string, delay: number) {
+ const [debouncedValue, setDebouncedValue] = useState(value);
+
+ useEffect(() => {
+ const timer = setTimeout(() => setDebouncedValue(value), delay);
+ return () => clearTimeout(timer);
+ }, [value, delay]);
+
+ return debouncedValue;
+}
+\`\`\`
+
+### 3. Compound Components
+Components that work together:
+
+\`\`\`typescript
+function Tabs({ children }) {
+ const [activeTab, setActiveTab] = useState(0);
+ return (
+
+ {children}
+
+ );
+}
+
+Tabs.List = function TabsList({ children }) {
+ return {children}
;
+};
+
+Tabs.Tab = function Tab({ index, children }) {
+ const { activeTab, setActiveTab } = useTabsContext();
+ return (
+
+ );
+};
+\`\`\`
+
+## Best Practices
+
+1. **Use TypeScript** - Type your props and state
+2. **Keep Components Small** - Single responsibility
+3. **Avoid Prop Drilling** - Use Context or state management
+4. **Memoize Expensive Calculations** - useMemo, useCallback
+5. **Test Components** - Unit and integration tests`,
+ checklist: [
+ 'Use TypeScript for type safety',
+ 'Keep components small and focused',
+ 'Use custom hooks for reusable logic',
+ 'Avoid prop drilling with Context',
+ 'Memoize expensive calculations',
+ 'Write unit and integration tests',
+ ],
+});
+
+/**
+ * Example skill registry for demonstration
+ */
+export const exampleSkills: Record = {
+ 'typescript-tdd': typescriptTddSkill,
+ 'plain-english': plainEnglishSkill,
+ 'react-patterns': reactPatternsSkill,
+};
diff --git a/packages/opencode-skills/src/index.ts b/packages/opencode-skills/src/index.ts
new file mode 100644
index 0000000..7e97cbb
--- /dev/null
+++ b/packages/opencode-skills/src/index.ts
@@ -0,0 +1,44 @@
+/**
+ * opencode-skills - TypeScript-based skill injection plugin for OpenCode
+ *
+ * This plugin provides automatic skill injection into chat context using smart
+ * pattern matching. Skills are TypeScript-defined knowledge blocks that are
+ * seamlessly injected when the user mentions them with intent.
+ *
+ * Key Features:
+ * - Type-safe skill definitions
+ * - Smart pattern matching with intent detection
+ * - Automatic injection via chat.message hook
+ * - No file system side effects
+ * - Configurable pattern matching behavior
+ *
+ * @see https://github.com/pantheon-org/opencode-skills
+ */
+
+// BM25 utilities
+export { type BM25Index, buildBM25Index, calculateBM25Score, getTopSkillsByBM25, rankSkillsByBM25 } from './bm25/index';
+// Main exports
+export { createSkillsPlugin } from './create-skills-plugin';
+export { defineSkill } from './define-skill';
+// Markdown parser utilities
+export {
+ markdownToSkill,
+ type ParsedSkill,
+ parseSkillMarkdown,
+ type SkillFrontmatter,
+ type SkillSections,
+ skillToMarkdown,
+} from './parsers/index';
+// Pattern matching utilities
+export { findMatchingSkills, hasIntentToUse } from './pattern-matching/index';
+// Type definitions
+export type { BM25Config, MatchResult, Skill, SkillMetadata, SkillsPluginConfig } from './types';
+// Validation utilities
+export {
+ formatValidationResult,
+ type ValidationError,
+ type ValidationResult,
+ type ValidationSuggestion,
+ type ValidationWarning,
+ validateSkill,
+} from './validation';
diff --git a/packages/opencode-skills/src/parsers/extract-frontmatter.ts b/packages/opencode-skills/src/parsers/extract-frontmatter.ts
new file mode 100644
index 0000000..beef759
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/extract-frontmatter.ts
@@ -0,0 +1,42 @@
+/**
+ * Extract YAML Frontmatter
+ *
+ * Extract and parse YAML frontmatter from markdown content.
+ */
+
+import { parse as parseYAML } from 'yaml';
+
+import type { SkillFrontmatter } from './types';
+
+/**
+ * Extract YAML frontmatter from markdown
+ *
+ * @param markdown - Markdown content with YAML frontmatter
+ * @returns Object containing parsed frontmatter and remaining content
+ * @throws Error if no valid YAML frontmatter is found
+ *
+ * @example
+ * ```typescript
+ * const md = `---
+ * name: typescript-tdd
+ * description: TypeScript with TDD
+ * ---
+ * # Content here`;
+ *
+ * const { frontmatter, content } = extractFrontmatter(md);
+ * // => { frontmatter: { name: 'typescript-tdd', ... }, content: '# Content here' }
+ * ```
+ */
+export const extractFrontmatter = (markdown: string): { frontmatter: SkillFrontmatter; content: string } => {
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
+ const match = markdown.match(frontmatterRegex);
+
+ if (!match) {
+ throw new Error('No YAML frontmatter found. Expected format:\n---\nname: skill-name\n...\n---');
+ }
+
+ const [, yamlContent, markdownContent] = match;
+ const frontmatter = parseYAML(yamlContent) as SkillFrontmatter;
+
+ return { frontmatter, content: markdownContent };
+};
diff --git a/packages/opencode-skills/src/parsers/extract-sections.test.ts b/packages/opencode-skills/src/parsers/extract-sections.test.ts
new file mode 100644
index 0000000..7ea9a22
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/extract-sections.test.ts
@@ -0,0 +1,120 @@
+/**
+ * Extract Sections Tests
+ *
+ * Tests for parsing markdown content and extracting structured sections.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { extractSections } from './extract-sections';
+
+describe('extractSections', () => {
+ it('should extract all standard sections', () => {
+ const markdown = `## What I do
+I help with TypeScript development
+
+## When to use me
+Use when building TypeScript projects
+
+## Instructions
+Follow these steps carefully
+
+## Checklist
+- [ ] Step 1
+- [ ] Step 2
+- [ ] Step 3`;
+
+ const sections = extractSections(markdown);
+
+ expect(sections.whatIDo).toBe('I help with TypeScript development');
+ expect(sections.whenToUseMe).toBe('Use when building TypeScript projects');
+ expect(sections.instructions).toBe('Follow these steps carefully');
+ expect(sections.checklist).toEqual(['Step 1', 'Step 2', 'Step 3']);
+ });
+
+ it('should handle markdown with only some sections', () => {
+ const markdown = `## What I do
+Core capabilities description
+
+## Checklist
+- [ ] Item A
+- [ ] Item B`;
+
+ const sections = extractSections(markdown);
+
+ expect(sections.whatIDo).toBe('Core capabilities description');
+ expect(sections.whenToUseMe).toBe('');
+ expect(sections.instructions).toBe('');
+ expect(sections.checklist).toEqual(['Item A', 'Item B']);
+ });
+
+ it('should handle empty content', () => {
+ const sections = extractSections('');
+
+ expect(sections.whatIDo).toBe('');
+ expect(sections.whenToUseMe).toBe('');
+ expect(sections.instructions).toBe('');
+ expect(sections.checklist).toEqual([]);
+ });
+
+ it('should handle content without any recognized sections', () => {
+ const markdown = `## Some Other Section
+This is not a standard section
+
+## Another Section
+More content here`;
+
+ const sections = extractSections(markdown);
+
+ expect(sections.whatIDo).toBe('');
+ expect(sections.whenToUseMe).toBe('');
+ expect(sections.instructions).toBe('');
+ expect(sections.checklist).toEqual([]);
+ });
+
+ it('should handle multiline content in sections', () => {
+ const markdown = `## What I do
+First line
+Second line
+Third line
+
+## When to use me
+Single line`;
+
+ const sections = extractSections(markdown);
+
+ expect(sections.whatIDo).toBe('First line\nSecond line\nThird line');
+ expect(sections.whenToUseMe).toBe('Single line');
+ });
+
+ it('should handle checklist with various item formats', () => {
+ const markdown = `## Checklist
+- [ ] Unchecked item
+- [x] Checked item
+- [ ] Item with - special * characters
+- [ ] Item with extra spaces `;
+
+ const sections = extractSections(markdown);
+
+ expect(sections.checklist).toEqual([
+ 'Unchecked item',
+ 'Checked item',
+ 'Item with - special * characters',
+ 'Item with extra spaces',
+ ]);
+ });
+
+ it('should ignore content before first heading', () => {
+ const markdown = `Some preamble text that should be ignored
+
+## What I do
+This is the actual content
+
+## Instructions
+Follow these steps`;
+
+ const sections = extractSections(markdown);
+
+ expect(sections.whatIDo).toBe('This is the actual content');
+ expect(sections.instructions).toBe('Follow these steps');
+ });
+});
diff --git a/packages/opencode-skills/src/parsers/extract-sections.ts b/packages/opencode-skills/src/parsers/extract-sections.ts
new file mode 100644
index 0000000..f47303f
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/extract-sections.ts
@@ -0,0 +1,63 @@
+/**
+ * Extract Markdown Sections
+ *
+ * Parse markdown content and extract structured sections by heading.
+ */
+
+import { parseChecklistItems } from './parse-checklist-items';
+import { splitByHeadings } from './split-by-headings';
+import type { SkillSections } from './types';
+
+/**
+ * Parse markdown content and extract sections by heading
+ *
+ * Extracts the following sections from markdown:
+ * - ## What I do
+ * - ## When to use me
+ * - ## Instructions
+ * - ## Checklist
+ *
+ * @param markdown - Markdown content to parse
+ * @returns Structured sections object
+ *
+ * @example
+ * ```typescript
+ * const content = `
+ * ## What I do
+ * I help with TypeScript development.
+ *
+ * ## Checklist
+ * - [ ] Write tests
+ * - [ ] Implement feature
+ * `;
+ *
+ * const sections = extractSections(content);
+ * // => { whatIDo: 'I help...', checklist: ['Write tests', 'Implement feature'], ... }
+ * ```
+ */
+export const extractSections = (markdown: string): SkillSections => {
+ const sections: SkillSections = {
+ whatIDo: '',
+ whenToUseMe: '',
+ instructions: '',
+ checklist: [],
+ };
+
+ const parts = splitByHeadings(markdown);
+
+ for (const part of parts) {
+ const heading = part.heading.toLowerCase();
+
+ if (heading === 'what i do') {
+ sections.whatIDo = part.content;
+ } else if (heading === 'when to use me') {
+ sections.whenToUseMe = part.content;
+ } else if (heading === 'instructions') {
+ sections.instructions = part.content;
+ } else if (heading === 'checklist') {
+ sections.checklist = parseChecklistItems(part.content);
+ }
+ }
+
+ return sections;
+};
diff --git a/packages/opencode-skills/src/parsers/index.ts b/packages/opencode-skills/src/parsers/index.ts
new file mode 100644
index 0000000..00d97ce
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/index.ts
@@ -0,0 +1,15 @@
+/**
+ * Markdown Parsers
+ *
+ * Utilities for parsing and serializing skills to/from markdown format.
+ * Supports YAML frontmatter and structured sections.
+ */
+
+export { extractFrontmatter } from './extract-frontmatter';
+export { extractSections } from './extract-sections';
+export { markdownToSkill } from './markdown-to-skill';
+export { parseChecklistItems } from './parse-checklist-items';
+export { parseSkillMarkdown } from './parse-skill-markdown';
+export { skillToMarkdown } from './skill-to-markdown';
+export { splitByHeadings } from './split-by-headings';
+export type { ParsedSkill, SkillFrontmatter, SkillSections } from './types';
diff --git a/packages/opencode-skills/src/parsers/markdown-parser.test.ts b/packages/opencode-skills/src/parsers/markdown-parser.test.ts
new file mode 100644
index 0000000..8c5f930
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/markdown-parser.test.ts
@@ -0,0 +1,182 @@
+/**
+ * Parser Module Tests
+ *
+ * Tests for the markdown parser modules.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { markdownToSkill } from './markdown-to-skill';
+import { parseSkillMarkdown } from './parse-skill-markdown';
+import { skillToMarkdown } from './skill-to-markdown';
+
+describe('parseSkillMarkdown', () => {
+ it('should parse skill from markdown with frontmatter', () => {
+ const markdown = `---
+name: test-skill
+description: A test skill
+version: 1.0.0
+license: MIT
+---
+
+## What I do
+Test the parser
+
+## Checklist
+- [ ] Item 1
+- [ ] Item 2
+`;
+
+ const result = parseSkillMarkdown(markdown);
+
+ expect(result.frontmatter.name).toBe('test-skill');
+ expect(result.frontmatter.description).toBe('A test skill');
+ expect(result.sections.whatIDo).toBe('Test the parser');
+ expect(result.sections.checklist).toEqual(['Item 1', 'Item 2']);
+ });
+
+ it('should handle markdown without frontmatter', () => {
+ const markdown = `## What I do
+Just content here`;
+
+ expect(() => parseSkillMarkdown(markdown)).toThrow('No YAML frontmatter found');
+ });
+
+ it('should parse all standard sections', () => {
+ const markdown = `---
+name: full-skill
+description: Full skill test
+---
+
+## What I do
+Core capabilities
+
+## When to use me
+Use cases
+
+## Instructions
+Follow these steps
+
+## Checklist
+- [ ] Step 1
+- [ ] Step 2
+`;
+
+ const result = parseSkillMarkdown(markdown);
+
+ expect(result.sections.whatIDo).toBe('Core capabilities');
+ expect(result.sections.whenToUseMe).toBe('Use cases');
+ expect(result.sections.instructions).toBe('Follow these steps');
+ expect(result.sections.checklist).toEqual(['Step 1', 'Step 2']);
+ });
+});
+
+describe('markdownToSkill', () => {
+ it('should convert markdown to skill object', () => {
+ const markdown = `---
+name: converted-skill
+description: Converted from markdown
+version: 2.0.0
+license: Apache-2.0
+metadata:
+ category: testing
+---
+
+## What I do
+Conversion test
+
+## Checklist
+- [ ] Test conversion
+`;
+
+ const skill = markdownToSkill(markdown);
+
+ expect(skill.name).toBe('converted-skill');
+ expect(skill.description).toBe('Converted from markdown');
+ expect(skill.version).toBe('2.0.0');
+ expect(skill.license).toBe('Apache-2.0');
+ expect(skill.metadata?.category).toBe('testing');
+ expect(skill.whatIDo).toBe('Conversion test');
+ expect(skill.checklist).toEqual(['Test conversion']);
+ });
+
+ it('should handle minimal skill markdown', () => {
+ const markdown = `---
+name: minimal
+description: Minimal skill
+---
+
+## What I do
+Minimal content
+`;
+
+ const skill = markdownToSkill(markdown);
+
+ expect(skill.name).toBe('minimal');
+ expect(skill.description).toBe('Minimal skill');
+ expect(skill.whatIDo).toBe('Minimal content');
+ expect(skill.checklist).toEqual([]);
+ });
+});
+
+describe('skillToMarkdown', () => {
+ it('should convert skill to markdown', () => {
+ const skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ version: '1.0.0',
+ whatIDo: 'Test functionality',
+ whenToUseMe: 'When testing',
+ instructions: 'Run tests',
+ checklist: ['Setup', 'Execute', 'Verify'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).toContain('---');
+ expect(markdown).toContain('name: test-skill');
+ expect(markdown).toContain('description: A test skill');
+ expect(markdown).toContain('## What I do');
+ expect(markdown).toContain('Test functionality');
+ expect(markdown).toContain('- [ ] Setup');
+ });
+
+ it('should handle skill with metadata', () => {
+ const skill = {
+ name: 'meta-skill',
+ description: 'Skill with metadata',
+ whatIDo: 'Testing',
+ metadata: {
+ category: 'testing',
+ author: 'test-author',
+ },
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).toContain('metadata:');
+ expect(markdown).toContain('category: testing');
+ });
+
+ it('should produce round-trip compatible markdown', () => {
+ const originalSkill = {
+ name: 'round-trip-skill',
+ description: 'Round trip test',
+ version: '1.0.0',
+ whatIDo: 'Test round trip',
+ whenToUseMe: 'Use for testing',
+ instructions: 'Follow steps',
+ checklist: ['Step 1', 'Step 2'],
+ license: 'MIT',
+ };
+
+ const markdown = skillToMarkdown(originalSkill);
+ const parsedSkill = markdownToSkill(markdown);
+
+ expect(parsedSkill.name).toBe(originalSkill.name);
+ expect(parsedSkill.description).toBe(originalSkill.description);
+ expect(parsedSkill.whatIDo).toBe(originalSkill.whatIDo);
+ expect(parsedSkill.checklist).toEqual(originalSkill.checklist);
+ });
+});
diff --git a/packages/opencode-skills/src/parsers/markdown-to-skill.test.ts b/packages/opencode-skills/src/parsers/markdown-to-skill.test.ts
new file mode 100644
index 0000000..60fd845
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/markdown-to-skill.test.ts
@@ -0,0 +1,125 @@
+/**
+ * Markdown to Skill Tests
+ *
+ * Tests for converting markdown content to Skill objects.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { markdownToSkill } from './markdown-to-skill';
+
+describe('markdownToSkill', () => {
+ it('should convert markdown to skill object', () => {
+ const markdown = `---
+name: converted-skill
+description: Converted from markdown
+license: Apache-2.0
+metadata:
+ category: testing
+---
+
+## What I do
+Conversion test
+
+## Checklist
+- [ ] Test conversion
+`;
+
+ const skill = markdownToSkill(markdown);
+
+ expect(skill.name).toBe('converted-skill');
+ expect(skill.description).toBe('Converted from markdown');
+ expect(skill.license).toBe('Apache-2.0');
+ expect(skill.metadata?.category).toBe('testing');
+ expect(skill.whatIDo).toBe('Conversion test');
+ expect(skill.checklist).toEqual(['Test conversion']);
+ });
+
+ it('should handle minimal skill markdown', () => {
+ const markdown = `---
+name: minimal
+description: Minimal skill
+---
+
+## What I do
+Minimal content
+`;
+
+ const skill = markdownToSkill(markdown);
+
+ expect(skill.name).toBe('minimal');
+ expect(skill.description).toBe('Minimal skill');
+ expect(skill.whatIDo).toBe('Minimal content');
+ expect(skill.checklist).toEqual([]);
+ });
+
+ it('should parse all standard sections', () => {
+ const markdown = `---
+name: full-skill
+description: Full skill test
+---
+
+## What I do
+Core capabilities
+
+## When to use me
+Use cases
+
+## Instructions
+Follow these steps
+
+## Checklist
+- [ ] Step 1
+- [ ] Step 2
+`;
+
+ const skill = markdownToSkill(markdown);
+
+ expect(skill.name).toBe('full-skill');
+ expect(skill.description).toBe('Full skill test');
+ expect(skill.whatIDo).toBe('Core capabilities');
+ expect(skill.whenToUseMe).toBe('Use cases');
+ expect(skill.instructions).toBe('Follow these steps');
+ expect(skill.checklist).toEqual(['Step 1', 'Step 2']);
+ });
+
+ it('should set default compatibility to opencode', () => {
+ const markdown = `---
+name: test-skill
+description: Test description
+---
+
+## What I do
+Test content
+`;
+
+ const skill = markdownToSkill(markdown);
+
+ expect(skill.compatibility).toBe('opencode');
+ });
+
+ it('should set timestamp on conversion', () => {
+ const markdown = `---
+name: test-skill
+description: Test description
+---
+
+## What I do
+Test content
+`;
+
+ const before = new Date().toISOString();
+ const skill = markdownToSkill(markdown);
+ const after = new Date().toISOString();
+
+ expect(skill.updatedAt).toBeDefined();
+ expect(skill.updatedAt! >= before).toBe(true);
+ expect(skill.updatedAt! <= after).toBe(true);
+ });
+
+ it('should throw error for markdown without frontmatter', () => {
+ const markdown = `## What I do
+Just content here`;
+
+ expect(() => markdownToSkill(markdown)).toThrow('No YAML frontmatter found');
+ });
+});
diff --git a/packages/opencode-skills/src/parsers/markdown-to-skill.ts b/packages/opencode-skills/src/parsers/markdown-to-skill.ts
new file mode 100644
index 0000000..8040db2
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/markdown-to-skill.ts
@@ -0,0 +1,47 @@
+/**
+ * Markdown to Skill Converter
+ *
+ * Convert markdown content to Skill object.
+ */
+
+import type { Skill } from '../types';
+import { parseSkillMarkdown } from './parse-skill-markdown';
+
+/**
+ * Convert parsed markdown to Skill object
+ *
+ * @param markdown - Complete markdown content with YAML frontmatter
+ * @returns Fully structured Skill object ready for use
+ *
+ * @example
+ * ```typescript
+ * const markdown = `---
+ * name: typescript-tdd
+ * description: TypeScript with TDD
+ * ---
+ *
+ * ## What I do
+ * I help with TypeScript development.
+ * `;
+ *
+ * const skill = markdownToSkill(markdown);
+ * // => { name: 'typescript-tdd', description: '...', whatIDo: '...', ... }
+ * ```
+ */
+export const markdownToSkill = (markdown: string): Skill => {
+ const { frontmatter, sections } = parseSkillMarkdown(markdown);
+
+ return {
+ name: frontmatter.name,
+ description: frontmatter.description,
+ version: frontmatter.version ?? '1.0.0',
+ updatedAt: new Date().toISOString(),
+ license: frontmatter.license,
+ compatibility: frontmatter.compatibility ?? 'opencode',
+ metadata: frontmatter.metadata,
+ whatIDo: sections.whatIDo,
+ whenToUseMe: sections.whenToUseMe,
+ instructions: sections.instructions,
+ checklist: sections.checklist,
+ };
+};
diff --git a/packages/opencode-skills/src/parsers/parse-checklist-items.ts b/packages/opencode-skills/src/parsers/parse-checklist-items.ts
new file mode 100644
index 0000000..5f0244e
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/parse-checklist-items.ts
@@ -0,0 +1,38 @@
+/**
+ * Parse Checklist Items
+ *
+ * Extract checklist items from markdown content.
+ */
+
+/**
+ * Parse checklist items from markdown content
+ *
+ * Extracts items in format `- [ ] Item` or `- [x] Item`
+ *
+ * @param content - Markdown content containing checklist
+ * @returns Array of checklist item strings
+ *
+ * @example
+ * ```typescript
+ * const content = `
+ * - [ ] Write tests
+ * - [x] Implement feature
+ * - [ ] Review code
+ * `;
+ *
+ * const items = parseChecklistItems(content);
+ * // => ['Write tests', 'Implement feature', 'Review code']
+ * ```
+ */
+export const parseChecklistItems = (content: string): string[] => {
+ const checklistRegex = /^[-*]\s+\[[ x]\]\s+(.+)$/gm;
+ const items: string[] = [];
+ let match: RegExpExecArray | null = checklistRegex.exec(content);
+
+ while (match !== null) {
+ items.push(match[1].trim());
+ match = checklistRegex.exec(content);
+ }
+
+ return items;
+};
diff --git a/packages/opencode-skills/src/parsers/parse-skill-markdown.ts b/packages/opencode-skills/src/parsers/parse-skill-markdown.ts
new file mode 100644
index 0000000..7a4219c
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/parse-skill-markdown.ts
@@ -0,0 +1,37 @@
+/**
+ * Parse Skill Markdown
+ *
+ * Main parser to convert SKILL.md file into structured ParsedSkill object.
+ */
+
+import { extractFrontmatter } from './extract-frontmatter';
+import { extractSections } from './extract-sections';
+import type { ParsedSkill } from './types';
+
+/**
+ * Parse SKILL.md file and convert to ParsedSkill object
+ *
+ * @param markdown - Complete markdown content with YAML frontmatter
+ * @returns Structured ParsedSkill with frontmatter and sections
+ *
+ * @example
+ * ```typescript
+ * const markdown = `---
+ * name: typescript-tdd
+ * description: TypeScript with TDD
+ * ---
+ *
+ * ## What I do
+ * I help with TypeScript development.
+ * `;
+ *
+ * const parsed = parseSkillMarkdown(markdown);
+ * // => { frontmatter: { name: 'typescript-tdd', ... }, sections: { whatIDo: '...', ... } }
+ * ```
+ */
+export const parseSkillMarkdown = (markdown: string): ParsedSkill => {
+ const { frontmatter, content } = extractFrontmatter(markdown);
+ const sections = extractSections(content);
+
+ return { frontmatter, sections };
+};
diff --git a/packages/opencode-skills/src/parsers/skill-to-markdown.test.ts b/packages/opencode-skills/src/parsers/skill-to-markdown.test.ts
new file mode 100644
index 0000000..641f3cc
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/skill-to-markdown.test.ts
@@ -0,0 +1,112 @@
+/**
+ * Skill to Markdown Tests
+ *
+ * Tests for converting Skill objects to markdown format.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { markdownToSkill } from './markdown-to-skill';
+import { skillToMarkdown } from './skill-to-markdown';
+
+describe('skillToMarkdown', () => {
+ it('should convert skill to markdown', () => {
+ const skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Test functionality',
+ whenToUseMe: 'When testing',
+ instructions: 'Run tests',
+ checklist: ['Setup', 'Execute', 'Verify'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).toContain('---');
+ expect(markdown).toContain('name: test-skill');
+ expect(markdown).toContain('description: A test skill');
+ expect(markdown).toContain('## What I do');
+ expect(markdown).toContain('Test functionality');
+ expect(markdown).toContain('- [ ] Setup');
+ });
+
+ it('should handle skill with metadata', () => {
+ const skill = {
+ name: 'meta-skill',
+ description: 'Skill with metadata',
+ whatIDo: 'Testing',
+ metadata: {
+ category: 'testing',
+ author: 'test-author',
+ },
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).toContain('metadata:');
+ expect(markdown).toContain('category: testing');
+ });
+
+ it('should produce round-trip compatible markdown', () => {
+ const originalSkill = {
+ name: 'round-trip-skill',
+ description: 'Round trip test',
+ whatIDo: 'Test round trip',
+ whenToUseMe: 'Use for testing',
+ instructions: 'Follow steps',
+ checklist: ['Step 1', 'Step 2'],
+ license: 'MIT',
+ };
+
+ const markdown = skillToMarkdown(originalSkill);
+ const parsedSkill = markdownToSkill(markdown);
+
+ expect(parsedSkill.name).toBe(originalSkill.name);
+ expect(parsedSkill.description).toBe(originalSkill.description);
+ expect(parsedSkill.whatIDo).toBe(originalSkill.whatIDo);
+ expect(parsedSkill.checklist).toEqual(originalSkill.checklist);
+ });
+
+ it('should omit optional fields when not provided', () => {
+ const skill = {
+ name: 'minimal-skill',
+ description: 'Minimal skill',
+ whatIDo: 'Do minimal things',
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).not.toContain('license:');
+ expect(markdown).not.toContain('compatibility:');
+ expect(markdown).not.toContain('metadata:');
+ });
+
+ it('should handle empty checklist', () => {
+ const skill = {
+ name: 'no-checklist-skill',
+ description: 'Skill without checklist',
+ whatIDo: 'Work without checklist',
+ checklist: [],
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).toContain('## Checklist');
+ });
+
+ it('should handle skill with special characters in content', () => {
+ const skill = {
+ name: 'special-skill',
+ description: 'Skill with special chars: "quotes" and \'apostrophes\'',
+ whatIDo: 'Handle content with: colons, dashes - and asterisks *',
+ checklist: ['Item with "quotes"', "Item with 'apostrophes'"],
+ };
+
+ const markdown = skillToMarkdown(skill);
+
+ expect(markdown).toContain('---');
+ expect(markdown).toContain('name: special-skill');
+ expect(markdown).toContain('- [ ] Item with "quotes"');
+ });
+});
diff --git a/packages/opencode-skills/src/parsers/skill-to-markdown.ts b/packages/opencode-skills/src/parsers/skill-to-markdown.ts
new file mode 100644
index 0000000..e6c2817
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/skill-to-markdown.ts
@@ -0,0 +1,68 @@
+/**
+ * Skill to Markdown Converter
+ *
+ * Convert Skill object back to markdown format.
+ */
+
+import { stringify as stringifyYAML } from 'yaml';
+
+import type { Skill } from '../types';
+
+/**
+ * Convert Skill object back to markdown format
+ *
+ * @param skill - Skill object to convert
+ * @returns Markdown string with YAML frontmatter
+ *
+ * @example
+ * ```typescript
+ * const skill = {
+ * name: 'typescript-tdd',
+ * description: 'TypeScript with TDD',
+ * whatIDo: 'I help with TypeScript development.',
+ * checklist: ['Write tests', 'Implement feature'],
+ * };
+ *
+ * const markdown = skillToMarkdown(skill);
+ * // Returns:
+ * // ---
+ * // name: typescript-tdd
+ * // description: TypeScript with TDD
+ * // ---
+ * //
+ * // # typescript-tdd
+ * //
+ * // ## What I do
+ * // I help with TypeScript development.
+ * // ...
+ * ```
+ */
+export const skillToMarkdown = (skill: Skill): string => {
+ const frontmatter = {
+ name: skill.name,
+ description: skill.description,
+ ...(skill.license && { license: skill.license }),
+ ...(skill.compatibility && { compatibility: skill.compatibility }),
+ ...(skill.metadata && { metadata: skill.metadata }),
+ };
+
+ const yamlFrontmatter = stringifyYAML(frontmatter);
+
+ const content = `
+# ${skill.name}
+
+## What I do
+${skill.whatIDo || ''}
+
+## When to use me
+${skill.whenToUseMe || ''}
+
+## Instructions
+${skill.instructions || ''}
+
+## Checklist
+${skill.checklist?.map((item) => `- [ ] ${item}`).join('\n') || ''}
+ `.trim();
+
+ return `---\n${yamlFrontmatter}---\n\n${content}`;
+};
diff --git a/packages/opencode-skills/src/parsers/split-by-headings.ts b/packages/opencode-skills/src/parsers/split-by-headings.ts
new file mode 100644
index 0000000..d5ac84d
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/split-by-headings.ts
@@ -0,0 +1,54 @@
+/**
+ * Split Markdown by Headings
+ *
+ * Parse markdown and split into heading/content pairs.
+ */
+
+interface HeadingPart {
+ heading: string;
+ content: string;
+}
+
+/**
+ * Split markdown content by ## headings
+ *
+ * @param markdown - Markdown content to parse
+ * @returns Array of heading/content pairs
+ *
+ * @example
+ * ```typescript
+ * const content = `
+ * ## What I do
+ * Content here
+ * ## Checklist
+ * - Item 1
+ * `;
+ *
+ * const parts = splitByHeadings(content);
+ * // => [{ heading: 'What I do', content: 'Content here' }, ...]
+ * ```
+ */
+export const splitByHeadings = (markdown: string): HeadingPart[] => {
+ const headingRegex = /^##\s+(.+?)$/gm;
+ const parts: HeadingPart[] = [];
+
+ let lastIndex = 0;
+ let match: RegExpExecArray | null = headingRegex.exec(markdown);
+
+ while (match !== null) {
+ if (lastIndex > 0) {
+ const previousHeading = parts[parts.length - 1];
+ previousHeading.content = markdown.slice(lastIndex, match.index).trim();
+ }
+ parts.push({ heading: match[1].trim(), content: '' });
+ lastIndex = match.index + match[0].length;
+ match = headingRegex.exec(markdown);
+ }
+
+ // Get content for last heading
+ if (parts.length > 0) {
+ parts[parts.length - 1].content = markdown.slice(lastIndex).trim();
+ }
+
+ return parts;
+};
diff --git a/packages/opencode-skills/src/parsers/types.ts b/packages/opencode-skills/src/parsers/types.ts
new file mode 100644
index 0000000..c297e9b
--- /dev/null
+++ b/packages/opencode-skills/src/parsers/types.ts
@@ -0,0 +1,37 @@
+/**
+ * Markdown Parser Type Definitions
+ *
+ * Type definitions for skill markdown parsing.
+ */
+
+import type { SkillMetadata } from '../types';
+
+/**
+ * Parsed skill structure with frontmatter and sections separated
+ */
+export interface ParsedSkill {
+ frontmatter: SkillFrontmatter;
+ sections: SkillSections;
+}
+
+/**
+ * Skill YAML frontmatter
+ */
+export interface SkillFrontmatter {
+ name: string;
+ description: string;
+ version?: string;
+ license?: string;
+ compatibility?: string;
+ metadata?: SkillMetadata;
+}
+
+/**
+ * Skill markdown sections
+ */
+export interface SkillSections {
+ whatIDo: string;
+ whenToUseMe: string;
+ instructions: string;
+ checklist: string[];
+}
diff --git a/packages/opencode-skills/src/pattern-matching/escape-regex.test.ts b/packages/opencode-skills/src/pattern-matching/escape-regex.test.ts
new file mode 100644
index 0000000..66ddc7e
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/escape-regex.test.ts
@@ -0,0 +1,77 @@
+/**
+ * Escape Regex Tests
+ *
+ * Tests for regex escaping functionality.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { escapeRegex } from './escape-regex';
+
+describe('escapeRegex', () => {
+ it('should escape dots', () => {
+ expect(escapeRegex('test.txt')).toBe('test\\.txt');
+ expect(escapeRegex('file.name')).toBe('file\\.name');
+ });
+
+ it('should escape asterisks', () => {
+ expect(escapeRegex('a*b')).toBe('a\\*b');
+ expect(escapeRegex('**')).toBe('\\*\\*');
+ });
+
+ it('should escape plus signs', () => {
+ expect(escapeRegex('a+b')).toBe('a\\+b');
+ expect(escapeRegex('c++')).toBe('c\\+\\+');
+ });
+
+ it('should escape question marks', () => {
+ expect(escapeRegex('what?')).toBe('what\\?');
+ expect(escapeRegex('??')).toBe('\\?\\?');
+ });
+
+ it('should escape caret', () => {
+ expect(escapeRegex('^start')).toBe('\\^start');
+ expect(escapeRegex('a^2')).toBe('a\\^2');
+ });
+
+ it('should escape dollar sign', () => {
+ expect(escapeRegex('$price')).toBe('\\$price');
+ expect(escapeRegex('100$')).toBe('100\\$');
+ });
+
+ it('should escape braces and brackets', () => {
+ expect(escapeRegex('test{1,2}')).toBe('test\\{1,2\\}');
+ expect(escapeRegex('[test]')).toBe('\\[test\\]');
+ expect(escapeRegex('(test)')).toBe('\\(test\\)');
+ });
+
+ it('should escape pipe', () => {
+ expect(escapeRegex('a|b')).toBe('a\\|b');
+ expect(escapeRegex('||')).toBe('\\|\\|');
+ });
+
+ it('should escape backslash', () => {
+ expect(escapeRegex('path\\to\\file')).toBe('path\\\\to\\\\file');
+ });
+
+ it('should handle empty string', () => {
+ expect(escapeRegex('')).toBe('');
+ });
+
+ it('should handle string with no special chars', () => {
+ expect(escapeRegex('skill-name')).toBe('skill-name');
+ expect(escapeRegex('typescript')).toBe('typescript');
+ expect(escapeRegex('test123')).toBe('test123');
+ });
+
+ it('should handle complex mixed patterns', () => {
+ expect(escapeRegex('(a+b)*c?')).toBe('\\(a\\+b\\)\\*c\\?');
+ expect(escapeRegex('[a-z]+')).toBe('\\[a-z\\]\\+');
+ expect(escapeRegex('test[1].txt')).toBe('test\\[1\\]\\.txt');
+ });
+
+ it('should handle skill names with special chars', () => {
+ expect(escapeRegex('regex.utils')).toBe('regex\\.utils');
+ expect(escapeRegex('file[test]')).toBe('file\\[test\\]');
+ expect(escapeRegex('test+v1.0')).toBe('test\\+v1\\.0');
+ });
+});
diff --git a/packages/opencode-skills/src/pattern-matching/escape-regex.ts b/packages/opencode-skills/src/pattern-matching/escape-regex.ts
new file mode 100644
index 0000000..ddd2064
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/escape-regex.ts
@@ -0,0 +1,24 @@
+/**
+ * Escape Regex
+ *
+ * Escape special regex characters in a string for safe pattern matching.
+ */
+
+/**
+ * Escape special regex characters in a string
+ *
+ * @param str - String to escape
+ * @returns Escaped string safe for use in RegExp
+ *
+ * @example
+ * ```typescript
+ * escapeRegex('test.txt')
+ * // => 'test\\.txt'
+ *
+ * escapeRegex('skill-name')
+ * // => 'skill-name'
+ * ```
+ */
+export const escapeRegex = (str: string): string => {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+};
diff --git a/packages/opencode-skills/src/pattern-matching/find-matching-skills.test.ts b/packages/opencode-skills/src/pattern-matching/find-matching-skills.test.ts
new file mode 100644
index 0000000..f4a3cfa
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/find-matching-skills.test.ts
@@ -0,0 +1,104 @@
+/**
+ * Find Matching Skills Tests
+ *
+ * Tests for batch skill matching with edge cases.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { findMatchingSkills } from './find-matching-skills';
+
+describe('findMatchingSkills', () => {
+ it('should find matching skill from array', () => {
+ const skills = ['typescript-tdd', 'react-hooks', 'node-api'];
+ const result = findMatchingSkills('Use typescript-tdd', skills);
+ expect(result).toEqual(['typescript-tdd']);
+ });
+
+ it('should return empty array when no matches', () => {
+ const skills = ['typescript-tdd', 'react-hooks'];
+ const result = findMatchingSkills('Use python-flask', skills);
+ expect(result).toEqual([]);
+ });
+
+ it('should find multiple matching skills', () => {
+ const skills = ['typescript-tdd', 'react-hooks', 'node-api'];
+ const result = findMatchingSkills('Use typescript-tdd and react-hooks', skills);
+ expect(result).toEqual(['typescript-tdd', 'react-hooks']);
+ });
+
+ it('should handle empty skills array', () => {
+ const result = findMatchingSkills('Use typescript-tdd', []);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle empty content', () => {
+ const skills = ['typescript-tdd', 'react-hooks'];
+ const result = findMatchingSkills('', skills);
+ expect(result).toEqual([]);
+ });
+
+ it('should use custom keywords map', () => {
+ const skills = ['typescript-tdd', 'react-hooks'];
+ const keywords = new Map([
+ ['typescript-tdd', ['tdd', 'test-driven']],
+ ['react-hooks', ['hooks', 'react']],
+ ]);
+ const result = findMatchingSkills('Use TDD approach', skills, keywords);
+ expect(result).toEqual(['typescript-tdd']);
+ });
+
+ it('should handle skills without keywords in map', () => {
+ const skills = ['typescript-tdd', 'react-hooks'];
+ const keywords = new Map([['typescript-tdd', ['tdd']]]);
+ const result = findMatchingSkills('Use react-hooks', skills, keywords);
+ expect(result).toEqual(['react-hooks']);
+ });
+
+ it('should handle special characters in content', () => {
+ const skills = ['regex.utils', 'file[test]'];
+ const result = findMatchingSkills('Use regex.utils pattern', skills);
+ expect(result).toEqual(['regex.utils']);
+ });
+
+ it('should handle large skills array', () => {
+ const skills = Array.from({ length: 100 }, (_, i) => `skill-${i}`);
+ const result = findMatchingSkills('Use skill-50', skills);
+ expect(result).toEqual(['skill-50']);
+ });
+
+ it('should maintain order of matches', () => {
+ const skills = ['typescript-tdd', 'react-hooks', 'node-api'];
+ const result = findMatchingSkills('Use node-api with typescript-tdd', skills);
+ expect(result).toEqual(['typescript-tdd', 'node-api']);
+ });
+
+ it('should not match skills in negated phrases', () => {
+ const skills = ['typescript-tdd', 'react-hooks'];
+ // With negation detection enabled, neither skill matches due to negation
+ const result = findMatchingSkills("Don't use typescript-tdd or react-hooks", skills);
+ expect(result).toEqual([]);
+ });
+
+ it('should handle mixed case skill names', () => {
+ const skills = ['TypeScript-TDD', 'React-Hooks'];
+ const result = findMatchingSkills('Use typescript-tdd', skills);
+ expect(result).toEqual(['TypeScript-TDD']);
+ });
+
+ it('should handle config options', () => {
+ const skills = ['typescript-tdd', 'react-hooks'];
+ const result = findMatchingSkills('Use typescript-tdd', skills, new Map(), {
+ wordBoundary: true,
+ intentDetection: true,
+ negationDetection: true,
+ });
+ expect(result).toEqual(['typescript-tdd']);
+ });
+
+ it('should handle very long content', () => {
+ const skills = ['typescript-tdd'];
+ const longContent = 'Use typescript-tdd'.repeat(1000);
+ const result = findMatchingSkills(longContent, skills);
+ expect(result).toEqual(['typescript-tdd']);
+ });
+});
diff --git a/packages/opencode-skills/src/pattern-matching/find-matching-skills.ts b/packages/opencode-skills/src/pattern-matching/find-matching-skills.ts
new file mode 100644
index 0000000..78674ff
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/find-matching-skills.ts
@@ -0,0 +1,49 @@
+/**
+ * Find Matching Skills
+ *
+ * Batch check multiple skills for pattern matches against content.
+ */
+
+import type { SkillsPluginConfig } from '../types';
+import { hasIntentToUse } from './has-intent-to-use';
+
+/**
+ * Batch check multiple skills for pattern matches
+ *
+ * @param content - The message content to analyze
+ * @param skillNames - Array of skill names to check
+ * @param skillKeywords - Map of skill names to additional keywords
+ * @param config - Optional configuration for pattern matching
+ * @returns Array of skill names that match
+ *
+ * @example
+ * ```typescript
+ * const skills = ['typescript-tdd', 'bun-runtime'];
+ * const keywords = new Map([
+ * ['typescript-tdd', ['TDD', 'test-driven']],
+ * ['bun-runtime', ['bun', 'runtime']],
+ * ]);
+ *
+ * findMatchingSkills('Use TDD approach', skills, keywords)
+ * // => ['typescript-tdd']
+ * ```
+ */
+export const findMatchingSkills = (
+ content: string,
+ skillNames: string[],
+ skillKeywords: Map = new Map(),
+ config?: SkillsPluginConfig['patternMatching'],
+): string[] => {
+ const matchingSkills: string[] = [];
+
+ for (const skillName of skillNames) {
+ const keywords = skillKeywords.get(skillName) || [];
+ const result = hasIntentToUse(content, skillName, keywords, config);
+
+ if (result.matches) {
+ matchingSkills.push(skillName);
+ }
+ }
+
+ return matchingSkills;
+};
diff --git a/packages/opencode-skills/src/pattern-matching/has-intent-to-use.test.ts b/packages/opencode-skills/src/pattern-matching/has-intent-to-use.test.ts
new file mode 100644
index 0000000..02cb884
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/has-intent-to-use.test.ts
@@ -0,0 +1,135 @@
+/**
+ * Has Intent To Use Tests
+ *
+ * Tests for intent detection with edge cases and negation.
+ */
+
+import { describe, expect, it } from 'bun:test';
+import { hasIntentToUse } from './has-intent-to-use';
+
+describe('hasIntentToUse', () => {
+ it('should match skill name with word boundaries', () => {
+ const result = hasIntentToUse('Use typescript-tdd skill', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ expect(result.matchedPattern).toBe('word-boundary');
+ });
+
+ it('should match skill name even within longer strings', () => {
+ // Implementation matches skill name regardless of context
+ const result = hasIntentToUse('Use my-typescript-tdd-test', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should detect skill name with intent nearby', () => {
+ // Implementation detects by word boundary primarily
+ const result = hasIntentToUse('I want to use typescript-tdd', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ expect(result.matchedPattern).toBe('word-boundary');
+ });
+
+ it('should detect skill name with trailing words', () => {
+ // Implementation detects by word boundary
+ const result = hasIntentToUse('typescript-tdd approach recommended', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ expect(result.matchedPattern).toBe('word-boundary');
+ });
+
+ it('should detect custom keywords', () => {
+ const result = hasIntentToUse('Use TDD approach', 'typescript-tdd', ['tdd', 'test-driven']);
+ expect(result.matches).toBe(true);
+ expect(result.matchedPattern).toBe('keyword:tdd');
+ });
+
+ it('should handle negation before skill name', () => {
+ const result = hasIntentToUse("Don't use typescript-tdd", 'typescript-tdd');
+ expect(result.matches).toBe(false);
+ expect(result.hasNegation).toBe(true);
+ });
+
+ it('should handle negation after skill name', () => {
+ const result = hasIntentToUse('Skip typescript-tdd for now', 'typescript-tdd');
+ expect(result.matches).toBe(false);
+ expect(result.hasNegation).toBe(true);
+ });
+
+ it('should handle empty content', () => {
+ const result = hasIntentToUse('', 'typescript-tdd');
+ expect(result.matches).toBe(false);
+ expect(result.hasNegation).toBe(false);
+ });
+
+ it('should handle whitespace-only content', () => {
+ const result = hasIntentToUse(' \n\t ', 'typescript-tdd');
+ expect(result.matches).toBe(false);
+ });
+
+ it('should handle case insensitivity', () => {
+ const result = hasIntentToUse('USE TypeScript-TDD', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should handle skill names with hyphens', () => {
+ const result = hasIntentToUse('Use my-custom-skill', 'my-custom-skill');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should handle skill names with dots', () => {
+ const result = hasIntentToUse('Use regex.utils pattern', 'regex.utils');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should handle multiple skill mentions', () => {
+ const result = hasIntentToUse('Use typescript-tdd or typescript-tdd again', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should handle special characters in content', () => {
+ const result = hasIntentToUse('Use typescript-tdd (v2.0)!', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should handle unicode content', () => {
+ const result = hasIntentToUse('Use typescript-tdd 技能', 'typescript-tdd');
+ expect(result.matches).toBe(true);
+ });
+
+ it('should handle disabled word boundary matching', () => {
+ const result = hasIntentToUse('typescript-tdd', 'typescript-tdd', [], { wordBoundary: false });
+ expect(result.matches).toBe(false); // No patterns without word boundary
+ });
+
+ it('should handle disabled intent detection', () => {
+ const result = hasIntentToUse('Use typescript-tdd', 'typescript-tdd', [], { intentDetection: false });
+ expect(result.matches).toBe(true); // Still matches word boundary
+ });
+
+ it('should handle disabled negation detection', () => {
+ const result = hasIntentToUse("Don't use typescript-tdd", 'typescript-tdd', [], { negationDetection: false });
+ expect(result.matches).toBe(true); // Ignores negation
+ expect(result.hasNegation).toBe(false);
+ });
+
+ it('should handle custom intent keywords', () => {
+ // With custom keywords, still matches via word boundary
+ const result = hasIntentToUse('Deploy typescript-tdd', 'typescript-tdd', [], {
+ customIntentKeywords: ['deploy', 'ship'],
+ });
+ expect(result.matches).toBe(true);
+ expect(result.matchedPattern).toBe('word-boundary');
+ });
+
+ it('should handle custom negation keywords', () => {
+ const result = hasIntentToUse('Bypass typescript-tdd', 'typescript-tdd', [], {
+ customNegationKeywords: ['bypass', 'circumvent'],
+ });
+ expect(result.matches).toBe(false);
+ expect(result.hasNegation).toBe(true);
+ });
+
+ it('should handle multiple matching patterns', () => {
+ const result = hasIntentToUse('Use typescript-tdd with tests', 'typescript-tdd', ['tests', 'tdd']);
+ expect(result.matches).toBe(true);
+ // Should match the first pattern found
+ expect(result.matchedPattern).toBeDefined();
+ });
+});
diff --git a/packages/opencode-skills/src/pattern-matching/has-intent-to-use.ts b/packages/opencode-skills/src/pattern-matching/has-intent-to-use.ts
new file mode 100644
index 0000000..de97a12
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/has-intent-to-use.ts
@@ -0,0 +1,133 @@
+/**
+ * Has Intent To Use
+ *
+ * Check if content has intent to use a skill based on pattern matching.
+ * Uses multiple strategies including word boundaries, intent detection, and negation detection.
+ */
+
+import type { MatchResult, SkillsPluginConfig } from '../types';
+import { escapeRegex } from './escape-regex';
+
+/**
+ * Default intent keywords that signal user wants to use a skill
+ */
+const DEFAULT_INTENT_KEYWORDS = ['use', 'apply', 'follow', 'implement', 'load', 'get', 'show', 'with'];
+
+/**
+ * Default negation keywords that signal user wants to avoid a skill
+ */
+const DEFAULT_NEGATION_KEYWORDS = ["don't", 'do not', 'avoid', 'skip', 'ignore', 'without', 'except', 'excluding'];
+
+/**
+ * Check if content has intent to use a skill based on pattern matching
+ *
+ * This function uses multiple strategies to detect if a user message
+ * indicates they want to use a particular skill:
+ *
+ * 1. Word boundary matching - exact skill name with word boundaries
+ * 2. Intent detection - skill name preceded/followed by intent keywords
+ * 3. Negation detection - rejects matches with negation keywords
+ * 4. Keyword matching - optional custom keywords for enhanced detection
+ *
+ * @param content - The message content to analyze
+ * @param skillName - The name of the skill to match
+ * @param keywords - Optional additional keywords to match
+ * @param config - Optional configuration for pattern matching
+ * @returns MatchResult indicating if skill should be injected
+ *
+ * @example
+ * ```typescript
+ * hasIntentToUse('Please use typescript-tdd', 'typescript-tdd')
+ * // => { matches: true, matchedPattern: 'intent-before:use', hasNegation: false }
+ *
+ * hasIntentToUse("Don't use typescript-tdd", 'typescript-tdd')
+ * // => { matches: false, hasNegation: true }
+ * ```
+ */
+export const hasIntentToUse = (
+ content: string,
+ skillName: string,
+ keywords: string[] = [],
+ config?: SkillsPluginConfig['patternMatching'],
+): MatchResult => {
+ // Normalize content for matching
+ const normalizedContent = content.toLowerCase();
+ const normalizedSkillName = skillName.toLowerCase();
+ const escapedSkillName = escapeRegex(normalizedSkillName);
+
+ // Default config values
+ const wordBoundary = config?.wordBoundary ?? true;
+ const intentDetection = config?.intentDetection ?? true;
+ const negationDetection = config?.negationDetection ?? true;
+
+ const patterns: Array<{ regex: RegExp; description: string }> = [];
+
+ // Pattern 1: Word boundary match (exact skill name)
+ if (wordBoundary) {
+ patterns.push({
+ regex: new RegExp(`\\b${escapedSkillName}\\b`, 'i'),
+ description: 'word-boundary',
+ });
+ }
+
+ // Pattern 2: Intent detection patterns
+ if (intentDetection) {
+ const intentKeywords = [...DEFAULT_INTENT_KEYWORDS, ...(config?.customIntentKeywords || [])];
+
+ for (const keyword of intentKeywords) {
+ // Intent keyword before skill name: "use typescript-tdd"
+ patterns.push({
+ regex: new RegExp(`\\b${keyword}\\b.*\\b${escapedSkillName}\\b`, 'i'),
+ description: `intent-before:${keyword}`,
+ });
+
+ // Intent keyword after skill name: "typescript-tdd approach"
+ patterns.push({
+ regex: new RegExp(`\\b${escapedSkillName}\\b.{0,20}\\b${keyword}\\b`, 'i'),
+ description: `intent-after:${keyword}`,
+ });
+ }
+ }
+
+ // Pattern 3: Match additional keywords if provided
+ for (const keyword of keywords) {
+ const escapedKeyword = escapeRegex(keyword.toLowerCase());
+ patterns.push({
+ regex: new RegExp(`\\b${escapedKeyword}\\b`, 'i'),
+ description: `keyword:${keyword}`,
+ });
+ }
+
+ // Check if any pattern matches
+ let matchedPattern: string | undefined;
+ const matches = patterns.some((pattern) => {
+ if (pattern.regex.test(normalizedContent)) {
+ matchedPattern = pattern.description;
+ return true;
+ }
+ return false;
+ });
+
+ // If no match, return early
+ if (!matches) {
+ return { matches: false, hasNegation: false };
+ }
+
+ // Check for negation (if enabled)
+ let hasNegation = false;
+ if (negationDetection) {
+ const negationKeywords = [...DEFAULT_NEGATION_KEYWORDS, ...(config?.customNegationKeywords || [])];
+
+ hasNegation = negationKeywords.some((negWord) => {
+ // Check if negation appears before skill name
+ const negPattern = new RegExp(`\\b${escapeRegex(negWord)}\\b.{0,50}\\b${escapedSkillName}\\b`, 'i');
+ return negPattern.test(normalizedContent);
+ });
+ }
+
+ return {
+ matches: matches && !hasNegation,
+ matchedPattern,
+ hasNegation,
+ };
+};
diff --git a/packages/opencode-skills/src/pattern-matching/index.ts b/packages/opencode-skills/src/pattern-matching/index.ts
new file mode 100644
index 0000000..88a089f
--- /dev/null
+++ b/packages/opencode-skills/src/pattern-matching/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Smart Pattern Matching for Skill Detection
+ *
+ * This module provides intelligent pattern matching for detecting user intent
+ * to use specific skills in their messages. It uses multiple strategies including:
+ *
+ * - Word boundary matching for exact skill names
+ * - Intent detection with configurable keywords
+ * - Negation detection to avoid false positives
+ * - Custom keyword matching for aliases
+ *
+ * @see https://github.com/pantheon-org/opencode-skills
+ */
+
+export { escapeRegex } from './escape-regex';
+export { findMatchingSkills } from './find-matching-skills';
+export { hasIntentToUse } from './has-intent-to-use';
diff --git a/packages/opencode-skills/src/types.ts b/packages/opencode-skills/src/types.ts
new file mode 100644
index 0000000..862c9b7
--- /dev/null
+++ b/packages/opencode-skills/src/types.ts
@@ -0,0 +1,191 @@
+/**
+ * Skill type definitions
+ */
+
+/**
+ * Metadata for a skill
+ */
+export interface SkillMetadata {
+ /** Skill category (workflow, development, documentation, testing, deployment, security) */
+ category: string;
+ /** Additional metadata fields */
+ [key: string]: string;
+}
+
+/**
+ * A skill represents a reusable piece of knowledge or guidance
+ * that can be automatically injected into chat context when needed.
+ */
+export interface Skill {
+ /** Unique identifier for the skill (kebab-case) */
+ name: string;
+
+ /** Brief description of what the skill provides */
+ description: string;
+
+ /** Core capabilities (What I do section) - required in v2 */
+ whatIDo?: string;
+
+ /** Conditions that should trigger this skill (When to use me section) - required in v2 */
+ whenToUseMe?: string;
+
+ /** Detailed guidance (Instructions section) - required in v2 */
+ instructions?: string;
+
+ /** Verification items (Checklist section) - required in v2 */
+ checklist?: string[];
+
+ /** License identifier (e.g., MIT) - required in v2 */
+ license?: string;
+
+ /** Compatibility identifier (e.g., opencode) - required in v2 */
+ compatibility?: string;
+
+ /** Structured metadata including category - required in v2 */
+ metadata?: SkillMetadata;
+
+ /** Optional version string for tracking updates */
+ version?: string;
+
+ /** Optional last update timestamp */
+ updatedAt?: string;
+
+ /** Optional list of skill names this skill depends on */
+ dependencies?: string[];
+
+ /** Optional keywords for enhanced pattern matching */
+ keywords?: string[];
+
+ /** @deprecated Use whatIDo, whenToUseMe, instructions, checklist instead */
+ content?: string;
+
+ /** @deprecated Use metadata.category instead */
+ category?: string;
+
+ /** Content sections for structured skill content */
+ contentSections?: Array<{
+ type: string;
+ content: string;
+ }>;
+
+ /** Usage examples for the skill */
+ examples?: Array<{
+ description: string;
+ context?: string;
+ }>;
+}
+
+/**
+ * BM25 ranking configuration for relevance scoring
+ */
+export interface BM25Config {
+ /** Enable BM25 relevance scoring (default: false) */
+ enabled?: boolean;
+
+ /** Term frequency saturation parameter (default: 1.5, typical range: 1.2-2.0) */
+ k1?: number;
+
+ /** Length normalization parameter (default: 0.75, typical range: 0-1) */
+ b?: number;
+
+ /** Minimum BM25 score threshold for injection (default: 0.0) */
+ threshold?: number;
+
+ /** Maximum number of skills to inject per message (default: 3) */
+ maxSkills?: number;
+}
+
+/**
+ * Configuration options for the skills plugin
+ */
+export interface SkillsPluginConfig {
+ /** Enable/disable auto-injection via chat hook */
+ autoInject?: boolean;
+
+ /** Enable/disable debug logging */
+ debug?: boolean;
+
+ /** Custom pattern matching configuration */
+ patternMatching?: {
+ /** Enable word boundary matching (default: true) */
+ wordBoundary?: boolean;
+
+ /** Enable intent detection patterns (default: true) */
+ intentDetection?: boolean;
+
+ /** Enable negation detection (default: true) */
+ negationDetection?: boolean;
+
+ /** Custom intent keywords (in addition to defaults) */
+ customIntentKeywords?: string[];
+
+ /** Custom negation keywords (in addition to defaults) */
+ customNegationKeywords?: string[];
+ };
+
+ /** BM25 relevance scoring configuration */
+ bm25?: BM25Config;
+}
+
+/**
+ * Result of pattern matching for a skill
+ */
+export interface MatchResult {
+ /** Whether the skill matches the content */
+ matches: boolean;
+
+ /** The specific pattern that matched (for debugging) */
+ matchedPattern?: string;
+
+ /** Whether negation was detected */
+ hasNegation: boolean;
+}
+
+// SkillDefinition is an alias for Skill for backward compatibility
+export type SkillDefinition = Skill;
+
+// Validation Types
+export type ValidationSeverity = 'error' | 'warning' | 'info';
+
+export interface ValidationError {
+ field: string;
+ message: string;
+ code: string;
+}
+
+export interface ValidationWarning {
+ field: string;
+ message: string;
+ code: string;
+}
+
+export interface ValidationSuggestion {
+ field: string;
+ message: string;
+}
+
+export interface ValidationResult {
+ valid: boolean;
+ errors: ValidationError[];
+ warnings: ValidationWarning[];
+ suggestions: ValidationSuggestion[];
+}
+
+export interface ValidationContext {
+ packageName?: string;
+ filePath?: string;
+ strict?: boolean;
+}
+
+export interface SkillValidator {
+ validate(skill: Skill): ValidationResult;
+}
+
+export interface SkillRegistry {
+ register(skill: Skill): void;
+ get(name: string): Skill | undefined;
+ has(name: string): boolean;
+ list(): Skill[];
+ clear(): void;
+ size(): number;
+}
diff --git a/packages/opencode-skills/src/validation/format-validation-result.ts b/packages/opencode-skills/src/validation/format-validation-result.ts
new file mode 100644
index 0000000..5fdf0e5
--- /dev/null
+++ b/packages/opencode-skills/src/validation/format-validation-result.ts
@@ -0,0 +1,41 @@
+import type { ValidationResult } from '../types.js';
+
+export const formatValidationResult = (result: ValidationResult, skillName: string): string => {
+ const lines: string[] = [];
+
+ lines.push(`Validation Results for "${skillName}"`);
+ lines.push('');
+
+ if (!result.valid) {
+ lines.push('[ERROR] Validation failed:');
+ for (const error of result.errors) {
+ lines.push(` - ${error.field}: ${error.message}`);
+ }
+ lines.push('');
+ lines.push('Skill validation failed');
+ } else {
+ if (result.warnings.length > 0) {
+ lines.push('[WARNING] Issues found:');
+ for (const warning of result.warnings) {
+ lines.push(` - ${warning.field}: ${warning.message}`);
+ }
+ lines.push('');
+ }
+
+ if (result.suggestions.length > 0) {
+ lines.push('[SUGGESTION] Improvements:');
+ for (const suggestion of result.suggestions) {
+ lines.push(` - ${suggestion.field}: ${suggestion.message}`);
+ }
+ lines.push('');
+ }
+
+ if (result.warnings.length === 0 && result.suggestions.length === 0) {
+ lines.push('[OK] Skill is valid');
+ } else {
+ lines.push('[OK] Skill is valid (with warnings/suggestions)');
+ }
+ }
+
+ return lines.join('\n');
+};
diff --git a/packages/opencode-skills/src/validation/index.ts b/packages/opencode-skills/src/validation/index.ts
new file mode 100644
index 0000000..68cc42f
--- /dev/null
+++ b/packages/opencode-skills/src/validation/index.ts
@@ -0,0 +1,3 @@
+export { formatValidationResult } from './format-validation-result';
+export type { ValidationError, ValidationResult, ValidationSuggestion, ValidationWarning } from './types';
+export { validateSkill } from './validate-skill';
diff --git a/packages/opencode-skills/src/validation/skill-validator.test.ts b/packages/opencode-skills/src/validation/skill-validator.test.ts
new file mode 100644
index 0000000..9c87418
--- /dev/null
+++ b/packages/opencode-skills/src/validation/skill-validator.test.ts
@@ -0,0 +1,299 @@
+import { describe, expect, it } from 'bun:test';
+
+import type { Skill } from '../types';
+
+import { formatValidationResult, validateSkill } from './index';
+
+describe('validateSkill', () => {
+ it('should validate skill with all required fields', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill that are very detailed',
+ whenToUseMe: 'Use when you need this',
+ instructions: 'Follow these steps',
+ checklist: ['Item 1', 'Item 2'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ metadata: {
+ category: 'workflow',
+ },
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(true);
+ expect(result.errors).toHaveLength(0);
+ expect(result.warnings).toHaveLength(0);
+ });
+
+ it('should error on missing name', () => {
+ const skill = {
+ description: 'A test skill',
+ } as Skill;
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'name')).toBe(true);
+ });
+
+ it('should error on invalid name format', () => {
+ const skill: Skill = {
+ name: 'InvalidName',
+ description: 'A test skill',
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'name' && e.message.includes('kebab-case'))).toBe(true);
+ });
+
+ it('should error on missing description', () => {
+ const skill = {
+ name: 'test-skill',
+ } as Skill;
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'description')).toBe(true);
+ });
+
+ it('should error on missing structured content fields', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'whatIDo')).toBe(true);
+ expect(result.errors.some((e) => e.field === 'whenToUseMe')).toBe(true);
+ expect(result.errors.some((e) => e.field === 'instructions')).toBe(true);
+ expect(result.errors.some((e) => e.field === 'checklist')).toBe(true);
+ });
+
+ it('should warn on deprecated content field', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ content: 'Legacy content',
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(true);
+ expect(result.warnings.some((w) => w.field === 'content' && w.message.includes('deprecated'))).toBe(true);
+ });
+
+ it('should error on missing whatIDo', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'whatIDo')).toBe(true);
+ });
+
+ it('should error on missing whenToUseMe', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'What',
+ instructions: 'How',
+ checklist: ['Item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'whenToUseMe')).toBe(true);
+ });
+
+ it('should error on missing instructions', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'What',
+ whenToUseMe: 'When',
+ checklist: ['Item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'instructions')).toBe(true);
+ });
+
+ it('should error on empty checklist', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'What',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: [],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.valid).toBe(false);
+ expect(result.errors.some((e) => e.field === 'checklist')).toBe(true);
+ });
+
+ it('should warn on missing license', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.warnings.some((w) => w.field === 'license')).toBe(true);
+ });
+
+ it('should warn on missing compatibility', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.warnings.some((w) => w.field === 'compatibility')).toBe(true);
+ });
+
+ it('should warn on missing metadata.category', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.warnings.some((w) => w.field === 'metadata.category')).toBe(true);
+ });
+
+ it('should suggest expanding short whatIDo', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Short',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item 1', 'Item 2'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.suggestions.some((s) => s.field === 'whatIDo')).toBe(true);
+ });
+
+ it('should suggest adding more checklist items', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill that are very detailed',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Single item'],
+ };
+
+ const result = validateSkill(skill);
+
+ expect(result.suggestions.some((s) => s.field === 'checklist')).toBe(true);
+ });
+
+ it('should fail in strict mode with warnings', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item 1', 'Item 2'],
+ };
+
+ const result = validateSkill(skill, true);
+
+ expect(result.valid).toBe(true);
+ expect(result.warnings.length).toBeGreaterThan(0);
+ });
+});
+
+describe('formatValidationResult', () => {
+ it('should format validation result with errors', () => {
+ const skill = {
+ name: 'test-skill',
+ } as Skill;
+
+ const result = validateSkill(skill);
+ const formatted = formatValidationResult(result, 'test-skill');
+
+ expect(formatted).toContain('Validation Results for "test-skill"');
+ expect(formatted).toContain('[ERROR]');
+ expect(formatted).toContain('description: Description is required');
+ expect(formatted).toContain('Skill validation failed');
+ });
+
+ it('should format validation result with warnings', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item 1', 'Item 2'],
+ };
+
+ const result = validateSkill(skill);
+ const formatted = formatValidationResult(result, 'test-skill');
+
+ expect(formatted).toContain('[WARNING]');
+ expect(formatted).toContain('license');
+ expect(formatted).toContain('[OK] Skill is valid');
+ });
+
+ it('should format validation result for valid skill', () => {
+ const skill: Skill = {
+ name: 'test-skill',
+ description: 'A test skill',
+ whatIDo: 'Core capabilities of the skill that are very detailed',
+ whenToUseMe: 'When',
+ instructions: 'How',
+ checklist: ['Item 1', 'Item 2'],
+ license: 'MIT',
+ compatibility: 'opencode',
+ metadata: {
+ category: 'workflow',
+ },
+ };
+
+ const result = validateSkill(skill);
+ const formatted = formatValidationResult(result, 'test-skill');
+
+ expect(formatted).toContain('[OK] Skill is valid');
+ expect(formatted).not.toContain('❌ Errors');
+ });
+});
diff --git a/packages/opencode-skills/src/validation/types.ts b/packages/opencode-skills/src/validation/types.ts
new file mode 100644
index 0000000..3432102
--- /dev/null
+++ b/packages/opencode-skills/src/validation/types.ts
@@ -0,0 +1,42 @@
+/**
+ * Validation Type Definitions
+ *
+ * Type definitions for skill validation results and errors.
+ */
+
+/**
+ * Validation result with errors, warnings, and suggestions
+ */
+export interface ValidationResult {
+ valid: boolean;
+ errors: ValidationError[];
+ warnings: ValidationWarning[];
+ suggestions: ValidationSuggestion[];
+}
+
+/**
+ * Validation error (blocking)
+ */
+export interface ValidationError {
+ field: string;
+ message: string;
+ severity: 'error';
+}
+
+/**
+ * Validation warning (non-blocking)
+ */
+export interface ValidationWarning {
+ field: string;
+ message: string;
+ severity: 'warning';
+}
+
+/**
+ * Validation suggestion (informational)
+ */
+export interface ValidationSuggestion {
+ field: string;
+ message: string;
+ severity: 'info';
+}
diff --git a/packages/opencode-skills/src/validation/validate-skill.ts b/packages/opencode-skills/src/validation/validate-skill.ts
new file mode 100644
index 0000000..a0731c7
--- /dev/null
+++ b/packages/opencode-skills/src/validation/validate-skill.ts
@@ -0,0 +1,135 @@
+import type { Skill, ValidationError, ValidationResult, ValidationSuggestion, ValidationWarning } from '../types';
+
+export const validateSkill = (skill: Skill, _strictMode = false): ValidationResult => {
+ const errors: ValidationError[] = [];
+ const warnings: ValidationWarning[] = [];
+ const suggestions: ValidationSuggestion[] = [];
+
+ // Validate name
+ if (!skill.name || skill.name.trim() === '') {
+ errors.push({
+ field: 'name',
+ message: 'Name is required',
+ code: 'MISSING_NAME',
+ });
+ } else if (!/^[a-z0-9-]+$/.test(skill.name)) {
+ errors.push({
+ field: 'name',
+ message: 'Name must be in kebab-case format (lowercase letters, numbers, and hyphens only)',
+ code: 'INVALID_NAME_FORMAT',
+ });
+ }
+
+ // Validate description
+ if (!skill.description || skill.description.trim() === '') {
+ errors.push({
+ field: 'description',
+ message: 'Description is required',
+ code: 'MISSING_DESCRIPTION',
+ });
+ } else if (skill.description.length < 10) {
+ warnings.push({
+ field: 'description',
+ message: 'Description should be at least 10 characters',
+ code: 'SHORT_DESCRIPTION',
+ });
+ }
+
+ // Check for legacy content field (v1 format)
+ const hasLegacyContent = skill.content && skill.content.trim() !== '';
+
+ // If using legacy content, warn about deprecation but don't require v2 fields
+ if (hasLegacyContent) {
+ warnings.push({
+ field: 'content',
+ message: 'content field is deprecated. Use whatIDo, whenToUseMe, instructions, checklist instead',
+ code: 'DEPRECATED_CONTENT',
+ });
+ }
+
+ // Validate v2 structured content fields (required only if not using legacy content)
+ if (!hasLegacyContent) {
+ // Validate whatIDo (required in v2)
+ if (!skill.whatIDo || skill.whatIDo.trim() === '') {
+ errors.push({
+ field: 'whatIDo',
+ message: 'whatIDo is required (Core capabilities section)',
+ code: 'MISSING_WHAT_I_DO',
+ });
+ } else if (skill.whatIDo.length < 20) {
+ suggestions.push({
+ field: 'whatIDo',
+ message: 'Consider expanding whatIDo to be more descriptive (at least 20 characters)',
+ });
+ }
+
+ // Validate whenToUseMe (required in v2)
+ if (!skill.whenToUseMe || skill.whenToUseMe.trim() === '') {
+ errors.push({
+ field: 'whenToUseMe',
+ message: 'whenToUseMe is required (When to use me section)',
+ code: 'MISSING_WHEN_TO_USE',
+ });
+ }
+
+ // Validate instructions (required in v2)
+ if (!skill.instructions || skill.instructions.trim() === '') {
+ errors.push({
+ field: 'instructions',
+ message: 'instructions is required (Instructions section)',
+ code: 'MISSING_INSTRUCTIONS',
+ });
+ }
+
+ // Validate checklist (required in v2)
+ if (!skill.checklist || skill.checklist.length === 0) {
+ errors.push({
+ field: 'checklist',
+ message: 'checklist is required with at least one item',
+ code: 'MISSING_CHECKLIST',
+ });
+ } else if (skill.checklist.length === 1) {
+ suggestions.push({
+ field: 'checklist',
+ message: 'Consider adding more checklist items for better verification',
+ });
+ }
+ }
+
+ // Validate license (required in v2)
+ if (!skill.license || skill.license.trim() === '') {
+ warnings.push({
+ field: 'license',
+ message: 'license is recommended (e.g., MIT)',
+ code: 'MISSING_LICENSE',
+ });
+ }
+
+ // Validate compatibility (required in v2)
+ if (!skill.compatibility || skill.compatibility.trim() === '') {
+ warnings.push({
+ field: 'compatibility',
+ message: 'compatibility is recommended (e.g., opencode)',
+ code: 'MISSING_COMPATIBILITY',
+ });
+ }
+
+ // Validate metadata.category
+ if (!skill.metadata?.category || skill.metadata.category.trim() === '') {
+ warnings.push({
+ field: 'metadata.category',
+ message: 'metadata.category is recommended',
+ code: 'MISSING_CATEGORY',
+ });
+ }
+
+ // Note: strictMode parameter kept for API compatibility
+ // Currently doesn't change validation behavior
+
+ return {
+ valid: errors.length === 0,
+ errors,
+ warnings,
+ suggestions,
+ };
+};
diff --git a/packages/opencode-skills/tsconfig.json b/packages/opencode-skills/tsconfig.json
new file mode 100644
index 0000000..1f1ea0c
--- /dev/null
+++ b/packages/opencode-skills/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "allowJs": true,
+ "moduleDetection": "force",
+ "noImplicitAny": false,
+ "types": ["bun-types"]
+ },
+ "include": ["**/*.ts", "**/*.js"],
+ "exclude": ["node_modules", "docs", ".astro-build"]
+}
diff --git a/packages/opencode-skills/tsconfig.test.json b/packages/opencode-skills/tsconfig.test.json
new file mode 100644
index 0000000..dd78eb0
--- /dev/null
+++ b/packages/opencode-skills/tsconfig.test.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "declaration": false,
+ "sourceMap": false,
+ "strict": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "typeRoots": ["./types", "./node_modules", "./node_modules/@types"],
+ "files": ["../../types/bun-test-shim.d.ts"],
+ "baseUrl": "."
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.spec.ts", "src/**/*.test.ts"],
+ "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/opencode-skills/types/bun-test.d.ts b/packages/opencode-skills/types/bun-test.d.ts
new file mode 100644
index 0000000..3cabbcf
--- /dev/null
+++ b/packages/opencode-skills/types/bun-test.d.ts
@@ -0,0 +1,28 @@
+declare module 'bun:test' {
+ export const describe: any;
+ export const it: any;
+ export const test: any;
+ export const expect: any;
+ export const beforeAll: any;
+ export const afterAll: any;
+ export const beforeEach: any;
+ export const afterEach: any;
+ export const vi: any;
+ export const mock: any;
+ export const setSystemTime: any;
+ export const spyOn: any;
+ export default {};
+}
+
+declare global {
+ const describe: any;
+ const it: any;
+ const test: any;
+ const expect: any;
+ const beforeAll: any;
+ const afterAll: any;
+ const beforeEach: any;
+ const afterEach: any;
+}
+
+export {};
diff --git a/packages/opencode-skills/types/import-meta.d.ts b/packages/opencode-skills/types/import-meta.d.ts
new file mode 100644
index 0000000..38ed951
--- /dev/null
+++ b/packages/opencode-skills/types/import-meta.d.ts
@@ -0,0 +1,7 @@
+declare global {
+ interface ImportMeta {
+ readonly dir: string;
+ }
+}
+
+export {};
diff --git a/tools/executors/package.json b/tools/executors/package.json
index ca79da1..dfe6401 100644
--- a/tools/executors/package.json
+++ b/tools/executors/package.json
@@ -1,7 +1,9 @@
{
- "name": "@pantheon-org/tools",
- "version": "1.0.0",
+ "name": "@pantheon-org/executors",
+ "version": "0.1.0",
"private": true,
- "main": "../src/index.ts",
- "executors": "./executors.json"
+ "dependencies": {
+ "@pantheon-org/opencode-skills": "workspace:*",
+ "glob": "^10.3.10"
+ }
}
diff --git a/tools/executors/validate-skill-md/executor.ts b/tools/executors/validate-skill-md/executor.ts
new file mode 100644
index 0000000..aca58ca
--- /dev/null
+++ b/tools/executors/validate-skill-md/executor.ts
@@ -0,0 +1,104 @@
+import { readFile } from 'node:fs/promises';
+import type { ExecutorContext } from '@nx/devkit';
+import { formatValidationResult, markdownToSkill, validateSkill } from '@pantheon-org/opencode-skills';
+import { glob } from 'glob';
+
+export interface ValidateSkillMdExecutorOptions {
+ pattern: string;
+ strict: boolean;
+ format: 'text' | 'json';
+}
+
+export default async (
+ options: ValidateSkillMdExecutorOptions,
+ context: ExecutorContext,
+): Promise<{ success: boolean; error?: string }> => {
+ try {
+ const projectName = context.projectName;
+ if (!projectName) {
+ return { success: false, error: 'No project name in context' };
+ }
+
+ const projectConfig = context.projectGraph?.nodes[projectName];
+ if (!projectConfig) {
+ return { success: false, error: 'Project configuration not found' };
+ }
+
+ const projectRoot = projectConfig.data.root;
+ const searchPattern = `${context.root}/${projectRoot}/${options.pattern}`;
+
+ // Find all SKILL.md files
+ const files = await glob(searchPattern);
+
+ if (files.length === 0) {
+ console.log(`✅ No SKILL.md files found matching pattern: ${options.pattern}`);
+ return { success: true };
+ }
+
+ let totalFiles = 0;
+ let valid = 0;
+ let withErrors = 0;
+ let withWarnings = 0;
+
+ // Validate each file
+ for (const filePath of files) {
+ totalFiles++;
+ const markdown = await readFile(filePath, 'utf-8');
+
+ try {
+ const skill = markdownToSkill(markdown);
+ const result = validateSkill(skill, options.strict);
+
+ if (result.valid) {
+ valid++;
+ }
+ if (result.errors.length > 0) {
+ withErrors++;
+ }
+ if (result.warnings.length > 0) {
+ withWarnings++;
+ }
+
+ // Output results
+ if (options.format === 'text') {
+ console.log(`\n📄 File: ${filePath}`);
+ console.log(formatValidationResult(result, skill.name));
+ }
+ } catch (parseError) {
+ withErrors++;
+ console.error(`\n❌ Failed to parse ${filePath}:`);
+ console.error(` ${(parseError as Error).message}`);
+ }
+ }
+
+ // Summary
+ if (options.format === 'text') {
+ console.log(`\n${'='.repeat(50)}`);
+ console.log('📊 Validation Summary');
+ console.log('='.repeat(50));
+ console.log(`Total files: ${totalFiles}`);
+ console.log(`✅ Valid: ${valid}`);
+ console.log(`❌ With errors: ${withErrors}`);
+ console.log(`⚠️ With warnings: ${withWarnings}`);
+ }
+
+ const shouldFail = withErrors > 0 || (options.strict && withWarnings > 0);
+
+ if (shouldFail) {
+ const errors = withErrors > 0 ? `${withErrors} file(s) with errors` : '';
+ const warnings = withWarnings > 0 && options.strict ? `${withWarnings} file(s) with warnings (strict mode)` : '';
+ const reason = [errors, warnings].filter(Boolean).join(', ');
+ return {
+ success: false,
+ error: `SKILL.md validation failed: ${reason}. Run with --verbose for details.`,
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to validate SKILL.md files: ${(error as Error).message}`,
+ };
+ }
+};
diff --git a/tools/executors/validate-skill-md/schema.d.ts b/tools/executors/validate-skill-md/schema.d.ts
new file mode 100644
index 0000000..72746c2
--- /dev/null
+++ b/tools/executors/validate-skill-md/schema.d.ts
@@ -0,0 +1,5 @@
+export interface ValidateSkillMdExecutorOptions {
+ pattern: string;
+ strict: boolean;
+ format: 'text' | 'json';
+}
diff --git a/tools/executors/validate-skill-md/schema.json b/tools/executors/validate-skill-md/schema.json
new file mode 100644
index 0000000..f19e7cf
--- /dev/null
+++ b/tools/executors/validate-skill-md/schema.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json-schema.org/schema",
+ "title": "Validate SKILL.md Files",
+ "description": "Validate markdown SKILL.md files with YAML frontmatter",
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "type": "string",
+ "description": "Glob pattern for SKILL.md files",
+ "default": ".opencode/skills/*/SKILL.md"
+ },
+ "strict": {
+ "type": "boolean",
+ "description": "Enable strict validation (fail on warnings)",
+ "default": false
+ },
+ "format": {
+ "type": "string",
+ "description": "Output format",
+ "enum": ["text", "json"],
+ "default": "text"
+ }
+ }
+}
diff --git a/tools/executors/validate-skills/executor.ts b/tools/executors/validate-skills/executor.ts
new file mode 100644
index 0000000..0048df7
--- /dev/null
+++ b/tools/executors/validate-skills/executor.ts
@@ -0,0 +1,90 @@
+import type { ExecutorContext } from '@nx/devkit';
+import {
+ formatValidationResult,
+ type Skill,
+ type ValidationResult,
+ validateSkill,
+} from '@pantheon-org/opencode-skills';
+
+export interface ValidateSkillsExecutorOptions {
+ skillsPath: string;
+ strict: boolean;
+ format: 'text' | 'json';
+}
+
+export default async (
+ options: ValidateSkillsExecutorOptions,
+ context: ExecutorContext,
+): Promise<{ success: boolean; error?: string }> => {
+ try {
+ const projectName = context.projectName;
+ if (!projectName) {
+ return { success: false, error: 'No project name in context' };
+ }
+
+ const projectConfig = context.projectGraph?.nodes[projectName];
+ if (!projectConfig) {
+ return { success: false, error: 'Project configuration not found' };
+ }
+
+ const projectRoot = projectConfig.data.root;
+ const skillsPath = `${context.root}/${projectRoot}/${options.skillsPath}`;
+
+ // Dynamically import skills
+ const skillsModule = await import(skillsPath);
+ const skills: Record = skillsModule.default || skillsModule;
+
+ const results = new Map();
+ let totalSkills = 0;
+ let valid = 0;
+ let withErrors = 0;
+ let withWarnings = 0;
+
+ // Validate each skill
+ for (const [_exportName, skill] of Object.entries(skills)) {
+ if (typeof skill === 'object' && skill.name) {
+ totalSkills++;
+ const result = validateSkill(skill, options.strict);
+ results.set(skill.name, result);
+
+ if (result.valid) {
+ valid++;
+ }
+ if (result.errors.length > 0) {
+ withErrors++;
+ }
+ if (result.warnings.length > 0) {
+ withWarnings++;
+ }
+
+ // Output individual results
+ if (options.format === 'text') {
+ console.log(formatValidationResult(result, skill.name));
+ }
+ }
+ }
+
+ // Summary
+ if (options.format === 'text') {
+ console.log(`\n${'='.repeat(50)}`);
+ console.log('📊 Validation Summary');
+ console.log('='.repeat(50));
+ console.log(`Total skills: ${totalSkills}`);
+ console.log(`✅ Valid: ${valid}`);
+ console.log(`❌ With errors: ${withErrors}`);
+ console.log(`⚠️ With warnings: ${withWarnings}`);
+ } else {
+ console.log(JSON.stringify({ totalSkills, valid, withErrors, withWarnings }, null, 2));
+ }
+
+ // Fail if errors found (or warnings in strict mode)
+ const shouldFail = withErrors > 0 || (options.strict && withWarnings > 0);
+
+ return { success: !shouldFail };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to validate skills: ${(error as Error).message}`,
+ };
+ }
+};
diff --git a/tools/executors/validate-skills/schema.d.ts b/tools/executors/validate-skills/schema.d.ts
new file mode 100644
index 0000000..49e0f1b
--- /dev/null
+++ b/tools/executors/validate-skills/schema.d.ts
@@ -0,0 +1,5 @@
+export interface ValidateSkillsExecutorOptions {
+ skillsPath: string;
+ strict: boolean;
+ format: 'text' | 'json';
+}
diff --git a/tools/executors/validate-skills/schema.json b/tools/executors/validate-skills/schema.json
new file mode 100644
index 0000000..3699678
--- /dev/null
+++ b/tools/executors/validate-skills/schema.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json-schema.org/schema",
+ "title": "Validate Skills",
+ "description": "Validate TypeScript skill definitions in a package",
+ "type": "object",
+ "properties": {
+ "skillsPath": {
+ "type": "string",
+ "description": "Path to skills directory or index file",
+ "default": "src/skills/index.ts"
+ },
+ "strict": {
+ "type": "boolean",
+ "description": "Enable strict validation (fail on warnings)",
+ "default": false
+ },
+ "format": {
+ "type": "string",
+ "description": "Output format",
+ "enum": ["text", "json"],
+ "default": "text"
+ }
+ }
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 335e6b4..9968132 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -17,6 +17,9 @@
"@pantheon-org/opencode-config": [
"packages/opencode-config/src/index.ts"
],
+ "@pantheon-org/opencode-skills": [
+ "packages/opencode-skills/src/index.ts"
+ ],
"@pantheon-org/opencode-warcraft-notifications-plugin": [
"packages/opencode-warcraft-notifications-plugin/src/index.ts"
],