Contributing to OrchestKit Plugin

September 19, 2026 · View on GitHub

Welcome to the OrchestKit plugin for Claude Code! We're excited that you're interested in contributing. This plugin extends Claude Code with specialized skills, agents, and hooks for AI-native development workflows.

How to Contribute

Where to start

If you want to contribute but do not have something specific in mind, these two labels are the front door:

  • good first issue Self-contained, and the root cause is already identified in the issue itself. You should not need deep familiarity with the repo to land one.
  • help wanted Real work we would genuinely welcome help on, but it needs some judgement calls or extra tooling, so it is a better second contribution than a first.

Small fixes are welcome on their own merits and do not need an issue first. A corrected path in the docs is a real contribution; PR #3248 was exactly that.

Comment on the issue before you start if you want to avoid duplicate work. If an issue looks stale or the description no longer matches the code, say so. That is useful signal in itself.

Getting Started

  1. Fork the repository on GitHub
  2. Clone your fork locally:
    git clone https://github.com/YOUR_USERNAME/orchestkit.git
    cd orchestkit
    
  3. Create a feature branch:
    git checkout -b feature/your-feature-name
    # or for issues:
    git checkout -b issue/123-description
    
  4. Make your changes following the guidelines below
  5. Test your changes thoroughly
  6. Submit a Pull Request to the main branch

Your first pull request

For a small first change, add a documentation scope example to the commit reference. Starting in your cloned fork, create a branch and install dependencies:

git checkout -b docs/commit-scope-example
npm install

Find the source file, even if you first saw the reference under plugins/:

git ls-files 'src/*' | grep 'commit/references/conventional-commits.md'
# src/skills/commit/references/conventional-commits.md

Open that src/ file in your editor. Under Scope Examples, add this line:

- `docs(commit): clarify scope examples`

Regenerate the plugin mirror, review the diff, and stage both copies:

npm run build
npm run build # Confirm the build has settled, as described below.
git diff
git add src/skills/commit/references/conventional-commits.md \
  plugins/ork/skills/commit/references/conventional-commits.md
git status --short

For this change, the status should show both files staged (M in the first column), with no unstaged changes:

M  plugins/ork/skills/commit/references/conventional-commits.md
M  src/skills/commit/references/conventional-commits.md

Run the checks, then commit with a conventional-commit title under 72 characters, push to your fork, and open a PR to main using the GitHub CLI:

npm test
npm run test:security
git commit -m "docs(commit): add a scope example"
git push -u origin docs/commit-scope-example
gh pr create --repo yonatangross/orchestkit --base main --web

In the PR form, describe the change and checks you ran. If it resolves an issue, include Closes #NNN with the real issue number.

Four common traps:

  • Editing plugins/ instead of src/: plugins/ is generated; the next build overwrites hand edits. Edit the source and commit its regenerated mirror.
  • Skipping npm run build: CI checks that source and generated output agree. Rebuild before staging, even for a one-line source documentation change.
  • A commit title over 72 characters: the local commit-msg hook rejects it before the commit completes. Keep the title short and put details in the body.
  • Skipping npm install in a fresh worktree: git worktree add does not copy node_modules. Run npm install inside each new worktree before building.

If you are contributing from a fork

Your CI will not start on its own. Every required check will sit at action_required / "Waiting for approval" until a maintainer approves the workflow run. This is a GitHub default for first-time contributors, not something you did wrong, and it is not a failing build.

What this looks like on your PR:

  • The merge box says blocked, but no check is red
  • Every workflow shows as queued or awaiting approval, with none of them started

There is nothing to fix on your end. Ping the PR if it has been sitting for a while and nobody has approved the run yet.

Branch Naming Convention

  • feature/ - New features or enhancements
  • fix/ - Bug fixes
  • docs/ - Documentation updates

Issue close-out (Closes #N)

If your PR ships work for a tracked issue, include Closes #NNN in the PR body so GitHub auto-closes the issue on merge. We never run gh issue close manually — issues should only close when the work that resolves them lands on main via CI. The .github/PULL_REQUEST_TEMPLATE.md includes a placeholder; replace Closes # with the issue number(s) when opening the PR.

If the work spans multiple issues, list each on its own Closes # line or use Closes #1, #2, #3 on a single line — both forms are recognized.

This convention prevents the "open issues that are silently done" drift class documented in #1554.

Strict main: required checks and serial merges

main is protected with strict status checks. All 18 required contexts must be green at the PR's exact head, and that head must contain the current main. A PR whose base moved reads BEHIND and cannot merge until it is updated.

Merges are therefore serial. After every merge to main, every other open PR goes BEHIND. Update one PR, wait for its checks, merge it, then update the next.

Update by merging main into the branch with GitHub's update-branch button or git merge origin/main, not by rebasing, so reviewed commits keep their IDs.

Lab data no longer conflicts (#4185): docs/site/lib/generated/lab-data.ts is not committed. It is regenerated at build, dev and test time from the fragments under docs/site/lab-manifest/<slug>.json, so a rebase never touches it. Commit the fragment and the public/lab/ copy; never commit the aggregate by hand, and run node docs/site/scripts/lab-manifest.mjs (or any docs/site npm build/dev/test script) if --check calls it absent or stale.

Build twice before pushing. One npm run build is not a fixed point for stamped counts. Issue GH-4116 fixed the search index order; confirm a second build and an empty git status before pushing.

Reviews are tied to a head. A review at an older head still counts only if the PR's own files are byte-identical at the new head because the update merge touched none of them. Otherwise, ask for a fresh read.

Workflow files under .github/workflows and plugin.json files are merged by the maintainer only.

Project Structure

OrchestKit uses a build system to assemble modular plugins from source files.

Source Files (Edit These)

src/
├── skills/               # <!--ork:skills-->107<!--/ork--> skills (flat SKILL.md structure, CC 2.1.59+)
│   └── <skill-name>/
│       ├── SKILL.md           # Required: Patterns and best practices
│       ├── references/        # Optional: Specific implementations
│       ├── scripts/           # Optional: Executable code and generators
│       ├── assets/            # Optional: Templates and copyable files
│       └── checklists/        # Optional: Implementation checklists
├── agents/               # <!--ork:agents-->36<!--/ork--> specialized AI personas
└── hooks/                # <!--ork:hooks-->171<!--/ork--> TypeScript hooks in 11 split bundles

manifests/                # Plugin definition
└── ork.json              # Single plugin (<!--ork:skills-->107<!--/ork--> skills, <!--ork:agents-->36<!--/ork--> agents, <!--ork:hooks-->171<!--/ork--> hooks)

Generated Files (Do Not Edit, But Do Commit)

These are build output. Never edit them by hand: npm run build overwrites them from src/. They are tracked in git, so commit the regenerated diff along with the src/ change that caused it.

One exception (#3578): the hook bundles src/hooks/dist/ and plugins/ork/hooks/dist/ are release-owned. Build them locally (tests read them) but never commit them from a feature branch: a bundle is a whole-file rewrite, so two PRs touching any two hooks conflicted on the same four files, and a conflicting PR gets no CI at all. The release-please branch rebuilds them, and CI fails a feature PR that changes them.

plugins/                  # Generated by build script
└── ork/                  # Single plugin

.claude-plugin/
└── marketplace.json      # Generated marketplace manifest

docs/site/lib/generated/  # Docs-site codegen (skills, changelog, skill graph)

Convention Anchor Directories (small, but load-bearing)

A few top-level directories hold only a handful of files but are not clutter — each is a path that a skill or CI hardcodes. Do not move or delete them without updating the referencing code. Each has its own README.md.

spec/          # Hook-event contract. hook-events.spec.yml is globbed by 4 CI
               # workflows + consumed by packages/hook-contract{,-py}.
vendor/        # Vendored 3rd-party data. sync-vercel-skills.sh reads
               # vercel-skills/mapping.json.
designs/       # Output dir for /ork:visualize-plan (designs/<branch>.md).
swarm-specs/   # Input specs for /ork:swarm-migrate (globs swarm-specs/**/*.yaml).

Build System

When you modify source files in src/ or manifests/, you MUST run the build script to regenerate plugins/:

npm run build
# OR
bash scripts/build-plugins.sh

What the build script does:

  1. Reads plugin definitions from manifests/*.json
  2. Copies skills, agents, hooks from src/ to plugins/<plugin-name>/
  3. Creates .claude-plugin/plugin.json for each plugin
  4. Generates .claude-plugin/marketplace.json with all plugins

Critical Rules:

  • ALWAYS edit files in src/ or manifests/, NEVER in plugins/
  • ALWAYS run npm run build after making changes
  • The plugins/ directory is generated but is tracked in git. Stage the regenerated output together with your src/ change, except the hook bundles under hooks/dist/ (release-owned, see above)
  • CI rebuilds from src/ and fails the Build check if the committed output differs. It does not build for you, so an edit made only in plugins/ fails that check and is reverted by the next build

Development Workflow:

  1. Edit source: src/skills/, src/agents/, or manifests/
  2. Build: npm run build
  3. Test: /plugin marketplace add file://$(pwd) then /plugin install ork
  4. Verify: /ork:doctor
  5. Commit: your src/ or manifests/ change and the regenerated build output (plugins/, docs/site/lib/generated/, README.md). The Build check compares a fresh build against what you committed

Adding New Skills

Skills use the native flat SKILL.md structure (introduced in CC 2.1.59, current minimum CC 2.1.108).

Important: Create skills in src/skills/, not skills/ (which is a symlink).

1. Create Skill Directory

mkdir -p src/skills/your-skill-name/references

2. Create SKILL.md (Required)

Create src/skills/your-skill-name/SKILL.md:

---
name: your-skill-name
description: Brief description for skill discovery
tags: [keyword1, keyword2, keyword3]
---

# Your Skill Name

Brief description of what this skill provides.

## When to Use

- Use case 1
- Use case 2

## Key Patterns

### Pattern 1
Explanation and code example.

### Pattern 2
Explanation and code example.

## Best Practices

- Practice 1
- Practice 2

## Anti-Patterns

- What NOT to do

3. Add References (Optional)

Create specific implementation guides in references/:

  • src/skills/your-skill-name/references/implementation-guide.md
  • src/skills/your-skill-name/references/advanced-patterns.md

4. Add Templates (Optional)

Create code templates in assets/:

  • src/skills/your-skill-name/assets/component-template.py
  • src/skills/your-skill-name/assets/test-template.py

5. Add to Plugin Manifest

Add your skill to the manifest (manifests/ork.json) — the build script uses "skills": "all":

vim manifests/ork.json

6. Build and Validate

# Build plugins to include your new skill
npm run build

# Validate structure
npm run test:skills

# Test locally
/plugin marketplace add file://$(pwd)
/plugin install ork
/ork:doctor

Adding New Agents

Agents are specialized AI personas defined in markdown.

Important: Create agents in src/agents/, not agents/ (which is a symlink).

1. Create Agent File

Create src/agents/your-agent.md:

# Your Agent Name

## Role
Brief description of the agent's specialized role.

## Capabilities
- Capability 1
- Capability 2

## Tools Available
- Tool 1
- Tool 2

## Workflow
How the agent approaches tasks.

## Success Criteria
What constitutes successful completion.

## Model Preference
haiku | sonnet | opus

2. Task Management Section (Required for agents with task tools)

Agents that include TaskCreate, TaskUpdate, or TaskList in their tools: array must have a Task Management section:

## Task Management
For multi-step work (3+ distinct steps), use CC 2.1.16 task tracking:
1. `TaskCreate` for each major step with descriptive `activeForm`
2. `TaskGet` to verify `blockedBy` is empty before starting
3. Set status to `in_progress` when starting a step
4. Use `addBlockedBy` for dependencies between steps
5. Mark `completed` only when step is fully verified
6. Check `TaskList` before starting to see pending work

3. Add to Plugin Manifest

Both plugins use "agents": "all", so new agents are automatically included. No manifest edit needed.

4. Build and Test

# Build plugins to include your new agent
npm run build

# Validate agents
npm run test:agents

# Test locally
/plugin marketplace add file://$(pwd)
/plugin install ork

# Test agent spawning (in Claude Code)
# Use Task tool with subagent_type: "your-agent"

Agent Taxonomy Fields (for docs generation)

All agents require these frontmatter fields for the auto-generated agents-data.ts:

FieldTypeDescription
taskTypesstring[]What the agent does: build, review, debug, test, deploy, design, research, document, optimize, secure, plan
keywordsstring[]Search terms for the agent selector (3-8 keywords)
examplePromptsstring[]Exactly 2 example prompts showing typical usage

These fields are validated by tests/agents/test-agent-frontmatter.sh.

Vendor Upstream Skills (Option E)

OrchestKit references Vercel Labs skills as upstream API documentation, deposited as references/upstream-*.md inside our own skill directories.

How it works

vendor/vercel-skills/mapping.json  → defines 37 refs across 4 Vercel repos
scripts/sync-vercel-skills.sh      → fetches + deposits into src/skills/*/references/
vendor/vercel-skills/manifest.json → tracks content hashes for dedup

Adding a new upstream reference

  1. Add entry to vendor/vercel-skills/mapping.json:
    { "repo": "vercel-labs/json-render", "skill_path": "skills/new-skill", "target_skill": "json-render-catalog", "ref_filename": "upstream-new.md" }
    
  2. Run bash scripts/sync-vercel-skills.sh
  3. Verify: bash scripts/sync-vercel-skills.sh --check
  4. Commit the mapping, manifest, and new reference file together

Modes

  • --check — verify all refs exist on disk (used in CI)
  • --dry-run — show what would change without writing
  • No flag — fetch and sync (requires network)

Adding New Hooks

Hooks are TypeScript, under src/hooks/src/<category>/<name>.ts, dispatched through run-hook.mjs. They are not bash scripts.

This section previously described a bash system with a top-level hooks/ directory, ../_lib/common.sh, and registration in .claude/settings.json. None of those exist. Following it produced a hook that never ran — the exact #959 dead-hook class warned about below. CLAUDE.md and .claude/rules/hooks-development.md were already correct; this file was not.

1. Write the handler

src/hooks/src/<category>/<name>.ts, exporting a function that takes HookInput and returns a HookResult:

import type { HookInput, HookResult, HookContext } from '../../types.js';
import { outputSilentSuccess, outputDeny } from '../../lib/common.js';
import { NOOP_CTX } from '../../lib/context.js';

export function myHook(input: HookInput, ctx: HookContext = NOOP_CTX): HookResult {
  // Guard at the TOP: fast exit for anything this hook does not handle.
  if (!input.tool_input?.command) return outputSilentSuccess();
  return outputSilentSuccess();
}

Use the output builders rather than hand-rolling JSON: outputSilentSuccess(), outputDeny(reason), outputPreToolAdvisory(text), outputAllowWithContext(text).

Picking between the last two matters. outputAllowWithContext sets permissionDecision: 'allow', which skips the permission prompt — do not use it to deliver a mere advisory. outputPreToolAdvisory informs without granting anything.

2. Register it in BOTH places

src/hooks/hooks.json                 <- when CC invokes it
src/hooks/src/entries/<event>.ts     <- what the dispatcher resolves it to

These are a pair. In hooks.json but missing from the entries map dispatches to nothing; in the entries map but missing from hooks.json is never invoked. Either way it fails silently. That asymmetry is the #959 class and has cost this repo months of lost telemetry more than once.

3. Build, re-stamp, record

cd src/hooks && npm run build
bash bin/validate-counts.sh          # counts live in hooks.json's description

Then add a Registry changelog entry in src/hooks/README.mdhooks.json mandates that history lives there, not in the JSON.

Hook categories

DirectoryPurpose
pretool/Validate before tool execution
posttool/Act after tool execution
permission/Auto-approve safe operations
lifecycle/Session start/end
stop/Conversation end handlers

Test it

cd src/hooks && npx tsc --noEmit -p tsconfig.json

# Run the real hook end to end and read its decision:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' \
  | node src/hooks/bin/run-hook.mjs pretool/bash/<name>

Note run-hook.mjs always exits 0 and returns its verdict as JSON on stdout, so assert on the payload, never on the exit code. For PreToolUse it also strips additionalContext (CC does not read it for that event, #1794) and routes advisory text through systemMessage.

Security Guidelines

Required Practices

  • Always use set -euo pipefail
  • Quote all variables: "${var}"
  • Validate file paths before operations
  • Use jq --arg for JSON variable interpolation

Prohibited Patterns

  • No eval - Never use dynamic execution
  • No network calls in hooks
  • No secrets in skill files
  • No --no-verify on git commands

Versioning

Do not manually run ./bin/bump-version.sh on branches prefixed with feat/, fix/, perf/, refactor/, docs/, chore/, ci/, style/, or test/. Release-please ("release-type": "simple") owns version decisions on these branches — it computes the next version from conventional-commit types since the last tag and opens a chore(main): release X.Y.Z PR automatically.

Manual bumps on these branches create ghost versions (see #1457): you bump to 7.62.0, release-please picks 7.63.0, v7.62.0 never gets tagged.

Hotfix branches with bare names (e.g. hotfix-auth-bug) still trigger the pre-push version check — those are for legitimate manual version control, typically cherry-picks or out-of-band releases.

If you need to override release-please's computed next version, use release-please's own release-as footer in the commit message.

Do not hand-edit release-please's governed files

The same "release-please owns version decisions" rule applies to any manual edit of the files release-please tracks — not just running bump-version.sh. The governed list lives in .release-please-config.json under extra-files, plus the manifest, version.txt, and CHANGELOG.md.

Hand-editing those files in a feature PR (e.g. an inner chore: bump to vX.Y.Z commit inside a squash) desyncs release-please's state machine: it can no longer find a tag matching the new manifest version, falls back to the bootstrap-sha, re-scans all history, and re-applies any feat!: or BREAKING CHANGE: it finds. The result is a wildly wrong major bump (see PR #1946 incident: a manual 8.0.0 → 8.1.0 in a feature PR caused the next release-please run to propose 9.0.0).

CI enforces this via .github/workflows/release-please-guard.yml — non-bot PRs touching governed files fail the guard check. For legitimate exceptions (drift fixes like #1947, schema changes to a governed file), apply the release-please-override label and the guard will skip.

Release Cadence

Release PRs are driven by two triggers:

  1. On every push to main — release-please analyzes new conventional-commit types since the last tag. A feat: or fix: opens / updates the chore(main): release X.Y.Z PR.
  2. Weekly cron (Mondays 09:00 UTC) — forces a release-please run even if no push has landed since the last tag. Prevents "release drought" when feat commits sit on main without triggering an update.

Release-please skips creating a PR when there's nothing to release, so the cron never produces empty or noisy release PRs. The cron is a floor, not a ceiling — bursty feat cadence (multiple releases per day) is still supported and expected.

Testing

Run All Tests

npm test
# OR
./tests/run-all-tests.sh

Individual Test Suites

# Security tests (MUST pass)
npm run test:security
# OR
./tests/security/run-security-tests.sh

# Skill structure validation
npm run test:skills
# OR
./tests/skills/structure/test-skill-md.sh

# Agent validation
npm run test:agents
# OR
./tests/agents/test-agent-frontmatter.sh

# Other tests
./tests/unit/test-shell-syntax.sh       # Shell syntax
./tests/schemas/test-plugin-schema.sh   # Plugin schema
./tests/schemas/test-marketplace-schema.sh  # Marketplace schema
./bin/validate-counts.sh                # Component counts

Before Submitting

  • New skills have SKILL.md with valid frontmatter
  • All tests pass locally
  • Hook scripts output valid JSON
  • No security violations
  • CHANGELOG.md updated

CC Version Bump Audit Checklist

Philosophy: "Every time there's a new model release, we delete a bunch of code." (Anthropic). OrchestKit accumulates workarounds, prompt engineering fixes, and compatibility shims that may become unnecessary as Claude Code improves. Treat each CC version bump as a code deletion opportunity.

When bumping the minimum CC version (the >= X.Y.Z in CLAUDE.md), run this audit before tagging the release:

1. Review CC CHANGELOG for new capabilities

# Check what's new in the target CC version
# Compare current minCCVersion to target
grep -r 'Claude Code.*>=' CLAUDE.md

For each new CC capability, ask: "Does OrchestKit have a workaround for this that can now be removed?"

2. Grep for version-gated code

# Find version checks, compatibility shims, and CC-specific workarounds
grep -rn 'minCCVersion\|CC 2\.1\.\|Claude Code 2\.' src/
grep -rn 'compatibility\|workaround\|shim\|polyfill\|fallback' src/skills/ src/hooks/

Remove any code gated behind versions older than the new minimum.

3. Audit prompt engineering

Review skills for prompt patterns that compensate for fixed model weaknesses:

  • Overly explicit instructions that the model now handles natively
  • Multi-step decomposition of tasks the model can now do in one shot
  • Defensive repetition ("IMPORTANT:", "CRITICAL:", "NEVER forget to...")
  • Redundant examples where the model now understands from description alone

4. Simplify multi-phase skills

Check if complex multi-phase workflows can be collapsed:

  • Can a 3-phase skill become 2 phases?
  • Are there intermediate validation steps that the model now handles reliably?
  • Can complexity: max skills be downgraded to high?

5. Audit hook necessity

# List all hooks and their descriptions
jq '.hooks[] | {event: .matcher.tool_name // .matcher.event, type: .type}' src/hooks/hooks.json

For each hook, ask: "Is this working around a CC bug that's now fixed?"

6. Measure before/after

MetricBeforeAfter
Total lines in src/skills/find src/skills -name '*.md' | xargs wc -l
Total lines in src/hooks/find src/hooks/src -name '*.ts' | xargs wc -l
Hook countCheck hooks.json
Skill countCheck manifests/ork.json
Average skill token countnpm run test:skills output

7. Track deletion as a health metric

Include in the release PR description:

  • Lines deleted vs. lines added (target: net negative or neutral)
  • Skills simplified or removed
  • Hooks removed
  • Workarounds eliminated (with CC changelog reference)

A CC version bump PR that only bumps the version number without auditing for deletable code is incomplete.

Questions?

  1. Check existing issues and discussions
  2. Open a new discussion for general questions
  3. Open an issue for bug reports or feature requests

Thank you for contributing to OrchestKit!