Creating Skills
Step-by-step guide to adding new expertise to your Arc OS project.
Before you write a new skill — check the global library
Arc OS ships with 40+ generic skills in skills/ covering common roles. Before authoring a new one, see if existing covers your need:
| Domain | Skills available |
|---|---|
| Engineering | commit-discipline, tdd-flow, react-best-practices, postgres-patterns, mcp-builder |
| Frontend / Design | frontend-design-principles, svg-graphics, slide-deck-html, service-design, scss-modular-architecture, responsive-mobile-first |
| Startup ops | market-analysis, gtm-strategy, financial-modeling, pitch-deck, customer-development, fundraising, founder-brand, metrics-dashboard, legal-startup |
| Marketing | seo-foundations, founder-brand |
| Process | standup-aggregator, commit-discipline |
| Database | postgres-patterns |
Browse full list: GET /api/crm/skills?category=<X> or bun -e "console.log(JSON.stringify(require('./skills/_registry.json').skills.map(s=>s.name),null,2))".
Skill = trigger-matched, not always-on
Skill auto-injects into worker prompt when message matches its triggers (in _registry.json). Description field is trigger logic ("when to use this"), not a summary. Context Router picks top-5 matches per message.
What Is a Skill
A skill is a portable instruction set that teaches your AI bot a specific capability. Skills live in the skills/ directory and consist of:
<name>.md— the instruction file (required)<name>.evals.json— automatic quality validation rules (optional but recommended)references/— supporting documents, schemas, examples (optional)
skills/
├── _registry.json ← Central registry
├── my-new-skill/
│ ├── skill.md ← What the AI should do
│ ├── my-new-skill.evals.json ← How to validate the output
│ └── references/ ← Context documents
└── library/
└── domain-expert.md ← Simple single-file skills
Step 1: Write skill.md
This is the core instruction. Claude reads this when the skill is activated.
Template
# Skill Name
## Purpose
What this skill accomplishes in one sentence.
## When to Use
- Trigger condition 1
- Trigger condition 2
## Protocol
1. First action
2. Second action
3. Third action
## Output Format
What the response should look like.
## Constraints
- What NOT to do
- Safety boundaries
Guidelines
- Under 500 words — longer skills dilute context
- Imperative mood — "Check the repository" not "You should check"
- Concrete examples — show expected input/output pairs
- Specific tool references — name the commands, APIs, or patterns to use
Example: Odoo Expert
# Odoo Expert
## Purpose
Odoo ERP module development: models, views, security, QWeb templates.
## When to Use
- Building or modifying Odoo modules
- Writing QWeb templates
- Configuring access rules and record rules
- Working with the ORM (create, write, search, browse)
## Protocol
1. Check if modifying an existing module or creating new
2. Follow Odoo 17 conventions (manifest, models/, views/, security/)
3. Use self.env['model.name'] for ORM operations
4. Always use _t() or t-call for translatable strings
5. Test with --test-tags after changes
## Constraints
- Never use cr.execute() for direct SQL — use ORM
- Never use sudo() unless security context explicitly requires it
- No inline JavaScript in QWeb templates
Step 2: Write evals (Quality Rules)
Evals are declarative rules that automatically validate every response. They run after Claude generates output and add warning footnotes if rules fail.
File naming
skills/<name>/<name>.evals.json
Schema
{
"version": 1,
"skill": "my-new-skill",
"rules": [
{
"id": "ms-001",
"name": "Human-readable rule description",
"type": "rule_type",
"value": "for string/length types",
"pattern": "for regex types",
"severity": "warning"
}
]
}
Available Rule Types
| Type | What It Checks | Uses Field |
|---|---|---|
string_contains |
Response includes literal text | value (string) |
string_not_contains |
Response does NOT include text | value (string) |
regex_match |
Response matches regex | pattern (string) |
regex_not_match |
Response does NOT match regex | pattern (string) |
max_length |
Response length <= N characters | value (number) |
min_length |
Response length >= N characters | value (number) |
Severity Levels
warning— Indicates a quality problem. Shown as⚠️.info— Advisory note. Shown asℹ️.
Example
{
"version": 1,
"skill": "odoo-expert",
"rules": [
{
"id": "oe-001",
"name": "Must use ORM, not raw SQL",
"type": "string_not_contains",
"value": "cr.execute",
"severity": "warning"
},
{
"id": "oe-002",
"name": "Must use translation helpers",
"type": "regex_match",
"pattern": "_t\\(|t-call|t-esc",
"severity": "warning"
},
{
"id": "oe-003",
"name": "Response under 5000 chars",
"type": "max_length",
"value": 5000,
"severity": "info"
}
]
}
Tips
- Start with 2-3 rules. Add more as you discover patterns from corrections.
- Safety first:
string_not_containsfor dangerous commands,regex_not_matchfor credential patterns. - ID convention:
<skill-prefix>-<number>(e.g.,oe-001,gm-002). - Test: Send a message that should trigger the skill, check if warnings appear correctly.
Step 3: Register in _registry.json
The registry tells the Context Router about your skill so it can be recommended for relevant messages.
Add an entry
{
"name": "my-new-skill",
"description": "One sentence (shown in SKILLS_HINT, max 80 chars).",
"triggers": ["direct-command", "explicit-request", "пряма команда"],
"keywords": ["broader", "semantic", "related-term"],
"agents": ["rick"],
"category": ["complex"],
"phase": "21.5"
}
Triggers vs Keywords
Triggers (2 points each): Direct invocation signals. The user explicitly wants this skill.
- "deploy", "review", "scaffold", "odoo", "audit"
Keywords (1 point each): Broader terms that suggest relevance without being commands.
- "OWASP", "Docker", "CI/CD", "production", "module"
Guidelines
- Include Ukrainian equivalents in triggers for bilingual teams
- Keep descriptions under 80 characters (truncated in SKILLS_HINT)
- 5-8 triggers per skill (more = noisy matching)
- Keywords are optional but significantly improve routing
Step 4: Library Skills (Simple Format)
For domain expertise that doesn't need evals or a dedicated directory, use skills/library/:
skills/library/
├── docker-ops.md
├── postgres-pro.md
└── react-patterns.md
These are single .md files. No directory, no evals, no references. Good for knowledge-heavy, validation-light skills.
Checklist
-
skills/<name>/skill.md— instruction with Purpose, Protocol, Constraints -
skills/<name>/<name>.evals.json— at least 2 validation rules - Entry in
skills/_registry.json— with triggers and keywords - Name matches everywhere — directory name = eval skill name = registry name
- Test: send a trigger message → verify SKILLS_HINT in logs
- Test: intentionally trigger an eval failure → verify warning appears
What Happens at Runtime
1. Bot starts → loadRegistry() reads _registry.json
loadEvals() reads all .evals.json files
loadLearnings() reads learnings.md
2. Message arrives → routeContext() scores skills → SKILLS_HINT injected
formatForPrompt() adds LEARNINGS block
3. Claude responds → checkOutput() runs eval rules
formatEvalWarnings() appends footnotes
4. Fix It / thumbs-down → addLearning() writes to learnings.md
qualityTracker logs feedback
5. Nightly → findUnderperformingSkills() checks metrics
Proposals sent to CEO for approval
Seeding skills into the global DB
Skills у DB (skills_global table) — це SSOT для Context Router. File on disk is the source of truth for content; DB is the fast index for matching + auto-injection. They sync via seeder scripts.
Generic seeder by category
# Reads skills/_registry.json, filters by category, upserts skills_global rows.
# Idempotent + drift-aware (skip if no change, update on content drift).
bun scripts/seed-skill-category.ts --category engineering
bun scripts/seed-skill-category.ts --category frontend
bun scripts/seed-skill-category.ts --category marketing
Works for any category present у _registry.json. Picks first category from array as DB column value.
Specific seeders
scripts/seed-startup-skills.ts— hardcoded для category="startup" (10 startup operations skills)scripts/migrate-skills-to-db.ts— historical bulk migration (всі files, тільки insert, не update)
When to run
- After creating new
.md+_registry.jsonentry → run seeder once - After content update of existing
.md→ drift-aware seeder updates DB - New project bootstrap → seeders run via
vps-sync.shStep 1.5 (planned)
Verifying
sqlite3 data/citadel.db "SELECT name, category, owner_project FROM skills_global ORDER BY id"
owner_project = NULL → global (всі проекти бачать). owner_project = 'my-project' → project-only (тільки той проект).
Project-only vs global skills
| Skill type | owner_project |
Use case |
|---|---|---|
| Global (template) | NULL | Generic methodology (TDD, Conventional Commits, AARRR) — корисне будь-якому проекту |
| Project-only | '<project-name>' |
Documents specific to OUR codebase (e.g. crm-api-reference лежить тільки у arc-v2) |
Move existing skill to project-only:
sqlite3 data/citadel.db "UPDATE skills_global SET owner_project='my-project' WHERE name='internal-api-docs'"
listForProject filter гейтить видимість: інші проекти не побачать project-only skill.