
Skill Builder
- 43 installs
- 416 repo stars
- Updated August 5, 2026
- boshu2/agentops
skill-builder is a Claude Code skill that scaffolds or absorbs a new SKILL.md against the AgentOps template and self-audits it before declaring success.
About
This skill materializes a new SKILL.md against the unified AgentOps template, with modes to scaffold from scratch, copy from a sibling skill, or absorb an external skill. After every build it runs skill-auditor as a mandatory self-check and aborts on a FAIL verdict. A developer uses it to author new Claude Code skills; it produces both Claude and Codex twin files and enforces a 250-line ceiling with overflow moved to references.
- Scaffolds or absorbs a new SKILL.md from the canonical AgentOps template
- Runs skill-auditor as a mandatory self-check before declaring success
- Produces Claude and Codex twin files with a 250-line ceiling
Skill Builder by the numbers
- 43 all-time installs (skills.sh)
- Ranked #345 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
skill-builder capabilities & compatibility
- Capabilities
- documentation
- Use cases
- documentation
What skill-builder says it does
Scaffold or absorb new SKILL.md files against the unified AgentOps template.
Runs `skill-auditor` on the new skill as a self-check before declaring success.
**250-line ceiling on new SKILL.md.** Use `references/` for overflow.
npx skills add https://github.com/boshu2/agentops --skill skill-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 416 |
| Last updated | August 5, 2026 |
| Repository | boshu2/agentops ↗ |
What it does
Scaffold or absorb a new SKILL.md from the AgentOps template with a mandatory auditor self-check.
Who is it for?
Scaffolding a new skill from a template or absorbing an external skill into the AgentOps template
Skip if: Scale authoring via the Workflow tool, which lacks file ownership and git evidence
When should I use this skill?
You want to create, scaffold, or absorb a new SKILL.md
What you get
A template-conforming SKILL.md plus its Codex twin, self-audited to a passing verdict.
- build-report.json
- SKILL.md
By the numbers
- 4 build modes
- 250-line SKILL.md ceiling
Files
/skill-builder — Scaffold or absorb a new SKILL.md
Materializes a new skill against the unified template at references/skill-template.md (extracted from anthropics/financial-services). Runs skill-auditor on the new skill as a self-check before declaring success.
If unsure whether the work should be a skill, a Workflow, or an NTM swarm, run `/automation-shape-routing` first — it is the front door that decides the shape and hands off to the right builder.
⚠️ Critical Constraints
- Template is canonical. All four modes produce SKILL.md files conforming to
references/skill-template.md. Do not invent ad-hoc structures. Why:skill-auditorvalidates against this template; drift creates auditor false-fails. - Self-audit is mandatory. After every successful build, the build script invokes
/skill-auditoragainst the new skill directory. A FAIL verdict aborts the build. Why: PR-002 (external validation gate) — the builder must not declare its own work complete. - Codex parity is day-1, not later.
from-scratch,from-template, andabsorb-externalmodes must produce bothskills/<name>/SKILL.mdANDskills-codex/<name>/SKILL.md+skills-codex/<name>/prompt.md. Why: finding2026-05-03-codex-skill-shape-is-dual-file— codex SKILL.md uses slim frontmatter (noskill_api_version); prompt.md is mandatory;audit-codex-parity.shis a content scanner that won't catch frontmatter drift. - Editing an EXISTING skill also needs a manual twin mirror. When you change
skills/<name>/references/*.mdorSKILL.md, manually mirror the content intoskills-codex/<name>/(runtime-native), THEN runscripts/regen-codex-hashes.sh --only <name>.make regen-allonly refreshes the twin's hash record, not its prose — a green✓ codex hashesover a stale twin looks handled but isn't. Verify with a content diff (grep -c <new-token>on both copies), not the hash exit code. Why: finding2026-06-16-codex-twin-content-not-auto-mirrored(age-aqu/age-yxl) — regen made the marker self-consistent with a stale twin (0-vs-2 token divergence) and nothing complained. The parity gate now blocks an un-mirroredreferences/**edit, but the mirror is still a manual step. - 250-line ceiling on new SKILL.md. Use
references/for overflow. Why: findingf-2026-05-01-025— every Skill() invocation reloads 5-15KB; multi-lifecycle sessions compound to 150-200KB+ pure scaffolding. - Clean-room factory inputs only. When using lessons learned from external corpora, read references/agentops-skill-factory.md and use only AgentOps-owned summaries, scripts, and rubrics. Why: productization must improve structure without copying protected third-party skill content.
- Real gate means exit code. Validate with
heal-skill --check --strict <skill-dir>andskill-auditor; never infer green from grep/regex output. Why: regex presence checks created false-greens during the 2026-06 scale build. - One skill directory = one writer. Bulk builds fan out only when each worker owns a distinct new
skills/<name>/plusskills-codex/<name>/; edits to existing skill dirs run in a later serial wave. Why: concurrent writers deleted untracked work and flipped HEAD mid-task. - Trust repository state, not subagent reports. Before declaring success, inspect
git status, generated hashes, final files, and gate exit codes. Why: sandbox-overlay and stale self-reports can claim work that never persisted. - Clean-room includes names. Do not reuse exact third-party skill names; mint AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed. Why: provenance/IP safety applies to labels as well as prose and scripts.
- Do not use the Workflow tool as the skill factory. For scale authoring, use deterministic wave scripts or NTM/Agent Mail lanes with one worker per skill. Why: skill creation needs file ownership and durable git evidence, not opaque background self-reporting.
Modes
| Mode | Status | Description |
|---|---|---|
from-scratch | stable | Interactive scaffold from canonical template. Produces full skill skeleton + scripts/validate.sh + codex parity. |
from-template | stable | --like <existing-skill> copies structure from a sibling skill, swaps domain-specific sections. |
absorb-external | stable | Reads external SKILL.md (e.g., from ~/dev/financial-services/<some-dir>/<skill>/SKILL.md), wraps in AgentOps frontmatter, invokes /converter for codex parity. |
from-pattern | alpha (passthrough) | Delegates to ao flywheel close-loop. Outputs land at .agents/knowledge/promoted/ per flywheel rules — they are NOT yet shaped as SKILL.md drafts. v2 will add skill-specific synthesis. Use from-scratch or absorb-external for SKILL.md output today. |
Workflow
Phase 1: Mode dispatch
scripts/build.sh reads $1 and routes:
build.sh from-scratch <new-skill-name> # → init.sh --interactive
build.sh from-template <new-skill-name> --like council
build.sh absorb-external <new-skill-name> --from /path/to/SKILL.md
build.sh from-pattern # → ao flywheel close-loopCheckpoint: Confirm with user the new skill's metadata.tier and metadata.dependencies before generation.
Phase 2: Materialize from template
scripts/init.sh reads references/skill-template.md (the canonical template section) and renders a SKILL.md skeleton with frontmatter pre-filled. For from-template, structure is copied from the source skill; section bodies are blanked and replaced with template stubs.
For absorb-external, the external SKILL.md's content (Constraints / Workflow / Output / Quality sections) is preserved verbatim where possible; AgentOps' structured frontmatter is added on top; the external description is reformatted to satisfy description-has-triggers.
Checkpoint: heal-skill --check --strict skills/<new-name> exits 0.
Phase 3: Codex parity
scripts/init.sh invokes /converter skills/<new-name> codex to produce skills-codex/<new-name>/{SKILL.md,prompt.md}. Then trims skill_api_version from the codex SKILL.md (converter may preserve it). Asserts prompt.md exists.
Checkpoint: bash scripts/audit-codex-parity.sh returns clean AND grep -q "^skill_api_version:" skills-codex/<name>/SKILL.md returns nothing.
Phase 4: Self-audit
The build script tail invokes /skill-auditor on skills/<new-name>. WARN is acceptable for v1 skills (e.g., experimental stability). FAIL aborts.
Checkpoint: audit_pass=true in build report.
Phase 5: Factory score overlay
For AgentOps skill upgrades, use the productization score as a patch selector, not as a replacement for skill-auditor:
python3 skills/skill-auditor/scripts/score_agentops_skill.py skills/<name> --markdownChoose the smallest patch that improves the score while preserving the canonical template and Codex parity constraints.
Phase 6: Scale factory discipline
For more than one skill, run in ownership waves:
1. Create-only wave: one worker per new skill directory. 2. Mutate wave: existing skill directories only after source creation settles. 3. Mirror/package wave: Codex mirrors and generated hashes after the canonical source corpus is complete.
Every wave ends with git status, scripts/regen-all.sh --check, and the relevant target gates by exit code. If ownership overlaps, stop and rescope.
Output Specification
Format: JSON conforming to schemas/build-report.json written to stdout; markdown audit report written to .agents/audits/<skill>-build.md.
Files created (from-scratch mode):
skills/<name>/
├── SKILL.md (≤250 lines, full template spine)
├── scripts/
│ └── validate.sh (self-validation per AgentOps convention)
└── references/ (only if expected to exceed 400 lines)
skills-codex/<name>/
├── SKILL.md (slim frontmatter — no skill_api_version)
└── prompt.md (~10-20 line Execution Profile)Quality Rubric
- [ ] All four modes produce skills that pass
skill-auditorPASS or WARN (not FAIL) - [ ]
heal-skill --check --strictexits 0 for every generated source and Codex skill directory - [ ] Codex parity files exist and pass slim-frontmatter check
- [ ] Batch authoring has one writer per skill directory and validates persisted git state
- [ ] Clean-room review covers exact names as well as prose, scripts, and examples
- [ ] No SKILL.md exceeds 250 lines (overflow goes to
references/) - [ ] Build report JSON validates against
schemas/build-report.json - [ ]
from-patternmode prominently marked alpha/passthrough in user output
Examples
Create a new skill from scratch:
/skill-builder from-scratch hello-world
# → interactive prompt: tier? deps? primary deliverable?
# → writes skills/hello-world/SKILL.md + skills-codex/hello-world/{SKILL.md,prompt.md}
# → runs /skill-auditor on the new skillClone structure from an existing skill:
/skill-builder from-template my-new-skill --like council
# → mirrors council's section spine; substitutes new metadataAbsorb a skill from anthropics/financial-services:
/skill-builder absorb-external dcf-helper \
--from ~/dev/financial-services/plugins/vertical-plugins/financial-analysis/skills/dcf-model/SKILL.md
# → preserves Constraints/Workflow/Output content, wraps in AgentOps frontmatterTroubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Self-audit FAIL | Generated SKILL.md missing required Pass-2 check | Re-run with --verbose; inspect which check failed; usually output-spec-explicit or trigger-clarity |
| Codex parity drift | /converter preserved skill_api_version | init.sh runs sed -i '/^skill_api_version:/d' skills-codex/<name>/SKILL.md; verify with grep |
| SKILL.md > 250 lines | Mode generated too much inline content | Move section bodies to references/<topic>.md; reference inline as [text](references/<topic>.md) |
from-pattern produces no SKILL.md | Expected behavior — passthrough only in v1 | Use from-scratch or absorb-external if you need a SKILL.md draft |
Corpus authoring health
Skill selection is pure LLM reasoning over the description field, so a missing trigger phrase is a skill that silently never fires. The per-skill auditor checks this only as a WARN, so the gap accumulates. Audit the whole corpus at once:
python3 skills/skill-builder/scripts/scan_descriptions.py skills # remediation report
python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any missThe scanner mirrors skill-auditor's three-form trigger detection and adds a suggested Triggers: stub per offender. See references/skill-authoring-standard.md for the full authoring doctrine and the best-practice-to-enforcement crosswalk.
See Also
- skill-auditor — companion audit gate, invoked by build self-check
- heal-skill — structural hygiene (Pass 1 of skill-auditor wraps heal.sh)
- converter — produces codex parity artifacts
- scaffold — scaffolds projects/components/CI (NOT skills)
- forge — mines transcripts into learnings (different layer)
References
- references/skill-template.md — canonical SKILL.md template + auditor checklist + PRODUCT.md alignment
- references/agentops-skill-factory.md — clean-room factory workflow and productization rules
- references/skill-authoring-standard.md — clean-room best-practices doctrine + best-practice-to-enforcement crosswalk; backs the
scan_descriptions.pytrigger scanner - references/skill-builder.feature — Executable spec: mode dispatch, materialize from template, Codex parity bundle, self-audit + factory score (soc-qk4b)
- references/hyper-extract-design-steals.md — authoring rules stolen from Hyper-Extract: the WHAT-vs-HOW (schema vs guideline) contract, canonical
{from}\|{rel}\|{to}identifier dedup-key form, and folded graph-designer/yaml-validator/template-optimizer patterns (age-bp1)
AgentOps Skill Factory Productization
This reference captures the local Codex agentops-skill-factory prototype as a repo workflow. The goal is not to ship the local prototype verbatim; the goal is to fold its proven behavior into the existing skill-builder and skill-auditor pair.
Clean-room Inputs
Use only AgentOps-owned artifacts:
docs/reference/skill-quality-rubric.mdskills/standards/references/skill-structure.mdskills/standards/references/external-source-attribution.md
Do not copy protected third-party skill prose, prompts, scripts, names, or examples into AgentOps skills. Extract reusable structure and quality signals only.
Factory Loop
1. Start with the built-in Codex skill-creator shape: a short SKILL.md kernel, progressive disclosure through references/, reusable scripts/, optional assets/, and validation evidence. 2. Score the target skill:
python3 skills/skill-auditor/scripts/score_agentops_skill.py skills/<name> --markdown3. Pick the smallest score-improving patch, usually one of:
- add or link
SELF-TEST.md; - move bulky context into
references/; - add a focused validation script;
- add an output contract or explicit quality rubric;
- tighten trigger language in frontmatter and body.
4. Re-run skill-auditor, heal-skill --check --strict, and any target-specific validation by exit code, not by grepping output text. 5. Mirror behavior into skills-codex/<name>/ or skills-codex-overrides/<name>/ when the Codex runtime needs different phrasing or execution instructions.
Scale Run Discipline
When authoring multiple skills, protect file ownership before parallelism:
- One skill equals one worker equals one source directory plus its Codex mirror.
- Run create-only work first; mutate existing skills only after the source corpus
is settled.
- Use deterministic scripts or NTM/Agent Mail lanes for batch work. Do not use
the Workflow tool as the skill factory.
- Trust
git status, generated hashes, final file contents, and gate exit codes
over worker self-reports.
- Clean-room review includes exact names. Rename third-party-derived labels into
AgentOps-owned names before source skills, Codex mirrors, or wrappers are keyed.
Productization Rule
Local prototype skills may guide the workflow, but PRs should land durable AgentOps artifacts:
- source skill changes under
skills/; - Codex runtime changes under
skills-codex/orskills-codex-overrides/; - reusable scoring/audit scripts under
skills/skill-auditor/scripts/; - clean-room standards under
docs/reference/andskills/standards/.
Avoid adding a duplicate top-level skill when an existing AgentOps skill already owns the domain. Extend skill-builder, skill-auditor, rpi, or evolve instead.
Hyper-Extract design steals (authoring rules)
Three reusable authoring contracts mined from Hyper-Extract's template-design skills (graph-designer, yaml-validator, template-optimizer). They apply to any AgentOps surface that pairs a machine-readable schema with human-written guidance — SKILL.md frontmatter + body, extraction templates, record/graph designers, and the corpus schemas under schemas/.
1. The WHAT-vs-HOW authoring contract
Rule: the schema defines WHAT (the fields, their types, their identity); the guideline defines HOW to do it well (extraction strategy, quality bars, creation conditions, common mistakes). A guideline that restates field definitions is drift — the schema already owns that. Keep them disjoint.
This is the single most load-bearing separation when a schema and a guideline travel together: every duplicated field description in the guideline is a place the two can silently diverge. Author the guideline as if the reader has already read the schema.
| Guideline SHOULD carry | Guideline should NOT carry (schema owns it) |
|---|---|
| Extraction strategy ("extract the valuable entities") | Field definitions ("name should be…") |
| Quality requirements ("keep naming consistent") | Type descriptions ("the type field is a str") |
| Creation conditions ("only when the text states it") | Reference requirements ("must point at name") |
| Common mistakes to avoid | Restated schema field descriptions |
Enforcement smell (steal from `template-optimizer` rule 4): flag any guideline/rules prose that repeats a field definition or a type description — that is a schema-vs-guideline boundary violation, not guidance. Fix by deleting the restated field text from the guideline; the schema is the single source of WHAT.
Applied to skill authoring: the SKILL.md frontmatter (name, description, hexagonal_role, consumes/produces) is the schema = WHAT the skill is; the SKILL.md body is the guideline = HOW to run it well. Do not restate frontmatter fields prose-side; spend the body on strategy, quality bars, and footguns.
2. Identifier dedup-key patterns (canonical dedup-key form)
Rule: a dedup key (identity key) for a relationship/edge is a template of field references, not a free-text string. The canonical form is:
'{from}|{rel}|{to}'i.e. pipe-joined {field} placeholders that resolve against the record's own fields. This replaces hand-written string dedup_keys (which drift from the schema and can't be validated). Hyper-Extract's live forms:
identifiers:
entity_id: name
relation_id: '{source}|{relation_type}|{target}' # graph edge
relation_id: '{source}|{relation_type}|{target}|{event_date}' # temporal edge
relation_id: '{source}|{relation_type}|{target}|{location}' # spatial edge
relation_members:
source: ...
target: ...Why a template, not a string:
- Validatable — every
{placeholder}must resolve to a declared field, so a
dedup key referencing a non-existent field is a catchable error (see the yaml-validator identifier checklist).
- Schema-anchored — the dedup key is derived from WHAT (the fields), so it
cannot silently diverge from the schema the way a free string can.
- Composable — add a dimension (time, location) by appending another
{field}; the dedup key extends with the schema instead of being rewritten.
Adopt as canonical: when a corpus/extraction surface needs an identity or dedup key, express it as a {field}|{field}|… template over declared fields — never an opaque string dedup_key.
3. Folded patterns from the three Hyper-Extract design skills
Useful, runtime-agnostic patterns lifted into our authoring doctrine:
- `graph-designer` → type-driven design + display labels. Confirm the shape
first (record vs graph vs hypergraph vs temporal/spatial), then design fields to the shape. Carry a human-readable display label derived from fields ('{name} ({category})') so output is legible without re-deriving identity. Mirror for skills: pick the skill mode first, then author to that mode.
- `yaml-validator` → tiered, ordered validation. Validate in a fixed order
(syntax → structure → identifiers → field-quality) and grade findings by level: ERROR (won't work — must fix), WARNING (quality risk — should fix), INFO (recommended). This is the same tiering our skill-auditor uses; prefer ordered + level-graded checks over a flat pass/fail.
- `template-optimizer` → information-density discipline. Flag > 5 fields per
entity/relation for review; prioritize Essential → Important → Optional and cut to the essential set. Standardize names to the concise canonical token (relation_type → type, event_date → time). Apply the same three optimization tiers to authoring fixes: Auto-fix (always-safe), Suggest (needs review), Review (a design decision, leave to the author). This is the field-count complement to our context-density rule.
Provenance
Steals captured from the Hyper-Extract reference clone (hyperextract-skills/{graph-designer,yaml-validator,template-optimizer}) under bead age-bp1. Folded as additive authoring rules; no runtime behavior depends on the Hyper-Extract code.
AgentOps Skill Authoring Standard
Clean-room distillation of the broadly accepted SKILL.md best practices (Anthropic Agent Skills guidance and the wider community consensus), restated in AgentOps's own words and cross-walked to what AgentOps tooling actually enforces. This is the doctrine layer behind skill-builder, skill-auditor, and heal-skill — read it before authoring or absorbing a skill.
This file contains only AgentOps-owned summaries and rules. It does not copy any third-party skill content (see agentops-skill-factory.md).
Contents
- Mental model: three loading levels
- The description is the trigger mechanism
- Naming (AgentOps house style)
- Progressive disclosure
- Degrees of freedom
- Anti-patterns
- Crosswalk: best practice to AgentOps enforcement
Mental model: three loading levels
A skill costs context in three tiers, and good authoring minimizes the lower tiers:
1. Metadata (name + description) — always in the system prompt. ~100 tokens. This is the only thing the runtime sees when deciding whether to load the skill. 2. SKILL.md body — loaded only after the skill triggers. Keep it under the 250-line ceiling; push overflow into references/. 3. Bundled `references/` and `scripts/` — read or executed only when the body points to them. Effectively unlimited, because they load on demand.
The implication that drives every rule below: the description does all the selection work, and the body is read far less often than authors assume.
The description is the trigger mechanism
Skill selection is pure LLM reasoning over descriptions — no embeddings, no keyword index. A description without explicit trigger phrases is a skill that silently never fires. This is the single largest latent gap in the corpus: most skills score 1/3 on trigger quality because they describe what the skill does but never when to invoke it.
Write descriptions that are:
- Third person. "Scaffolds a new skill", not "I help you scaffold".
- Specific. Name the artifact and the domain, not "helps with skills".
- Trigger-bearing. Include the phrases a user or agent would actually say.
skill-auditor accepts a trigger in any of three forms; satisfy at least one:
- Block scalar —
description: |with**Use when:**/**Triggers:**
lines inside the folded value.
- Explicit marker — a
Triggers:orUse when:clause in the
single-line description (the most common AgentOps form).
- Triggers list — a
metadata.triggers:YAML list with three or more items.
Audit the whole corpus for this gap at any time:
python3 skills/skill-builder/scripts/scan_descriptions.py skills
python3 skills/skill-builder/scripts/scan_descriptions.py skills --strict # exit 1 on any miss
python3 skills/skill-builder/scripts/scan_descriptions.py skills --json # robot modeThe scanner applies the exact detection logic of skill-auditor/scripts/audit.sh, so its verdict never contradicts the per-skill auditor; it adds the prioritized remediation list and a suggested Triggers: stub the auditor does not provide.
Naming (AgentOps house style)
The external standard prefers gerund names (processing-pdfs). AgentOps deliberately diverges: skills are named for the noun or verb of the workflow (skill-builder, bug-hunt, crank, council) so they read as commands in the /skill slash-menu. This is an intentional, documented deviation — keep new skills consistent with the existing corpus rather than introducing gerunds.
Progressive disclosure
- Keep
references/one level deep. No chains
(SKILL.md -> a.md -> b.md); the runtime may partial-read a deep file.
- Reference files over ~100 lines should open with a short table of contents.
- A SKILL.md over ~250 lines must externalize the overflow into
references/.
skill-auditor warns (references-modularization) above 400 lines with no references/ directory.
- Distinguish execute from read when pointing at a script: "Run
python scripts/x.py" versus "See scripts/x.py for the algorithm".
Degrees of freedom
Match instruction specificity to how fragile the task is:
- High freedom — multiple valid approaches (e.g. review guidelines). Give
direction, not steps.
- Medium freedom — a preferred pattern with acceptable variation (e.g.
report templates). Give a default and an escape hatch.
- Low freedom — error-prone, consistency-critical (e.g. a migration
command). Give the exact invocation and forbid alternatives.
Anti-patterns
- Multiple options, no default. Pick one tool, name it, then offer the
escape hatch ("for scanned PDFs, use X instead").
- Human docs in the skill. No README / CHANGELOG / install guide; skills are
for agents.
- Inconsistent terminology. Choose one term for a concept and use it
throughout.
- Time-sensitive claims. "Currently" and dated facts rot; move volatile
detail to an "old patterns" section or a generated artifact.
- Hardcoded absolute paths. Use repo-relative paths so the skill is portable.
- Vague descriptions. The fastest way to ship a skill that never triggers.
Crosswalk: best practice to AgentOps enforcement
| Best practice | AgentOps enforcement | Gate |
|---|---|---|
| Description carries triggers | description-has-triggers, trigger-clarity | auditor WARN (corpus drift accumulates) |
name matches directory | heal.sh NAME_MISMATCH | CI FAIL (skills-integrity) |
name + description present | heal.sh MISSING_NAME / MISSING_DESC | CI FAIL |
| Output spec names format + path | output-spec-explicit | auditor FAIL |
| Constraints front-loaded with rationale | constraints-frontloaded, rationale-present | auditor WARN |
| References one level deep, linked | heal.sh UNLINKED_REF / DEAD_REF | CI FAIL / WARN |
| 250-line ceiling | skill-builder build rejects > 250 | build-time only (no CI gate) |
| Frontmatter schema valid | validate-skill-schema.sh, v2 frontmatter | CI FAIL |
| Dependencies resolve | dependency-resolution check | CI FAIL |
| Codex parity (dual-file) | manual skills-codex/, parity-drift audit | CI FAIL (semantic), manual mirror |
| Registry / catalog current | generate-registry.sh, generate-skill-catalog.sh | CI FAIL (registry) / advisory (catalog) |
WARN-severity checks do not block a merge, which is exactly why trigger quality drifted across the corpus. Treat a WARN as a real finding, not noise — run the scanner and close the backlog incrementally.
# Executable spec for the /skill-builder skill — skill scaffolding (BC1 Corpus / Skill Catalog).
# /skill-builder scaffolds a new SKILL.md (or absorbs an existing one) against the unified
# AgentOps template, then generates the Codex parity bundle, self-audits, and overlays a
# factory score. Hexagon: supporting; consumes: a build request + the unified template;
# produces: a new skill + build-report.json. (soc-qk4b)
Feature: Skill-builder materializes template-conformant skills
As an author adding a skill to the catalog
I want it scaffolded from the unified template and parity-checked
So that every new skill is well-formed and Codex-mirrored from the start
Background:
Given the unified AgentOps SKILL.md template
Scenario: A mode is dispatched for build or absorb
When /skill-builder runs
Then it dispatches the requested mode (scaffold a new skill or absorb an existing one)
Scenario: The skill is materialized from the template
When the build proceeds
Then it produces a SKILL.md conformant to the unified template
Scenario: The Codex parity bundle is generated
When the skill is materialized
Then it generates the matching skills-codex bundle and hashes
Scenario: The build self-audits and scores before reporting
When materialization completes
Then it self-audits the result and overlays a factory score in build-report.json
Unified SKILL.md Template + Auditor Checklist
Source:anthropics/financial-services@ commitbb4a2b3e53cf27f8900b33ed6a2d95ed32e57f1d(cloned 2026-05-06). Re-extract diff if upstream restructures.
Methodology: Combines content discipline from financial-services (front-loaded constraints, verification checkpoints, output spec, quality rubric) with AgentOps' structured frontmatter (skill_api_version, context, metadata, output_contract).
This is the canonical template skill-builder materializes and skill-auditor validates against. Two artifacts in one document because both skills need identical truth.
---
1. Canonical SKILL.md template
---
name: <slug-with-hyphens>
description: |
<one-line: verb + object + domain>
**Use when:**
- <Trigger 1>
- <Trigger 2>
**Perfect for:**
- <Scenario 1>
**Not ideal for:**
- <Anti-scenario 1>
skill_api_version: 1
user-invocable: <true|false>
context:
window: <isolated|fork|inherit>
intent:
mode: <none|task|questions>
sections:
exclude: [HISTORY]
intel_scope: <none|topic|full>
metadata:
tier: <judgment|execution|library|session|product|contribute|meta|background|orchestration|cross-vendor|knowledge>
dependencies: [<other-skill-names>]
stability: <experimental|stable>
output_contract: <path-to-schema-or-description>
---
# <Title matching slug>
<1-2 sentence purpose paragraph>
## Overview / When to Use
<Detailed explanation of what + why + when>
## ⚠️ Critical Constraints
<Safety rules, data quality, governance — front-loaded, NOT buried>
- **Rule N:** <constraint>. **Why:** <rationale tied to a real consequence>
## Workflow / Methodology
<Step-by-step with verification checkpoints between phases>
### Phase 1: <name>
<instructions>
**Checkpoint:** <what to confirm before next phase>
### Phase 2: <name>
...
## Output Specification
<Format + filename + structure — explicit>
**Format:** <markdown | json | excel | etc.>
**Filename:** <naming convention>
**Structure:** <key sections / fields>
## Quality Rubric
<Checklist of deliverable criteria + sanity checks + common mistakes>
- [ ] <Check 1>
- [ ] <Check 2>
## Examples
<Usage scenarios>
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
## See Also / References
<Cross-skill links + references/*.md links>---
2. Auditor 15-check checklist
Audits run in two passes. Pass 1 runs heal-skill --check --strict for structural hygiene and gates on the exit code. Pass 2 adds 8 NEW checks not covered by heal.
Pass 1 — delegated to heal-skill (7 checks)
| Check | heal.sh code | Severity |
|---|---|---|
Frontmatter name present | MISSING_NAME | FAIL |
Frontmatter description present | MISSING_DESC | FAIL |
name matches directory | NAME_MISMATCH | FAIL |
All references/*.md linked from SKILL.md | UNLINKED_REF | WARN |
| References point at existing files | DEAD_REF | WARN |
| Scripts referenced exist | SCRIPT_REF_MISSING | WARN |
| User-invocable in catalog | CATALOG_MISSING | WARN |
Pass 2 — 8 NEW checks beyond heal-skill
The check iddescription-has-triggers(NOTdescription-multiline) is the canonical name. AgentOps' established convention is single-linedescription: '...'; the auditor must NOT false-fail that style. Three valid forms are accepted (any one passes).
| # | Check id | What passes | Severity |
|---|---|---|---|
| 1 | description-has-triggers | ANY of: (a) YAML `\ | block scalar in description, (b) body of description contains Triggers: / Use when: / Perfect for: markers, (c) metadata.triggers` array with 3+ items |
| 2 | constraints-frontloaded | First H2 within 80 lines after closing frontmatter --- contains Constraints or ⚠️ | WARN |
| 3 | rationale-present | Each constraint bullet contains why, because, this matters, or similar rationale token | WARN |
| 4 | verification-checkpoints | If skill has multi-phase Workflow/Methodology, body contains Checkpoint, confirm, or Wait for markers between phases | WARN |
| 5 | output-spec-explicit | Has ## Output / ## Deliverables / ## Returns H2 mentioning format AND filename/path convention | FAIL |
| 6 | quality-rubric | Has ## Quality / ## Checklist / ## Rubric / ## Best Practices H2 with 3+ bullet items | WARN |
| 7 | references-modularization | If SKILL.md > 400 lines, references/ subdirectory exists | WARN |
| 8 | trigger-clarity | Frontmatter description contains explicit Use when: or Triggers: markers an LLM can match | FAIL |
Verdict rule
fails > 0 → FAIL
warns > 0 → WARN
otherwise → PASS---
3. PRODUCT.md alignment mapping
Each NEW Pass-2 check maps to AgentOps' design principles in PRODUCT.md, so the auditor enforces architectural intent rather than arbitrary stylistic preferences.
| Auditor check | PRODUCT.md anchor |
|---|---|
constraints-frontloaded (⚠️ near top) | Operational Principle #6 (atomic changes compose) — constraint visibility prevents large rework |
rationale-present | Operational Principle #1 (agents are ephemeral; system carries state) — rationale must be inside the artifact, not in human memory |
verification-checkpoints | Operational Principle #5 (two-tier execution) — checkpoints prevent worker drift between phases |
output-spec-explicit | Pillar #4 (Kubernetes control loops) — declared state must be machine-readable |
quality-rubric | Operational Principle #3 (context quality determines output quality) |
references-modularization | Finding f-2026-05-01-025 (SKILL.md churn budget — every Skill() invocation reloads 5-15KB) |
trigger-clarity | Operational Principle #1 (agents are ephemeral) — invocation criteria must be in artifact |
description-has-triggers (renamed from description-multiline) | Pillar #6 (knowledge flywheel) — searchability requires structured description. Three valid forms preserve AgentOps' single-line convention. |
---
4. Section spine — REQUIRED order
H1 title
└── Overview / When to Use
└── ⚠️ Critical Constraints ← MUST appear within first 80 lines after frontmatter
└── Workflow / Methodology ← MUST contain checkpoints between phases when multi-phase
└── Output Specification ← MUST include format + filename
└── Quality Rubric ← MUST contain 3+ bullets
└── Examples
└── Troubleshooting
└── See Also / ReferencesThe ## Examples and ## Troubleshooting sections are recommended but not enforced (heal-skill catches missing references; the auditor leaves these to taste).
---
5. Frontmatter requirements (cross-reference)
Validates against schemas/skill-frontmatter.v1.schema.json. Required fields:
name(string, lowercase-hyphen, must match directory)description(string, see check #1 for accepted forms)skill_api_version(integer, const: 1)
Plus expected:
metadata.tier(one of the 11 enum values)context.window(one of:isolated,fork,inherit)output_contract(path to JSON Schema or description string)
---
6. Codex parity contract (per learning 2026-05-03-codex-skill-shape-is-dual-file)
For every shipped AgentOps skill, both files must exist:
skills-codex/<name>/SKILL.md— slim frontmatter (NOskill_api_version)skills-codex/<name>/prompt.md— short Execution Profile (~10-20 lines)
scripts/audit-codex-parity.sh is a content scanner; it will NOT catch frontmatter shape violations. Explicit grep checks (no skill_api_version: in codex SKILL.md; prompt.md exists) belong in the skill's own validation block.
---
7. Out-of-scope for v1 (stocktake territory)
The following deeper audits are described in skills/heal-skill/references/skill-stocktake.md but NOT yet implemented anywhere — defer to v2:
- Actionability (does it produce concrete artifacts?)
- Scope fit (right tier for the task?)
- Uniqueness (overlap with other skills?)
- Currency (referenced tools/APIs still current?)
- LLM-decidable trigger clarity (deeper than
trigger-claritycheck above)
{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "Skill Build Report",
"description": "Output contract for skill-builder. Reports what mode ran, which files were created, and whether the post-build self-audit passed.",
"type": "object",
"required": ["mode", "skill_name", "files_created", "audit_pass"],
"properties": {
"mode": {
"type": "string",
"enum": ["from-scratch", "from-template", "absorb-external", "from-pattern"],
"description": "Which builder mode produced this skill. from-pattern is alpha (passthrough to ao flywheel close-loop)."
},
"skill_name": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$",
"description": "Lowercase-hyphen slug for the new skill (matches directory name)."
},
"files_created": {
"type": "array",
"items": {"type": "string"},
"description": "Absolute or repo-relative paths of files written during this build."
},
"audit_pass": {
"type": "boolean",
"description": "Did the immediate post-build self-audit (skill-auditor) emit VERDICT: PASS or VERDICT: WARN? (False if FAIL.)"
},
"audit_report_path": {
"type": "string",
"description": "Where the self-audit report was written (typically .agents/audits/<skill>-build.md)."
},
"warnings": {
"type": "array",
"items": {"type": "string"},
"description": "Non-fatal issues surfaced during build (e.g., template SHA pin mismatch, missing optional sections)."
},
"source": {
"type": "object",
"description": "Mode-specific provenance.",
"properties": {
"external_path": {"type": "string", "description": "For absorb-external: source SKILL.md path"},
"template_skill": {"type": "string", "description": "For from-template: skill copied from"},
"flywheel_run_id": {"type": "string", "description": "For from-pattern: ao flywheel close-loop run id"}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
#!/usr/bin/env bash
# build.sh — skill-builder mode dispatcher
# Usage:
# build.sh from-scratch <skill-name>
# build.sh from-template <skill-name> --like <existing-skill>
# build.sh absorb-external <skill-name> --from <path-to-external-SKILL.md>
# build.sh from-pattern # alpha: passthrough to ao flywheel close-loop
#
# Always runs skill-auditor on the new skill as a self-check before declaring success.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
INIT_SH="$SCRIPT_DIR/init.sh"
AUDITOR_SH="$REPO_ROOT/skills/skill-auditor/scripts/audit.sh"
usage() {
cat <<EOF
Usage:
$0 from-scratch <skill-name>
$0 from-template <skill-name> --like <existing-skill>
$0 absorb-external <skill-name> --from <path-to-external-SKILL.md>
$0 from-pattern # alpha: passthrough to ao flywheel close-loop
Modes:
from-scratch Interactive scaffold from canonical template
from-template Copy structure from a sibling skill
absorb-external Wrap an external SKILL.md in AgentOps frontmatter
from-pattern ALPHA — delegates to 'ao flywheel close-loop'.
Outputs at .agents/knowledge/promoted/, NOT shaped as SKILL.md drafts.
Use from-scratch or absorb-external for SKILL.md output today.
EOF
exit 2
}
[[ $# -lt 1 ]] && usage
MODE="$1"
shift
case "$MODE" in
from-pattern)
# Alpha passthrough — explicitly documented in SKILL.md
echo "[skill-builder] from-pattern is ALPHA — delegating to 'ao flywheel close-loop'"
echo "[skill-builder] Output will NOT be a SKILL.md draft; it lands at .agents/knowledge/promoted/"
exec ao flywheel close-loop "$@"
;;
from-scratch)
[[ $# -lt 1 ]] && { echo "Error: from-scratch requires <skill-name>" >&2; usage; }
SKILL_NAME="$1"; shift
bash "$INIT_SH" --interactive "$SKILL_NAME" "$@"
;;
from-template)
[[ $# -lt 1 ]] && { echo "Error: from-template requires <skill-name>" >&2; usage; }
SKILL_NAME="$1"; shift
bash "$INIT_SH" --like-flag-mode "$SKILL_NAME" "$@"
;;
absorb-external)
[[ $# -lt 1 ]] && { echo "Error: absorb-external requires <skill-name>" >&2; usage; }
SKILL_NAME="$1"; shift
bash "$INIT_SH" --absorb "$SKILL_NAME" "$@"
;;
*)
echo "Error: unknown mode '$MODE'" >&2
usage
;;
esac
# Post-build self-audit (mandatory per Critical Constraints)
NEW_SKILL_DIR="$REPO_ROOT/skills/$SKILL_NAME"
if [[ ! -d "$NEW_SKILL_DIR" ]]; then
echo "[skill-builder] ERROR: expected $NEW_SKILL_DIR to exist after init.sh" >&2
exit 1
fi
# The build report (written by init.sh) carries audit_pass=null as a pre-audit
# placeholder. Patch it to the REAL audit outcome here so the report records what
# actually happened and stays schema-valid (build-report.json requires
# audit_pass to be a boolean). Without this the report's audit_pass was always
# null — the original defect this fixes (age-fix-skill-factory-mcc).
BUILD_REPORT="$REPO_ROOT/.agents/audits/${SKILL_NAME}-build.json"
# patch_audit_pass <true|false> — record the audit outcome in the build report.
# Never fails the build: a missing report or absent jq/python3 only warns.
patch_audit_pass() {
[[ -f "$BUILD_REPORT" ]] || return 0
local val="$1" tmp
# Create the temp in the report's OWN directory so the mv is an atomic
# same-filesystem rename (a cross-device mv can degrade to a non-atomic copy
# and corrupt the report on failure).
tmp="$(mktemp "$(dirname "$BUILD_REPORT")/.audit-patch.XXXXXX")" || return 0
if command -v jq >/dev/null 2>&1; then
# Fold the mv into the tested condition so a failed write only WARNs (never
# aborts the build under set -e).
if jq --argjson ap "$val" '.audit_pass = $ap' "$BUILD_REPORT" >"$tmp" 2>/dev/null \
&& mv "$tmp" "$BUILD_REPORT" 2>/dev/null; then
:
else
rm -f "$tmp"; echo "[skill-builder] WARN: could not patch audit_pass in $BUILD_REPORT" >&2
fi
elif command -v python3 >/dev/null 2>&1; then
if python3 - "$BUILD_REPORT" "$val" >"$tmp" 2>/dev/null <<'PY' && mv "$tmp" "$BUILD_REPORT" 2>/dev/null
import json, sys
d = json.load(open(sys.argv[1]))
d["audit_pass"] = (sys.argv[2] == "true")
json.dump(d, sys.stdout, indent=2)
PY
then
:
else
rm -f "$tmp"; echo "[skill-builder] WARN: could not patch audit_pass in $BUILD_REPORT" >&2
fi
else
rm -f "$tmp"; echo "[skill-builder] WARN: no jq/python3 to record audit_pass in $BUILD_REPORT" >&2
fi
return 0
}
if [[ -x "$AUDITOR_SH" ]]; then
echo ""
echo "[skill-builder] Running self-audit on $NEW_SKILL_DIR..."
if bash "$AUDITOR_SH" "$NEW_SKILL_DIR"; then
patch_audit_pass true
echo "[skill-builder] Self-audit PASS or WARN — build complete (audit_pass=true)"
else
# Record the failure before aborting so the report reflects reality.
patch_audit_pass false
echo "[skill-builder] Self-audit FAIL — build aborted (audit_pass=false)" >&2
exit 1
fi
else
# An unaudited build cannot claim a pass: record false rather than leaving the
# null placeholder (which would be schema-invalid and read as "audited").
patch_audit_pass false
echo "[skill-builder] WARN: skill-auditor not found at $AUDITOR_SH; skipping self-audit (audit_pass=false)" >&2
fi
#!/usr/bin/env bash
# init.sh — materialize a new skill from the canonical template
# Invoked by build.sh; not typically called directly.
#
# Usage:
# init.sh --interactive <skill-name>
# init.sh --like-flag-mode <skill-name> --like <source-skill>
# init.sh --absorb <skill-name> --from <path-to-external-SKILL.md>
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
TEMPLATE_REF="$REPO_ROOT/skills/skill-builder/references/skill-template.md"
[[ -f "$TEMPLATE_REF" ]] || { echo "init.sh: missing $TEMPLATE_REF" >&2; exit 1; }
MODE="${1:?usage: init.sh --interactive|--like-flag-mode|--absorb <name> [opts]}"
shift
SKILL_NAME="${1:?missing <skill-name>}"
shift
# Validate slug
[[ "$SKILL_NAME" =~ ^[a-z][a-z0-9-]*$ ]] || {
echo "init.sh: skill name '$SKILL_NAME' must be lowercase-hyphen (e.g. my-skill)" >&2
exit 1
}
NEW_DIR="$REPO_ROOT/skills/$SKILL_NAME"
NEW_SKILL_MD="$NEW_DIR/SKILL.md"
[[ -e "$NEW_DIR" ]] && { echo "init.sh: $NEW_DIR already exists; aborting" >&2; exit 1; }
mkdir -p "$NEW_DIR/scripts"
# --- Per-mode population --------------------------------------------------
case "$MODE" in
--interactive)
# Minimal non-blocking defaults; skip prompts in CI by reading env vars
TIER="${SKILL_TIER:-execution}"
DEPS="${SKILL_DEPS:-[]}"
INTENT_MODE="${SKILL_INTENT_MODE:-task}"
;;
--like-flag-mode)
LIKE_FLAG="${1:-}"; SOURCE_SKILL="${2:-}"
[[ "$LIKE_FLAG" == "--like" && -n "$SOURCE_SKILL" ]] || {
echo "init.sh --like-flag-mode requires '--like <source-skill>'" >&2
exit 1
}
SOURCE_DIR="$REPO_ROOT/skills/$SOURCE_SKILL"
[[ -f "$SOURCE_DIR/SKILL.md" ]] || {
echo "init.sh: source skill $SOURCE_DIR/SKILL.md not found" >&2
exit 1
}
# Extract frontmatter values from source for sane defaults
TIER="$(awk '/^---$/{n++;next} n==1 && /^[ ]+tier:/{print $2; exit}' "$SOURCE_DIR/SKILL.md")"
TIER="${TIER:-execution}"
DEPS="[]"
INTENT_MODE="$(awk '/^---$/{n++;next} n==1 && /^[ ]+mode:/{print $2; exit}' "$SOURCE_DIR/SKILL.md")"
INTENT_MODE="${INTENT_MODE:-task}"
;;
--absorb)
FROM_FLAG="${1:-}"; SOURCE_PATH="${2:-}"
[[ "$FROM_FLAG" == "--from" && -n "$SOURCE_PATH" ]] || {
echo "init.sh --absorb requires '--from <path-to-external-SKILL.md>'" >&2
exit 1
}
[[ -f "$SOURCE_PATH" ]] || { echo "init.sh: external SKILL.md not found at $SOURCE_PATH" >&2; exit 1; }
TIER="${SKILL_TIER:-execution}"
DEPS="[]"
INTENT_MODE="task"
;;
*)
echo "init.sh: unknown mode '$MODE'" >&2
exit 2
;;
esac
# --- Render frontmatter + skeleton ---------------------------------------
cat > "$NEW_SKILL_MD" <<EOF
---
name: $SKILL_NAME
description: |
<one-line: verb + object + domain>
**Use when:**
- <Trigger 1>
- <Trigger 2>
**Triggers:** "<trigger phrase 1>", "<trigger phrase 2>"
**Not ideal for:**
- <Anti-scenario 1>
skill_api_version: 1
context:
window: fork
intent:
mode: $INTENT_MODE
sections:
exclude: [HISTORY]
intel_scope: topic
metadata:
tier: $TIER
dependencies: $DEPS
stability: experimental
output_contract: "TODO: path to schema or output description"
---
# /$SKILL_NAME — <Title matching slug>
<1-2 sentence purpose paragraph>
## Overview
<What this skill does, why it matters, and when to use it>
## ⚠️ Critical Constraints
- **Rule 1:** <constraint>. **Why:** <rationale>
## Workflow
### Phase 1: <name>
<instructions>
**Checkpoint:** <what to confirm before next phase>
## Output Specification
**Format:** <markdown | json | excel | etc.>
**Filename:** <naming convention>
**Structure:** <key sections / fields>
## Quality Rubric
- [ ] <Check 1>
- [ ] <Check 2>
- [ ] <Check 3>
## Examples
\`\`\`bash
/$SKILL_NAME <example-args>
\`\`\`
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
## See Also
- [skill-auditor](../skill-auditor/SKILL.md) — audit this skill before declaring stable
EOF
# --- Mode-specific content injection -------------------------------------
if [[ "$MODE" == "--absorb" ]]; then
# Append a "Source" reference section pointing at the absorbed external doc
cat >> "$NEW_SKILL_MD" <<EOF
## References
- Source absorbed from: \`$SOURCE_PATH\`
EOF
fi
# --- Companion files -----------------------------------------------------
cat > "$NEW_DIR/scripts/validate.sh" <<'EOF'
#!/usr/bin/env bash
# validate.sh — minimal self-validation
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)"
exec bash "$REPO_ROOT/skills/skill-auditor/scripts/audit.sh" "$SKILL_DIR"
EOF
chmod +x "$NEW_DIR/scripts/validate.sh"
chmod +x "$NEW_DIR" 2>/dev/null || true
# --- Codex parity (slim frontmatter + prompt.md) -------------------------
CODEX_DIR="$REPO_ROOT/skills-codex/$SKILL_NAME"
mkdir -p "$CODEX_DIR"
# Try /converter if present, otherwise hand-build
CONVERTER="$REPO_ROOT/skills/converter/scripts/convert.sh"
if [[ -x "$CONVERTER" ]]; then
bash "$CONVERTER" "skills/$SKILL_NAME" codex 2>/dev/null || {
echo "init.sh: converter failed; falling back to hand-built codex artifacts" >&2
}
fi
# Hand-build codex SKILL.md (slim frontmatter — NO skill_api_version per learning 2026-05-03)
if [[ ! -f "$CODEX_DIR/SKILL.md" ]]; then
cat > "$CODEX_DIR/SKILL.md" <<EOF
---
name: $SKILL_NAME
description: <copy from skills/$SKILL_NAME/SKILL.md description>
---
# /$SKILL_NAME
See \`skills/$SKILL_NAME/SKILL.md\` for the canonical specification.
## Codex Execution Profile
See \`prompt.md\` in this directory.
EOF
fi
# Always trim skill_api_version from codex SKILL.md if present
if grep -q "^skill_api_version:" "$CODEX_DIR/SKILL.md"; then
sed -i.bak '/^skill_api_version:/d' "$CODEX_DIR/SKILL.md" && rm -f "$CODEX_DIR/SKILL.md.bak"
fi
# Hand-build prompt.md
if [[ ! -f "$CODEX_DIR/prompt.md" ]]; then
cat > "$CODEX_DIR/prompt.md" <<EOF
# Execution Profile: $SKILL_NAME
You are running /$SKILL_NAME.
See \`SKILL.md\` in this directory for full specification, OR
read \`skills/$SKILL_NAME/SKILL.md\` in the host repo for the canonical document.
Workflow:
1. Read the user's request
2. Apply the skill's Workflow section
3. Produce output per the Output Specification
4. Self-check against the Quality Rubric
EOF
fi
# --- Build report --------------------------------------------------------
BUILD_REPORT="$REPO_ROOT/.agents/audits/${SKILL_NAME}-build.json"
mkdir -p "$(dirname "$BUILD_REPORT")"
cat > "$BUILD_REPORT" <<EOF
{
"mode": "${MODE#--}",
"skill_name": "$SKILL_NAME",
"files_created": [
"skills/$SKILL_NAME/SKILL.md",
"skills/$SKILL_NAME/scripts/validate.sh",
"skills-codex/$SKILL_NAME/SKILL.md",
"skills-codex/$SKILL_NAME/prompt.md"
],
"audit_pass": null,
"warnings": ["v1 skeleton — manual content fill required for description, constraints, workflow"]
}
EOF
# --- New-skill plumbing (ag-cw2y): make the scaffold one-shot-green ----------
# The local/CI gates that silently tripped /burndown #600 are pre-empted here:
# 1. Dispositions row — else heal.sh Check 12 (MISSING_DISPOSITION).
if [[ -f "$REPO_ROOT/scripts/append-skill-disposition.sh" ]]; then
bash "$REPO_ROOT/scripts/append-skill-disposition.sh" "$SKILL_NAME" "$REPO_ROOT" \
|| echo "init.sh: WARN could not append dispositions row — add one manually" >&2
fi
# 2. Narrative skill counts — --fix-counts bumps the "N checked-in skills" tokens
# in the domain-map + bdd Gherkin so the new skill doesn't trip registry-drift.
if [[ -x "$REPO_ROOT/scripts/check-registry-drift.sh" ]]; then
bash "$REPO_ROOT/scripts/check-registry-drift.sh" --fix-counts >/dev/null 2>&1 \
|| echo "init.sh: WARN registry-drift --fix-counts could not run — bump counts manually" >&2
fi
# 3. Codex override catalog entry — else validate-codex-override-coverage fails
# ("source skill missing from Codex catalog"). Default parity_only (derived).
if [[ -f "$REPO_ROOT/scripts/append-codex-override-entry.sh" ]]; then
bash "$REPO_ROOT/scripts/append-codex-override-entry.sh" "$SKILL_NAME" "$REPO_ROOT" \
|| echo "init.sh: WARN could not add codex override catalog entry — add one manually" >&2
fi
# 4. registry.json SKU catalog — else contracts-sync + correctness(ubuntu) BOTH
# fail ("registry.json is stale" / "SKU_CATALOG: DRIFT"). This is the 5th
# one-shot-green surface ag-cw2y missed; it cost /burndown #600 a 2nd
# fix-and-repush (ag-ekyq). MUST run last — it scans the whole skills/ tree,
# so the new skeleton must already exist on disk.
if [[ -f "$REPO_ROOT/scripts/generate-registry.sh" ]]; then
bash "$REPO_ROOT/scripts/generate-registry.sh" >/dev/null 2>&1 \
|| echo "init.sh: WARN could not regen registry.json — run scripts/generate-registry.sh manually" >&2
fi
echo "init.sh: created skill skeleton at $NEW_DIR"
echo "init.sh: codex parity at $CODEX_DIR"
echo "init.sh: build report at $BUILD_REPORT"
echo "init.sh: dispositions row + narrative counts scaffolded (refine the placeholder row)"
#!/usr/bin/env python3
"""Corpus-wide skill description trigger scanner.
The per-skill `skill-auditor` runs `description-has-triggers` / `trigger-clarity`
as WARN checks, so a missing trigger phrase never blocks a merge and the gap
accumulates silently across the corpus. This scanner is the corpus-wide
companion: it walks every `skills/*/SKILL.md`, applies the *same* three-form
trigger detection as `skill-auditor/scripts/audit.sh`, scores each description,
and emits a prioritized remediation list with a suggested `Triggers:` stub for
each skill that lacks one.
Discovery in the runtime is pure LLM reasoning over the `description` field, so
a missing trigger phrase is a material skill-selection risk, not cosmetic. See
`skills/skill-builder/references/skill-authoring-standard.md`.
Usage:
python3 scan_descriptions.py [SKILLS_DIR] [--json] [--strict] [--quiet]
python3 scan_descriptions.py [SKILLS_DIR] --probe "<phrase>" [--json]
python3 scan_descriptions.py [SKILLS_DIR] --list-probes
Probe mode (`--probe "<phrase>"`) ranks every skill against the phrase using
ONLY the deterministic lexical ranker below — no live model, no `claude -p`, no
network — and asserts the skill that DECLARES the phrase in its
`trigger_probes:` frontmatter list ranks #1. Output is byte-stable across runs.
Exit codes:
0 every description carries a trigger (or --strict not set);
in --probe mode: the declaring skill ranks #1 for the phrase
1 one or more descriptions lack a trigger AND --strict is set;
in --probe mode: the declaring skill does NOT rank #1
2 usage error (skills dir not found, or no skill declares the phrase)
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
# Phrases that count as an explicit trigger marker, mirroring the regex in
# skill-auditor/scripts/audit.sh (Form b). Keep these in sync with the auditor.
TRIGGER_MARKERS = (
"**Use when:",
"**Triggers:",
"**Perfect for:",
"Use when:",
"Triggers:",
)
# Stop-words stripped when deriving a suggested trigger stub from the name.
_STOPWORDS = frozenset({"the", "a", "an", "for", "and", "to", "of", "with"})
@dataclass
class SkillScan:
"""Result of scanning one SKILL.md for trigger quality."""
name: str
path: Path
description: str
has_trigger: bool
forms: list[str] = field(default_factory=list)
score: int = 0
suggestion: str = ""
def to_dict(self) -> dict:
"""Return a JSON-serializable view for --json / robot mode."""
return {
"name": self.name,
"path": str(self.path),
"has_trigger": self.has_trigger,
"forms": self.forms,
"score": self.score,
"suggestion": self.suggestion,
}
def split_frontmatter(text: str) -> tuple[str, str]:
"""Split a SKILL.md into (frontmatter, body). Empty frontmatter if absent."""
if not text.startswith("---"):
return "", text
parts = text.split("\n---", 1)
if len(parts) != 2:
return "", text
frontmatter = parts[0][len("---") :]
body = parts[1].lstrip("-\n")
return frontmatter, body
def parse_field(frontmatter: str, key: str) -> str:
"""Extract a single top-level scalar field's first line from frontmatter."""
match = re.search(rf"^{re.escape(key)}:\s*(.*)$", frontmatter, re.MULTILINE)
return match.group(1).strip() if match else ""
def description_block(frontmatter: str) -> str:
"""Return the full description value, including folded/literal continuations."""
lines = frontmatter.splitlines()
out: list[str] = []
capturing = False
for line in lines:
if line.startswith("description:"):
capturing = True
out.append(line)
continue
if capturing:
# A new top-level key (no leading whitespace, ends the block).
if re.match(r"^[A-Za-z_-]+:", line):
break
out.append(line)
return "\n".join(out)
def count_trigger_list(frontmatter: str) -> int:
"""Count items under a `metadata.triggers:` (or `triggers:`) YAML list."""
lines = frontmatter.splitlines()
in_list = False
count = 0
for line in lines:
if re.match(r"^\s+triggers:\s*$", line):
in_list = True
continue
if in_list:
if re.match(r"^\s+-\s+", line):
count += 1
continue
if re.match(r"^\s*[A-Za-z_-]+:", line):
break
return count
def split_flow_items(inner: str) -> list[str]:
"""Split a simple YAML flow sequence body without breaking quoted commas."""
items: list[str] = []
current: list[str] = []
quote = ""
i = 0
while i < len(inner):
char = inner[i]
if quote:
if char == "\\" and quote == '"' and i + 1 < len(inner):
current.append(inner[i + 1])
i += 2
continue
if char == quote:
if quote == "'" and i + 1 < len(inner) and inner[i + 1] == "'":
current.append("'")
i += 2
continue
quote = ""
else:
current.append(char)
elif char in ("'", '"'):
quote = char
elif char == ",":
items.append("".join(current))
current = []
else:
current.append(char)
i += 1
items.append("".join(current))
return items
def parse_trigger_probes(frontmatter: str) -> list[str]:
"""Return the items under a top-level `trigger_probes:` YAML list.
Supports the flow form (`trigger_probes: ["a", "b"]`) and the block form
(`trigger_probes:` followed by indented `- item` lines). Quotes are
stripped; order is preserved. Purely lexical — no YAML library required so
the scanner stays dependency-free and deterministic.
"""
lines = frontmatter.splitlines()
probes: list[str] = []
for idx, line in enumerate(lines):
flow = re.match(r"^trigger_probes:\s*\[(.*)\]\s*$", line)
if flow:
inner = flow.group(1).strip()
if inner:
for item in split_flow_items(inner):
cleaned = item.strip().strip("'\"").strip()
if cleaned:
probes.append(cleaned)
return probes
if re.match(r"^trigger_probes:\s*$", line):
for follow in lines[idx + 1 :]:
item = re.match(r"^\s+-\s+(.*)$", follow)
if item:
cleaned = item.group(1).strip().strip("'\"").strip()
if cleaned:
probes.append(cleaned)
continue
if re.match(r"^\S", follow):
break
return probes
return probes
_WORD_RE = re.compile(r"[a-z0-9]+")
def _tokens(text: str) -> list[str]:
"""Lowercase alphanumeric tokens, in order, for deterministic scoring."""
return _WORD_RE.findall(text.lower())
def lexical_score(phrase: str, scan: SkillScan) -> tuple:
"""Deterministic lexical relevance of one skill to a probe phrase.
Pure token math over the skill's own SEARCHABLE TEXT (name + description) —
no model, no network, and deliberately NOT a function of the skill's
`trigger_probes:` declaration. The declaration only identifies *which*
skill is expected to win; the ranking itself is earned purely by lexical
overlap, so a skill that stops describing its phrase genuinely drops in
rank. Returns a tuple sort key (higher is more relevant); ties break by
name so the ranking is total and byte-stable. Signals, in priority order:
1. fraction of phrase tokens present in the searchable text (coverage),
2. raw count of phrase-token hits,
3. name-token overlap (a phrase word that is also a name word).
"""
phrase_tokens = _tokens(phrase)
name_tokens = set(_tokens(scan.name))
haystack = " ".join([scan.name, scan.description])
hay_tokens = _tokens(haystack)
hay_set = set(hay_tokens)
if phrase_tokens:
present = sum(1 for t in phrase_tokens if t in hay_set)
coverage = present / len(phrase_tokens)
hits = sum(hay_tokens.count(t) for t in set(phrase_tokens))
else:
coverage = 0.0
hits = 0
name_overlap = sum(1 for t in set(phrase_tokens) if t in name_tokens)
return (coverage, hits, name_overlap)
@dataclass
class ProbeResult:
"""One skill's deterministic rank for a probe phrase."""
name: str
score_key: tuple
declares_phrase: bool
def to_dict(self) -> dict:
"""JSON-serializable view (score_key as a list for stable output)."""
return {
"name": self.name,
"score_key": list(self.score_key),
"declares_phrase": self.declares_phrase,
}
def probe_corpus(skills_dir: Path, phrase: str) -> list[ProbeResult]:
"""Rank every skill against `phrase` using the deterministic lexical ranker.
Sorted by descending score, then ascending name — a total, byte-stable
order. Each result records whether that skill declares the phrase in its
`trigger_probes:` list.
"""
ranked: list[ProbeResult] = []
for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
scan = scan_skill(skill_md)
if scan is None:
continue
frontmatter, _ = split_frontmatter(skill_md.read_text(encoding="utf-8"))
probes = parse_trigger_probes(frontmatter)
key = lexical_score(phrase, scan)
declares = phrase.strip().lower() in {p.strip().lower() for p in probes}
ranked.append(ProbeResult(name=scan.name, score_key=key, declares_phrase=declares))
def sort_key(result: ProbeResult) -> tuple:
# Descending score (negate the numeric components), ascending name.
return (tuple(-x for x in result.score_key), result.name)
ranked.sort(key=sort_key)
return ranked
def render_probe(phrase: str, ranked: list[ProbeResult]) -> str:
"""Render a deterministic human-readable probe report."""
declaring = [r.name for r in ranked if r.declares_phrase]
lines = [
"# Trigger probe",
"",
f"- Phrase: {phrase!r}",
f"- Skills ranked: {len(ranked)}",
f"- Declaring skills: {', '.join(declaring) if declaring else '(none)'}",
"",
"## Ranking (deterministic lexical, no model)",
"",
"| Rank | Skill | Declares | Score key |",
"|------|-------|----------|-----------|",
]
for i, r in enumerate(ranked, start=1):
mark = "yes" if r.declares_phrase else ""
lines.append(f"| {i} | `{r.name}` | {mark} | {list(r.score_key)} |")
return "\n".join(lines)
def detect_trigger(text: str, frontmatter: str) -> list[str]:
"""Return the trigger forms present, matching audit.sh's three forms.
Form a: `description: |` literal block scalar.
Form b: an explicit marker (`Use when:` / `Triggers:` / `Perfect for:`)
anywhere in the file, mirroring audit.sh's whole-file grep.
Form c: a `triggers:` YAML list with three or more items.
"""
forms: list[str] = []
desc_value = parse_field(frontmatter, "description")
if desc_value.startswith("|"):
forms.append("block-scalar")
if any(marker in text for marker in TRIGGER_MARKERS):
forms.append("explicit-marker")
if count_trigger_list(frontmatter) >= 3:
forms.append("triggers-list")
return forms
def score_trigger(description: str) -> int:
"""Score 0-3, mirroring skill-auditor/scripts/score_agentops_skill.py."""
signals = sum(
marker.lower().strip("*").rstrip(":") in description.lower()
for marker in ("Use when", "Triggers", "Perfect for")
)
return min(3, int(bool(description.strip())) + signals)
def suggest_triggers(name: str, description: str) -> str:
"""Derive a deterministic `Triggers:` stub from the skill name + first verb."""
tokens = [t for t in name.split("-") if t not in _STOPWORDS]
spaced = " ".join(tokens)
first_sentence = re.split(r"[.\n]", description.strip(), maxsplit=1)[0]
words = first_sentence.split()
verb = words[0].lower().strip("'\"") if words else ""
candidates = [name, spaced]
# Only add a verb phrase when the verb adds a word not already in the name.
if verb and tokens and verb not in tokens:
candidates.append(f"{verb} {tokens[-1]}")
seen: list[str] = []
for phrase in candidates:
cleaned = " ".join(dict.fromkeys(phrase.strip().lower().split()))
if cleaned and cleaned not in seen:
seen.append(cleaned)
quoted = ", ".join(f'"{p}"' for p in seen)
return f"Triggers: {quoted}"
def scan_skill(skill_md: Path) -> SkillScan | None:
"""Scan one SKILL.md. Returns None if the file is unreadable/empty."""
try:
text = skill_md.read_text(encoding="utf-8")
except OSError:
return None
frontmatter, _body = split_frontmatter(text)
name = parse_field(frontmatter, "name") or skill_md.parent.name
description = description_block(frontmatter)
forms = detect_trigger(text, frontmatter)
has_trigger = bool(forms)
scan = SkillScan(
name=name,
path=skill_md,
description=description,
has_trigger=has_trigger,
forms=forms,
score=score_trigger(description),
)
if not has_trigger:
scan.suggestion = suggest_triggers(name, parse_field(frontmatter, "description"))
return scan
def scan_corpus(skills_dir: Path) -> list[SkillScan]:
"""Scan every `<skill>/SKILL.md` under skills_dir, sorted by name."""
results: list[SkillScan] = []
for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
scan = scan_skill(skill_md)
if scan is not None:
results.append(scan)
return results
def list_probe_pairs(skills_dir: Path) -> list[tuple[str, str]]:
"""Return every (skill-id, probe-phrase) pair declared in the corpus.
The skill-id is the SKILL.md's parent directory name (matching what the
rest of the tooling keys on). Phrases come from the SAME `parse_trigger_probes`
parser used by --probe, so any downstream consumer that wants the parsed
pairs reuses this one parser instead of reimplementing the YAML walk.
Sorted by (skill-id, phrase) for byte-stable output.
"""
pairs: list[tuple[str, str]] = []
for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
try:
text = skill_md.read_text(encoding="utf-8")
except OSError:
continue
frontmatter, _body = split_frontmatter(text)
sid = skill_md.parent.name
for phrase in parse_trigger_probes(frontmatter):
pairs.append((sid, phrase))
return sorted(set(pairs))
def render_markdown(results: list[SkillScan]) -> str:
"""Render a human-readable remediation report."""
total = len(results)
missing = [r for r in results if not r.has_trigger]
lines = [
"# Skill description trigger scan",
"",
f"- Skills scanned: **{total}**",
f"- With trigger marker: **{total - len(missing)}**",
f"- Missing trigger marker: **{len(missing)}** "
f"({(len(missing) / total * 100):.0f}%)" if total else "- Missing: 0",
"",
]
if not missing:
lines.append("All descriptions carry a trigger marker. ✅")
return "\n".join(lines)
lines += [
"## Remediation backlog (add a trigger marker to each)",
"",
"| Skill | Score | Suggested stub |",
"|-------|-------|----------------|",
]
for r in missing:
lines.append(f"| `{r.name}` | {r.score}/3 | `{r.suggestion}` |")
return "\n".join(lines)
def _run_probe(skills_dir: Path, phrase: str, *, json_mode: bool, quiet: bool) -> int:
"""Drive --probe: rank the corpus and assert the declaring skill wins.
Returns 2 if no skill declares the phrase (a usage error — nothing to
assert), 0 if the declaring skill ranks #1, 1 otherwise.
"""
ranked = probe_corpus(skills_dir, phrase)
declaring = [r for r in ranked if r.declares_phrase]
top = ranked[0] if ranked else None
declarer_is_top = bool(top and top.declares_phrase)
if json_mode:
payload = {
"phrase": phrase,
"ranked": len(ranked),
"declaring": [r.name for r in declaring],
"top": top.name if top else None,
"declarer_is_top": declarer_is_top,
"skills": [r.to_dict() for r in ranked],
}
print(json.dumps(payload, indent=2, sort_keys=True))
elif not quiet:
print(render_probe(phrase, ranked))
if not declaring:
if not json_mode:
print(f"error: no skill declares the probe phrase: {phrase!r}", file=sys.stderr)
return 2
return 0 if declarer_is_top else 1
def main(argv: list[str] | None = None) -> int:
"""CLI entry point."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"skills_dir",
nargs="?",
default="skills",
help="Path to the skills/ directory (default: skills)",
)
parser.add_argument("--json", action="store_true", help="Emit JSON (robot mode)")
parser.add_argument(
"--strict", action="store_true", help="Exit 1 if any description lacks a trigger"
)
parser.add_argument("--quiet", action="store_true", help="Suppress the human report")
parser.add_argument(
"--probe",
metavar="PHRASE",
default=None,
help="Deterministic lexical probe: assert the skill that declares PHRASE "
"in trigger_probes: ranks #1 (no live model, no network)",
)
parser.add_argument(
"--list-probes",
action="store_true",
help="Emit every declared (skill-id<TAB>phrase) pair, one per line, using "
"the SAME parser as --probe (so consumers don't reimplement the YAML walk)",
)
args = parser.parse_args(argv)
skills_dir = Path(args.skills_dir)
if not skills_dir.is_dir():
print(f"error: skills dir not found: {skills_dir}", file=sys.stderr)
return 2
if args.list_probes:
for sid, phrase in list_probe_pairs(skills_dir):
print(f"{sid}\t{phrase}")
return 0
if args.probe is not None:
return _run_probe(skills_dir, args.probe, json_mode=args.json, quiet=args.quiet)
results = scan_corpus(skills_dir)
missing = [r for r in results if not r.has_trigger]
if args.json:
payload = {
"scanned": len(results),
"missing": len(missing),
"skills": [r.to_dict() for r in results],
}
print(json.dumps(payload, indent=2))
elif not args.quiet:
print(render_markdown(results))
return 1 if (args.strict and missing) else 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env bash
# validate.sh — self-validation for skill-builder
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)"
# Run heal-skill structural check on ourselves by exit code.
bash "$REPO_ROOT/skills/heal-skill/scripts/heal.sh" --check --strict "$SKILL_DIR"
# Verify required artifacts exist
for f in SKILL.md scripts/build.sh scripts/init.sh references/skill-template.md schemas/build-report.json; do
[[ -f "$SKILL_DIR/$f" ]] || { echo "validate.sh: missing $SKILL_DIR/$f" >&2; exit 1; }
done
# Verify scale-factory lessons stay encoded.
for phrase in \
"heal-skill --check --strict" \
"One skill directory = one writer" \
"git status" \
"Clean-room includes names" \
"Workflow tool"; do
grep -q "$phrase" "$SKILL_DIR/SKILL.md" "$SKILL_DIR/references/agentops-skill-factory.md" || {
echo "validate.sh: missing scale-factory lesson: $phrase" >&2
exit 1
}
done
# Verify SKILL.md is within churn budget
LINES="$(wc -l < "$SKILL_DIR/SKILL.md")"
if (( LINES > 250 )); then
echo "validate.sh: SKILL.md is $LINES lines (>250 budget per finding f-2026-05-01-025)" >&2
exit 1
fi
# Verify build.sh and init.sh are executable
for s in scripts/build.sh scripts/init.sh; do
[[ -x "$SKILL_DIR/$s" ]] || chmod +x "$SKILL_DIR/$s"
done
echo "validate.sh: skill-builder PASS ($LINES lines, all artifacts present)"