Creating Packs
Complete guide to creating community packs: pack.yml manifest, validators, agents, knowledge, and publishing.
What is a Pack?
A pack is a modular extension that adds language-specific or framework-specific capabilities to AI Craftsman Superpowers. Packs can include:
- Rules: Validation patterns for your language/framework, with their wording and default severity
- Validators: Regex scripts the gate runs on every file of the pack’s languages
- Static analysis: Level 2 adapters for the tools of your ecosystem
- Agents: Specialized agents with domain expertise
- Canonical examples: Iron Law templates for code generation
- Knowledge: Guides and architectural patterns
Seven packs ship with the plugin: Symfony, React, Python, Go, Rust, Bash, and AI/ML. Go and Rust are the two minimal complete packs: a manifest, validators, a structure scanner, a Level 2 adapter, a canonical example and a test file. Neither ships agents or templates, which keeps them readable as a starting point. Symfony is the most complete, with agents, templates and static analysis wired in.
Quick Start
/craftsman:scaffold pack my-pack-name
This generates the pack directory structure. Edit pack.yml to configure it.
Pack Structure
packs/<name>/
├── pack.yml # Pack manifest (required)
├── hooks/
│ └── <name>-validator.sh # Regex validator, defines pack_validate_<lang>()
├── static-analysis/
│ └── <name>-analysis.sh # Level 2 tool integration
├── commands/
│ └── scaffold-types/ # Scaffold templates for /craftsman:scaffold
├── agents/
│ └── <name>-craftsman.md # Pack-specific agents
├── knowledge/
│ └── canonical/ # Canonical code examples (Iron Law)
└── templates/ # Code generation templates
pack.yml Manifest
The essentials of the shipped Go pack, packs/go/pack.yml:
name: go
version: "1.0.0"
description: "Go craftsman pack - error handling, context propagation, layer boundaries, structure"
compatibility:
core: ">=4.9.0"
stack: ["*"] # a language pack is universal: dispatch is by extension
languages: # REQUIRED for a language pack
- id: go
extensions: [go] # no leading dot
entry_markers: [go.mod, go.sum] # files that say "this project is Go"
protected_configs: [".golangci.yml", ".golangci.yaml"]
validators: ["hooks/go-validator.sh", "hooks/layer-validator.sh"]
static_analysis: ["static-analysis/go-analysis.sh"]
supersedes:
- errcheck=GO004,GO006 # errcheck's verdict outranks these two regex rules
test_commands: ["go test ./...", "gotestsum"]
lsp: "gopls"
rules:
owned: # id, wording and default severity, declared once
- id: GO001
group: Go
text: "no panic outside main and outside a Must-prefixed constructor - return an error"
default_severity: block
builtin: ["GO001", "GO002", "GO003", "GO004", "GO005", "GO006", "WARN-GO001",
"LAYER001", "NEST001", "LOC001", "PARAM001"]
static_analysis: ["ERRCHECK001"]
hooks:
validators: ["hooks/go-validator.sh", "hooks/layer-validator.sh"]
static_analysis:
tools: ["static-analysis/go-analysis.sh"]
agents: []
knowledge: ["knowledge/"]
The languages block is not optional
The engine holds no list of languages. It compiles every loaded pack.yml into the registry the hooks and craftsman-ci.sh both read. A pack without a languages: entry declaring its extensions is loaded, sourced, and never called: that is the most common reason a new pack appears to do nothing.
The other capabilities a language entry may declare:
| Key | Meaning |
|---|---|
empty_run_markers |
Lines (POSIX ERE) a test runner prints when it tested nothing. From 4.14.0, a passing run that printed one grants no verification evidence: an npm script that only echoes, a suite with every test skipped, PHPUnit with zero assertions. Shipped by the Python, React and Symfony packs |
metrics_dialect |
c-like or php-like only. It gives NEST001, LOC001, GOD001 and PARAM001 from the shared extractor; a language it cannot read declares none and emits those rules itself, as Go, Rust and Python do |
supersedes |
<tool>=<RULE>,<RULE>: a Level 2 or 3 analyser whose verdict outranks the Level 1 rule. A claimed rule is deferred, never dropped: if the tool times out, crashes or skips the rule, the regex finding is reported |
Writing a Validator
A validator is a bash script that defines pack_validate_<lang>(). The pack loader sources it and calls the function for each file of the matching language:
#!/usr/bin/env bash
pack_validate_mylang() {
local file="$1"
# MYPACK001: Description
if grep -q "bad_pattern" "$file" 2>/dev/null; then
add_violation "MYPACK001" "Descriptive error message"
fi
# WARN-MYPACK001: Soft rule
if grep -q "questionable_pattern" "$file" 2>/dev/null; then
add_warning "WARN-MYPACK001" "Consider improving this pattern"
fi
}
| Helper | Purpose |
|---|---|
add_violation "RULE_ID" "message" |
Report a finding; its severity is resolved by the rules engine, per file |
add_warning "RULE_ID" "message" |
Report an advisory finding |
line_has_ignore "$line" "RULE_ID" |
Check a line for craftsman-ignore: RULE_ID |
Severity is the engine's decision
A validator never decides whether a finding blocks: the default lives in rules.owned (default_severity), and the user’s .craft-config.yml and .craft-rules.yml override it. A hook script exits 0 (pass) or 2 (block), never 1. Keep validators regex-only; tools such as eslint, phpstan or clippy belong in static-analysis/.
Use a rule ID prefix of your own (GO, RUST, PY, SH, PHP, TS are taken), and the WARN- prefix for advisory rules.
Writing Agents
Pack agents follow the same structure as the core agents: Markdown with YAML frontmatter that states the model and the tool grant.
---
name: my-craftsman
description: Senior expert for my language: idioms, Clean Architecture, error handling
model: sonnet
effort: medium
memory: project
maxTurns: 30
tools:
- Read
- Glob
- Grep
- Bash
---
# My Pack Craftsman
You are a senior craftsman for this language...
Grant Write and Edit only to an agent whose job is to change code.
Testing Your Pack
Give the pack a test suite: source your validator, provide mock add_violation, add_warning and line_has_ignore, and assert one refused and one accepted fixture per rule. The shipped suites live in the plugin’s tests/packs/ (test-go.sh and test-rust.sh are the reference), which is where a contributed pack’s suite goes too.
Validate the pack against the conventions, and check its rule IDs for collisions:
bash <plugin-root>/scripts/validate-pack.sh packs/my-pack/
bash <plugin-root>/scripts/validate-pack.sh packs/my-pack/ --check-collisions <plugin-root>/packs/
Publishing Your Pack
- Validate: run
scripts/validate-pack.shand fix every issue - Test: run the pack test suite
- Fork: fork the ai-craftsman-superpowers repository
- Add: place your pack in
packs/my-pack/ - PR: open a pull request with a description of what your pack adds
Using External Packs
Point to a local pack in your global ~/.claude/.craft-config.yml:
packs:
external:
- path: "~/my-custom-pack" # Local path
- path: "/usr/local/share/go-pack" # System path
A pack is sourced as code, so only the machine owner can declare one: a project .craft-config.yml that lists external packs is ignored. External packs are loaded by pack-loader.sh at session start.