Claude opencode Skill

skill-builder

Create a metadata-complete AgentOps skill source package, regenerate its derived projections, and check or repair structural hygiene in skill packages. Triggers: "create a skill", "scaffold skill", "absorb external skill", "new skill", "heal skill", "repair skill hygiene", "audit

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download boshu2-agentops-skills_skill-builder-9ac484e.zip · 71 KB
boshu2/agentops 445 41 forks Apache-2.0 Updated 1d ago
Part of boshu2/agentops — 73 skills

Install

skills CLI npx skills add https://github.com/boshu2/agentops/tree/main/skills/skill-builder
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install boshu2-agentops@llmmart
Git git clone https://github.com/boshu2/agentops.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole boshu2/agentops collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Skill Builder

Create, repair, audit or export one canonical skill package, or turn supported expertise into a small authoring proposal. Search existing owners before adding a root. Extend the owner that already handles the behavior.

Choose the requested operation

Need Entry point
Create a source package scripts/build.sh with from-scratch, from-template or absorb-external
Check package structure scripts/heal.sh --check [--strict] skills/<slug>
Repair owned projections scripts/heal.sh --fix skills/<slug>
Audit authoring quality scripts/audit.sh [--strict] [--json <path>] skills/<slug>
Export to another platform Conversion
Make repeated expertise reusable Distill expertise

Run only the selected operation. Skills remain optional tools within the native caller's authorized outcome; this skill does not add execution phases, own work, operate Git, validate a software candidate, or decide delivery and retries.

Create and maintain

Treat external skills as structural signals only. Clean-room output must not copy their names, prose, prompts, scripts or examples. from-template reuses metadata defaults; absorb-external <slug> --from <path> verifies an input and creates a blank source package. Neither imports another skill's content.

For creation, supply one input to scripts/build.sh, then replace placeholders with the actual behavior. The caller can supply SKILL_TIER, SKILL_DEPENDENCIES, SKILL_CAPABILITIES and SKILL_EFFECTS; lists are JSON arrays. The result is one source package with SKILL.md and scripts/validate.sh. The builder's report is .agents/scratch/skill-builder/<slug>-build.json under build-report.json.

Edit skills/<slug>/ as the source owner. Check the completed source with scripts/heal.sh --check --strict skills/<slug>, then regenerate its owned projections through the repository's owning commands. scripts/regen-all.sh is the integrated projection recipe; scripts/generate-skill-mesh.py, scripts/codex-sync.sh --only <slug> and scripts/regen-codex-hashes.sh --only <slug> are the existing scoped surfaces. Do not repeat work already performed by build.sh unless source changes require it. Inspect the generated diff; hand-edit no projection.

Check/heal targets must be real direct children of skills/; reject missing paths, traversal and symlink spellings. Check mode is read-only. Fix mode regenerates owned projections for explicit targets and does not invent source behavior. Findings name their code, target and concrete issue; --strict returns nonzero for findings. Check the slug/name match, description, API version, metadata, live dependencies and linked resources.

Deep audit reports structural and advisory authoring findings; it is not a candidate verdict. Its optional JSON follows audit-report.json. Interpret static scores as structure and authoring signals, not proof that a skill works. Exact checks live in audit checks, authoring doctrine, and Codex parity.

Conversion

Use bash skills/skill-builder/scripts/converter/convert.sh <skill-dir> <target> [output-dir] for an explicit out-of-tree export. Targets are codex, cursor and test; --all selects all source packages, and --codex-layout inline selects the legacy inline Codex layout. Read SkillBundle when format details matter. Parse the source once, render the target, then validate resource parity and target format. Report layout and any omitted Cursor references.

The default export is .agents/projections/converter/<target>/<skill-name>/. The exporter clean-writes its output directory, so use only the explicit derived target: refuse a source package, its ancestor, or the repository root. Preserve the source unchanged and fix the source or adapter instead of editing output. A parse, write, format or required-resource failure leaves an incomplete export. The shipped skills-codex/** remains owned by scripts/codex-sync.sh through scripts/regen-all.sh; this ad-hoc exporter never replaces that authority.

Distill expertise

When the caller wants a reusable rule, begin with cited occurrences or a named authoritative source. State the trigger, desired behavior, inputs, outputs, negative example and limits. Prefer an addition to an existing reference or skill over a new root, library, gate or workflow; no action is a valid result.

An abstraction needs three independently evidenced real occurrences and a successful reapplication to a source case without missing context. Preserve short source excerpts or command results with resolvable citations. Fewer occurrences support a narrow reference note; an authoritative source substitutes only for a faithful statement of that source, not a wider generalization. Use Research's pattern mode when the claim needs exemplars and a holdout before packaging.

A proposed process artifact must have a concrete consumer, a subject or release decision it informs, an observed defect and a retirement condition. If any is missing, omit the artifact. Code written only to consume it supplies no consumer. Minimal recovery state needs a named evidence-loss or corruption risk. Show a negative/holdout case and how the proposed rule returns the right decision.

Return the proposal inline unless a durable proposal was requested. Respect Memory's source and destination rules for mined material. Evidence cannot publish itself as policy. Build an artifact only when the caller's authorization includes adoption; a proposal-only request ends with the proposal. Repair ordinary known defects within existing authority; tool failures remain explicit facts for the native caller, not an automatic helper chain.

For an actual package edit, use the source template for required fields and context density guidance when deciding which prose earns a place. Neither requires adding a new skill.

Files (agentops)
  • references
    • converter
      • skill-bundle-schema.md 3.2 KB
        # SkillBundle Interchange Format
        
        The SkillBundle is the universal intermediate representation produced by the converter's parse stage. Every target adapter consumes a SkillBundle and transforms it into platform-specific output.
        
        ## Schema
        
        ```yaml
        SkillBundle:
          name: string          # from frontmatter 'name' field
          description: string   # from frontmatter 'description' field
          body: string          # markdown content after frontmatter (closing --- to EOF)
          references:           # files found in references/ directory
            - name: string      # filename (e.g. 'output-format.md')
              content: string   # full file content
          scripts:              # files found in scripts/ directory
            - name: string      # filename (e.g. 'validate.sh')
              content: string   # full file content
          frontmatter: object   # full parsed YAML frontmatter as key-value pairs
        ```
        
        ## Field Details
        
        ### name (string, required)
        
        The skill's short name, extracted from the `name` field in SKILL.md YAML frontmatter.
        
        Example: `council`, `validate`, `plan`
        
        ### description (string, required)
        
        The skill's description, extracted from the `description` field in SKILL.md frontmatter. May contain trigger lists and usage summaries.
        
        ### body (string, required)
        
        The full markdown content of SKILL.md after the closing `---` of the frontmatter block. This is the skill's instructions, workflow documentation, and inline agent definitions.
        
        ### references (array of objects)
        
        Each file in the skill's `references/` directory becomes one entry:
        
        - **name**: The filename without path prefix (e.g. `output-format.md`)
        - **content**: The complete file contents as a string
        
        If no `references/` directory exists, this is an empty array.
        
        ### scripts (array of objects)
        
        Each file in the skill's `scripts/` directory becomes one entry:
        
        - **name**: The filename without path prefix (e.g. `validate.sh`)
        - **content**: The complete file contents as a string
        
        If no `scripts/` directory exists, this is an empty array.
        
        ### frontmatter (object)
        
        The complete parsed YAML frontmatter as a flat or nested key-value structure. This includes all fields -- not just `name` and `description` -- so target adapters can access `metadata.tier`, `metadata.dependencies`, and any custom fields.
        
        Example:
        
        ```yaml
        frontmatter:
          name: council
          description: 'Multi-model consensus council...'
          metadata:
            tier: orchestration
            dependencies:
              - standards
            replaces: judge
        ```
        
        ## Usage in Target Adapters
        
        Target adapters receive the SkillBundle and decide which fields to use:
        
        | Adapter | Fields Used | Notes |
        |---------|-------------|-------|
        | codex | name, description, body, references, scripts | Emits `SKILL.md` + `prompt.md`; modular by default (copies + links resources), `--codex-layout inline` appends them |
        | cursor | name, description, body, references, scripts | Emits a single `<name>.mdc` rule (+ optional `mcp.json`), budget-fitted to 100KB |
        | test | all | Dumps the full bundle as structured markdown for inspection |
        
        ## Serialization
        
        The SkillBundle is an in-memory structure passed between pipeline stages. When written to disk (e.g. by the `test` target), it is rendered as structured markdown with clear section headers for each field.
        
    • audit-checks.md 5.3 KB
      # Deep Skill Audit Checks
      
      `audit.sh` runs the structural `heal.sh --check --strict` pass, eight content
      checks, an advisory static package-readiness score, and advisory craft
      instrumentation. The checks protect usability without rewarding ceremony or
      package size.
      
      Executable thresholds and severities come from
      `skills/skill-builder/references/skill-conformance-profiles.yaml`.
      
      ## Verdicts
      
      | Severity | Result |
      |---|---|
      | FAIL | The skill contract is incomplete or unsafe. |
      | WARN | A concrete usability issue should be reviewed. |
      | PASS | No configured defect was found. |
      
      `--strict` makes WARN exit nonzero. Advisory scoring never changes the verdict.
      
      ## Checks
      
      ### `description-has-triggers` and `trigger-clarity` (WARN)
      
      The frontmatter description must state when the skill should load. Accepted
      forms are an inline or block `Triggers:` / `Use when:` marker, or—for the first
      check only—a `metadata.triggers` list accepted by the active profile.
      
      ### `constraints-frontloaded` (WARN)
      
      Skills longer than 100 lines need an early `Constraints` or `⚠️` section within
      the first 80 body lines. Concise kernels pass without a ceremonial section
      because their boundaries are already visible in one read.
      
      ### `rationale-present` (WARN)
      
      When a constraints section contains bullets, at least half should explain why
      the constraint exists. A skill with no constraint bullets passes this check.
      
      ### `verification-checkpoints` (WARN)
      
      A workflow with two or more named subphases should contain a checkpoint or an
      explicit verify-before boundary. One-step procedures pass without a checkpoint.
      
      ### `output-spec-explicit` (FAIL)
      
      A nonempty frontmatter `output_contract` passes. Skills without that AgentOps
      field must instead provide one output section containing every component
      required by the selected profile. This lets small inline adapters declare a
      sentence-shaped result without inventing an artifact directory, filename,
      schema, validator, and downstream controller.
      
      ### `quality-rubric` (WARN)
      
      Skills longer than 100 lines need at least three bullets under `Quality`,
      `Checks`, `Checklist`, `Rubric`, `Best Practices`, or `Acceptance`. Concise
      kernels pass because their evidence and stop conditions are directly visible.
      
      ### `references-modularization` (WARN)
      
      The canonical repo-runtime kernel limit is 250 lines. Move genuinely detailed
      material into linked references instead of expanding the always-loaded kernel.
      
      ## Craft instrumentation (Pass 4, advisory)
      
      `craft_score.py` adds three advisory blocks to the report. None of them ever
      changes the verdict or exit code; they name gaps for the author and the fresh
      validator to judge.
      
      - **Craft score** — presence of the 12 craft elements enumerated in
        [skill-template.md](skill-template.md) section 7, reported as
        `craft n/12; missing: <element-ids>`. Detection is cheap pattern matching
        over authored prose (HTML comments are stripped, so `init.sh` scaffold stubs
        never count). Presence, never quality.
      - **Provenance resolution** — repo paths and `.agents/ao` verdict/intent digest
        citations (full or abbreviated `prefix...suffix`) extracted from prose must
        resolve against the repository; each dead citation is a named finding.
        Fenced code blocks are treated as examples, not citations.
      - **Loop safety** — any section with iteration prose (`repeat`, `iterate`,
        `loop`) must contain a checkable stop-condition phrase (`stop after`,
        `at most N`, `until ... exit 0`); an agent-dispatch loop must also carry a
        budget phrase. Vague goals ("until it feels done") do not count as stop
        conditions.
      
      The scorer's detection power is itself mutation-tested:
      
      ```bash
      bash skills/skill-builder/scripts/test-craft-mutations.sh
      ```
      
      ## Authoring prose scan (Pass 5, advisory)
      
      `authoring_scan.py` adds an advisory `authoring` block naming mechanical
      suspects for three failure modes from
      [authoring-doctrine.md](authoring-doctrine.md). Like density and craft, it
      never changes the verdict or exit code — the no-op test is model-relative and
      prohibitions are sometimes correct guardrails, so a human (or fresh validator)
      owns the judgment.
      
      - **`noop-phrase`** — phrasing the model already obeys by default ("be
        thorough", "make sure to", "carefully"), reported with the offending line.
        The fix is a sharper, behavior-changing instruction, not a louder wish.
      - **`negation-without-positive`** — a bullet/paragraph whose every clause
        prohibits ("Never edit generated files.") with no positive counterpart in
        the same unit. Pairing the prohibition with the target behavior ("Edit the
        source and regenerate; never edit generated files directly.") clears it.
      - **`step-missing-done-condition`** — a `###` subphase under a
        Workflow/Process/Methodology/Execution section with no checkable
        done-condition phrasing ("Done when", "Checkpoint:", "Stop after",
        "until ... exit 0"). One finding per offending subphase.
      
      Detection power is mutation-tested:
      
      ```bash
      bash skills/skill-builder/scripts/test-authoring-mutations.sh
      ```
      
      ## Calibration rule
      
      Before tightening a check, run it across every canonical skill. A proposed
      rule that fails valid concise skills is miscalibrated unless the repository
      contract itself requires those skills to change. Do not add boilerplate solely
      to satisfy a heuristic.
      
      ```bash
      for skill in skills/*; do
        [[ -f "$skill/SKILL.md" ]] || continue
        bash skills/skill-builder/scripts/audit.sh "$skill" >/dev/null
      done
      ```
      
    • authoring-doctrine.md 5.3 KB
      # Skill Authoring Doctrine
      
      Principles for writing `SKILL.md` bodies so an agent follows the same
      *process* on every run. Structure (frontmatter, sections, output contracts)
      lives in [skill-template.md](skill-template.md) and
      [audit-checks.md](audit-checks.md). This reference governs the sentences
      inside that structure. The deep audit's advisory `authoring` block
      (see [audit-checks.md](audit-checks.md)) mechanically flags three detectable
      failure modes; everything else is author judgment.
      
      Idea provenance: distilled clean-room from the skill-authoring ideas at
      <https://github.com/mattpocock/skills> (MIT). Concepts only — no upstream
      prose, names, prompts, scripts, or examples are reproduced here.
      
      ## 1. The no-op test
      
      A sentence belongs in the skill only when it alters what the agent would
      otherwise do. Vague intensifiers such as "be thorough" fail: they spend
      tokens without changing the default. Prefer a concrete, observable
      instruction ("read every `references/*.md` before editing") or a single
      strong pretrained cue the model already knows how to obey.
      
      Whether a line is load-bearing depends on the model. Authors who disagree
      should settle it by executing the skill, not by debating intent. Because of
      that relativity, the audit's `noop-phrase` finding is advisory: it names
      suspects from a fixed phrase list; the author decides.
      
      ## 2. Negation
      
      Telling the agent what to avoid tends to surface the forbidden pattern in
      context and raise its salience. Prefer naming the desired behavior
      ("write one-line comments") so the unwanted pattern is never primed.
      
      Hard bans remain valid when nothing positive can replace them (this
      repository's ban on `claude -p` is one). Even then, put the recovery path
      in the same paragraph — ban plus what to do instead — which matches this
      repo's "Anti-pattern: X. Corrective: Y" habit. The audit's
      `negation-without-positive` finding flags a paragraph where every clause
      forbids and none directs.
      
      ## 3. Completion criteria
      
      End each workflow step on a condition the agent can evaluate, and make that
      condition cover the whole obligation. Two properties matter:
      
      - **Checkable** — done vs not-done is decidable. "Understanding reached" is
        not; "every changed path appears in the manifest" is.
      - **Exhaustive** — the bar covers the full duty. "Produce a change list"
        allows a partial list; "every modified file accounted for" does not.
      
      A fuzzy exit invites *premature completion*: attention drifts from the work
      to finishing, and the step ends early. Tighten the exit criterion before
      restructuring the skill — that is the cheap local fix. The audit's
      `step-missing-done-condition` finding flags workflow subphases that lack any
      done phrasing ("Done when", "Checkpoint:", "Stop after", "until … exit 0").
      
      ## 4. Leading words
      
      A leading word is one pretrained concept token that holds a behavioral
      region: *tight* (loop), *red* (failing check), *fresh* (context),
      *frontier*, *quarantine*. Used as a token — not re-explained each time —
      it builds a shared meaning across the skill and leans on priors the model
      already has, buying behavior for almost no context cost.
      
      Look for restated qualities that want collapsing: three near-synonyms for
      "fast and deterministic" are one idea; *tight* says it once. Invented terms
      need a single clear definition; pretrained words are free. A leading word
      that does not beat the model's default is itself a no-op — strengthen the
      word rather than adding more prose.
      
      ## 5. Description discipline
      
      The frontmatter description loads every turn, so prune it hardest. It has
      two jobs: name what the skill does, and list genuinely distinct trigger
      branches. One trigger per branch — synonym padding for the same branch is
      duplication that burns context and dulls matching. Prefer trigger wording
      callers actually use, especially shared leading words, so the description
      aligns with real prompts. This sharpens, rather than replaces, the
      structural `description-has-triggers` and `trigger-clarity` checks.
      
      ## 6. Context load vs cognitive load
      
      Every skill bills one of two accounts. A model-discoverable skill keeps its
      description in the window every turn — a standing **context load** whether
      or not it fires. A human-only skill costs nothing per turn but spends
      **cognitive load**: the human indexes which skills exist and when to call
      them.
      
      Choosing the account is a required authoring decision. This repository
      already exposes the levers: `user-invocable` frontmatter, `tier` and
      `disposition` metadata, and the generated router (`docs/SKILL-ROUTER.md`)
      that reduces cognitive load once human-reached skills multiply. Prefer
      model discovery only when autonomous reach or cross-skill invocation is
      required; otherwise keep the window clean. Splitting one skill into several
      also spends one of those loads — split only when a distinct trigger
      vocabulary or independently reachable behavior pays for the new entry.
      
      ## Applying the doctrine
      
      When creating or healing a skill, walk the body once per principle: remove
      no-op lines, convert bare bans into positive targets (keeping paired
      guardrails), give each workflow step a checkable exhaustive exit, collapse
      restatements into leading words, prune the description to one trigger per
      branch, and confirm the intended load account. The advisory `authoring`
      audit block names the mechanical suspects; the author owns the judgment
      calls.
      
    • codex-parity.md 2.3 KB
      # Codex Parity Repair
      
      Use this workflow when `skills/<name>/SKILL.md` is canonically correct but
      `skills-codex/<name>/SKILL.md` has drifted into bad Codex UX in the checked-in runtime artifact.
      
      ## Principles
      
      1. `skills/<name>/SKILL.md` remains the canonical workflow contract.
      2. `skills-codex/<name>/` is the checked-in Codex runtime artifact and may need direct maintenance.
      3. Durable Codex-only body edits that should survive broader refactors belong in `skills-codex-overrides/<name>/SKILL.md`.
      4. Codex operator-layer prompt edits belong in `skills-codex-overrides/<name>/prompt.md`.
      
      ## Audit First
      
      Run:
      
      ```bash
      bash scripts/audit-codex-parity.sh
      ```
      
      Or target one skill:
      
      ```bash
      bash scripts/audit-codex-parity.sh --skill swarm
      ```
      
      The audit flags the failure classes that Codex maintenance keeps missing today:
      
      - Claude-era task primitives
      - Claude-only backend reference names and team terminology
      - duplicated runtime phrases created by blind search/replace
      
      ## Repair Loop
      
      For each flagged skill:
      
      1. Read `skills/<name>/SKILL.md` to confirm whether the canonical contract is correct.
      2. Read `skills-codex/<name>/SKILL.md` to see the broken checked-in Codex body.
      3. Read `skills-codex-overrides/<name>/prompt.md` and `skills-codex-overrides/catalog.json`.
      4. If the source contract is wrong, fix `skills/<name>/SKILL.md` first.
      5. If the shipped Codex artifact is wrong, update `skills-codex/<name>/SKILL.md`.
      6. If the source is correct but Codex needs a durable tailoring layer, create or update `skills-codex-overrides/<name>/SKILL.md`.
      7. Re-run validation:
         - `bash scripts/audit-codex-parity.sh`
         - `bash scripts/validate-codex-generated-artifacts.sh --scope worktree`
         - `bash scripts/validate-codex-override-coverage.sh`
      
      ## LLM Repair Guidance
      
      When doing the actual rewrite, the LLM should:
      
      - preserve the behavior contract from `skills/<name>/SKILL.md`
      - remove Claude-only primitive/tool names from the Codex body
      - replace mechanical rewrites with real Codex-native instructions
      - keep durable Codex-only delta in `skills-codex-overrides/<name>/SKILL.md` when it should remain distinct from the checked-in artifact
      
      If a skill keeps needing Codex-only body surgery, update
      `skills-codex-overrides/catalog.json` so the treatment matches reality instead
      of pretending the skill is still parity-only.
      
    • context-density-checks.md 1.7 KB
      # Advisory Context Density Checks
      
      This reference defines the density block of skill-builder's deep audit mode
      (absorbed from the retired `/skill-auditor`). It is report-only.
      It helps reviewers find skill prose that does not carry one of the six Context
      Density Rule fields before that prose is passed into a fresh context session.
      
      ## Fields
      
      | Field | Meaning | Advisory signals |
      |---|---|---|
      | `intent` | What behavior or capability the skill is trying to produce | intent, goal, behavior, capability |
      | `boundary` | Where the work starts/stops | boundary, bounded context, write scope, non-goal |
      | `evidence` | How the skill knows work is true or complete | evidence, test, verdict, validation, acceptance |
      | `decision` | Why this approach was chosen | decision, rationale, why, because, chosen |
      | `constraint` | Limits, safety rails, and non-negotiables | constraint, guardrail, limit, scope |
      | `next_action` | The next command, artifact, or handoff | next action, next steps, completion marker |
      
      ## Behavior
      
      - Missing fields produce `density.status: "warn"`.
      - Missing fields do not change the aggregate audit verdict.
      - Missing fields are not CI failures.
      - False positives should be recorded as findings or bead notes before any check
        is promoted.
      - This check does not satisfy execution-packet enforcement. The packet-boundary
        invariant is owned by `soc-2c1p.1`.
      
      ## Runnable Examples
      
      ```bash
      bash skills/skill-builder/scripts/audit.sh skills/plan
      bash skills/skill-builder/scripts/audit.sh skills/implement
      bash skills/skill-builder/scripts/audit.sh skills/validate
      ```
      
      The expected result is a JSON `density` object with six `fields[]` entries. The
      field count is the contract; the individual pattern matches are advisory.
      
    • heal.feature 1.2 KB · in bundle
    • skill-auditor.feature 1.3 KB · in bundle
    • skill-builder.feature 1.4 KB · in bundle
    • skill-conformance-profiles.yaml 4.5 KB
      version: 1
      default_profile: repo-runtime
      profiles:
        repo-runtime:
          id: repo-runtime
          kernel_max_lines: 250
          trigger_forms:
            accepted:
              - inline-marker
              - block-marker
              - metadata-list
            description_markers:
              - "Triggers:"
              - "Use when:"
            metadata_list_min_items: 3
          output_contract:
            section_headings:
              - Output
              - Output Specification
              - Output Format
              - Deliverables
              - Returns
            required_components:
              artifact-path:
                markers:
                  - "artifact directory"
                  - "**path:**"
                  - ".agents/"
                  - "stdout"
              filename-convention:
                markers:
                  - "filename convention"
                  - "**filename:**"
              serialization-schema:
                markers:
                  - "serialization/schema format"
                  - "**format:**"
                  - "schema"
              validator-command:
                markers:
                  - "validator command"
                  - "**exit code:**"
                  - "validation command"
                  - "validate with"
              downstream-handoff:
                markers:
                  - "downstream handoff"
                  - "consumed by"
                  - "confirming the fixture loaded"
          clean_room:
            enabled: true
            external_content_policy: observe-structure-only
            prohibited_copy_categories:
              - prose
              - prompts
              - scripts
              - examples
              - names
            copy_detection:
              minimum_fragment_characters: 24
              minimum_name_characters: 8
              protected_frontmatter_fields:
                - description
              ignored_exact_lines:
                - "---"
              ignored_line_prefixes:
                - "#"
          rule_order:
            - description-has-triggers
            - constraints-frontloaded
            - rationale-present
            - verification-checkpoints
            - output-spec-explicit
            - quality-rubric
            - references-modularization
            - trigger-clarity
          rules:
            description-has-triggers:
              severity: WARN
              accepted_forms:
                - inline-marker
                - block-marker
                - metadata-list
            constraints-frontloaded:
              severity: WARN
            rationale-present:
              severity: WARN
            verification-checkpoints:
              severity: WARN
            output-spec-explicit:
              severity: FAIL
            quality-rubric:
              severity: WARN
            references-modularization:
              severity: WARN
            trigger-clarity:
              severity: WARN
              accepted_forms:
                - inline-marker
                - block-marker
        external-observation:
          id: external-observation
          kernel_max_lines: 250
          trigger_forms:
            accepted:
              - inline-marker
              - block-marker
              - metadata-list
            description_markers:
              - "Triggers:"
              - "Use when:"
            metadata_list_min_items: 3
          output_contract:
            section_headings:
              - Output
              - Output Specification
              - Output Format
              - Deliverables
              - Returns
            required_components:
              filename-convention:
                markers:
                  - "filename convention"
                  - "**filename:**"
              serialization-schema:
                markers:
                  - "serialization/schema format"
                  - "**format:**"
                  - "schema"
          clean_room:
            enabled: true
            external_content_policy: observe-structure-only
            prohibited_copy_categories:
              - prose
              - prompts
              - scripts
              - examples
              - names
            copy_detection:
              minimum_fragment_characters: 24
              minimum_name_characters: 8
              protected_frontmatter_fields:
                - description
              ignored_exact_lines:
                - "---"
              ignored_line_prefixes:
                - "#"
          rule_order:
            - description-has-triggers
            - constraints-frontloaded
            - rationale-present
            - verification-checkpoints
            - output-spec-explicit
            - quality-rubric
            - references-modularization
            - trigger-clarity
          rules:
            description-has-triggers:
              severity: WARN
              accepted_forms:
                - inline-marker
                - block-marker
                - metadata-list
            constraints-frontloaded:
              severity: WARN
            rationale-present:
              severity: WARN
            verification-checkpoints:
              severity: WARN
            output-spec-explicit:
              severity: FAIL
            quality-rubric:
              severity: WARN
            references-modularization:
              severity: WARN
            trigger-clarity:
              severity: WARN
              accepted_forms:
                - inline-marker
                - block-marker
      
    • skill-template.md 10.3 KB
      # Unified SKILL.md Template + Auditor Checklist
      
      > **Authority:** Executable rule IDs, severities, trigger forms, output-handoff
      > requirements, clean-room behavior, and the 250-line limit come from
      > [skill-conformance-profiles.yaml](skill-conformance-profiles.yaml).
      
      This is the canonical template `skill-builder` materializes and its deep audit mode validates against. Two artifacts in one document because both modes need identical truth.
      
      This template governs *structure*. The prose inside that structure is governed
      by [authoring-doctrine.md](authoring-doctrine.md) — the no-op test, negation,
      completion criteria, leading words, description discipline, and the context
      load vs cognitive load decision. Apply both when authoring or healing a skill.
      
      ---
      
      ## 1. Canonical SKILL.md template
      
      ```markdown
      ---
      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]
      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
      
      <Complete executable handoff>
      
      **Artifact directory:** <directory or path>
      **Filename convention:** <naming convention>
      **Serialization/schema format:** <format and schema>
      **Validator command:** <exact validator invocation>
      **Downstream handoff:** <consumer or next workflow>
      
      ## 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.sh --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.sh (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 dispositions ledger | MISSING_DISPOSITION | FAIL in strict mode |
      
      ### Pass 2 — 8 NEW checks beyond heal.sh
      
      > The check id `description-has-triggers` (NOT `description-multiline`) is the canonical name. AgentOps' established convention is single-line `description: '...'`; 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` | A profile-accepted marker inside the description value, or a metadata list meeting the profile minimum | Profile |
      | 2 | `constraints-frontloaded` | Kernels over 100 lines put `Constraints` or `⚠️` within the first 80 body lines; concise kernels pass | Profile |
      | 3 | `rationale-present` | Each constraint bullet contains `why`, `because`, `this matters`, or similar rationale token | Profile |
      | 4 | `verification-checkpoints` | If skill has multi-phase Workflow/Methodology, body contains `Checkpoint`, `confirm`, or `Wait for` markers between phases | Profile |
      | 5 | `output-spec-explicit` | Nonempty `output_contract`, or one output section supplies every profile-required component | Profile |
      | 6 | `quality-rubric` | Kernels over 100 lines have 3+ bullets under Quality/Checks/Checklist/Rubric/Best Practices/Acceptance; concise kernels pass | Profile |
      | 7 | `references-modularization` | SKILL.md is at or below the profile's 250-line kernel limit | Profile |
      | 8 | `trigger-clarity` | Frontmatter `description` contains a profile-accepted marker an LLM can match | Profile |
      
      ### 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`) | Product surfaces — structured invocation criteria make the right capability discoverable without loading every skill body. 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 the complete executable handoff
      └── Quality Rubric               ← MUST contain 3+ bullets
      └── Examples
      └── Troubleshooting
      └── See Also / References
      ```
      
      The `## Examples` and `## Troubleshooting` sections are recommended but not enforced (heal.sh 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 (NO `skill_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. The 12 craft elements (advisory Pass-4 instrumentation)
      
      `scripts/craft_score.py` detects the presence of these 12 authoring elements
      (never their quality) and reports an advisory `craft n/12` with named gaps in
      the deep audit. `scripts/init.sh` scaffolds one `<!-- craft:<id> ... -->` stub
      per element; the scorer strips HTML comments, so stubs never satisfy an
      element — only authored prose counts.
      
      | # | Element id | One-line authoring prompt |
      |---|------------|---------------------------|
      | 1 | `causal-insight-line` | State the one causal insight that makes this skill work (`Insight:` / `**Why:**` / a `because` clause). |
      | 2 | `named-failure-mode` | Name the concrete failure mode this skill exists to prevent (`fails when`, `failure mode`, a Failure behavior section). |
      | 3 | `frozen-prompts` | Provide any reusable prompt as a fenced block marked copy-paste-only. |
      | 4 | `named-loop-stop-condition` | If the skill iterates, name the loop and give a checkable stop condition in the same section (`stop after`, `at most N passes`, `until ... exit 0`). |
      | 5 | `quantified-rules` | Quantify at least one rule with a number and unit (`at most 3 attempts`, `250 lines`). |
      | 6 | `negative-space` | State what this skill is NOT for (`non-goals`, `not for`, `do not use when`). |
      | 7 | `anti-pattern-with-corrective` | Pair each anti-pattern with its corrective in the same section (`avoid X; instead Y`). |
      | 8 | `provenance-citation` | Cite at least one resolvable source: a repo path or a `.agents/ao` verdict/intent digest (abbreviated `prefix...suffix` accepted). |
      | 9 | `measurable-done` | Give a machine-checkable done signal (`done when`, `exit 0`, a validator command). |
      | 10 | `router-shape` | Map trigger phrases to modes/entry points in a routing table or Modes section. |
      | 11 | `trigger-rich-description` | Put `Triggers:` / `Use when` phrases callers actually say in the frontmatter description. |
      | 12 | `runnable-commands` | Include at least one fenced block with runnable commands. |
      
      ---
      
      ## 8. Out-of-scope for v1 (stocktake territory)
      
      The following deeper audits are described in a future `skills/skill-builder/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-clarity` check above)
      
  • schemas
    • audit-report.json 17.4 KB
      {
        "$schema": "https://json-schema.org/draft-07/schema#",
        "title": "Skill Audit Report",
        "description": "Output contract for the skill-builder deep audit. Pass 1 wraps heal.sh structural checks; Pass 2 adds 8 content-discipline checks beyond heal.sh; Pass 3 reports an advisory 0-30 static package-readiness score that evaluates neither safety nor behavioral effectiveness; later passes add advisory craft and authoring signals.",
        "type": "object",
        "required": ["target", "profile_id", "verdict", "pass1", "pass2"],
        "properties": {
          "target": {
            "type": "string",
            "description": "skills/<name> path being audited"
          },
          "profile_id": {
            "type": "string",
            "description": "Selected authoritative skill-conformance profile ID."
          },
          "verdict": {
            "type": "string",
            "enum": ["PASS", "WARN", "FAIL"],
            "description": "Aggregate Pass-2 verdict. A nonzero Pass-1 exit additionally forces FAIL only for a repository-owned skills/* or skills-codex/* target; external targets retain Pass-1 diagnostics without binding the aggregate."
          },
          "pass1": {
            "type": "object",
            "description": "heal structural results (delegated to bash skills/skill-builder/scripts/heal.sh --check --strict <target>).",
            "required": ["status", "exit_code", "findings"],
            "properties": {
              "status": {
                "type": "string",
                "enum": ["pass", "fail"],
                "description": "Strict heal verdict based on process exit code, not parsed finding text."
              },
              "exit_code": {
                "type": "integer",
                "description": "Actual exit code from heal.sh --check --strict. Nonzero forces aggregate FAIL only when the target is under this repository's skills/* or skills-codex/* tree."
              },
              "strict": {
                "type": "boolean",
                "const": true,
                "description": "Always true: Pass 1 uses heal strict mode."
              },
              "findings": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": ["code", "path", "msg"],
                  "properties": {
                    "code": {
                      "type": "string",
                      "description": "heal.sh finding code (e.g., MISSING_NAME, UNLINKED_REF, DEAD_REF)"
                    },
                    "path": {"type": "string"},
                    "msg": {"type": "string"}
                  }
                }
              },
              "autofixable": {
                "type": "integer",
                "description": "Count of findings heal.sh can auto-fix with --fix"
              }
            }
          },
          "pass2": {
            "type": "object",
            "description": "Eight NEW checks beyond heal structural hygiene.",
            "required": ["checks"],
            "properties": {
              "checks": {
                "type": "array",
                "minItems": 8,
                "maxItems": 8,
                "items": {
                  "type": "object",
                  "required": ["id", "severity", "status"],
                  "properties": {
                    "id": {
                      "type": "string",
                      "enum": [
                        "description-has-triggers",
                        "constraints-frontloaded",
                        "rationale-present",
                        "verification-checkpoints",
                        "output-spec-explicit",
                        "quality-rubric",
                        "references-modularization",
                        "trigger-clarity"
                      ],
                      "description": "Stable check identifier. NOTE: 'description-has-triggers' (NOT 'description-multiline') — the broader form accepts AgentOps' single-line description convention plus financial-services' multi-line block-scalar form plus metadata.triggers arrays. See skills/skill-builder/references/skill-template.md §2 for accepted forms."
                    },
                    "status": {
                      "type": "string",
                      "enum": ["pass", "warn", "fail", "n/a"],
                      "description": "Status derived from the selected profile severity."
                    },
                    "severity": {
                      "type": "string",
                      "enum": ["WARN", "FAIL"]
                    },
                    "forms": {
                      "type": "array",
                      "items": {
                        "type": "string",
                        "enum": ["inline-marker", "block-marker", "metadata-list"]
                      }
                    },
                    "evidence": {
                      "type": "string",
                      "description": "Specific finding (line, snippet, or grep match) supporting the status."
                    }
                  }
                }
              }
            }
          },
          "density": {
            "type": "object",
            "description": "Advisory-only Context Density Rule coverage. This does not affect the aggregate verdict and does not enforce execution-packet density.",
            "required": ["status", "advisory", "fields", "summary"],
            "properties": {
              "status": {
                "type": "string",
                "enum": ["pass", "warn"],
                "description": "pass when all six advisory density fields are present, warn otherwise."
              },
              "advisory": {
                "type": "boolean",
                "const": true,
                "description": "Always true. Density coverage is report-only at the skill-auditor layer."
              },
              "fields": {
                "type": "array",
                "minItems": 6,
                "maxItems": 6,
                "items": {
                  "type": "object",
                  "required": ["id", "present", "evidence"],
                  "properties": {
                    "id": {
                      "type": "string",
                      "enum": [
                        "intent",
                        "boundary",
                        "evidence",
                        "decision",
                        "constraint",
                        "next_action"
                      ]
                    },
                    "present": {"type": "boolean"},
                    "evidence": {"type": "string"}
                  },
                  "additionalProperties": false
                }
              },
              "summary": {"type": "string"}
            },
            "additionalProperties": false
          },
          "rubric": {
            "description": "Advisory-only Pass-3 static package-readiness score (docs/reference/skill-quality-rubric.md). Folded in from score_agentops_skill.py --audit-block. It evaluates neither the safety gate nor behavioral effectiveness and never affects the aggregate verdict. Emitted as null when python3 or the scorer is unavailable (fail-open).",
            "oneOf": [
              {"type": "null"},
              {
                "type": "object",
                "required": ["scope", "safety_gate_evaluated", "effectiveness_evaluated", "total_score", "max_score", "rating", "advisory", "categories"],
                "properties": {
                  "scope": {
                    "type": "string",
                    "const": "static-package-readiness",
                    "description": "The score covers visible package properties only."
                  },
                  "safety_gate_evaluated": {
                    "type": "boolean",
                    "const": false,
                    "description": "Always false: boundary-word heuristics are not a full-bundle safety review."
                  },
                  "effectiveness_evaluated": {
                    "type": "boolean",
                    "const": false,
                    "description": "Always false: structural scoring contains no baseline-versus-treatment behavioral evaluation."
                  },
                  "total_score": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Emitter-reported sum of the 10 static category scores (0-30). Draft-07 bounds this value; focused emitter tests verify the arithmetic."
                  },
                  "max_score": {
                    "type": "integer",
                    "const": 30
                  },
                  "rating": {
                    "type": "string",
                    "enum": ["C", "B", "A", "S"],
                    "description": "Emitter-reported static band: C (0-10), B (11-20), A (21-26), S (27-30). Focused emitter tests verify consistency with total_score."
                  },
                  "advisory": {
                    "type": "boolean",
                    "const": true,
                    "description": "Always true. The static readiness score is report-only and never gates the verdict."
                  },
                  "categories": {
                    "type": "array",
                    "minItems": 10,
                    "maxItems": 10,
                    "description": "Exactly one entry for each of the 10 static package-readiness categories, each scored 0-3 with a deterministic reason. Absent optional components receive uncertainty score 1, never automatic full credit.",
                    "allOf": [
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "trigger_quality"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "kernel_clarity"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "progressive_disclosure"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "helper_scripts"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "validation"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "self_test"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "assets_templates"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "subagents_roles"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "safety_boundaries"}}}},
                      {"contains": {"required": ["category"], "properties": {"category": {"const": "packaging"}}}}
                    ],
                    "items": {
                      "type": "object",
                      "required": ["category", "score", "reason"],
                      "properties": {
                        "category": {
                          "type": "string",
                          "enum": [
                            "trigger_quality",
                            "kernel_clarity",
                            "progressive_disclosure",
                            "helper_scripts",
                            "validation",
                            "self_test",
                            "assets_templates",
                            "subagents_roles",
                            "safety_boundaries",
                            "packaging"
                          ]
                        },
                        "score": {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 3,
                          "description": "0 missing and required; 1 weak or no visible evidence with necessity not inferred; 2 solid visible evidence; 3 mechanically strong or unusually complete."
                        },
                        "reason": {
                          "type": "string",
                          "description": "Deterministic explanation derived from the skill directory contents."
                        }
                      },
                      "additionalProperties": false
                    }
                  }
                },
                "additionalProperties": false
              }
            ]
          },
          "craft": {
            "description": "Advisory-only Pass-4 craft instrumentation (craft_score.py): 12-element craft score with named gaps, provenance-citation resolution, and loop-safety findings. Additive and never affects the aggregate verdict or exit code. Emitted as null when python3 or the scorer is unavailable (fail-open).",
            "oneOf": [
              {"type": "null"},
              {
                "type": "object",
                "required": ["score", "max", "advisory", "missing", "elements", "provenance", "loop_safety", "summary"],
                "properties": {
                  "score": {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 12,
                    "description": "Count of the 12 craft elements detected as present."
                  },
                  "max": {"type": "integer", "const": 12},
                  "advisory": {
                    "type": "boolean",
                    "const": true,
                    "description": "Always true. Craft instrumentation is report-only and never gates."
                  },
                  "missing": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Element ids not detected; the named gaps."
                  },
                  "elements": {
                    "type": "array",
                    "minItems": 12,
                    "maxItems": 12,
                    "items": {
                      "type": "object",
                      "required": ["id", "present", "evidence"],
                      "properties": {
                        "id": {
                          "type": "string",
                          "enum": [
                            "causal-insight-line",
                            "named-failure-mode",
                            "frozen-prompts",
                            "named-loop-stop-condition",
                            "quantified-rules",
                            "negative-space",
                            "anti-pattern-with-corrective",
                            "provenance-citation",
                            "measurable-done",
                            "router-shape",
                            "trigger-rich-description",
                            "runnable-commands"
                          ]
                        },
                        "present": {"type": "boolean"},
                        "evidence": {"type": "string"}
                      },
                      "additionalProperties": false
                    }
                  },
                  "provenance": {
                    "type": "object",
                    "required": ["advisory", "citations", "resolved", "dead"],
                    "properties": {
                      "advisory": {"type": "boolean", "const": true},
                      "citations": {"type": "integer", "minimum": 0},
                      "resolved": {"type": "integer", "minimum": 0},
                      "dead": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "required": ["citation", "kind"],
                          "properties": {
                            "citation": {"type": "string"},
                            "kind": {"type": "string", "enum": ["digest", "path"]}
                          },
                          "additionalProperties": false
                        }
                      }
                    },
                    "additionalProperties": false
                  },
                  "loop_safety": {
                    "type": "object",
                    "required": ["advisory", "findings"],
                    "properties": {
                      "advisory": {"type": "boolean", "const": true},
                      "findings": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "required": ["type", "section", "evidence"],
                          "properties": {
                            "type": {
                              "type": "string",
                              "enum": ["loop-missing-stop-condition", "dispatch-loop-missing-budget"]
                            },
                            "section": {"type": "string"},
                            "evidence": {"type": "string"}
                          },
                          "additionalProperties": false
                        }
                      }
                    },
                    "additionalProperties": false
                  },
                  "summary": {
                    "type": "string",
                    "description": "One-line rollup, e.g. 'craft 7/12; missing: causal-insight-line, frozen-prompts'."
                  }
                },
                "additionalProperties": false
              }
            ]
          },
          "authoring": {
            "description": "Advisory-only Pass-5 authoring prose-quality scan (authoring_scan.py): mechanical suspects for the failure modes named in references/authoring-doctrine.md. Never affects the aggregate verdict or exit code. Emitted as null when python3 or the scanner is unavailable (fail-open).",
            "oneOf": [
              {"type": "null"},
              {
                "type": "object",
                "required": ["advisory", "findings", "counts", "summary"],
                "properties": {
                  "advisory": {
                    "type": "boolean",
                    "const": true,
                    "description": "Always true. Authoring findings are report-only and never gate."
                  },
                  "findings": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": ["id", "line", "evidence"],
                      "properties": {
                        "id": {
                          "type": "string",
                          "enum": [
                            "noop-phrase",
                            "negation-without-positive",
                            "step-missing-done-condition"
                          ]
                        },
                        "line": {"type": "integer", "minimum": 1},
                        "evidence": {"type": "string"}
                      },
                      "additionalProperties": false
                    }
                  },
                  "counts": {
                    "type": "object",
                    "required": [
                      "noop-phrase",
                      "negation-without-positive",
                      "step-missing-done-condition"
                    ],
                    "properties": {
                      "noop-phrase": {"type": "integer", "minimum": 0},
                      "negation-without-positive": {"type": "integer", "minimum": 0},
                      "step-missing-done-condition": {"type": "integer", "minimum": 0}
                    },
                    "additionalProperties": false
                  },
                  "summary": {"type": "string"}
                },
                "additionalProperties": false
              }
            ]
          },
          "summary": {
            "type": "string",
            "description": "Human-readable one-paragraph rollup. Suitable for embedding in a markdown audit report."
          }
        },
        "additionalProperties": false
      }
      
    • build-report.json 674 B
      {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "title": "Skill Build Report",
        "type": "object",
        "required": ["mode", "skill_name", "files_created", "structure_check_pass"],
        "properties": {
          "mode": {
            "type": "string",
            "enum": ["from-scratch", "from-template", "absorb-external"]
          },
          "skill_name": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9-]*$"
          },
          "files_created": {
            "type": "array",
            "items": {"type": "string"},
            "minItems": 2,
            "uniqueItems": true
          },
          "structure_check_pass": {"type": "boolean"},
          "source_hint": {"type": "string"}
        },
        "additionalProperties": false
      }
      
  • scripts
    • converter
      • convert.sh 24.9 KB
        #!/usr/bin/env bash
        # convert.sh — Cross-platform skill converter pipeline
        # Usage: bash skills/skill-builder/scripts/converter/convert.sh <skill-dir> <target> [output-dir]
        #        bash skills/skill-builder/scripts/converter/convert.sh --all <target> [output-dir]
        set -euo pipefail
        
        # This script uses namerefs (`local -n`), which require Bash >= 4.3. On stock
        # macOS /bin/bash (3.2.57) a nameref is an invalid option that aborts under
        # `set -e` with an opaque message and zero files written. Fail closed with a
        # clear diagnostic instead of an unrunnable surprise.
        if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then
          echo "ERROR: convert.sh requires Bash >= 4.3 (namerefs); found ${BASH_VERSION}." >&2
          echo "       On macOS install a newer bash ('brew install bash') and invoke it explicitly." >&2
          exit 2
        fi
        
        SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
        REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
        SKILL_PATTERN=""
        CODEX_LAYOUT="modular"
        ARGS_SKILL_DIR_OR_FLAG=""
        ARGS_TARGET=""
        ARGS_OUTPUT_DIR=""
        
        # ─── Helpers ───────────────────────────────────────────────────────────
        
        die() { echo "ERROR: $*" >&2; exit 1; }
        
        usage() {
          cat <<'EOF'
        Usage:
          bash skills/skill-builder/scripts/converter/convert.sh [--codex-layout modular|inline] <skill-dir> <target> [output-dir]
          bash skills/skill-builder/scripts/converter/convert.sh [--codex-layout modular|inline] --all <target> [output-dir]
        
        Targets: codex, cursor, test
        
        Examples:
          bash skills/skill-builder/scripts/converter/convert.sh skills/council codex
          bash skills/skill-builder/scripts/converter/convert.sh --codex-layout inline skills/council codex
          bash skills/skill-builder/scripts/converter/convert.sh --all codex
          bash skills/skill-builder/scripts/converter/convert.sh skills/vibe test /tmp/out
        EOF
          exit 1
        }
        
        parse_args() {
          local positional=()
        
          while [[ $# -gt 0 ]]; do
            case "$1" in
              --codex-layout)
                [[ $# -ge 2 ]] || die "--codex-layout requires a value: modular|inline"
                CODEX_LAYOUT="$2"
                shift 2
                ;;
              --codex-layout=*)
                CODEX_LAYOUT="${1#*=}"
                shift
                ;;
              --all)
                positional+=("--all")
                shift
                ;;
              -h|--help)
                usage
                ;;
              --)
                shift
                while [[ $# -gt 0 ]]; do
                  positional+=("$1")
                  shift
                done
                break
                ;;
              -*)
                die "Unknown flag: $1"
                ;;
              *)
                positional+=("$1")
                shift
                ;;
            esac
          done
        
          [[ "$CODEX_LAYOUT" == "modular" || "$CODEX_LAYOUT" == "inline" ]] \
            || die "Invalid --codex-layout '$CODEX_LAYOUT'. Expected: modular|inline"
        
          [[ ${#positional[@]} -ge 2 ]] || usage
          ARGS_SKILL_DIR_OR_FLAG="${positional[0]}"
          ARGS_TARGET="${positional[1]}"
          ARGS_OUTPUT_DIR="${positional[2]:-}"
        }
        
        yaml_escape_single_quote() {
          printf '%s' "$1" | sed "s/'/''/g"
        }
        
        # Build an alternation regex for all known skill names.
        load_skill_pattern() {
          local names=()
          local d
          for d in "$REPO_ROOT"/skills/*/; do
            [[ -f "$d/SKILL.md" ]] || continue
            names+=("$(basename "$d")")
          done
        
          # Some command aliases are valid in docs even when no source skill directory
          # exists in this repo (for example, migrated or generated-only skills).
          # Keep these in the rewrite pattern so slash forms still convert to $ forms.
          names+=("knowledge" "learn" "extract" "inbox")
        
          if [[ ${#names[@]} -eq 0 ]]; then
            SKILL_PATTERN=""
            return
          fi
        
          local escaped=()
          local name
          for name in "${names[@]}"; do
            escaped+=("$(printf '%s' "$name" | sed -E 's/[][(){}.^$*+?|\\-]/\\&/g')")
          done
          SKILL_PATTERN="$(IFS='|'; printf '%s' "${escaped[*]}")"
        }
        
        # Rewrite Claude-style slash command references to Codex-style dollar references.
        # Example: /plan -> $plan (for known skill names only).
        codex_rewrite_text() {
          local input="$1"
          local output="$input"
        
          if [[ -n "$SKILL_PATTERN" ]]; then
            output="$(printf '%s' "$output" | SKILL_PATTERN="$SKILL_PATTERN" perl -0pe '
              my $pattern = qr/$ENV{SKILL_PATTERN}/;
              s{(?<![A-Za-z0-9_/])/($pattern)(?![A-Za-z0-9-])}{\$$1}g;
            ')"
          fi
        
          output="$(printf '%s' "$output" | perl -0pe '
            s/\bClaude[ ]Code\b/Codex/g;
            s/\bClaude[ ]Native[ ]Teams\b/Codex sub-agents/g;
            s/\bClaude[ ]native[ ]team\b/Codex sub-agent/g;
            s/\bClaude[ ]teams\b/Codex sub-agents/g;
            s/\bclaude[ ]teams\b/codex sub-agents/g;
            s/\bClaude[ ]session(s)?\b/Codex session$1/g;
            s/\bclaude[ ]session(s)?\b/codex session$1/g;
            s/\bClaude[ ]runtime\b/Codex runtime/g;
            s/\bclaude[ ]runtime\b/codex runtime/g;
            s/\bClaude[ ]workers\b/Codex workers/g;
            s/\bclaude[ ]workers\b/codex workers/g;
            s/\bclaude-native-teams\b/codex-sub-agents/g;
            s{~/.claude/skills/}{~/.agents/skills/}g;
            s{~/.claude/}{~/.codex/}g;
            s{\$HOME/.claude/}{\$HOME/.codex/}g;
            s{/.claude/}{/.codex/}g;
            s{\.claude/}{.codex/}g;
            s/backend-claude-teams\.md/backend-codex-subagents.md/g;
            s/\bclaude agents\b/codex agents/g;
            # Map Claude tools to Codex tools
            s/\bthe Read tool\b/read_file/g;
            s/\bthe Edit tool\b/apply_patch/g;
            s/\bthe Grep tool\b/rg/g;
            s/\bthe Glob tool\b/glob_file_search/g;
            s/\bAgent\(subagent_type="Explore"/spawn a sub-agent (explorer role/g;
            s/\bsubagent_type:\s*"Explore"/role: explorer/g;
            # Rewrite Skill() tool invocations to $skill syntax
            s/Skill\(skill="([^"]+)"(?:,\s*args="([^"]*)")?\)/\$$1 $2/g;
            # Strip lines with Claude primitives (no Codex equivalent — empirically verified)
            s/.*\b(?:TaskCreate|TaskUpdate|TaskList|TaskGet|TaskStop)\b.*\n?//g;
            s/.*\b(?:TeamCreate|TeamDelete)\b.*\n?//g;
            s/.*\b(?:SendMessage)\b.*\n?//g;
            s/.*\b(?:EnterPlanMode|ExitPlanMode|EnterWorktree)\b.*\n?//g;
            s/.*\*\*USE THE TASK TOOL\*\*.*\n?//g;
            s/.*\bTool:\s*Task\b.*\n?//g;
            # Post-rewrite dedup: collapse doubled runtime phrases
            s/Codex sub-agents in Codex sessions, Codex sub-agents in Codex sessions/Codex sub-agents in Codex sessions/g;
            s/Codex session -> Codex sub-agents; Codex session -> Codex sub-agents/Codex session -> Codex sub-agents/g;
          ')"
        
          printf '%s' "$output"
        }
        
        # Deduplicate semantically equivalent Codex runtime headings while preserving
        # all section content. If multiple "In Codex" headings exist after rewrites,
        # keep the first heading and drop subsequent duplicate heading lines.
        codex_dedupe_runtime_headings() {
          local input="$1"
          printf '%s' "$input" | awk '
            function norm(line, t) {
              t = tolower(line)
              gsub(/^[[:space:]]*#+[[:space:]]*/, "", t)
              gsub(/^[[:space:]]*\*\*[[:space:]]*/, "", t)
              gsub(/[[:space:]]*\*\*[[:space:]]*$/, "", t)
              gsub(/[[:space:]]*:[[:space:]]*$/, "", t)
              gsub(/^[[:space:]]+|[[:space:]]+$/, "", t)
              return t
            }
            {
              key = norm($0)
              if (key == "in codex") {
                if (seen[key] == 1) {
                  next
                }
                seen[key] = 1
              }
              print
            }
          '
        }
        
        # ─── Stage 1: Parse ───────────────────────────────────────────────────
        
        # Parse SKILL.md frontmatter and body.
        # Sets: BUNDLE_NAME, BUNDLE_DESC, BUNDLE_BODY, BUNDLE_FRONTMATTER
        parse_skill_md() {
          local skill_md="$1"
          [[ -f "$skill_md" ]] || die "SKILL.md not found: $skill_md"
        
          local content
          content="$(<"$skill_md")"
        
          # Extract frontmatter (between first and second --- lines)
          local in_fm=0
          local fm_lines=()
          local body_lines=()
          local fm_ended=0
          local line_num=0
        
          while IFS= read -r line; do
            line_num=$((line_num + 1))
            if [[ $line_num -eq 1 && "$line" == "---" ]]; then
              in_fm=1
              continue
            fi
            if [[ $in_fm -eq 1 && "$line" == "---" ]]; then
              in_fm=0
              fm_ended=1
              continue
            fi
            if [[ $in_fm -eq 1 ]]; then
              fm_lines+=("$line")
            elif [[ $fm_ended -eq 1 ]]; then
              body_lines+=("$line")
            fi
          done <<< "$content"
        
          BUNDLE_FRONTMATTER="$(printf '%s\n' "${fm_lines[@]}")"
        
          # Extract name and description from frontmatter
          BUNDLE_NAME="$(echo "$BUNDLE_FRONTMATTER" | sed -n 's/^name: *//p' | tr -d "'" | tr -d '"')"
          BUNDLE_DESC="$(
            awk '
              BEGIN {
                capture = 0
                first = 1
              }
              /^description:[[:space:]]*[>|]-?[[:space:]]*$/ {
                capture = 1
                next
              }
              /^description:[[:space:]]*/ {
                sub(/^description:[[:space:]]*/, "", $0)
                gsub(/^'\''|'\''$/, "", $0)
                gsub(/^"|"$/, "", $0)
                print
                exit
              }
              capture {
                if ($0 ~ /^[^[:space:]]/ && $0 !~ /^$/) {
                  exit
                }
                line = $0
                sub(/^[[:space:]]+/, "", line)
                if (line == "") {
                  next
                }
                if (!first) {
                  printf " "
                }
                printf "%s", line
                first = 0
              }
            ' <<< "$BUNDLE_FRONTMATTER"
          )"
        
          # Body: join with newlines
          BUNDLE_BODY="$(printf '%s\n' "${body_lines[@]}")"
        }
        
        # Collect files from a subdirectory into parallel arrays.
        # Args: <dir> <array-name-names> <array-name-contents>
        # Scope: top-level regular files of <dir> only. Nested reference/script files
        # are NOT inlined here; copy_passthrough_resources() preserves them recursively
        # in the written output, so nested resources survive a conversion even though
        # they are not flattened into the target SKILL.md body.
        collect_files() {
          local dir="$1"
          local -n names_arr="$2"
          local -n contents_arr="$3"
          names_arr=()
          contents_arr=()
        
          if [[ -d "$dir" ]]; then
            local f _old_lc="${LC_ALL:-}"
            LC_ALL=C
            for f in "$dir"/*; do
              [[ -f "$f" ]] || continue
              names_arr+=("$(basename "$f")")
              contents_arr+=("$(<"$f")")
            done
            LC_ALL="${_old_lc}"
          fi
        }
        
        # Full parse: populate all BUNDLE_* variables and REF/SCRIPT arrays
        parse_bundle() {
          local skill_dir="$1"
          parse_skill_md "$skill_dir/SKILL.md"
          collect_files "$skill_dir/references" REF_NAMES REF_CONTENTS
          collect_files "$skill_dir/scripts" SCRIPT_NAMES SCRIPT_CONTENTS
        }
        
        # ─── Stage 2: Convert ─────────────────────────────────────────────────
        
        # Test target: emit SkillBundle as structured markdown
        convert_test() {
          local out=""
          out+="# SkillBundle: ${BUNDLE_NAME}"$'\n\n'
          out+="## Name"$'\n\n'
          out+="${BUNDLE_NAME}"$'\n\n'
          out+="## Description"$'\n\n'
          out+="${BUNDLE_DESC}"$'\n\n'
          out+="## Frontmatter"$'\n\n'
          out+='```yaml'$'\n'
          out+="${BUNDLE_FRONTMATTER}"$'\n'
          out+='```'$'\n\n'
          out+="## Body"$'\n\n'
          out+="${BUNDLE_BODY}"$'\n\n'
        
          out+="## References (${#REF_NAMES[@]})"$'\n\n'
          local i
          for i in "${!REF_NAMES[@]}"; do
            out+="### ${REF_NAMES[$i]}"$'\n\n'
            out+='```'$'\n'
            out+="${REF_CONTENTS[$i]}"$'\n'
            out+='```'$'\n\n'
          done
        
          out+="## Scripts (${#SCRIPT_NAMES[@]})"$'\n\n'
          for i in "${!SCRIPT_NAMES[@]}"; do
            out+="### ${SCRIPT_NAMES[$i]}"$'\n\n'
            out+='```'$'\n'
            out+="${SCRIPT_CONTENTS[$i]}"$'\n'
            out+='```'$'\n\n'
          done
        
          CONVERTED_OUTPUT="$out"
          CONVERTED_FILENAME="bundle.md"
        }
        
        # Codex target: SKILL.md + prompt.md
        # Codex may load these skills from ~/.codex/skills or from a native plugin cache.
        # Description max 1024 chars, no hooks support, tool names pass through
        convert_codex() {
          local desc="$BUNDLE_DESC"
          local body
          body="$(codex_rewrite_text "$BUNDLE_BODY")"
          body="$(codex_dedupe_runtime_headings "$body")"
        
          # Truncate description to 1024 chars at word boundary
          if [[ ${#desc} -gt 1024 ]]; then
            desc="${desc:0:1021}"
            # Trim to last word boundary (space)
            desc="${desc% *}..."
          fi
          desc="$(codex_rewrite_text "$desc")"
          local desc_escaped
          desc_escaped="$(yaml_escape_single_quote "$desc")"
        
          # ── Build SKILL.md ──
          local skill_md=""
          skill_md+="---"$'\n'
          skill_md+="name: ${BUNDLE_NAME}"$'\n'
          skill_md+="description: '${desc_escaped}'"$'\n'
          skill_md+="---"$'\n\n'
          skill_md+="${body}"$'\n'
        
          if [[ "$CODEX_LAYOUT" == "inline" ]]; then
            # Inline references as appended sections (legacy/portable mode)
            if [[ ${#REF_NAMES[@]} -gt 0 ]]; then
              skill_md+=$'\n'"---"$'\n\n'
              skill_md+="## References"$'\n\n'
              local i
              for i in "${!REF_NAMES[@]}"; do
                skill_md+="### ${REF_NAMES[$i]}"$'\n\n'
                skill_md+="$(codex_rewrite_text "${REF_CONTENTS[$i]}")"$'\n\n'
              done
            fi
        
            # Inline scripts as code blocks (legacy/portable mode)
            if [[ ${#SCRIPT_NAMES[@]} -gt 0 ]]; then
              skill_md+=$'\n'"---"$'\n\n'
              skill_md+="## Scripts"$'\n\n'
              local i
              for i in "${!SCRIPT_NAMES[@]}"; do
                # Detect language from extension
                local ext="${SCRIPT_NAMES[$i]##*.}"
                local lang=""
                case "$ext" in
                  sh|bash) lang="bash" ;;
                  py)      lang="python" ;;
                  js)      lang="javascript" ;;
                  ts)      lang="typescript" ;;
                  *)       lang="$ext" ;;
                esac
                skill_md+="### ${SCRIPT_NAMES[$i]}"$'\n\n'
                skill_md+="\`\`\`${lang}"$'\n'
                skill_md+="$(codex_rewrite_text "${SCRIPT_CONTENTS[$i]}")"$'\n'
                skill_md+="\`\`\`"$'\n\n'
              done
            fi
          else
            # Modular mode: keep SKILL.md concise and reference copied resources.
            if [[ ${#REF_NAMES[@]} -gt 0 || ${#SCRIPT_NAMES[@]} -gt 0 ]]; then
              skill_md+=$'\n'"## Local Resources"$'\n\n'
              local i
              if [[ ${#REF_NAMES[@]} -gt 0 ]]; then
                skill_md+="### references/"$'\n\n'
                for i in "${!REF_NAMES[@]}"; do
                  skill_md+="- [references/${REF_NAMES[$i]}](references/${REF_NAMES[$i]})"$'\n'
                done
                skill_md+=$'\n'
              fi
              if [[ ${#SCRIPT_NAMES[@]} -gt 0 ]]; then
                skill_md+="### scripts/"$'\n\n'
                for i in "${!SCRIPT_NAMES[@]}"; do
                  skill_md+="- \`scripts/${SCRIPT_NAMES[$i]}\`"$'\n'
                done
                skill_md+=$'\n'
              fi
            fi
          fi
        
          # ── Build prompt.md ──
          local prompt_md=""
          prompt_md+="# ${BUNDLE_NAME}"$'\n\n'
          prompt_md+="${desc}"$'\n\n'
          prompt_md+="## Instructions"$'\n\n'
          prompt_md+="Load and follow the skill instructions from the sibling \`SKILL.md\` file for this skill."$'\n'
          if [[ "$CODEX_LAYOUT" == "modular" && ( ${#REF_NAMES[@]} -gt 0 || ${#SCRIPT_NAMES[@]} -gt 0 ) ]]; then
            prompt_md+="Then read local files in \`references/\` and \`scripts/\` when needed."$'\n'
          fi
        
          # Set primary output (SKILL.md)
          CONVERTED_OUTPUT="$skill_md"
          CONVERTED_FILENAME="SKILL.md"
        
          # Set secondary output (prompt.md)
          CONVERTED_OUTPUT_2="$prompt_md"
          CONVERTED_FILENAME_2="prompt.md"
        }
        
        # Cursor target: .mdc rule file with YAML frontmatter + optional mcp.json
        # Cursor rules format: .cursor/rules/<name>.mdc (Cursor 0.40+)
        # Max output size: 100KB (102400 bytes). References are budget-fitted.
        CURSOR_MAX_BYTES=102400
        
        convert_cursor() {
          local out=""
        
          # ── YAML frontmatter ──
          # Single-quote and escape the description: an unquoted value containing a
          # colon, quote, or leading special char yields invalid Cursor YAML (CV-9).
          out+="---"$'\n'
          out+="description: '$(yaml_escape_single_quote "$BUNDLE_DESC")'"$'\n'
          out+="globs: "$'\n'
          out+="alwaysApply: false"$'\n'
          out+="---"$'\n\n'
        
          # ── Body content ──
          out+="${BUNDLE_BODY}"$'\n'
        
          # ── Scripts as code blocks (included before references — smaller, higher value) ──
          if [[ ${#SCRIPT_NAMES[@]} -gt 0 ]]; then
            out+=$'\n'"## Scripts"$'\n\n'
            local i
            for i in "${!SCRIPT_NAMES[@]}"; do
              local ext="${SCRIPT_NAMES[$i]##*.}"
              local lang=""
              case "$ext" in
                sh|bash) lang="bash" ;;
                py)      lang="python" ;;
                js)      lang="javascript" ;;
                ts)      lang="typescript" ;;
                *)       lang="$ext" ;;
              esac
              out+="### ${SCRIPT_NAMES[$i]}"$'\n\n'
              out+="\`\`\`${lang}"$'\n'
              out+="${SCRIPT_CONTENTS[$i]}"$'\n'
              out+="\`\`\`"$'\n\n'
            done
          fi
        
          # ── Inline references (budget-fitted to stay under CURSOR_MAX_BYTES) ──
          if [[ ${#REF_NAMES[@]} -gt 0 ]]; then
            local current_size=${#out}
            local budget=$(( CURSOR_MAX_BYTES - current_size - 200 ))  # 200 byte margin for section header + omission note
            local ref_section=""
            local omitted=0
            local i
        
            ref_section+=$'\n'"## References"$'\n\n'
            for i in "${!REF_NAMES[@]}"; do
              local entry=""
              entry+="### ${REF_NAMES[$i]}"$'\n\n'
              entry+="${REF_CONTENTS[$i]}"$'\n\n'
              local entry_size=${#entry}
        
              if [[ $budget -ge $entry_size ]]; then
                ref_section+="$entry"
                budget=$(( budget - entry_size ))
              else
                omitted=$(( omitted + 1 ))
              fi
            done
        
            if [[ $omitted -gt 0 ]]; then
              ref_section+="*${omitted} reference(s) omitted to stay under 100KB size limit.*"$'\n\n'
              echo "WARN: ${BUNDLE_NAME}: omitted $omitted reference(s) to stay under 100KB" >&2
            fi
        
            out+="$ref_section"
          fi
        
          CONVERTED_OUTPUT="$out"
          CONVERTED_FILENAME="${BUNDLE_NAME}.mdc"
        
          # ── MCP detection: scan body + references for MCP server references ──
          # If skill content references MCP servers, generate a stub mcp.json
          local all_content="${BUNDLE_BODY}"
          local i
          for i in "${!REF_CONTENTS[@]}"; do
            all_content+=$'\n'"${REF_CONTENTS[$i]}"
          done
        
          if echo "$all_content" | grep -qiE '(mcpServers|mcp_server|"mcp"|mcp\.json)'; then
            CONVERTED_OUTPUT_2='{
          "mcpServers": {}
        }'
            CONVERTED_FILENAME_2="mcp.json"
          fi
        }
        
        run_convert() {
          local target="$1"
          case "$target" in
            test)   convert_test ;;
            codex)  convert_codex ;;
            cursor) convert_cursor ;;
            *)      die "Unknown target: $target. Supported: codex, cursor, test" ;;
          esac
        }
        
        # ─── Stage 3: Write ───────────────────────────────────────────────────
        
        copy_passthrough_resources() {
          local source_dir="$1"
          local output_dir="$2"
          local entry base
        
          # Preserve non-generated skill resources (e.g., templates/, assets/, schemas/,
          # examples/, agents/, and other auxiliary files) so converted skills retain
          # runnable/supporting artifacts beyond SKILL.md/prompt.md.
          while IFS= read -r -d '' entry; do
            base="$(basename "$entry")"
        
            case "$base" in
              SKILL.md|prompt.md)
                continue
                ;;
            esac
        
            # Copy and dereference symlinks to keep output plugin-compatible.
            if [[ -d "$entry" ]]; then
              # For directories (references/, scripts/, etc.), copy then rewrite .md files
              rsync -a --copy-links "$entry" "$output_dir"/
              local subdir="$output_dir/$base"
              if [[ -d "$subdir" ]]; then
                while IFS= read -r md_file; do
                  local content
                  content="$(<"$md_file")"
                  local rewritten
                  rewritten="$(codex_rewrite_text "$content")"
                  if [[ "$rewritten" != "$content" ]]; then
                    printf '%s\n' "$rewritten" > "$md_file"
                  fi
                done < <(find "$subdir" -name '*.md' -type f 2>/dev/null)
              fi
            else
              rsync -a --copy-links "$entry" "$output_dir"/
              # Rewrite top-level .md files too (e.g., validation-contract.md)
              if [[ "$entry" == *.md ]]; then
                local out_file="$output_dir/$base"
                if [[ -f "$out_file" ]]; then
                  local content
                  content="$(<"$out_file")"
                  local rewritten
                  rewritten="$(codex_rewrite_text "$content")"
                  if [[ "$rewritten" != "$content" ]]; then
                    printf '%s\n' "$rewritten" > "$out_file"
                  fi
                fi
              fi
            fi
          done < <(find "$source_dir" -mindepth 1 -maxdepth 1 -print0)
        }
        
        verify_passthrough_resources() {
          local source_dir="$1"
          local output_dir="$2"
          local entry base
          local missing=()
        
          while IFS= read -r -d '' entry; do
            base="$(basename "$entry")"
            case "$base" in
              SKILL.md|prompt.md)
                continue
                ;;
            esac
        
            if [[ ! -e "$output_dir/$base" ]]; then
              missing+=("$base")
            fi
          done < <(find "$source_dir" -mindepth 1 -maxdepth 1 -print0)
        
          if [[ ${#missing[@]} -gt 0 ]]; then
            die "Passthrough parity check failed for '$source_dir'; missing in output: ${missing[*]}"
          fi
        }
        
        # Resolve a possibly-nonexistent absolute path to its physical form, collapsing
        # `..` and symlinks against the deepest existing ancestor. Used before any
        # destructive comparison so the guard cannot be fooled by an unresolved path.
        resolve_physical() {
          local target="$1" suffix=""
          while [[ ! -e "$target" ]]; do
            suffix="/$(basename "$target")$suffix"
            target="$(dirname "$target")"
            [[ "$target" == "/" ]] && break
          done
          if [[ -d "$target" ]]; then
            printf '%s%s\n' "$(cd "$target" && pwd -P)" "$suffix"
          else
            printf '%s/%s%s\n' "$(cd "$(dirname "$target")" && pwd -P)" "$(basename "$target")" "$suffix"
          fi
        }
        
        # within CHILD PARENT -> 0 if CHILD equals PARENT or is nested under PARENT.
        # Both arguments must already be canonical (symlink/.. resolved).
        within() {
          local child="$1" parent="$2"
          # Root is the ancestor of everything; the general glob below would build
          # the broken pattern "//*" for it.
          [[ "$parent" == "/" ]] && return 0
          case "$child/" in
            "$parent"/*) return 0 ;;
          esac
          return 1
        }
        
        # Refuse a clean-write target that would destroy the source or the repository.
        # The write stage rm -rf's output_dir; both paths are canonicalized (symlinks
        # and `..` resolved via resolve_physical / `pwd -P`) before comparison so the
        # guard cannot be bypassed by an alias. The containment test is BIDIRECTIONAL:
        # refuse when the output equals the source, contains it (ancestor), OR lives
        # inside it (descendant) — any of those puts source files under the rm -rf — and
        # refuse when the output is, or is an ancestor of, the repository root (an output
        # above the repo would take the whole tree with it). CV-1: a source-dir output
        # went 4 files -> 2 at exit 0. Fix by refusal, never by silently appending a
        # subdir.
        assert_safe_output_dir() {
          local output_dir="$1" source_dir="$2"
          local out_abs src_abs repo_abs
          out_abs="$(resolve_physical "$output_dir")"
          src_abs="$(cd "$source_dir" && pwd -P)"
          repo_abs="$(cd "$REPO_ROOT" && pwd -P)"
        
          if within "$out_abs" "$src_abs"; then
            die "refusing to clean-write '$out_abs': it is the source package, or lives inside it ('$src_abs')"
          fi
          if within "$src_abs" "$out_abs"; then
            die "refusing to clean-write '$out_abs': it contains the source package '$src_abs'"
          fi
          if within "$repo_abs" "$out_abs"; then
            die "refusing to clean-write '$out_abs': it is, or contains, the repository root '$repo_abs'"
          fi
        }
        
        write_output() {
          local output_dir="$1"
          local source_dir="$2"
        
          # Guard BEFORE the destructive clean-write below.
          assert_safe_output_dir "$output_dir" "$source_dir"
        
          # Clean-write: delete target dir before writing
          if [[ -d "$output_dir" ]]; then
            rm -rf "$output_dir"
          fi
          mkdir -p "$output_dir"
        
          printf '%s\n' "$CONVERTED_OUTPUT" > "$output_dir/$CONVERTED_FILENAME"
          echo "OK: $output_dir/$CONVERTED_FILENAME"
        
          # Write secondary output if present (e.g., codex prompt.md)
          if [[ -n "${CONVERTED_OUTPUT_2:-}" && -n "${CONVERTED_FILENAME_2:-}" ]]; then
            printf '%s\n' "$CONVERTED_OUTPUT_2" > "$output_dir/$CONVERTED_FILENAME_2"
            echo "OK: $output_dir/$CONVERTED_FILENAME_2"
          fi
        
          copy_passthrough_resources "$source_dir" "$output_dir"
          verify_passthrough_resources "$source_dir" "$output_dir"
        }
        
        # ─── Main ─────────────────────────────────────────────────────────────
        
        convert_one_skill() {
          local skill_dir="$1"
          local target="$2"
          local output_dir="$3"
        
          # Resolve skill_dir to absolute if relative
          if [[ "$skill_dir" != /* ]]; then
            skill_dir="$REPO_ROOT/$skill_dir"
          fi
        
          [[ -d "$skill_dir" ]] || die "Skill directory not found: $skill_dir"
          [[ -f "$skill_dir/SKILL.md" ]] || die "No SKILL.md in: $skill_dir"
        
          parse_bundle "$skill_dir"
        
          [[ -n "$BUNDLE_NAME" ]] || die "Failed to parse name from $skill_dir/SKILL.md"
        
          # Default output dir (ADR-0016 closed set: generated projection tier)
          if [[ -z "$output_dir" ]]; then
            output_dir="$REPO_ROOT/.agents/projections/converter/$target/$BUNDLE_NAME"
          elif [[ "$output_dir" != /* ]]; then
            output_dir="$REPO_ROOT/$output_dir"
          fi
        
          # Reset output variables
          CONVERTED_OUTPUT=""
          CONVERTED_FILENAME=""
          CONVERTED_OUTPUT_2=""
          CONVERTED_FILENAME_2=""
        
          run_convert "$target"
          write_output "$output_dir" "$skill_dir"
        }
        
        main() {
          parse_args "$@"
        
          local skill_dir_or_flag="$ARGS_SKILL_DIR_OR_FLAG"
          local target="$ARGS_TARGET"
          local output_dir="$ARGS_OUTPUT_DIR"
        
          load_skill_pattern
        
          if [[ "$skill_dir_or_flag" == "--all" ]]; then
            local skills_root="$REPO_ROOT/skills"
            local count=0
            for d in "$skills_root"/*/; do
              [[ -f "$d/SKILL.md" ]] || continue
              local sname
              sname="$(basename "$d")"
              local out="$output_dir"
              if [[ -n "$out" ]]; then
                # Per-skill subdir under the provided output dir
                if [[ "$out" != /* ]]; then
                  out="$REPO_ROOT/$out/$sname"
                else
                  out="$out/$sname"
                fi
              fi
              convert_one_skill "$d" "$target" "$out"
              count=$((count + 1))
            done
            echo "Converted $count skills to target '$target'"
          else
            convert_one_skill "$skill_dir_or_flag" "$target" "$output_dir"
          fi
        }
        
        main "$@"
        
      • validate.sh 3.5 KB
        #!/usr/bin/env bash
        # Behavioral self-test for the converter. Replaces the prior vacuous check
        # ("SKILL.md exists" only, which passed while the pipeline could delete its own
        # source) with real proofs (CV-2):
        #   1. a happy-path conversion writes the expected target + passthrough files;
        #   2. the destructive clean-write path is CLOSED in BOTH directions — an output
        #      dir equal to, an ancestor of, a descendant of, or a symlink into the
        #      source package is refused; and an output that is or contains the repo
        #      root is refused (CV-1). Each refusal is proven to happen BEFORE any
        #      deletion: the source content digest is unchanged AND the exit is nonzero.
        set -euo pipefail
        
        SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
        CONVERT="$SKILL_DIR/scripts/converter/convert.sh"
        REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd -P)"
        FAIL=0
        pass() { printf 'PASS: %s\n' "$1"; }
        fail() { printf 'FAIL: %s\n' "$1"; FAIL=$((FAIL + 1)); }
        
        # Content + structure digest of a directory tree (path-relative, order-stable).
        dir_digest() {
          ( cd "$1" && find . -type f -exec shasum -a 256 {} + | LC_ALL=C sort ) \
            | shasum -a 256 | awk '{print $1}'
        }
        
        # assert_refused DESC OUTPUT — the conversion must exit nonzero AND leave the
        # source content digest unchanged (refusal before any deletion).
        assert_refused() {
          local desc="$1" out="$2"
          if bash "$CONVERT" "$FIX" codex "$out" >/dev/null 2>&1; then
            fail "$desc: expected refusal, got success"
            return
          fi
          if [[ "$(dir_digest "$FIX")" == "$SRC_DIGEST" ]]; then
            pass "$desc: refused before any deletion; source content intact"
          else
            fail "$desc: source content changed — refusal came too late"
          fi
        }
        
        if [[ -f "$SKILL_DIR/SKILL.md" ]]; then pass "SKILL.md exists"; else fail "SKILL.md exists"; fi
        if [[ -f "$CONVERT" ]]; then pass "convert.sh exists"; else fail "convert.sh exists"; fi
        
        # Disposable fixture, left in place on exit (no rm -rf): the guard under test
        # must never be handed a destructive cleanup to imitate.
        WORK="$(mktemp -d "${TMPDIR:-/tmp}/converter-selftest.XXXXXX")"
        FIX="$WORK/fixture-skill"
        mkdir -p "$FIX/references" "$FIX/scripts"
        printf -- '---\nname: fixture-skill\ndescription: converter self-test fixture\n---\n# Body\n' > "$FIX/SKILL.md"
        printf 'reference payload\n' > "$FIX/references/note.md"
        printf 'echo fixture\n' > "$FIX/scripts/tool.sh"
        SRC_DIGEST="$(dir_digest "$FIX")"
        
        # 1. Happy path: a distinct output dir converts, writes the target files, and
        # does not touch the source.
        out="$WORK/out"
        if bash "$CONVERT" "$FIX" codex "$out" >/dev/null 2>&1 \
          && [[ -f "$out/SKILL.md" && -f "$out/prompt.md" && -f "$out/references/note.md" && -f "$out/scripts/tool.sh" ]] \
          && [[ "$(dir_digest "$FIX")" == "$SRC_DIGEST" ]]; then
          pass "happy-path conversion writes target + passthrough files; source intact"
        else
          fail "happy-path conversion writes target + passthrough files; source intact"
        fi
        
        # 2. Bidirectional + repo containment refusals.
        assert_refused "output == source"                 "$FIX"
        assert_refused "output is an ancestor of source"  "$WORK"
        assert_refused "output is a descendant of source" "$FIX/references"
        assert_refused "output is an ancestor of the repo root" "$(dirname "$REPO_ROOT")"
        
        # Symlink resolving into the source must be canonicalized and refused.
        ln -s "$FIX/references" "$WORK/link-into-src"
        assert_refused "output is a symlink into the source" "$WORK/link-into-src"
        
        echo ""
        if [[ "$FAIL" -eq 0 ]]; then
          echo "converter self-test: PASS"
          exit 0
        fi
        echo "converter self-test: FAIL ($FAIL failed)" >&2
        exit 1
        
    • audit.sh 17.3 KB
      #!/usr/bin/env bash
      # audit.sh — two-pass skill audit (skill-builder deep audit mode; absorbed from /skill-auditor)
      # Pass 1 gates through heal.sh --check --strict; Pass 2 adds 8 NEW content-discipline checks.
      # Canonical SKILL.md template: skills/skill-builder/references/skill-template.md
      #
      # Usage:
      #   audit.sh [--strict] [--json <path>] <skills/path>
      #
      # Exit codes:
      #   0  — PASS or WARN (success)
      #   1  — FAIL (or WARN under --strict)
      #   2  — usage error or missing target
      
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
      HEAL_SH="$SCRIPT_DIR/heal.sh"
      SCORE_PY="$SCRIPT_DIR/score_agentops_skill.py"
      CRAFT_PY="$SCRIPT_DIR/craft_score.py"
      AUTHORING_PY="$SCRIPT_DIR/authoring_scan.py"
      PROFILE_TOOL="$REPO_ROOT/skills/skill-builder/scripts/conformance_profile.py"
      
      STRICT=0
      JSON_OUT=""
      TARGET=""
      
      usage() {
        echo "Usage: $0 [--strict] [--json <path>] <skills/path>" >&2
        exit 2
      }
      
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --strict) STRICT=1; shift ;;
          --json)   JSON_OUT="${2:-}"; shift 2 ;;
          --help|-h) usage ;;
          --*)      echo "Unknown flag: $1" >&2; usage ;;
          *)        TARGET="$1"; shift ;;
        esac
      done
      
      [[ -n "$TARGET" ]] || usage
      [[ -d "$TARGET" ]] || { echo "audit.sh: target $TARGET is not a directory" >&2; exit 2; }
      
      SKILL_MD="$TARGET/SKILL.md"
      [[ -f "$SKILL_MD" ]] || { echo "audit.sh: no SKILL.md at $SKILL_MD" >&2; exit 2; }
      
      # Load profile identity, rule order/severities, and shared evaluations before
      # emitting any verdict. Missing or malformed configuration fails closed.
      TARGET_ABS="$(cd "$TARGET" && pwd)"
      CANONICAL_TARGET=0
      case "$TARGET_ABS" in
        "$REPO_ROOT"/skills/*|"$REPO_ROOT"/skills-codex/*) CANONICAL_TARGET=1 ;;
      esac
      SELECTED_PROFILE="${SKILL_CONFORMANCE_PROFILE_ID:-}"
      if [[ -z "$SELECTED_PROFILE" && "$CANONICAL_TARGET" -eq 0 ]] \
        && ! grep -q '^skill_api_version:' "$SKILL_MD"; then
        SELECTED_PROFILE="external-observation"
      fi
      profile_args=(--repo-root "$REPO_ROOT" --audit-tsv "$SKILL_MD")
      if [[ -n "$SELECTED_PROFILE" ]]; then
        profile_args+=(--profile-id "$SELECTED_PROFILE")
      fi
      if [[ ! -f "$PROFILE_TOOL" ]]; then
        echo "profile configuration missing: $PROFILE_TOOL" >&2
        exit 2
      fi
      if ! PROFILE_DATA="$(python3 "$PROFILE_TOOL" "${profile_args[@]}")"; then
        exit 2
      fi
      
      PROFILE_ID=""
      KERNEL_MAX_LINES=""
      PROFILE_LINE_COUNT=""
      TRIGGER_FORMS=""
      OUTPUT_COMPLETE="false"
      RULE_IDS=()
      declare -A CHECK_SEVERITY=()
      declare -A PROFILE_RULE_FORMS=()
      while IFS=$'\t' read -r kind value extra forms; do
        case "$kind" in
          profile_id) PROFILE_ID="$value" ;;
          kernel_max_lines) KERNEL_MAX_LINES="$value" ;;
          line_count) PROFILE_LINE_COUNT="$value" ;;
          trigger_forms) TRIGGER_FORMS="$value" ;;
          output_complete) OUTPUT_COMPLETE="$value" ;;
          rule)
            RULE_IDS+=("$value")
            CHECK_SEVERITY[$value]="$extra"
            PROFILE_RULE_FORMS[$value]="$forms"
            ;;
        esac
      done <<<"$PROFILE_DATA"
      
      if [[ -z "$PROFILE_ID" || -z "$KERNEL_MAX_LINES" || ${#RULE_IDS[@]} -eq 0 ]]; then
        echo "profile configuration error: incomplete evaluated profile data" >&2
        exit 2
      fi
      
      # --- Pass 1: heal structural ----------------------------------------------
      PASS1_OUT=""
      PASS1_FINDINGS_JSON="[]"
      PASS1_AUTOFIXABLE=0
      PASS1_STATUS="pass"
      PASS1_EXIT_CODE=0
      PASS1_FINDING_COUNT=0
      
      if [[ -x "$HEAL_SH" ]]; then
        if PASS1_OUT="$(bash "$HEAL_SH" --check --strict "$TARGET" 2>&1)"; then
          PASS1_STATUS="pass"
          PASS1_EXIT_CODE=0
        else
          PASS1_EXIT_CODE=$?
          PASS1_STATUS="fail"
        fi
        # Parse [CODE] path: msg lines into JSON. Use Python here because BSD awk
        # lacks gawk's match(..., array) extension.
        PASS1_FINDINGS_JSON=$(PASS1_OUT="$PASS1_OUT" python3 - <<'PY'
      import json
      import os
      import re
      
      findings = []
      pattern = re.compile(r"^\[([A-Z_]+)\] ([^:]+): (.*)$")
      for line in os.environ.get("PASS1_OUT", "").splitlines():
          match = pattern.match(line)
          if match:
              code, path, msg = match.groups()
              findings.append({"code": code, "path": path, "msg": msg})
      print(json.dumps(findings))
      PY
      )
        # Count the complete heal.sh auto-fix allowlist.
        PASS1_AUTOFIXABLE=$(echo "$PASS1_OUT" | grep -cE '^\[(MISSING_NAME|MISSING_DESC|NAME_MISMATCH|UNLINKED_REF|EMPTY_DIR|MISSING_API_VERSION)\]' || true)
      else
        PASS1_STATUS="fail"
        PASS1_EXIT_CODE=2
        PASS1_OUT="heal delegate missing or not executable: $HEAL_SH"
        PASS1_FINDINGS_JSON='[{"code":"HEAL_SKILL_MISSING","path":"skills/skill-builder/scripts/heal.sh","msg":"heal delegate missing or not executable"}]'
      fi
      PASS1_FINDING_COUNT=$(PASS1_FINDINGS_JSON="$PASS1_FINDINGS_JSON" python3 - <<'PY'
      import json
      import os
      
      try:
          print(len(json.loads(os.environ.get("PASS1_FINDINGS_JSON", "[]"))))
      except Exception:
          print(0)
      PY
      )
      
      # --- Pass 2: 8 NEW checks ------------------------------------------------
      
      # Check 1: description-has-triggers (WARN on miss; run_check registers severity)
      check_description_has_triggers() {
        profile_rule_has_form description-has-triggers
      }
      
      profile_rule_has_form() {
        local rule_id="$1" accepted form
        accepted=",${PROFILE_RULE_FORMS[$rule_id]},"
        IFS=',' read -r -a found_forms <<<"$TRIGGER_FORMS"
        for form in "${found_forms[@]}"; do
          [[ -n "$form" && "$accepted" == *",$form,"* ]] && return 0
        done
        return 1
      }
      
      # Check 2: constraints-frontloaded (WARN on miss)
      check_constraints_frontloaded() {
        local skill_md="$1"
        if (( PROFILE_LINE_COUNT <= 100 )); then return 0; fi
        awk '
          BEGIN{n=0; i=0; found=0}
          /^---$/{n++; next}
          n==2 {
            i++
            if (i > 80) { exit 1 }
            if (/^## .*[Cc]onstraints/ || /^## .*⚠️/) { found=1; exit 0 }
          }
          END{ exit (found ? 0 : 1) }
        ' "$skill_md"
      }
      
      # Check 3: rationale-present (WARN on miss)
      check_rationale_present() {
        local skill_md="$1"
        awk '
          function flush_bullet() {
            if (!bullet_open) return
            bullets++
            if (bullet_text ~ /[Ww][Hh][Yy]|[Bb]ecause|[Tt]his matters|[Tt]o prevent|[Rr]ationale:|[Mm]otivation:/) with_why++
            bullet_open=0
            bullet_text=""
          }
          BEGIN{in_constraints=0; bullets=0; with_why=0; bullet_open=0}
          /^## .*([Cc]onstraints|⚠️)/{in_constraints=1; next}
          in_constraints && /^## /{flush_bullet(); exit}
          in_constraints && /^[ ]*[-*] /{
            flush_bullet()
            bullet_open=1
            bullet_text=$0
            next
          }
          in_constraints && bullet_open{bullet_text=bullet_text " " $0}
          END{
            flush_bullet()
            if (bullets == 0) exit 0
            exit (with_why * 2 >= bullets ? 0 : 1)
          }
        ' "$skill_md"
      }
      
      # Check 4: verification-checkpoints (WARN on miss, conditional)
      check_verification_checkpoints() {
        local skill_md="$1"
        local phases checkpoints
        phases=$(awk '/^## (Workflow|Methodology|Process|Execution)/{in_w=1; next} in_w && /^## /{exit} in_w && /^### /{n++} END{print n+0}' "$skill_md")
        if (( phases < 2 )); then return 0; fi
        checkpoints=$(grep -cE '\*\*Checkpoint:|confirm before|Wait for|verify before' "$skill_md" 2>/dev/null || echo 0)
        (( checkpoints >= 1 ))
      }
      
      # Check 5: output-spec-explicit (FAIL on miss)
      check_output_spec_explicit() {
        [[ "$OUTPUT_COMPLETE" == "true" ]]
      }
      
      # Check 6: quality-rubric (WARN on miss)
      check_quality_rubric() {
        local skill_md="$1"
        if (( PROFILE_LINE_COUNT <= 100 )); then return 0; fi
        awk '
          BEGIN{in_q=0; bullets=0}
          /^## (Quality|Checks|Checklist|Rubric|Best Practices|Acceptance)/{in_q=1; next}
          in_q && /^## /{exit}
          in_q && /^[ ]*[-*] /{bullets++}
          END{exit (bullets >= 3 ? 0 : 1)}
        ' "$skill_md"
      }
      
      # Check 7: references-modularization (WARN on miss, conditional)
      check_references_modularization() {
        (( PROFILE_LINE_COUNT <= KERNEL_MAX_LINES ))
      }
      
      # Check 8: trigger-clarity (WARN on miss; run_check registers severity)
      check_trigger_clarity() {
        profile_rule_has_form trigger-clarity
      }
      
      # --- Run all 8 checks ----------------------------------------------------
      declare -A CHECK_STATUS=()
      declare -A CHECK_EVIDENCE=()
      
      run_check() {
        local id="$1"
        local fn="$2"
        local severity="${CHECK_SEVERITY[$id]}"
        if "$fn" "$SKILL_MD"; then
          CHECK_STATUS[$id]="pass"
          CHECK_EVIDENCE[$id]="check passed"
        else
          CHECK_STATUS[$id]="${severity,,}"
          CHECK_EVIDENCE[$id]="check failed"
        fi
      }
      
      run_check description-has-triggers   check_description_has_triggers
      run_check constraints-frontloaded    check_constraints_frontloaded
      run_check rationale-present          check_rationale_present
      run_check verification-checkpoints   check_verification_checkpoints
      run_check output-spec-explicit       check_output_spec_explicit
      run_check quality-rubric             check_quality_rubric
      run_check references-modularization  check_references_modularization
      run_check trigger-clarity            check_trigger_clarity
      
      # --- Advisory density report ---------------------------------------------
      # This is deliberately not part of the PASS/WARN/FAIL verdict. Packet-boundary
      # enforcement belongs to the execution-packet schema; this block helps reviewers
      # find low-signal skill prose before fresh-context dispatch.
      declare -A DENSITY_PRESENT=()
      declare -A DENSITY_EVIDENCE=()
      
      check_density_field() {
        local id="$1"
        local pattern="$2"
        if grep -Eiq -- "$pattern" "$SKILL_MD"; then
          DENSITY_PRESENT[$id]="true"
          DENSITY_EVIDENCE[$id]="matched advisory pattern"
        else
          DENSITY_PRESENT[$id]="false"
          DENSITY_EVIDENCE[$id]="missing advisory pattern"
        fi
      }
      
      check_density_field intent 'intent|goal|behavior|capability'
      check_density_field boundary 'boundary|bounded context|write scope|non-goal|non-goals'
      check_density_field evidence 'evidence|test|tests|verdict|validation|acceptance'
      check_density_field decision 'decision|rationale|why|because|chosen'
      check_density_field constraint 'constraint|constraints|guardrail|guardrails|limit|limits|scope'
      check_density_field next_action 'next_action|next action|next steps|completion marker|report completion'
      
      density_present_count=0
      for id in intent boundary evidence decision constraint next_action; do
        if [[ "${DENSITY_PRESENT[$id]}" == "true" ]]; then
          density_present_count=$((density_present_count + 1))
        fi
      done
      if (( density_present_count == 6 )); then
        DENSITY_STATUS="pass"
      else
        DENSITY_STATUS="warn"
      fi
      
      # --- Pass 3: static package-readiness scoring (advisory) -----------------
      # Folds the 10-category Skill Quality Rubric (docs/reference/skill-quality-rubric.md)
      # into the report via score_agentops_skill.py --audit-block. Each category gets a
      # deterministic 0-3 score plus an explainable reason; total is 0-30 with a C/B/A/S
      # readiness band. Advisory-only: it never changes the PASS/WARN/FAIL verdict and
      # explicitly evaluates neither the safety gate nor behavioral effectiveness.
      # Reason: a low score on a structurally clean skill is a triage signal, while a
      # high score still cannot prove that the skill is safe or improves outcomes.
      RUBRIC_JSON="null"
      RUBRIC_SUMMARY=""
      RUBRIC_SCORE="n/a"
      RUBRIC_MAX="n/a"
      RUBRIC_RATING="?"
      if [[ -f "$SCORE_PY" ]] && command -v python3 >/dev/null 2>&1; then
        if rubric_out="$(python3 "$SCORE_PY" "$TARGET" --audit-block 2>/dev/null)"; then
          RUBRIC_JSON="$rubric_out"
          RUBRIC_SCORE="$(printf '%s' "$rubric_out" | awk -F': ' '/"total_score"/{gsub(/[, ]/,"",$2); print $2; exit}')"
          RUBRIC_MAX="$(printf '%s' "$rubric_out" | awk -F': ' '/"max_score"/{gsub(/[, ]/,"",$2); print $2; exit}')"
          RUBRIC_RATING="$(printf '%s' "$rubric_out" | awk -F'"' '/"rating"/{print $4; exit}')"
          RUBRIC_SUMMARY=" Static readiness: ${RUBRIC_SCORE}/${RUBRIC_MAX} (${RUBRIC_RATING}) [advisory; safety/effectiveness not evaluated]."
        fi
      fi
      
      # --- Pass 4: craft instrumentation (advisory) -----------------------------
      # 12-element craft score, provenance resolution, and loop-safety findings via
      # craft_score.py. Advisory-only by design: it never changes the PASS/WARN/FAIL
      # verdict or the exit code — presence of craft elements is cheaply detectable,
      # but craft quality stays the fresh validator's judgment. Fail-open to null
      # like the Pass 3 rubric.
      CRAFT_JSON="null"
      CRAFT_LINES=""
      if [[ -f "$CRAFT_PY" ]] && command -v python3 >/dev/null 2>&1; then
        if craft_out="$(python3 "$CRAFT_PY" "$TARGET" --audit-block --repo-root "$REPO_ROOT" 2>/dev/null)"; then
          CRAFT_JSON="$craft_out"
          CRAFT_LINES="$(CRAFT_OUT="$craft_out" python3 - 2>/dev/null <<'PY' || true
      import json
      import os
      
      report = json.loads(os.environ["CRAFT_OUT"])
      lines = [f"Pass 4 craft (advisory): {report['summary']}"]
      prov = report["provenance"]
      lines.append(f"Provenance (advisory): {prov['resolved']}/{prov['citations']} citations resolve")
      for dead in prov["dead"]:
          lines.append(f"  dead citation: {dead['citation']} ({dead['kind']})")
      loop = report["loop_safety"]["findings"]
      lines.append(f"Loop-safety (advisory): {len(loop)} finding(s)")
      for finding in loop:
          lines.append(f"  {finding['type']} in section '{finding['section']}'")
      print("\n".join(lines))
      PY
      )"
        fi
      fi
      
      # --- Pass 5: authoring prose-quality scan (advisory) ----------------------
      # Advisory suspects for the failure modes named in
      # references/authoring-doctrine.md (noop-phrase, negation-without-positive,
      # step-missing-done-condition). Advisory-only: the doctrine's no-op test is
      # model-relative and prohibitions are sometimes correct guardrails, so these
      # findings never change the PASS/WARN/FAIL verdict or exit code. Fail-open to
      # null like the Pass 3 rubric and Pass 4 craft blocks.
      AUTHORING_JSON="null"
      AUTHORING_LINES=""
      if [[ -f "$AUTHORING_PY" ]] && command -v python3 >/dev/null 2>&1; then
        if authoring_out="$(python3 "$AUTHORING_PY" "$TARGET" --audit-block 2>/dev/null)"; then
          AUTHORING_JSON="$authoring_out"
          AUTHORING_LINES="$(AUTHORING_OUT="$authoring_out" python3 - 2>/dev/null <<'PY' || true
      import json
      import os
      
      report = json.loads(os.environ["AUTHORING_OUT"])
      lines = [f"Pass 5 authoring (advisory): {report['summary']}"]
      for finding in report["findings"]:
          lines.append(f"  {finding['id']} line {finding['line']}: {finding['evidence']}")
      print("\n".join(lines))
      PY
      )"
        fi
      fi
      
      # --- Aggregate verdict ---------------------------------------------------
      fails=0
      warns=0
      for id in "${RULE_IDS[@]}"; do
        case "${CHECK_STATUS[$id]}" in
          fail) fails=$((fails+1)) ;;
          warn) warns=$((warns+1)) ;;
        esac
      done
      
      if [[ "$PASS1_STATUS" == "fail" && "$CANONICAL_TARGET" -eq 1 ]]; then
        VERDICT="FAIL"
      elif (( fails > 0 )); then
        VERDICT="FAIL"
      elif (( warns > 0 )); then
        VERDICT="WARN"
      else
        VERDICT="PASS"
      fi
      
      # --- Emit report ---------------------------------------------------------
      emit_json() {
        printf '{\n'
        printf '  "target": "%s",\n' "$TARGET"
        printf '  "profile_id": "%s",\n' "$PROFILE_ID"
        printf '  "verdict": "%s",\n' "$VERDICT"
        printf '  "pass1": {\n'
        printf '    "status": "%s",\n' "$PASS1_STATUS"
        printf '    "exit_code": %s,\n' "$PASS1_EXIT_CODE"
        printf '    "strict": true,\n'
        printf '    "findings": %s,\n' "$PASS1_FINDINGS_JSON"
        printf '    "autofixable": %s\n' "$PASS1_AUTOFIXABLE"
        printf '  },\n'
        printf '  "pass2": {\n'
        printf '    "checks": [\n'
        local first=1
        for id in "${RULE_IDS[@]}"; do
          if (( ! first )); then printf ',\n'; fi
          first=0
        printf '      {"id":"%s","status":"%s","severity":"%s","evidence":"%s"' \
          "$id" "${CHECK_STATUS[$id]}" "${CHECK_SEVERITY[$id]}" "${CHECK_EVIDENCE[$id]}"
        if [[ "$id" == "description-has-triggers" || "$id" == "trigger-clarity" ]]; then
          forms_json="$(python3 - "$TRIGGER_FORMS" <<'PY'
      import json
      import sys
      
      print(json.dumps([item for item in sys.argv[1].split(",") if item]))
      PY
      )"
          printf ',"forms":%s' "$forms_json"
        fi
        printf '}'
        done
        printf '\n    ]\n'
        printf '  },\n'
        printf '  "density": {\n'
        printf '    "status": "%s",\n' "$DENSITY_STATUS"
        printf '    "advisory": true,\n'
        printf '    "fields": [\n'
        first=1
        for id in intent boundary evidence decision constraint next_action; do
          if (( ! first )); then printf ',\n'; fi
          first=0
          printf '      {"id":"%s","present":%s,"evidence":"%s"}' "$id" "${DENSITY_PRESENT[$id]}" "${DENSITY_EVIDENCE[$id]}"
        done
        printf '\n    ],\n'
        printf '    "summary": "%d/6 density signals present; advisory-only and not execution-packet enforcement."\n' "$density_present_count"
        printf '  },\n'
        printf '  "rubric": %s,\n' "$RUBRIC_JSON"
        printf '  "craft": %s,\n' "$CRAFT_JSON"
        printf '  "authoring": %s,\n' "$AUTHORING_JSON"
        printf '  "summary": "Pass1: %s via heal --strict (exit %d, %d findings, %d autofixable). Pass2: %d fails, %d warns.%s Verdict: %s."\n' \
          "$PASS1_STATUS" "$PASS1_EXIT_CODE" "$PASS1_FINDING_COUNT" "$PASS1_AUTOFIXABLE" "$fails" "$warns" "$RUBRIC_SUMMARY" "$VERDICT"
        printf '}\n'
      }
      
      if [[ -n "$JSON_OUT" ]]; then
        emit_json > "$JSON_OUT"
      fi
      
      # Always print human-readable summary to stderr
      {
        echo "=== Skill Audit: $TARGET ==="
        echo "Profile: $PROFILE_ID"
        echo "Pass 1 (heal --strict): $PASS1_STATUS (exit $PASS1_EXIT_CODE), $PASS1_FINDING_COUNT findings ($PASS1_AUTOFIXABLE autofixable)"
        echo "Pass 2 (8 NEW checks):"
        for id in "${RULE_IDS[@]}"; do
          printf "  [%-4s] %s\n" "${CHECK_STATUS[$id]}" "$id"
        done
        echo "Density advisory: $density_present_count/6 fields present ($DENSITY_STATUS)"
        echo "Pass 3 static readiness (advisory): ${RUBRIC_SCORE}/${RUBRIC_MAX} (${RUBRIC_RATING}); safety/effectiveness not evaluated"
        if [[ -n "$CRAFT_LINES" ]]; then
          echo "$CRAFT_LINES"
        fi
        if [[ -n "$AUTHORING_LINES" ]]; then
          echo "$AUTHORING_LINES"
        fi
        echo "VERDICT: $VERDICT"
      } >&2
      
      # Always print JSON to stdout (unless --json file was supplied)
      if [[ -z "$JSON_OUT" ]]; then
        emit_json
      fi
      
      # --- Exit code -----------------------------------------------------------
      case "$VERDICT" in
        PASS) exit 0 ;;
        WARN) [[ "$STRICT" -eq 1 ]] && exit 1 || exit 0 ;;
        FAIL) exit 1 ;;
      esac
      
    • authoring_scan.py 7.1 KB
      #!/usr/bin/env python3
      """authoring_scan.py — advisory prose-quality findings for one skill package.
      
      Emits the deep audit's `authoring` block: mechanical suspects for the three
      detectable failure modes named in references/authoring-doctrine.md.
      
          noop-phrase                — phrasing the model already obeys by default
          negation-without-positive  — a prohibition with no positive counterpart
                                       in the same bullet/paragraph
          step-missing-done-condition — a workflow subphase with no checkable
                                       done-condition phrasing
      
      Advisory-only by design: findings never gate a verdict or exit code. The
      doctrine explains why (the no-op test is model-relative; prohibitions are
      sometimes correct guardrails). Output is JSON on stdout; exit 0 on any
      successful scan, 2 on usage/read errors.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      import sys
      from pathlib import Path
      
      NOOP_PHRASES = [
          r"be thorough(?:ly)?\b",
          r"be careful\b",
          r"make sure to\b",
          r"\bcarefully\b",
          r"do your best\b",
          r"as appropriate\b",
          r"remember to\b",
          r"be diligent\b",
      ]
      
      NEGATION_START = re.compile(r"^\s*(?:do not|don't|never|avoid)\b", re.IGNORECASE)
      NEGATION_TOKEN = re.compile(r"\b(?:do not|don't|never|avoid)\b", re.IGNORECASE)
      POSITIVE_MARKER = re.compile(
          r"\b(?:instead|corrective:|prefer|rather,)\s*", re.IGNORECASE
      )
      
      DONE_CONDITION = re.compile(
          r"(?:done when|checkpoint:|stop after|complete when|finished when|"
          r"until\b[^\n]*exit 0|exit 0|verify before|confirm before|wait for|"
          r"at most \d+)",
          re.IGNORECASE,
      )
      
      WORKFLOW_HEADING = re.compile(
          r"^##\s+.*(?:workflow|process|methodology|execution)", re.IGNORECASE
      )
      
      
      def strip_non_prose(text: str) -> str:
          """Blank out frontmatter, fenced code blocks, and HTML comments while
          preserving line numbering."""
          lines = text.splitlines()
          out: list[str] = []
          in_front = False
          in_fence = False
          for i, line in enumerate(lines):
              stripped = line.strip()
              if i == 0 and stripped == "---":
                  in_front = True
                  out.append("")
                  continue
              if in_front:
                  out.append("")
                  if stripped == "---":
                      in_front = False
                  continue
              if stripped.startswith("```"):
                  in_fence = not in_fence
                  out.append("")
                  continue
              if in_fence:
                  out.append("")
                  continue
              out.append(re.sub(r"<!--.*?-->", "", line))
          body = "\n".join(out)
          # multi-line HTML comments
          return re.sub(r"<!--.*?-->", lambda m: "\n" * m.group(0).count("\n"), body, flags=re.DOTALL)
      
      
      def find_noop_phrases(prose: str) -> list[dict]:
          findings = []
          for lineno, line in enumerate(prose.splitlines(), start=1):
              for pat in NOOP_PHRASES:
                  if re.search(pat, line, re.IGNORECASE):
                      findings.append(
                          {
                              "id": "noop-phrase",
                              "line": lineno,
                              "evidence": line.strip()[:160],
                          }
                      )
                      break
          return findings
      
      
      def units_with_lines(prose: str):
          """Yield (start_line, unit_text) for paragraph/bullet units."""
          lines = prose.splitlines()
          unit: list[str] = []
          start = 1
          for i, line in enumerate(lines, start=1):
              is_bullet = bool(re.match(r"^\s*[-*] ", line))
              if not line.strip():
                  if unit:
                      yield start, "\n".join(unit)
                      unit = []
                  continue
              if is_bullet and unit:
                  yield start, "\n".join(unit)
                  unit = []
              if not unit:
                  start = i
              unit.append(line)
          if unit:
              yield start, "\n".join(unit)
      
      
      def find_negation_without_positive(prose: str) -> list[dict]:
          findings = []
          for start, unit in units_with_lines(prose):
              if unit.lstrip().startswith("#"):
                  continue
              if not NEGATION_TOKEN.search(unit):
                  continue
              if POSITIVE_MARKER.search(unit):
                  continue
              clauses = [c.strip() for c in re.split(r"[.;]", unit) if c.strip()]
              # A clause free of negation tokens is treated as the positive
              # counterpart; the unit is flagged only when no such clause exists.
              if any(not NEGATION_TOKEN.search(c) for c in clauses):
                  continue
              findings.append(
                  {
                      "id": "negation-without-positive",
                      "line": start,
                      "evidence": unit.strip().replace("\n", " ")[:160],
                  }
              )
          return findings
      
      
      def find_steps_missing_done_condition(prose: str) -> list[dict]:
          findings = []
          lines = prose.splitlines()
          in_workflow = False
          sub_name = None
          sub_start = 0
          sub_buf: list[str] = []
      
          def flush():
              if sub_name is None:
                  return
              text = "\n".join(sub_buf)
              if not DONE_CONDITION.search(text):
                  findings.append(
                      {
                          "id": "step-missing-done-condition",
                          "line": sub_start,
                          "evidence": f"subphase '{sub_name}' has no checkable done condition",
                      }
                  )
      
          for i, line in enumerate(lines, start=1):
              if line.startswith("## "):
                  flush()
                  sub_name = None
                  sub_buf = []
                  in_workflow = bool(WORKFLOW_HEADING.match(line))
                  continue
              if in_workflow and line.startswith("### "):
                  flush()
                  sub_name = line[4:].strip()
                  sub_start = i
                  sub_buf = []
                  continue
              if sub_name is not None:
                  sub_buf.append(line)
          flush()
          return findings
      
      
      def scan(skill_md: Path) -> dict:
          prose = strip_non_prose(skill_md.read_text(encoding="utf-8"))
          findings = (
              find_noop_phrases(prose)
              + find_negation_without_positive(prose)
              + find_steps_missing_done_condition(prose)
          )
          counts = {
              "noop-phrase": 0,
              "negation-without-positive": 0,
              "step-missing-done-condition": 0,
          }
          for f in findings:
              counts[f["id"]] += 1
          return {
              "advisory": True,
              "findings": findings,
              "counts": counts,
              "summary": "authoring: %d advisory finding(s) (%s)"
              % (
                  len(findings),
                  ", ".join(f"{k}={v}" for k, v in counts.items()),
              ),
          }
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("target", help="skill package directory containing SKILL.md")
          parser.add_argument("--audit-block", action="store_true", help="emit the audit report block (default output shape)")
          args = parser.parse_args()
      
          skill_md = Path(args.target) / "SKILL.md"
          if not skill_md.is_file():
              print(f"authoring_scan: no SKILL.md at {skill_md}", file=sys.stderr)
              return 2
          try:
              report = scan(skill_md)
          except OSError as exc:
              print(f"authoring_scan: {exc}", file=sys.stderr)
              return 2
          json.dump(report, sys.stdout, indent=2)
          print()
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • build.sh 1.4 KB
      #!/usr/bin/env bash
      # Create, structurally check, and project one skill exactly once.
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      REPO_ROOT="${SKILL_BUILDER_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
      
      usage() {
        cat >&2 <<EOF
      usage:
        build.sh from-scratch <slug>
        build.sh from-template <slug> --like <existing-slug>
        build.sh absorb-external <slug> --from <path>
      EOF
        exit 2
      }
      
      [[ $# -ge 2 ]] || usage
      mode="$1"
      slug="$2"
      shift 2
      
      case "$mode" in
        from-scratch) init_mode=--scratch ;;
        from-template) init_mode=--template ;;
        absorb-external) init_mode=--external ;;
        *) usage ;;
      esac
      
      bash "$SCRIPT_DIR/init.sh" "$init_mode" "$slug" "$@"
      
      report="$REPO_ROOT/.agents/scratch/skill-builder/${slug}-build.json"
      if ! HEAL_REPO_ROOT="$REPO_ROOT" bash "$REPO_ROOT/skills/skill-builder/scripts/heal.sh" \
        --check --strict "$REPO_ROOT/skills/$slug"; then
        echo "skill-builder: structural check failed" >&2
        exit 1
      fi
      
      python3 "$REPO_ROOT/scripts/generate-skill-mesh.py"
      bash "$REPO_ROOT/scripts/codex-sync.sh" --only "$slug"
      bash "$REPO_ROOT/scripts/regen-codex-hashes.sh" --only "$slug"
      
      python3 - "$report" <<'PY'
      import json
      from pathlib import Path
      import sys
      
      path = Path(sys.argv[1])
      payload = json.loads(path.read_text(encoding="utf-8"))
      payload["structure_check_pass"] = True
      path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
      PY
      
      echo "skill-builder: created and projected $slug"
      
    • conformance_profile.py 18 KB
      #!/usr/bin/env python3
      """Load and evaluate the canonical AgentOps skill-conformance profile."""
      
      from __future__ import annotations
      
      import argparse
      import re
      import sys
      from pathlib import Path
      from typing import Any
      
      import yaml
      
      PROFILE_RELATIVE_PATH = Path(
          "skills/skill-builder/references/skill-conformance-profiles.yaml"
      )
      KNOWN_SEVERITIES = {"WARN", "FAIL"}
      KNOWN_TRIGGER_FORMS = {"inline-marker", "block-marker", "metadata-list"}
      REQUIRED_RULE_IDS = (
          "description-has-triggers",
          "constraints-frontloaded",
          "rationale-present",
          "verification-checkpoints",
          "output-spec-explicit",
          "quality-rubric",
          "references-modularization",
          "trigger-clarity",
      )
      KNOWN_EXTERNAL_CONTENT_POLICIES = {"observe-structure-only"}
      REQUIRED_PROHIBITED_COPY_CATEGORIES = {
          "prose",
          "prompts",
          "scripts",
          "examples",
          "names",
      }
      REQUIRED_PROTECTED_FRONTMATTER_FIELDS = {"description"}
      
      
      class ProfileError(ValueError):
          """Raised when the conformance profile is missing or malformed."""
      
      
      def _mapping(value: Any, label: str) -> dict[str, Any]:
          if not isinstance(value, dict):
              raise ProfileError(f"profile configuration error: {label} must be a mapping")
          return value
      
      
      def _string_list(value: Any, label: str) -> list[str]:
          if not isinstance(value, list) or not value or not all(isinstance(v, str) for v in value):
              raise ProfileError(
                  f"profile configuration error: {label} must be a non-empty string list"
              )
          return value
      
      
      def _positive_int(value: Any, label: str) -> int:
          if not isinstance(value, int) or value < 1:
              raise ProfileError(f"profile configuration error: {label} must be positive")
          return value
      
      
      def load_profile(repo_root: Path, profile_id: str | None = None) -> dict[str, Any]:
          """Load and fully validate one profile from the authoritative YAML file."""
          path = repo_root / PROFILE_RELATIVE_PATH
          if not path.is_file():
              raise ProfileError(f"profile configuration missing: {path}")
          try:
              document = yaml.safe_load(path.read_text(encoding="utf-8"))
          except (OSError, yaml.YAMLError) as exc:
              raise ProfileError(f"profile configuration error in {path}: {exc}") from exc
      
          document = _mapping(document, "document")
          profiles = _mapping(document.get("profiles"), "profiles")
          selected = profile_id or document.get("default_profile")
          if not isinstance(selected, str) or not selected:
              raise ProfileError("profile configuration error: default_profile must be a string")
          if selected not in profiles:
              raise ProfileError(f"profile configuration error: unknown profile {selected!r}")
          profile = _mapping(profiles[selected], f"profiles.{selected}")
          if profile.get("id") != selected:
              raise ProfileError(
                  f"profile configuration error: profile id must equal selected id {selected!r}"
              )
      
          limit = profile.get("kernel_max_lines")
          if not isinstance(limit, int) or limit < 1:
              raise ProfileError("profile configuration error: kernel_max_lines must be positive")
      
          trigger = _mapping(profile.get("trigger_forms"), "trigger_forms")
          accepted = _string_list(trigger.get("accepted"), "trigger_forms.accepted")
          unknown_forms = sorted(set(accepted) - KNOWN_TRIGGER_FORMS)
          if unknown_forms:
              raise ProfileError(
                  f"profile configuration error: unknown trigger form(s): {', '.join(unknown_forms)}"
              )
          _string_list(trigger.get("description_markers"), "trigger_forms.description_markers")
          minimum = trigger.get("metadata_list_min_items")
          if not isinstance(minimum, int) or minimum < 1:
              raise ProfileError(
                  "profile configuration error: metadata_list_min_items must be positive"
              )
      
          output = _mapping(profile.get("output_contract"), "output_contract")
          _string_list(output.get("section_headings"), "output_contract.section_headings")
          components = _mapping(
              output.get("required_components"), "output_contract.required_components"
          )
          if not components:
              raise ProfileError(
                  "profile configuration error: output_contract.required_components is empty"
              )
          for component_id, component in components.items():
              if not isinstance(component_id, str):
                  raise ProfileError("profile configuration error: component id must be a string")
              component = _mapping(component, f"output component {component_id}")
              _string_list(component.get("markers"), f"output component {component_id}.markers")
      
          clean_room = _mapping(profile.get("clean_room"), "clean_room")
          if clean_room.get("enabled") is not True:
              raise ProfileError("profile configuration error: clean_room.enabled must be true")
          policy = clean_room.get("external_content_policy")
          if policy not in KNOWN_EXTERNAL_CONTENT_POLICIES:
              raise ProfileError(
                  "profile configuration error: clean_room.external_content_policy "
                  f"must be one of {sorted(KNOWN_EXTERNAL_CONTENT_POLICIES)}, got {policy!r}"
              )
          categories = _string_list(
              clean_room.get("prohibited_copy_categories"),
              "clean_room.prohibited_copy_categories",
          )
          if len(categories) != len(set(categories)):
              raise ProfileError(
                  "profile configuration error: clean_room.prohibited_copy_categories "
                  "contains duplicates"
              )
          if set(categories) != REQUIRED_PROHIBITED_COPY_CATEGORIES:
              missing = sorted(REQUIRED_PROHIBITED_COPY_CATEGORIES - set(categories))
              unknown = sorted(set(categories) - REQUIRED_PROHIBITED_COPY_CATEGORIES)
              raise ProfileError(
                  "profile configuration error: clean_room.prohibited_copy_categories "
                  f"must name the exact known categories (missing={missing}, unknown={unknown})"
              )
          copy_detection = _mapping(clean_room.get("copy_detection"), "clean_room.copy_detection")
          _positive_int(
              copy_detection.get("minimum_fragment_characters"),
              "clean_room.copy_detection.minimum_fragment_characters",
          )
          _positive_int(
              copy_detection.get("minimum_name_characters"),
              "clean_room.copy_detection.minimum_name_characters",
          )
          protected_fields = _string_list(
              copy_detection.get("protected_frontmatter_fields"),
              "clean_room.copy_detection.protected_frontmatter_fields",
          )
          if len(protected_fields) != len(set(protected_fields)):
              raise ProfileError(
                  "profile configuration error: "
                  "clean_room.copy_detection.protected_frontmatter_fields contains duplicates"
              )
          if set(protected_fields) != REQUIRED_PROTECTED_FRONTMATTER_FIELDS:
              missing = sorted(REQUIRED_PROTECTED_FRONTMATTER_FIELDS - set(protected_fields))
              unknown = sorted(set(protected_fields) - REQUIRED_PROTECTED_FRONTMATTER_FIELDS)
              raise ProfileError(
                  "profile configuration error: "
                  "clean_room.copy_detection.protected_frontmatter_fields must name the "
                  f"exact known fields (missing={missing}, unknown={unknown})"
              )
          _string_list(
              copy_detection.get("ignored_exact_lines"),
              "clean_room.copy_detection.ignored_exact_lines",
          )
          _string_list(
              copy_detection.get("ignored_line_prefixes"),
              "clean_room.copy_detection.ignored_line_prefixes",
          )
      
          rule_order = _string_list(profile.get("rule_order"), "rule_order")
          if len(rule_order) != len(set(rule_order)):
              raise ProfileError("profile configuration error: rule_order contains duplicates")
          rules = _mapping(profile.get("rules"), "rules")
          if set(rule_order) != set(rules):
              missing = sorted(set(rule_order) - set(rules))
              extra = sorted(set(rules) - set(rule_order))
              raise ProfileError(
                  "profile configuration error: rule_order/rules mismatch "
                  f"(missing={missing}, extra={extra})"
              )
          known_rules = set(REQUIRED_RULE_IDS)
          actual_rules = set(rules)
          if actual_rules != known_rules:
              missing = sorted(known_rules - actual_rules)
              unknown = sorted(actual_rules - known_rules)
              raise ProfileError(
                  "profile configuration error: profile must declare the exact known rule IDs "
                  f"(missing={missing}, unknown={unknown})"
              )
          if rule_order != list(REQUIRED_RULE_IDS):
              raise ProfileError(
                  "profile configuration error: rule_order must use the canonical known rule "
                  f"order {list(REQUIRED_RULE_IDS)}"
              )
          for rule_id in rule_order:
              rule = _mapping(rules[rule_id], f"rules.{rule_id}")
              severity = rule.get("severity")
              if severity not in KNOWN_SEVERITIES:
                  raise ProfileError(
                      f"profile configuration error: rule {rule_id} has unknown severity {severity!r}"
                  )
              if "accepted_forms" in rule:
                  forms = _string_list(
                      rule["accepted_forms"], f"rules.{rule_id}.accepted_forms"
                  )
                  unknown = sorted(set(forms) - set(accepted))
                  if unknown:
                      raise ProfileError(
                          f"profile configuration error: rule {rule_id} names unknown forms {unknown}"
                      )
          return profile
      
      
      def split_frontmatter(text: str) -> tuple[str, str]:
          """Return raw YAML frontmatter and Markdown body."""
          if not text.startswith("---\n"):
              return "", text
          parts = text.split("\n---", 1)
          if len(parts) != 2:
              return "", text
          return parts[0][4:], parts[1].lstrip("-\n")
      
      
      def _frontmatter_mapping(text: str) -> tuple[str, dict[str, Any]]:
          raw, _ = split_frontmatter(text)
          try:
              payload = yaml.safe_load(raw) if raw else {}
          except yaml.YAMLError as exc:
              raise ProfileError(f"skill frontmatter configuration error: {exc}") from exc
          return raw, _mapping(payload, "skill frontmatter")
      
      
      def trigger_forms(text: str, profile: dict[str, Any]) -> list[str]:
          """Return accepted trigger form IDs found only in frontmatter semantics."""
          raw, frontmatter = _frontmatter_mapping(text)
          trigger = profile["trigger_forms"]
          markers = trigger["description_markers"]
          description = frontmatter.get("description", "")
          description = description if isinstance(description, str) else ""
          has_marker = any(marker.casefold() in description.casefold() for marker in markers)
          scalar = re.search(r"^description:\s*([|>])", raw, re.MULTILINE)
      
          found: set[str] = set()
          if has_marker:
              found.add("block-marker" if scalar else "inline-marker")
          metadata = frontmatter.get("metadata")
          trigger_list = metadata.get("triggers") if isinstance(metadata, dict) else None
          if isinstance(trigger_list, list) and len(trigger_list) >= trigger["metadata_list_min_items"]:
              found.add("metadata-list")
          return [form for form in trigger["accepted"] if form in found]
      
      
      def output_component_results(text: str, profile: dict[str, Any]) -> dict[str, bool]:
          """Evaluate every required executable-handoff component in one output section."""
          _, body = split_frontmatter(text)
          headings = {heading.casefold() for heading in profile["output_contract"]["section_headings"]}
          section_lines: list[str] = []
          capturing = False
          for line in body.splitlines():
              heading = re.match(r"^##\s+(.+?)\s*$", line)
              if heading:
                  normalized = heading.group(1).strip().casefold()
                  if capturing:
                      break
                  capturing = normalized in headings
                  continue
              if capturing:
                  section_lines.append(line)
          section = "\n".join(section_lines).casefold()
          results: dict[str, bool] = {}
          for component_id, component in profile["output_contract"]["required_components"].items():
              results[component_id] = any(
                  marker.casefold() in section for marker in component["markers"]
              )
          return results
      
      
      def evaluation(skill_md: Path, profile: dict[str, Any]) -> dict[str, Any]:
          """Evaluate shared trigger, boundary, and output semantics for one skill."""
          try:
              text = skill_md.read_text(encoding="utf-8")
          except OSError as exc:
              raise ProfileError(f"skill read error: {skill_md}: {exc}") from exc
          _, frontmatter = _frontmatter_mapping(text)
          forms = trigger_forms(text, profile)
          components = output_component_results(text, profile)
          declared_output = frontmatter.get("output_contract")
          has_declared_output = (
              isinstance(declared_output, str) and bool(declared_output.strip())
          ) or (isinstance(declared_output, (dict, list)) and bool(declared_output))
          return {
              "profile_id": profile["id"],
              "kernel_max_lines": profile["kernel_max_lines"],
              "line_count": len(text.splitlines()),
              "trigger_forms": forms,
              "output_components": components,
              "output_complete": has_declared_output or all(components.values()),
          }
      
      
      def clean_room_copies(
          external_source: Path, generated_dirs: list[Path], profile: dict[str, Any]
      ) -> list[str]:
          """Return external content copied into generated output under profile policy."""
          try:
              source = external_source.read_text(encoding="utf-8")
              generated = "\n".join(
                  path.read_text(encoding="utf-8", errors="replace")
                  for root in generated_dirs
                  for path in root.rglob("*")
                  if path.is_file()
              )
          except OSError as exc:
              raise ProfileError(f"clean-room verification read error: {exc}") from exc
      
          raw_frontmatter, body = split_frontmatter(source)
          try:
              source_metadata = yaml.safe_load(raw_frontmatter) if raw_frontmatter else {}
          except yaml.YAMLError as exc:
              raise ProfileError(f"external skill frontmatter error: {exc}") from exc
          source_metadata = _mapping(source_metadata, "external skill frontmatter")
      
          clean_room = profile["clean_room"]
          policy = clean_room["external_content_policy"]
          if policy != "observe-structure-only":
              raise ProfileError(
                  f"profile configuration error: unsupported external_content_policy {policy!r}"
              )
          categories = set(clean_room["prohibited_copy_categories"])
          detection = clean_room["copy_detection"]
          minimum_fragment = detection["minimum_fragment_characters"]
          ignored_exact = set(detection["ignored_exact_lines"])
          ignored_prefixes = tuple(detection["ignored_line_prefixes"])
          generated_folded = generated.casefold()
          generated_normalized = " ".join(generated.split()).casefold()
          copied: list[str] = []
      
          if categories & {"prose", "prompts", "scripts", "examples"}:
              protected = {
                  line.strip()
                  for line in body.splitlines()
                  if len(line.strip()) >= minimum_fragment
                  and line.strip() not in ignored_exact
                  and not line.lstrip().startswith(ignored_prefixes)
              }
              copied.extend(line for line in protected if line.casefold() in generated_folded)
      
              for field in detection["protected_frontmatter_fields"]:
                  value = source_metadata.get(field)
                  if not isinstance(value, str):
                      continue
                  normalized = " ".join(value.split())
                  if (
                      len(normalized) >= minimum_fragment
                      and normalized.casefold() in generated_normalized
                  ):
                      copied.append(normalized)
      
          if "names" in categories:
              external_name = source_metadata.get("name", "")
              if (
                  isinstance(external_name, str)
                  and len(external_name.strip()) >= detection["minimum_name_characters"]
                  and external_name.strip().casefold() in generated_folded
              ):
                  copied.append(external_name.strip())
          return sorted(set(copied))
      
      
      def emit_audit_tsv(skill_md: Path, profile: dict[str, Any]) -> None:
          """Emit shell-safe, validated profile/evaluation data for audit.sh."""
          result = evaluation(skill_md, profile)
          print(f"profile_id\t{result['profile_id']}")
          print(f"kernel_max_lines\t{result['kernel_max_lines']}")
          print(f"line_count\t{result['line_count']}")
          print(f"trigger_forms\t{','.join(result['trigger_forms'])}")
          print(f"output_complete\t{str(result['output_complete']).lower()}")
          for component_id, present in result["output_components"].items():
              print(f"output_component\t{component_id}\t{str(present).lower()}")
          for rule_id in profile["rule_order"]:
              rule = profile["rules"][rule_id]
              accepted = ",".join(rule.get("accepted_forms", []))
              print(f"rule\t{rule_id}\t{rule['severity']}\t{accepted}")
      
      
      def main(argv: list[str] | None = None) -> int:
          """Validate a profile and optionally emit evaluation data for audit.sh."""
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("--repo-root", type=Path, required=True)
          parser.add_argument("--profile-id", default=None)
          parser.add_argument("--audit-tsv", type=Path)
          parser.add_argument("--verify-clean-room", type=Path, metavar="EXTERNAL_SKILL_MD")
          parser.add_argument("--generated-dir", type=Path, action="append", default=[])
          args = parser.parse_args(argv)
          try:
              profile = load_profile(args.repo_root, args.profile_id)
              if args.audit_tsv and args.verify_clean_room:
                  raise ProfileError(
                      "profile configuration error: choose one of --audit-tsv or --verify-clean-room"
                  )
              if args.verify_clean_room:
                  if not args.generated_dir:
                      raise ProfileError(
                          "profile configuration error: --verify-clean-room requires --generated-dir"
                      )
                  copied = clean_room_copies(args.verify_clean_room, args.generated_dir, profile)
                  if copied:
                      raise ProfileError(
                          f"clean-room violation under profile {profile['id']}: copied external "
                          f"content: {copied[0]}"
                      )
                  print(profile["id"])
              elif args.audit_tsv:
                  emit_audit_tsv(args.audit_tsv, profile)
              else:
                  print(profile["id"])
          except ProfileError as exc:
              print(str(exc), file=sys.stderr)
              return 2
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • craft_score.py 12.2 KB
      #!/usr/bin/env python3
      """Advisory craft instrumentation for one skill package (audit Pass 4).
      
      Reports three advisory blocks over a skill's SKILL.md:
      
      1. A 12-element craft score with named gaps. Elements are the cheaply
         machine-detectable authoring elements enumerated in
         references/skill-template.md (section 8). Presence is detected, never
         quality; scoring quality stays a fresh validator's judgment.
      2. Provenance resolution: cited repo paths and .agents/ao verdict/intent
         digests (full or prefix...suffix abbreviated) must resolve; dead
         citations are named findings.
      3. Loop safety: iteration prose lacking a checkable stop-condition phrase
         in the same section, and agent-dispatch loops lacking a budget phrase,
         are named findings.
      
      Everything here is advisory-only: this script never gates, and audit.sh
      embeds its output without letting it change exit codes or verdicts.
      
      HTML comments are stripped before detection so the init.sh scaffolding
      stubs (<!-- craft:... -->) never satisfy an element; only authored prose
      counts.
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      from pathlib import Path
      
      import yaml
      
      MAX_SCORE = 12
      
      # Checkable stop-condition phrases: a bare "until <vague goal>" does not
      # count; the phrase must name a greppable boundary (count, exit code,
      # passing check).
      STOP_CONDITION = re.compile(
          r"(?i)("
          r"stop[- ](when|after|if|condition)s?|stops (when|after)|halt (when|after)"
          r"|at most \d+|no more than \d+|max(imum)?\s*(of\s+)?\d+"
          r"|\d+\s+(iterations?|attempts?|passes|times|rounds)"
          r"|give up after|exit (when|0|code 0)"
          r"|until [^.\n]*\b(exit 0|exits 0|passes|pass\b|green\b|zero findings|\d+)"
          r")"
      )
      
      LOOP_PROSE = re.compile(r"(?i)\b(repeat(ed|s)?|iterat(e|es|ing|ion|ions)|loop(s|ing)?)\b")
      
      DISPATCH_PROSE = re.compile(
          r"(?i)\b(agents?|subagents?|dispatch(es|ing)?|spawn(s|ed|ing)?|workers?|lanes?|swarm)\b"
      )
      
      BUDGET_PHRASE = re.compile(
          r"(?i)\b("
          r"budget|timebox|deadline"
          r"|at most \d+|no more than \d+|max(imum)?\s*(of\s+)?\d+"
          r"|\d+\s+(agents?|subagents?|workers?|lanes?|dispatches|attempts?|iterations?)"
          r")\b"
      )
      
      DIGEST_ABBREV = re.compile(r"\b([0-9a-f]{6,63})(?:\.\.\.|…)([0-9a-f]{2,63})\b")
      DIGEST_PREFIX = re.compile(r"\b([0-9a-f]{8,63})(?:\.\.\.|…)(?![0-9a-f])")
      DIGEST_FULL = re.compile(r"\b[0-9a-f]{64}\b")
      
      REPO_PATH = re.compile(
          r"(?<![\w/.-])"
          r"((?:docs|scripts|skills|skills-codex|tests|cli|schemas|evidence|\.agentops|\.agents)"
          r"/[A-Za-z0-9._\-][A-Za-z0-9._/\-]*)"
      )
      
      
      def frontmatter_and_body(text: str) -> tuple[dict, str]:
          parts = text.split("---", 2)
          if len(parts) != 3:
              return {}, text
          try:
              data = yaml.safe_load(parts[1]) or {}
          except yaml.YAMLError:
              data = {}
          if not isinstance(data, dict):
              data = {}
          return data, parts[2]
      
      
      def strip_html_comments(text: str) -> str:
          return re.sub(r"<!--.*?-->", "", text, flags=re.S)
      
      
      def split_sections(body: str) -> list[tuple[str, str]]:
          """Split body into (heading, section_text) pairs; preamble uses ''."""
          sections: list[tuple[str, str]] = []
          heading = ""
          lines: list[str] = []
          for line in body.splitlines():
              match = re.match(r"^#{1,6}\s+(.*)$", line)
              if match:
                  sections.append((heading, "\n".join(lines)))
                  heading = match.group(1).strip()
                  lines = []
              else:
                  lines.append(line)
          sections.append((heading, "\n".join(lines)))
          return sections
      
      
      def resolve_digest(citation: str, digest_stems: list[str]) -> bool:
          if "..." in citation or "…" in citation:
              prefix, _, suffix = re.split(r"(\.\.\.|…)", citation, maxsplit=1)
              return any(
                  stem.startswith(prefix) and (not suffix or stem.endswith(suffix))
                  for stem in digest_stems
              )
          return citation in digest_stems
      
      
      def check_provenance(text: str, repo_root: Path, skill_dir: Path) -> dict:
          # Fenced code blocks are illustrative examples, not citations; extracting
          # from them produces false dead findings (e.g. "skills/example").
          text = re.sub(r"```.*?```", "", text, flags=re.S)
          digest_stems = [
              path.stem
              for pattern in ("verdicts", "intents")
              for path in sorted((repo_root / ".agents" / "ao" / pattern / "sha256").glob("*"))
              if path.is_file()
          ]
      
          citations: list[tuple[str, str]] = []
          seen: set[str] = set()
          for regex in (DIGEST_ABBREV, DIGEST_PREFIX):
              for match in regex.finditer(text):
                  token = match.group(0)
                  if token not in seen:
                      seen.add(token)
                      citations.append((token, "digest"))
          for match in DIGEST_FULL.finditer(text):
              token = match.group(0)
              if token not in seen and not any(token in c for c, _ in citations):
                  seen.add(token)
                  citations.append((token, "digest"))
          for match in REPO_PATH.finditer(text):
              token = match.group(1).rstrip("./")
              if token and token not in seen:
                  seen.add(token)
                  citations.append((token, "path"))
      
          dead = []
          resolved = 0
          for citation, kind in citations:
              if kind == "digest":
                  ok = resolve_digest(citation, digest_stems)
              else:
                  ok = (repo_root / citation).exists() or (skill_dir / citation).exists()
              if ok:
                  resolved += 1
              else:
                  dead.append({"citation": citation, "kind": kind})
      
          return {
              "advisory": True,
              "citations": len(citations),
              "resolved": resolved,
              "dead": dead,
          }
      
      
      def check_loop_safety(sections: list[tuple[str, str]]) -> list[dict]:
          findings = []
          for heading, text in sections:
              if not LOOP_PROSE.search(text):
                  continue
              label = heading or "(preamble)"
              if not STOP_CONDITION.search(text):
                  findings.append(
                      {
                          "type": "loop-missing-stop-condition",
                          "section": label,
                          "evidence": "iteration prose without a checkable stop-condition phrase in the same section",
                      }
                  )
              if DISPATCH_PROSE.search(text) and not BUDGET_PHRASE.search(text):
                  findings.append(
                      {
                          "type": "dispatch-loop-missing-budget",
                          "section": label,
                          "evidence": "agent-dispatch loop without a budget phrase in the same section",
                      }
                  )
          return findings
      
      
      def detect_elements(
          description: str,
          body: str,
          sections: list[tuple[str, str]],
          provenance: dict,
      ) -> list[dict]:
          def grep(pattern: str, text: str) -> bool:
              return re.search(pattern, text, re.I) is not None
      
          named_loop = False
          for _, text in sections:
              if LOOP_PROSE.search(text) and STOP_CONDITION.search(text):
                  named_loop = True
                  break
      
          anti_pattern_paired = False
          for _, text in sections:
              if grep(r"\b(anti-pattern|avoid|never|don'?t|do not)\b", text) and grep(
                  r"\b(instead|corrective|rather than|replace with)\b", text
              ):
                  anti_pattern_paired = True
                  break
      
          fenced = re.findall(r"```([a-zA-Z]*)\n(.*?)```", body, re.S)
          runnable = any(
              lang.lower() in ("bash", "sh", "shell", "console", "zsh")
              or re.search(r"(?m)^\s*(bash|python3|ao|sh)\s+\S", code)
              for lang, code in fenced
          )
      
          router = grep(r"(?m)^#{1,6}\s.*\b(modes|routing|router)\b", body) or grep(
              r"(?m)^\|[^\n]*\b(mode|trigger)\b[^\n]*\|", body
          )
      
          checks = [
              (
                  "causal-insight-line",
                  grep(r"(insight:|\*\*why:?\*\*|\bbecause\b|why this works)", body),
                  "a line stating the causal mechanism (Insight:/Why:/because)",
              ),
              (
                  "named-failure-mode",
                  grep(r"(failure mode|fails when|failure behavior|known failure)", body),
                  "a named failure mode or failure-behavior section",
              ),
              (
                  "frozen-prompts",
                  grep(r"(copy-paste-only|copy paste only|frozen prompt|verbatim prompt)", body),
                  "a prompt block marked copy-paste-only/frozen/verbatim",
              ),
              (
                  "named-loop-stop-condition",
                  named_loop,
                  "loop prose with a checkable stop-condition phrase in the same section",
              ),
              (
                  "quantified-rules",
                  grep(
                      r"(at most \d|at least \d|no more than \d|within \d|max(imum)? (of )?\d"
                      r"|<=\s?\d|>=\s?\d|\b\d+ (lines|files|attempts|iterations|passes|seconds|minutes|checks|bullets)\b)",
                      body,
                  ),
                  "a rule with a number and unit or comparator",
              ),
              (
                  "negative-space",
                  grep(
                      r"(non-goal|not for\b|not ideal for|do not use|not when|out of scope|does not\b|never\b)",
                      body,
                  ),
                  "explicit negative space (non-goals / not-for)",
              ),
              (
                  "anti-pattern-with-corrective",
                  anti_pattern_paired,
                  "an anti-pattern paired with a corrective (instead/corrective) in the same section",
              ),
              (
                  "provenance-citation",
                  provenance["citations"] > 0 and provenance["resolved"] > 0,
                  "at least one resolvable repo-path or .agents/ao digest citation",
              ),
              (
                  "measurable-done",
                  grep(
                      r"(done when|complete when|exit (code )?0|exits? 0\b|exits? nonzero|passes when|validator command)",
                      body,
                  ),
                  "a machine-checkable done signal (exit code / done-when phrase)",
              ),
              (
                  "router-shape",
                  router,
                  "a modes/routing table or heading mapping triggers to entry points",
              ),
              (
                  "trigger-rich-description",
                  grep(r"(triggers:|use when)", description or ""),
                  "frontmatter description with Triggers:/Use when phrases",
              ),
              (
                  "runnable-commands",
                  runnable,
                  "a fenced block with runnable commands",
              ),
          ]
      
          return [
              {
                  "id": element_id,
                  "present": bool(present),
                  "evidence": ("found: " if present else "missing: ") + what,
              }
              for element_id, present, what in checks
          ]
      
      
      def craft_report(skill_dir: Path, repo_root: Path) -> dict:
          skill_md = skill_dir / "SKILL.md"
          if not skill_md.is_file():
              raise SystemExit(f"SKILL.md not found: {skill_md}")
          raw = skill_md.read_text(encoding="utf-8")
          text = strip_html_comments(raw)
          fm, body = frontmatter_and_body(text)
          sections = split_sections(body)
      
          provenance = check_provenance(text, repo_root, skill_dir)
          loop_findings = check_loop_safety(sections)
          elements = detect_elements(str(fm.get("description") or ""), body, sections, provenance)
      
          score = sum(1 for element in elements if element["present"])
          missing = [element["id"] for element in elements if not element["present"]]
          summary = f"craft {score}/{MAX_SCORE}"
          if missing:
              summary += "; missing: " + ", ".join(missing)
      
          return {
              "score": score,
              "max": MAX_SCORE,
              "advisory": True,
              "missing": missing,
              "elements": elements,
              "provenance": provenance,
              "loop_safety": {"advisory": True, "findings": loop_findings},
              "summary": summary,
          }
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("skill_path")
          parser.add_argument(
              "--audit-block",
              action="store_true",
              help="Emit the advisory craft block embedded by audit.sh (same JSON as default).",
          )
          parser.add_argument(
              "--repo-root",
              default=None,
              help="Repo root for provenance resolution (default: this script's repo).",
          )
          args = parser.parse_args()
      
          script_repo = Path(__file__).resolve().parents[3]
          repo_root = Path(args.repo_root).resolve() if args.repo_root else script_repo
          report = craft_report(Path(args.skill_path).expanduser().resolve(), repo_root)
          print(json.dumps(report, indent=2))
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • heal.sh 3.4 KB
      #!/usr/bin/env bash
      # One-pass structural audit for source skill packages.
      set -euo pipefail
      
      MODE=check
      STRICT=0
      TARGETS=()
      while [[ $# -gt 0 ]]; do
        case "$1" in
          --check) MODE=check ;;
          --fix) MODE=fix ;;
          --strict) STRICT=1 ;;
          -h|--help)
            echo "usage: heal.sh [--check|--fix] [--strict] [skills/<slug> ...]"
            exit 0
            ;;
          *) TARGETS+=("$1") ;;
        esac
        shift
      done
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      REPO_ROOT="${HEAL_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
      REPO_ROOT="$(cd "$REPO_ROOT" && pwd -P)"
      
      if [[ ${#TARGETS[@]} -eq 0 ]]; then
        for path in "$REPO_ROOT/skills"/*; do
          [[ -d "$path" && -f "$path/SKILL.md" ]] && TARGETS+=("$path")
        done
      fi
      
      normalized=()
      for target in "${TARGETS[@]}"; do
        [[ "$target" = /* ]] || target="$REPO_ROOT/$target"
        [[ -d "$target" ]] || { echo "heal.sh: target does not exist: $target" >&2; exit 2; }
        [[ ! -L "$target" ]] || { echo "heal.sh: symlink targets are not accepted: $target" >&2; exit 2; }
        resolved="$(cd "$target" && pwd -P)"
        case "$(dirname "$resolved")" in
          "$REPO_ROOT/skills") ;;
          *) echo "heal.sh: target is not a direct skill package: $target" >&2; exit 2 ;;
        esac
        normalized+=("$resolved")
      done
      
      set +e
      python3 - "$REPO_ROOT" "${normalized[@]}" <<'PY'
      from pathlib import Path
      import re
      import sys
      import yaml
      
      repo = Path(sys.argv[1])
      findings = []
      for root in map(Path, sys.argv[2:]):
          skill = root / "SKILL.md"
          rel = root.relative_to(repo).as_posix()
          if not skill.is_file():
              findings.append(("MISSING_SKILL", rel, "SKILL.md is missing"))
              continue
          text = skill.read_text(encoding="utf-8")
          parts = text.split("---", 2)
          if len(parts) != 3:
              findings.append(("INVALID_FRONTMATTER", rel, "leading YAML frontmatter is missing"))
              continue
          try:
              data = yaml.safe_load(parts[1]) or {}
          except yaml.YAMLError as exc:
              findings.append(("INVALID_FRONTMATTER", rel, str(exc).splitlines()[0]))
              continue
          slug = root.name
          if data.get("name") != slug:
              findings.append(("NAME_MISMATCH", rel, f"name must be {slug!r}"))
          if not isinstance(data.get("description"), str) or not data["description"].strip():
              findings.append(("MISSING_DESC", rel, "description must be nonempty"))
          if data.get("skill_api_version") != 1:
              findings.append(("MISSING_API_VERSION", rel, "skill_api_version must be 1"))
          metadata = data.get("metadata")
          if not isinstance(metadata, dict) or not isinstance(metadata.get("disposition"), str) or not metadata["disposition"]:
              findings.append(("MISSING_DISPOSITION", rel, "metadata.disposition must be nonempty"))
          body = parts[2]
          for match in re.finditer(r"\]\((references|scripts)/([^\s)#?]+)", body):
              linked = root / match.group(1) / match.group(2)
              if not linked.exists():
                  findings.append(("DEAD_REF", rel, f"missing {linked.relative_to(root)}"))
      
      for code, path, message in findings:
          print(f"[{code}] {path}: {message}")
      sys.exit(1 if findings else 0)
      PY
      rc=$?
      set -e
      
      if [[ "$MODE" == fix ]]; then
        # Source behavior remains human-authored. Repair only owned projections.
        python3 "$REPO_ROOT/scripts/generate-skill-mesh.py"
        names="$(printf '%s\n' "${normalized[@]}" | sed 's#.*/##' | sort -u | paste -sd, -)"
        bash "$REPO_ROOT/scripts/codex-sync.sh" --force --only "$names"
      fi
      
      if [[ $rc -ne 0 && ( $STRICT -eq 1 || "$MODE" == fix ) ]]; then
        exit 1
      fi
      exit 0
      
    • init.sh 5.2 KB
      #!/usr/bin/env bash
      # Create one metadata-complete canonical skill source package.
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      REPO_ROOT="${SKILL_BUILDER_REPO_ROOT:-$(cd "$SCRIPT_DIR/../../.." && pwd)}"
      
      usage() {
        echo "usage: init.sh --scratch|--template|--external <slug> [--like <slug>|--from <path>]" >&2
        exit 2
      }
      
      [[ $# -ge 2 ]] || usage
      mode="$1"
      slug="$2"
      shift 2
      
      [[ "$slug" =~ ^[a-z][a-z0-9-]*$ ]] || {
        echo "init.sh: slug must be lowercase-hyphen: $slug" >&2
        exit 2
      }
      
      source_hint=""
      case "$mode" in
        --scratch)
          [[ $# -eq 0 ]] || usage
          ;;
        --template)
          [[ $# -eq 2 && "$1" == "--like" ]] || usage
          source_hint="$2"
          [[ -f "$REPO_ROOT/skills/$source_hint/SKILL.md" ]] || {
            echo "init.sh: unknown template skill: $source_hint" >&2
            exit 2
          }
          ;;
        --external)
          [[ $# -eq 2 && "$1" == "--from" ]] || usage
          source_hint="$2"
          [[ -f "$source_hint" ]] || {
            echo "init.sh: external source does not exist: $source_hint" >&2
            exit 2
          }
          ;;
        *) usage ;;
      esac
      
      target="$REPO_ROOT/skills/$slug"
      [[ ! -e "$target" ]] || {
        echo "init.sh: target already exists: $target" >&2
        exit 1
      }
      
      tier="${SKILL_TIER:-execution}"
      dependencies="${SKILL_DEPENDENCIES:-[]}"
      capabilities="${SKILL_CAPABILITIES:-[\"${slug//-/_}\"]}"
      effects="${SKILL_EFFECTS:-[]}"
      
      python3 - "$dependencies" "$capabilities" "$effects" <<'PY'
      import json
      import sys
      for value in sys.argv[1:]:
          parsed = json.loads(value)
          if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed):
              raise SystemExit("skill metadata lists must be JSON arrays of strings")
      PY
      
      mkdir -p "$target/scripts"
      
      # The <!-- craft:... --> stubs mirror the 12 craft elements enumerated in
      # references/skill-template.md section 7. The craft scorer strips HTML
      # comments, so a fresh scaffold scores low until the author replaces stubs
      # with real prose.
      cat >"$target/SKILL.md" <<EOF
      ---
      name: $slug
      description: 'TODO: state the behavior and concrete trigger phrases for $slug.'
      practices: []
      skill_api_version: 1
      hexagonal_role: supporting
      consumes: []
      produces: []
      context_rel: []
      user-invocable: true
      metadata:
        tier: $tier
        dependencies: $dependencies
        capabilities: $capabilities
        effects: $effects
        canonical_status: canonical
        disposition: keep_specialist
        stability: experimental
      ---
      
      # /$slug
      
      TODO: Explain the bounded behavior this skill provides.
      
      <!-- craft:trigger-rich-description Put Triggers:/Use when phrases callers actually say in the frontmatter description. -->
      <!-- craft:causal-insight-line State the one causal insight (Insight:/Why:/a-because-clause) that makes this skill work. -->
      <!-- craft:named-failure-mode Name the concrete failure mode this skill exists to prevent. -->
      <!-- craft:router-shape Map trigger phrases to modes/entry points in a routing table when the skill has modes. -->
      
      ## Inputs
      
      TODO: List required inputs and explicit non-goals.
      
      <!-- craft:negative-space State what this skill is NOT for (non-goals / not-for / do-not-use-when). -->
      
      ## Procedure
      
      1. TODO: Perform one bounded operation.
      2. TODO: Check the output against the stated contract.
      3. Report the result and stop.
      
      <!-- craft:named-loop-stop-condition If any step iterates, name the loop and give a checkable stop condition in the same section. -->
      <!-- craft:quantified-rules Quantify at least one rule with a number and unit. -->
      <!-- craft:anti-pattern-with-corrective Pair each anti-pattern with its corrective in the same section. -->
      <!-- craft:frozen-prompts Provide any reusable prompt as a fenced block marked copy-paste-only. -->
      <!-- craft:runnable-commands Include at least one fenced block with runnable commands. -->
      
      ## Output
      
      TODO: Define the artifact or response shape and how a caller checks it.
      
      <!-- craft:measurable-done Give a machine-checkable done signal (done-when phrase, exit 0, validator command). -->
      
      ## Checks
      
      - The output satisfies the declared behavior.
      - No undeclared side effect occurred.
      
      <!-- craft:provenance-citation Cite at least one resolvable repo path or .agents/ao verdict/intent digest grounding this skill. -->
      
      ## Failure behavior
      
      Report the concrete failure and stop. The caller owns any revision.
      EOF
      
      cat >"$target/scripts/validate.sh" <<'EOF'
      #!/usr/bin/env bash
      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-builder/scripts/heal.sh" --check --strict "$SKILL_DIR"
      EOF
      chmod +x "$target/scripts/validate.sh"
      
      mkdir -p "$REPO_ROOT/.agents/scratch/skill-builder"
      report="$REPO_ROOT/.agents/scratch/skill-builder/${slug}-build.json"
      python3 - "$report" "$mode" "$slug" "$source_hint" <<'PY'
      import json
      from pathlib import Path
      import sys
      
      path = Path(sys.argv[1])
      mode = {"--scratch": "from-scratch", "--template": "from-template", "--external": "absorb-external"}[sys.argv[2]]
      payload = {
          "mode": mode,
          "skill_name": sys.argv[3],
          "files_created": [f"skills/{sys.argv[3]}/SKILL.md", f"skills/{sys.argv[3]}/scripts/validate.sh"],
          "structure_check_pass": False,
      }
      if sys.argv[4]:
          payload["source_hint"] = sys.argv[4]
      path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
      PY
      
      echo "init.sh: created $target"
      
    • scan_descriptions.py 21.2 KB
      #!/usr/bin/env python3
      """Corpus-wide skill description trigger scanner.
      
      The per-skill deep audit (skill-builder audit mode) 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-builder/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/SKILL.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 emitted profile check passes (or --strict not set);
             in --probe mode: the declaring skill ranks #1 for the phrase
          1  one or more emitted profile checks is WARN/FAIL 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 os
      import re
      import sys
      from dataclasses import dataclass, field
      from pathlib import Path
      
      try:
          from conformance_profile import ProfileError, load_profile, trigger_forms
      except ModuleNotFoundError as exc:
          ProfileError = ValueError  # type: ignore[misc,assignment]
          load_profile = None  # type: ignore[assignment]
          trigger_forms = None  # type: ignore[assignment]
          _PROFILE_IMPORT_ERROR = exc
      else:
          _PROFILE_IMPORT_ERROR = None
      
      REPO_ROOT = Path(__file__).resolve().parents[3]
      
      # 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 = ""
          profile_id: str = ""
          checks: list[dict[str, str]] = field(default_factory=list)
      
          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,
                  "profile_id": self.profile_id,
                  "checks": self.checks,
              }
      
      
      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, profile: dict | None = None
      ) -> 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")):
              if profile is None:
                  try:
                      text = skill_md.read_text(encoding="utf-8")
                  except OSError:
                      continue
                  frontmatter, _ = split_frontmatter(text)
                  scan = SkillScan(
                      name=parse_field(frontmatter, "name") or skill_md.parent.name,
                      path=skill_md,
                      description=description_block(frontmatter),
                      has_trigger=False,
                  )
              else:
                  scan = scan_skill(skill_md, profile)
              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, profile: dict) -> list[str]:
          """Return canonical trigger form IDs from the selected profile semantics."""
          if trigger_forms is None:
              raise ProfileError(f"profile configuration loader missing: {_PROFILE_IMPORT_ERROR}")
          return trigger_forms(text, profile)
      
      
      def score_trigger(description: str) -> int:
          """Score 0-3, mirroring skill-builder/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, profile: dict | None = None) -> 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
          if profile is None:
              if load_profile is None:
                  raise ProfileError(f"profile configuration loader missing: {_PROFILE_IMPORT_ERROR}")
              profile = load_profile(REPO_ROOT, os.environ.get("SKILL_CONFORMANCE_PROFILE_ID"))
          frontmatter, _body = split_frontmatter(text)
          if re.search(r"^implementation:\s*false\s*$", frontmatter, re.MULTILINE):
              return None
          name = parse_field(frontmatter, "name") or skill_md.parent.name
          description = description_block(frontmatter)
          forms = detect_trigger(text, profile)
          has_trigger = bool(forms)
          rules = profile["rules"]
          checks = []
          for rule_id in ("description-has-triggers", "trigger-clarity"):
              accepted = rules[rule_id].get("accepted_forms", profile["trigger_forms"]["accepted"])
              passed = any(form in accepted for form in forms)
              severity = rules[rule_id]["severity"]
              checks.append(
                  {
                      "id": rule_id,
                      "severity": severity,
                      "status": "pass" if passed else severity.lower(),
                  }
              )
          scan = SkillScan(
              name=name,
              path=skill_md,
              description=description,
              has_trigger=has_trigger,
              forms=forms,
              score=score_trigger(description),
              profile_id=profile["id"],
              checks=checks,
          )
          if not has_trigger:
              scan.suggestion = suggest_triggers(name, parse_field(frontmatter, "description"))
          return scan
      
      
      def scan_corpus(skills_dir: Path, profile: dict | None = None) -> 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, profile)
              if scan is not None:
                  results.append(scan)
          return results
      
      
      def aggregate_verdict(results: list[SkillScan]) -> str:
          """Derive the scanner verdict solely from emitted profile check statuses."""
          statuses = {
              check["status"] for result in results for check in result.checks
          }
          if "fail" in statuses:
              return "FAIL"
          if any(status != "pass" for status in statuses):
              return "WARN"
          return "PASS"
      
      
      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], profile_id: str = "") -> str:
          """Render a human-readable remediation report."""
          total = len(results)
          missing = [r for r in results if not r.has_trigger]
          selected_profile = profile_id or (results[0].profile_id if results else "unknown")
          verdict = aggregate_verdict(results)
          lines = [
              "# Skill description trigger scan",
              "",
              f"- Profile: **{selected_profile}**",
              f"- Verdict: **{verdict}**",
              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,
          *,
          profile: dict | None,
          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, profile)
          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 emitted profile check is non-pass"
          )
          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,
                  profile=None,
                  json_mode=args.json,
                  quiet=args.quiet,
              )
      
          if load_profile is None:
              print(f"profile configuration loader missing: {_PROFILE_IMPORT_ERROR}", file=sys.stderr)
              return 2
          try:
              profile = load_profile(REPO_ROOT, os.environ.get("SKILL_CONFORMANCE_PROFILE_ID"))
          except ProfileError as exc:
              print(str(exc), file=sys.stderr)
              return 2
      
          results = scan_corpus(skills_dir, profile)
          missing = [r for r in results if not r.has_trigger]
          verdict = aggregate_verdict(results)
      
          if args.json:
              payload = {
                  "profile_id": profile["id"],
                  "verdict": verdict,
                  "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, profile["id"]))
      
          return 1 if (args.strict and verdict != "PASS") else 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
    • score_agentops_skill.py 12 KB
      #!/usr/bin/env python3
      """Score static package readiness for an AgentOps skill."""
      
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import re
      from pathlib import Path
      
      import yaml
      
      
      CATEGORIES = [
          "trigger_quality",
          "kernel_clarity",
          "progressive_disclosure",
          "helper_scripts",
          "validation",
          "self_test",
          "assets_templates",
          "subagents_roles",
          "safety_boundaries",
          "packaging",
      ]
      
      
      def frontmatter(text: str) -> dict:
          match = re.match(r"^---\n(.*?)\n---", text, re.S)
          if not match:
              return {}
          try:
              data = yaml.safe_load(match.group(1)) or {}
          except yaml.YAMLError:
              return {}
          return data if isinstance(data, dict) else {}
      
      
      def count_files(path: Path, *parts: str) -> int:
          target = path.joinpath(*parts)
          if not target.exists():
              return 0
          return sum(1 for p in target.rglob("*") if p.is_file())
      
      
      def has_named_script(path: Path, patterns: tuple[str, ...]) -> bool:
          scripts = path / "scripts"
          if not scripts.exists():
              return False
          for script in scripts.rglob("*"):
              if not script.is_file():
                  continue
              name = script.name.lower()
              if any(pattern in name for pattern in patterns):
                  return True
          return False
      
      
      def collect_metrics(path: Path, text: str) -> dict:
          lines = text.splitlines()
          scripts_dir = path / "scripts"
          executable_scripts = 0
          if scripts_dir.exists():
              executable_scripts = sum(
                  1 for p in scripts_dir.rglob("*") if p.is_file() and os.access(p, os.X_OK)
              )
      
          return {
              "total_files": sum(1 for p in path.rglob("*") if p.is_file()),
              "skill_md_lines": len(lines),
              "headings": len([line for line in lines if line.startswith("#")]),
              "reference_links": len(re.findall(r"references/", text)),
              "reference_files": count_files(path, "references"),
              "script_files": count_files(path, "scripts"),
              "asset_files": count_files(path, "assets"),
              "subagent_files": count_files(path, "subagents"),
              "self_test_exists": (path / "SELF-TEST.md").exists(),
              "symlinks": sum(1 for p in path.rglob("*") if p.is_symlink()),
              "executable_scripts": executable_scripts,
          }
      
      
      def score_trigger(description: str) -> tuple[int, str]:
          if not description:
              return 0, "Description missing."
          lowered = description.lower()
          if any(term in lowered for term in ("not for", "do not use", "not when", "only when")):
              return 3, "Description contains a literal false-positive boundary phrase."
          if "triggers:" in lowered or "use when" in lowered:
              return 2, "Description contains a literal trigger marker."
          return 1, "Description is present without a literal trigger or boundary marker."
      
      
      def score_kernel(metrics: dict) -> tuple[int, str]:
          lines = metrics["skill_md_lines"]
          headings = metrics["headings"]
          if lines <= 220 and headings >= 3:
              score = 3
          elif lines <= 500 and headings >= 2:
              score = 2
          elif lines <= 800:
              score = 1
          else:
              score = 0
          return score, f"SKILL.md has {lines} lines and {headings} headings."
      
      
      def score_progressive_disclosure(metrics: dict) -> tuple[int, str]:
          reference_files = metrics["reference_files"]
          reference_links = metrics["reference_links"]
          if not reference_files and metrics["skill_md_lines"] <= 100:
              return 2, "SKILL.md is at most 100 lines with no reference files; loading semantics are not evaluated."
          score = min(3, (1 if reference_files else 0) + min(2, reference_links))
          return score, f"{reference_files} reference files, {reference_links} direct reference links."
      
      
      def score_helper_scripts(path: Path, metrics: dict) -> tuple[int, str]:
          script_files = metrics["script_files"]
          if not script_files:
              return 1, "No helper scripts are visible; necessity is not inferred."
          recognized_helper = has_named_script(path, ("validate", "check", "audit", "score", "doctor"))
          score = 2 if recognized_helper else 1
          if script_files >= 2 and score == 2:
              score = 3
          return score, f"{script_files} script files; recognized helper name={int(recognized_helper)}."
      
      
      def score_validation(path: Path, body: str, metrics: dict) -> tuple[int, str]:
          validation_terms = ("validate", "test", "check", "lint", "verify", "heal.sh")
          keyword_signal = int(any(term in body.lower() for term in validation_terms))
          named_helper = int(has_named_script(path, ("validate", "check", "test", "audit")))
          self_test = int(metrics["self_test_exists"])
          score = keyword_signal + named_helper + self_test
          note = (
              f"keyword signal={keyword_signal}, recognized helper={named_helper}, "
              f"SELF-TEST.md={self_test}."
          )
          return min(3, score), note
      
      
      def score_self_test(path: Path, metrics: dict) -> tuple[int, str]:
          if not metrics["self_test_exists"]:
              if any(path.rglob("*.feature")):
                  return 2, "At least one .feature file is present."
              return 1, "No focused self-test or feature artifact is visible."
          self_test = (path / "SELF-TEST.md").read_text(encoding="utf-8").lower()
          score = min(
              3,
              1
              + int("trigger" in self_test)
              + int("non-trigger" in self_test or "failure" in self_test),
          )
          return score, "SELF-TEST.md present."
      
      
      def score_assets(path: Path, metrics: dict) -> tuple[int, str]:
          asset_files = metrics["asset_files"]
          if not asset_files:
              return 1, "No asset files are visible; necessity is not inferred."
          template_named = any(
              "template" in p.name.lower() for p in (path / "assets").rglob("*") if p.is_file()
          )
          score = 3 if template_named else 2
          return score, f"{asset_files} asset files; template-named file={int(template_named)}."
      
      
      def score_subagents(metrics: dict) -> tuple[int, str]:
          subagent_files = metrics["subagent_files"]
          if not subagent_files:
              return 1, "No subagent files are visible; necessity is not inferred."
          score = 2 if subagent_files < 3 else 3
          return score, f"{subagent_files} subagent files."
      
      
      def score_safety(body: str) -> tuple[int, str]:
          safety_terms = ("do not", "never", "forbidden", "non-goal", "scope", "clean-room", "auth")
          safety_hits = sum(term in body.lower() for term in safety_terms)
          return min(3, safety_hits), f"{safety_hits} safety boundary signals."
      
      
      def score_packaging(metrics: dict) -> tuple[int, str]:
          score = 0
          if metrics["total_files"] <= 50 and metrics["symlinks"] == 0:
              score += 2
          if metrics["script_files"] == 0 or metrics["executable_scripts"] > 0:
              score += 1
          note = (
              f"{metrics['total_files']} files, {metrics['symlinks']} symlinks, "
              f"{metrics['executable_scripts']} executable scripts."
          )
          return min(3, score), note
      
      
      def add_score(
          scores: dict[str, int],
          notes: dict[str, str],
          category: str,
          result: tuple[int, str],
      ) -> None:
          scores[category], notes[category] = result
      
      
      def readiness_rating(total: int) -> str:
          """Map a 0-30 static package-readiness score to its advisory band."""
          if total >= 27:
              return "S"
          if total >= 21:
              return "A"
          if total >= 11:
              return "B"
          return "C"
      
      
      def score_skill(path: Path) -> dict:
          skill_md = path / "SKILL.md"
          if not skill_md.exists():
              raise SystemExit(f"SKILL.md not found: {skill_md}")
      
          text = skill_md.read_text(encoding="utf-8")
          fm = frontmatter(text)
          body = re.sub(r"^---\n.*?\n---\n?", "", text, flags=re.S)
          metrics = collect_metrics(path, text)
      
          scores: dict[str, int] = {}
          notes: dict[str, str] = {}
      
          add_score(scores, notes, "trigger_quality", score_trigger(fm.get("description", "")))
          add_score(scores, notes, "kernel_clarity", score_kernel(metrics))
          add_score(scores, notes, "progressive_disclosure", score_progressive_disclosure(metrics))
          add_score(scores, notes, "helper_scripts", score_helper_scripts(path, metrics))
          add_score(scores, notes, "validation", score_validation(path, body, metrics))
          add_score(scores, notes, "self_test", score_self_test(path, metrics))
          add_score(scores, notes, "assets_templates", score_assets(path, metrics))
          add_score(scores, notes, "subagents_roles", score_subagents(metrics))
          add_score(scores, notes, "safety_boundaries", score_safety(body))
          add_score(scores, notes, "packaging", score_packaging(metrics))
      
          total = sum(scores.values())
          rating = readiness_rating(total)
      
          gaps = [
              {"category": category, "score": scores[category], "note": notes[category]}
              for category in CATEGORIES
              if scores[category] < 2
          ]
      
          return {
              "skill": str(path),
              "name": path.name,
              "scope": "static-package-readiness",
              "safety_gate_evaluated": False,
              "effectiveness_evaluated": False,
              "total_score": total,
              "max_score": 30,
              "rating": rating,
              "scores": scores,
              "notes": notes,
              "categories": [
                  {"category": category, "score": scores[category], "reason": notes[category]}
                  for category in CATEGORIES
              ],
              "gaps": gaps,
              "metrics": {
                  "total_files": metrics["total_files"],
                  "skill_md_lines": metrics["skill_md_lines"],
                  "reference_files": metrics["reference_files"],
                  "script_files": metrics["script_files"],
                  "asset_files": metrics["asset_files"],
                  "subagent_files": metrics["subagent_files"],
                  "self_test_exists": metrics["self_test_exists"],
                  "symlinks": metrics["symlinks"],
                  "executable_scripts": metrics["executable_scripts"],
              },
          }
      
      
      def audit_block(report: dict) -> dict:
          """Compact static-readiness object for the deep audit report (Pass 3).
      
          Mirrors the rubric schema block: per-category 0-3 score plus an explainable
          reason, the 0-30 total, max, and the C/B/A/S readiness band. It is derived
          only from directory contents and cannot evaluate safety or effectiveness.
          """
          return {
              "scope": report["scope"],
              "safety_gate_evaluated": report["safety_gate_evaluated"],
              "effectiveness_evaluated": report["effectiveness_evaluated"],
              "total_score": report["total_score"],
              "max_score": report["max_score"],
              "rating": report["rating"],
              "advisory": True,
              "categories": report["categories"],
          }
      
      
      def markdown_report(report: dict) -> str:
          lines = [
              f"# Static Skill Package Readiness: {report['name']}",
              "",
              f"Static score: {report['total_score']}/{report['max_score']} ({report['rating']})",
              "",
              "This score does not evaluate the safety gate or behavioral effectiveness.",
              "",
              "## Category Scores",
              "",
              "| Category | Score | Note |",
              "|---|---:|---|",
          ]
          for category in CATEGORIES:
              lines.append(
                  f"| `{category}` | {report['scores'][category]} | {report['notes'][category]} |"
              )
          lines.extend(["", "## Highest Leverage Gaps", ""])
          if report["gaps"]:
              for gap in report["gaps"]:
                  lines.append(f"- `{gap['category']}` ({gap['score']}): {gap['note']}")
          else:
              lines.append("- No category scored below 2.")
          lines.extend(["", "## Metrics", "", "```json", json.dumps(report["metrics"], indent=2), "```"])
          return "\n".join(lines)
      
      
      def main() -> int:
          parser = argparse.ArgumentParser()
          parser.add_argument("skill_path")
          group = parser.add_mutually_exclusive_group()
          group.add_argument("--markdown", action="store_true", help="Emit a markdown report.")
          group.add_argument(
              "--audit-block",
              action="store_true",
              help="Emit the compact rubric block consumed by the skill-builder deep audit Pass 3.",
          )
          args = parser.parse_args()
      
          report = score_skill(Path(args.skill_path).expanduser().resolve())
          if args.markdown:
              print(markdown_report(report))
          elif args.audit_block:
              print(json.dumps(audit_block(report), indent=2))
          else:
              print(json.dumps(report, indent=2))
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • test-authoring-mutations.sh 3.3 KB
      #!/usr/bin/env bash
      # test-authoring-mutations.sh — proves the advisory authoring scanner detects
      # prose degradation (references/authoring-doctrine.md failure modes).
      #
      # Baseline: a doctrine-clean fixture yields zero authoring findings. Each
      # mutation introduces exactly one failure mode and MUST surface the
      # corresponding named finding.
      #
      # Single documented command:
      #   bash skills/skill-builder/scripts/test-authoring-mutations.sh
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      AUTHORING_PY="$SCRIPT_DIR/authoring_scan.py"
      FIX="$(cd "$(mktemp -d)" && pwd -P)"
      trap 'rm -rf "$FIX"' EXIT
      
      fail() { echo "test-authoring-mutations: FAIL — $1" >&2; exit 1; }
      
      count_of() { # count_of <dir> <finding-id>
        python3 "$AUTHORING_PY" "$1" \
          | python3 -c "import json,sys; print(json.load(sys.stdin)['counts']['$2'])"
      }
      
      total_of() {
        python3 "$AUTHORING_PY" "$1" \
          | python3 -c "import json,sys; print(len(json.load(sys.stdin)['findings']))"
      }
      
      # --- Baseline fixture: doctrine-clean ---------------------------------------
      BASE="$FIX/base"
      mkdir -p "$BASE"
      cat >"$BASE/SKILL.md" <<'EOF'
      ---
      name: base
      description: 'Refine a draft. Triggers: "refine draft".'
      ---
      # base
      
      Read every reference file before editing. Edit the source and regenerate;
      never edit generated files directly.
      
      ## Workflow
      
      ### Gather
      
      Collect the inputs. Done when every input path resolves.
      
      ### Apply
      
      Make the edits. Done when the checker exits 0.
      EOF
      
      (( $(total_of "$BASE") == 0 )) \
        || fail "baseline fixture unexpectedly has authoring findings"
      
      # --- Mutation 1: introduce a no-op phrase -----------------------------------
      MUT1="$FIX/mut1"
      mkdir -p "$MUT1"
      sed 's/Read every reference file before editing\./Be thorough when editing./' \
        "$BASE/SKILL.md" >"$MUT1/SKILL.md"
      (( $(count_of "$MUT1" noop-phrase) >= 1 )) \
        || fail "no-op phrase mutation not surfaced as noop-phrase"
      
      # --- Mutation 2: strip the positive counterpart from a prohibition ----------
      MUT2="$FIX/mut2"
      mkdir -p "$MUT2"
      python3 - "$BASE/SKILL.md" "$MUT2/SKILL.md" <<'PY'
      import sys
      text = open(sys.argv[1]).read()
      text = text.replace(
          "Read every reference file before editing. Edit the source and regenerate;\nnever edit generated files directly.",
          "Never edit generated files.",
      )
      open(sys.argv[2], "w").write(text)
      PY
      (( $(count_of "$MUT2" negation-without-positive) >= 1 )) \
        || fail "bare prohibition mutation not surfaced as negation-without-positive"
      
      # --- Mutation 3: strip a done condition from a workflow subphase ------------
      MUT3="$FIX/mut3"
      mkdir -p "$MUT3"
      sed 's/Collect the inputs\. Done when every input path resolves\./Collect the inputs./' \
        "$BASE/SKILL.md" >"$MUT3/SKILL.md"
      (( $(count_of "$MUT3" step-missing-done-condition) == 1 )) \
        || fail "stripped done condition not surfaced as step-missing-done-condition"
      
      # --- Clearing direction: adding the done condition back clears the finding --
      MUT4="$FIX/mut4"
      mkdir -p "$MUT4"
      sed 's/Collect the inputs\./Collect the inputs. Done when every input path resolves./' \
        "$MUT3/SKILL.md" >"$MUT4/SKILL.md"
      (( $(count_of "$MUT4" step-missing-done-condition) == 0 )) \
        || fail "restored done condition did not clear the finding"
      
      echo "authoring mutation detection: PASS (baseline 0 findings; noop, negation, done-condition mutations each surfaced; restore clears)"
      
    • test-craft-mutations.sh 3.2 KB
      #!/usr/bin/env bash
      # test-craft-mutations.sh — proves the advisory craft scorer detects degradation.
      #
      # Baseline: a craft-rich fixture scores N/12. Mutations that strip a stop
      # condition or an anti-pattern corrective MUST drop the score and name the
      # lost element as a gap. Runs independently of test-mutation-boundaries.sh
      # (which has a known pre-existing failure at its first assertion, tracked
      # separately; do not conflate the two).
      #
      # Single documented command:
      #   bash skills/skill-builder/scripts/test-craft-mutations.sh
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
      CRAFT_PY="$SCRIPT_DIR/craft_score.py"
      FIX="$(cd "$(mktemp -d)" && pwd -P)"
      trap 'rm -rf "$FIX"' EXIT
      
      fail() { echo "test-craft-mutations: FAIL — $1" >&2; exit 1; }
      
      score_of() {
        python3 "$CRAFT_PY" "$1" --repo-root "$REPO_ROOT" \
          | python3 -c 'import json,sys; print(json.load(sys.stdin)["score"])'
      }
      
      missing_of() {
        python3 "$CRAFT_PY" "$1" --repo-root "$REPO_ROOT" \
          | python3 -c 'import json,sys; print(",".join(json.load(sys.stdin)["missing"]))'
      }
      
      # --- Baseline fixture: rich in the two elements under mutation -------------
      BASE="$FIX/base"
      mkdir -p "$BASE"
      cat >"$BASE/SKILL.md" <<'EOF'
      ---
      name: base
      description: 'Refine a draft. Triggers: "refine draft", "polish draft".'
      ---
      # base
      
      Insight: drafts converge because each pass removes one named defect.
      
      ## Refinement loop
      
      Repeat the review pass. Stop after at most 3 passes or when the checker
      exits 0, whichever comes first.
      
      Avoid rewriting the whole draft in one pass; instead change one section
      per pass.
      
      ## Failure behavior
      
      Fails when the checker never reaches exit 0 within the pass budget.
      EOF
      
      baseline="$(score_of "$BASE")"
      baseline_missing="$(missing_of "$BASE")"
      [[ "$baseline_missing" != *"named-loop-stop-condition"* ]] \
        || fail "baseline unexpectedly missing named-loop-stop-condition"
      [[ "$baseline_missing" != *"anti-pattern-with-corrective"* ]] \
        || fail "baseline unexpectedly missing anti-pattern-with-corrective"
      
      # --- Mutation 1: strip the stop condition ----------------------------------
      MUT1="$FIX/mut1"
      mkdir -p "$MUT1"
      sed -e 's/Stop after at most 3 passes or when the checker/Keep going until it feels done./' \
          -e '/^exits 0, whichever comes first\.$/d' \
          "$BASE/SKILL.md" >"$MUT1/SKILL.md"
      mut1="$(score_of "$MUT1")"
      (( mut1 < baseline )) \
        || fail "stripping the stop condition did not drop the score (baseline=$baseline mutated=$mut1)"
      [[ "$(missing_of "$MUT1")" == *"named-loop-stop-condition"* ]] \
        || fail "stop-condition mutation not named as a gap"
      
      # --- Mutation 2: strip the anti-pattern corrective --------------------------
      MUT2="$FIX/mut2"
      mkdir -p "$MUT2"
      sed -e '/^Avoid rewriting the whole draft in one pass; instead change one section$/d' \
          -e '/^per pass\.$/d' \
          "$BASE/SKILL.md" >"$MUT2/SKILL.md"
      mut2="$(score_of "$MUT2")"
      (( mut2 < baseline )) \
        || fail "stripping the anti-pattern corrective did not drop the score (baseline=$baseline mutated=$mut2)"
      [[ "$(missing_of "$MUT2")" == *"anti-pattern-with-corrective"* ]] \
        || fail "anti-pattern mutation not named as a gap"
      
      echo "craft mutation detection: PASS (baseline $baseline/12; stop-condition strip -> $mut1/12; anti-pattern strip -> $mut2/12)"
      
    • test-mutation-boundaries.sh 2.5 KB
      #!/usr/bin/env bash
      set -euo pipefail
      
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
      HEAL="$SCRIPT_DIR/heal.sh"
      AUDIT="$SCRIPT_DIR/audit.sh"
      FIX="$(cd "$(mktemp -d)" && pwd -P)"
      trap 'rm -rf "$FIX"' EXIT
      
      digest_tree() {
        find "$1" -type f -exec shasum -a 256 {} + | LC_ALL=C sort | shasum -a 256 | awk '{print $1}'
      }
      
      write_fixture_skill() {
        local path="$1" name="$2"
        mkdir -p "$path"
        printf '%s\n' '---' "name: $name" "description: Fixture $name." '---' "# $name" >"$path/SKILL.md"
      }
      
      expect_check_accepts() {
        local spelling="$1" rc
        set +e
        HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check "$spelling" >/dev/null 2>&1
        rc=$?
        set -e
        [[ "$rc" -eq 0 ]]
      }
      
      expect_rejected_unchanged() {
        local spelling="$1" before after rc
        before="$(digest_tree "$FIX")"
        set +e
        HEAL_REPO_ROOT="$FIX" bash "$HEAL" --fix "$spelling" >/dev/null 2>&1
        rc=$?
        set -e
        after="$(digest_tree "$FIX")"
        [[ "$rc" -eq 2 && "$before" == "$after" ]]
      }
      
      mkdir -p "$FIX/skills"
      write_fixture_skill "$FIX/skills/target" target
      write_fixture_skill "$FIX/skills/sibling" sibling
      
      sibling_before="$(shasum -a 256 "$FIX/skills/sibling/SKILL.md" | awk '{print $1}')"
      set +e
      HEAL_REPO_ROOT="$FIX" bash "$HEAL" --fix skills/target >/dev/null 2>&1
      fix_rc=$?
      set -e
      [[ "$fix_rc" -eq 1 ]]
      grep -q '^skill_api_version: 1$' "$FIX/skills/target/SKILL.md"
      if grep -q '^skill_api_version:' "$FIX/skills/sibling/SKILL.md"; then exit 1; fi
      [[ "$(shasum -a 256 "$FIX/skills/sibling/SKILL.md" | awk '{print $1}')" == "$sibling_before" ]]
      
      expect_check_accepts skills/target
      expect_check_accepts ./skills/target
      expect_check_accepts "$FIX/skills/target"
      
      write_fixture_skill "$FIX/outside" outside
      ln -s "$FIX/skills/target" "$FIX/skills/target-alias"
      ln -s "$FIX/outside" "$FIX/skills/outside-alias"
      ln -s "$FIX" "$FIX/repo-alias"
      expect_rejected_unchanged skills/target/../../outside
      expect_rejected_unchanged "$FIX/outside"
      expect_rejected_unchanged skills/target-alias
      expect_rejected_unchanged skills/outside-alias
      expect_rejected_unchanged "$FIX/repo-alias/skills/target"
      expect_rejected_unchanged skills/missing
      
      check_before="$(digest_tree "$FIX")"
      HEAL_REPO_ROOT="$FIX" bash "$HEAL" --check skills/sibling >/dev/null
      [[ "$(digest_tree "$FIX")" == "$check_before" ]]
      
      audit_before="$(digest_tree "$REPO_ROOT/skills")"
      bash "$AUDIT" "$REPO_ROOT/skills/skill-builder" >/dev/null 2>&1
      [[ "$(digest_tree "$REPO_ROOT/skills")" == "$audit_before" ]]
      
      echo "heal mutation boundaries: PASS"
      
    • validate.sh 1.8 KB
      #!/usr/bin/env bash
      set -euo pipefail
      SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
      REPO_ROOT="$(cd "$SKILL_DIR/../.." && pwd)"
      
      for path in \
        SKILL.md \
        scripts/build.sh \
        scripts/init.sh \
        scripts/heal.sh \
        scripts/audit.sh \
        scripts/score_agentops_skill.py \
        schemas/build-report.json \
        schemas/audit-report.json \
        references/audit-checks.md \
        references/codex-parity.md; do
        [[ -f "$SKILL_DIR/$path" ]] || {
          echo "skill-builder validate: missing $path" >&2
          exit 1
        }
      done
      
      for script in scripts/build.sh scripts/init.sh; do
        [[ -x "$SKILL_DIR/$script" ]] || {
          echo "skill-builder validate: not executable: $script" >&2
          exit 1
        }
      done
      
      bash -n "$SKILL_DIR/scripts/heal.sh" "$SKILL_DIR/scripts/audit.sh"
      bash "$SKILL_DIR/scripts/heal.sh" --check --strict "$SKILL_DIR"
      
      before="$(find "$SKILL_DIR" -type f -exec shasum -a 256 {} + | sort | shasum -a 256 | awk '{print $1}')"
      bash "$SKILL_DIR/scripts/heal.sh" --check "$SKILL_DIR" >/dev/null
      after="$(find "$SKILL_DIR" -type f -exec shasum -a 256 {} + | sort | shasum -a 256 | awk '{print $1}')"
      [[ "$before" == "$after" ]] || {
        echo "skill-builder validate: check mode mutated its target" >&2
        exit 1
      }
      
      if rg -n 'from-pattern|flywheel close-loop|append-skill-disposition' "$SKILL_DIR/SKILL.md" \
        || rg -n 'git (status|commit|push)|ao land|retry|queue|lease' \
          "$SKILL_DIR/scripts/build.sh" "$SKILL_DIR/scripts/init.sh"; then
        echo "skill-builder validate: obsolete lifecycle behavior remains" >&2
        exit 1
      fi
      
      if rg -n 'ao land|git (commit|push)|append-skill-disposition|flywheel close-loop' \
        "$SKILL_DIR/scripts/heal.sh" "$SKILL_DIR/scripts/audit.sh" \
        "$SKILL_DIR/scripts/score_agentops_skill.py"; then
        echo "skill-builder validate: lifecycle authority remains" >&2
        exit 1
      fi
      
      echo "skill-builder validate: PASS"
      
  • SKILL.md 7.2 KB
    ---
    name: skill-builder
    description: 'Create, adapt, consolidate or repair skill packages and projections. Use when: authoring guidance, descriptions or structure; Skill Eval measures behavioral benefit.'
    practices:
    - pragmatic-programmer
    - refactoring
    hexagonal_role: supporting
    consumes: []
    produces:
    - skill-source-package
    - skill-hygiene-report
    - converted-skill
    - operationalization-proposal
    context_rel: []
    skill_api_version: 1
    user-invocable: true
    context:
      window: fork
      intent:
        mode: questions
      sections:
        exclude:
        - HISTORY
    metadata:
      capabilities: [skill_builder, heal_skill, export_skill, distill_expertise]
      effects: [write_skill_source, write_build_report, regenerate_skill_projections, repair_skill_projections, write_converted_skill_projection, write_advisory_proposal]
      canonical_status: canonical
      disposition: keep_specialist
      tier: meta
      dependencies: []
      stability: experimental
    output_contract: build-report.json for creation, audit-report.json for audit, target-valid exported files for conversion, or an advisory expertise proposal
    ---
    # Skill Builder
    
    Create, repair, audit or export one canonical skill package, or turn supported
    expertise into a small authoring proposal. Search existing owners before adding
    a root. Extend the owner that already handles the behavior.
    
    ## Choose the requested operation
    
    | Need | Entry point |
    |---|---|
    | Create a source package | `scripts/build.sh` with `from-scratch`, `from-template` or `absorb-external` |
    | Check package structure | `scripts/heal.sh --check [--strict] skills/<slug>` |
    | Repair owned projections | `scripts/heal.sh --fix skills/<slug>` |
    | Audit authoring quality | `scripts/audit.sh [--strict] [--json <path>] skills/<slug>` |
    | Export to another platform | [Conversion](#conversion) |
    | Make repeated expertise reusable | [Distill expertise](#distill-expertise) |
    
    Run only the selected operation. Skills remain optional tools within the native
    caller's authorized outcome; this skill does not add execution phases, own work,
    operate Git, validate a software candidate, or decide delivery and retries.
    
    ## Create and maintain
    
    Treat external skills as structural signals only. Clean-room output must not
    copy their names, prose, prompts, scripts or examples. `from-template` reuses
    metadata defaults; `absorb-external <slug> --from <path>` verifies an input and
    creates a blank source package. Neither imports another skill's content.
    
    For creation, supply one input to `scripts/build.sh`, then replace placeholders
    with the actual behavior. The caller can supply `SKILL_TIER`,
    `SKILL_DEPENDENCIES`, `SKILL_CAPABILITIES` and `SKILL_EFFECTS`; lists are JSON
    arrays. The result is one source package with `SKILL.md` and
    `scripts/validate.sh`. The builder's report is
    `.agents/scratch/skill-builder/<slug>-build.json` under
    [build-report.json](schemas/build-report.json).
    
    Edit `skills/<slug>/` as the source owner. Check the completed source with
    `scripts/heal.sh --check --strict skills/<slug>`, then regenerate its owned
    projections through the repository's owning commands. `scripts/regen-all.sh`
    is the integrated projection recipe; `scripts/generate-skill-mesh.py`,
    `scripts/codex-sync.sh --only <slug>` and
    `scripts/regen-codex-hashes.sh --only <slug>` are the existing scoped surfaces.
    Do not repeat work already performed by `build.sh` unless source changes
    require it. Inspect the generated diff; hand-edit no projection.
    
    Check/heal targets must be real direct children of `skills/`; reject missing
    paths, traversal and symlink spellings. Check mode is read-only. Fix mode
    regenerates owned projections for explicit targets and does not invent source
    behavior. Findings name their code, target and concrete issue; `--strict`
    returns nonzero for findings. Check the slug/name match, description, API
    version, metadata, live dependencies and linked resources.
    
    Deep audit reports structural and advisory authoring findings; it is not a
    candidate verdict. Its optional JSON follows
    [audit-report.json](schemas/audit-report.json). Interpret static scores as
    structure and authoring signals, not proof that a skill works. Exact checks live
    in [audit checks](references/audit-checks.md),
    [authoring doctrine](references/authoring-doctrine.md), and
    [Codex parity](references/codex-parity.md).
    
    ## Conversion
    
    Use `bash skills/skill-builder/scripts/converter/convert.sh <skill-dir> <target>
    [output-dir]` for an explicit out-of-tree export. Targets are `codex`, `cursor`
    and `test`; `--all` selects all source packages, and `--codex-layout inline`
    selects the legacy inline Codex layout. Read
    [SkillBundle](references/converter/skill-bundle-schema.md) when format details
    matter. Parse the source once, render the target, then validate resource parity
    and target format. Report layout and any omitted Cursor references.
    
    The default export is `.agents/projections/converter/<target>/<skill-name>/`.
    The exporter clean-writes its output directory, so use only the explicit derived
    target: refuse a source package, its ancestor, or the repository root. Preserve
    the source unchanged and fix the source or adapter instead of editing output.
    A parse, write, format or required-resource failure leaves an incomplete export.
    The shipped `skills-codex/**` remains owned by `scripts/codex-sync.sh` through
    `scripts/regen-all.sh`; this ad-hoc exporter never replaces that authority.
    
    ## Distill expertise
    
    When the caller wants a reusable rule, begin with cited occurrences or a named
    authoritative source. State the trigger, desired behavior, inputs, outputs,
    negative example and limits. Prefer an addition to an existing reference or
    skill over a new root, library, gate or workflow; no action is a valid result.
    
    An abstraction needs three independently evidenced real occurrences and a
    successful reapplication to a source case without missing context. Preserve
    short source excerpts or command results with resolvable citations. Fewer
    occurrences support a narrow reference note; an authoritative source substitutes
    only for a faithful statement of that source, not a wider generalization.
    Use Research's [pattern mode](../research/SKILL.md#pattern-evidence) when the
    claim needs exemplars and a holdout before packaging.
    
    A proposed process artifact must have a concrete consumer, a subject or release
    decision it informs, an observed defect and a retirement condition. If any is
    missing, omit the artifact. Code written only to consume it supplies no consumer.
    Minimal recovery state needs a named evidence-loss or corruption risk. Show a
    negative/holdout case and how the proposed rule returns the right decision.
    
    Return the proposal inline unless a durable proposal was requested. Respect
    [Memory's source and destination rules](../memory/SKILL.md) for mined material.
    Evidence cannot publish itself as policy. Build an artifact only when the
    caller's authorization includes adoption; a proposal-only request ends with the
    proposal. Repair ordinary known defects within existing authority; tool failures
    remain explicit facts for the native caller, not an automatic helper chain.
    
    For an actual package edit, use the [source template](references/skill-template.md)
    for required fields and [context density guidance](references/context-density-checks.md)
    when deciding which prose earns a place. Neither requires adding a new skill.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related