Claude Skill

skf-create-skill

Compile a skill from a brief. Supports --batch for multiple briefs. Use when the user requests to "create a skill" or "compile a skill."

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

Full trust report

Download armelhbobdad-bmad-module-skill-forge-src_skf-create-skill-492e73e.zip · 132 KB
Part of armelhbobdad/bmad-module-skill-forge — 15 skills

Install

skills CLI npx skills add https://github.com/armelhbobdad/bmad-module-skill-forge/tree/main/src/skf-create-skill
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install armelhbobdad-bmad-module-skill-forge@llmmart
Git git clone https://github.com/armelhbobdad/bmad-module-skill-forge.git

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

Skill manifest

Create Skill

Overview

Compiles a verified agent skill from a skill-brief.yaml and source code, producing an agentskills.io-compliant SKILL.md with provenance map, evidence report, and progressive disclosure references. The workflow is mostly autonomous with three interaction points — after ecosystem check (if match found), after source extraction (to confirm findings), and after content quality review (when tessl produces suggestions). Steps adapt behavior based on forge tier (Quick/Forge/Forge+/Deep). Zero hallucination tolerance: every instruction in the output must trace to source code with a confidence tier citation. A single run is not resumable — if it is interrupted mid-compile, re-run from the brief (only --batch checkpoints progress across briefs).

Conventions

  • Bare paths (e.g. references/<name>.md) resolve from the skill root.
  • references/ holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter nextStepFile, plus static reference docs); scripts/ and assets/ hold deterministic helpers and templates.
  • {skill-root} resolves to this skill's installed directory (where customize.toml lives, if present).
  • {project-root}-prefixed paths resolve from the project working directory.
  • {skill-name} resolves to the skill directory's basename.

Role

You are operating in Ferris Architect mode — a skill compilation engine performing structural extraction and assembly. Apply zero hallucination tolerance: uncitable content is excluded, not guessed.

Workflow Rules

These rules apply to every step in this workflow:

  • Never include content in SKILL.md that cannot be cited to source code
  • Only load one step file at a time — never preload future steps
  • Always communicate in {communication_language}
  • If {headless_mode} is true, auto-proceed through confirmation gates with their default action, logging each auto-decision to the in-context headless_decisions[] buffer AND appending it as a JSON line to the on-disk auto-decision sink (established at step 1 §3) the moment it lands, so the audit trail survives context compaction before step 5 first writes the evidence report

Stages

# Step File Auto-proceed
1 Load Brief references/load-brief.md Yes
2 Ecosystem Check references/ecosystem-check.md Conditional
2b CCC Discover references/sub/ccc-discover.md Yes
3 Extract references/extract.md No (confirm)
3b Fetch Temporal references/sub/fetch-temporal.md Yes
3c Fetch Docs references/sub/fetch-docs.md Yes
3d Component Extraction references/component-extraction.md Conditional
4 Enrich references/enrich.md Yes
5 Compile references/compile.md Yes
5a Doc Sources references/step-doc-sources.md Yes
5b Auto-Shard references/step-auto-shard.md Yes
5c Doc-Rot references/step-doc-rot.md Yes
6 Validate references/validate.md Conditional
7 Generate Artifacts references/generate-artifacts.md Yes
8 Report references/report.md Yes
9 Workflow Health Check references/health-check.md Yes

Sub-steps under references/sub/ are conditional branches (CCC discovery, temporal/doc enrichment) kept out of the top-level step count so main-line steps 1–9 drive the workflow. Step 3d (Component Extraction) stays top-level as an alternative main step that replaces the standard extraction path when scope.type: "component-library".

Invocation Contract

Aspect Detail
Inputs brief_path (path to skill-brief.yaml) [required], --batch [optional]
Gates step 2: Choice Gate [P] (if match) step 3: Review Gate [C] step 6: Content-Quality Gate [C] (if novel tessl suggestions)
Outputs SKILL.md, context-snippet.md, metadata.json, provenance-map.json, evidence-report.md, references/
Headless All gates auto-resolve with default action when {headless_mode} is true

On Activation

  1. Load config from {project-root}/_bmad/skf/config.yaml and resolve:

    • output_folder, user_name, communication_language, document_output_language, sidecar_path, skills_output_folder, forge_data_folder
  2. Resolve {headless_mode}: true if --headless or -H was passed as an argument, or if headless_mode: true in preferences.yaml. Default: false.

  3. Resolve workflow customization. Run:

    python3 {project-root}/_bmad/scripts/resolve_customization.py \
        --skill {skill-root} --key workflow
    

    The script merges the three customization layers per bmad-customize's structural merge rules (scalars override, arrays append): {skill-root}/customize.toml (bundled defaults), _bmad/custom/skf-create-skill.toml under {project-root} (team overrides, committed), and _bmad/custom/skf-create-skill.user.toml under {project-root} (personal overrides, gitignored). If the script fails or is missing, fall back to reading {skill-root}/customize.toml directly.

    Apply the resolved values so the surface is not a silent no-op: execute each entry in workflow.activation_steps_prepend in order now; treat every entry in workflow.persistent_facts as standing context for the whole run (entries prefixed file: are paths or globs whose contents load as facts); and stash {onCompleteCommand} ← workflow.on_complete (empty string = no-op) for the final stage to invoke after the result JSON and metadata.json are finalized. After activation completes, execute each entry in workflow.activation_steps_append in order.

  4. Load, read the full file, and then execute references/load-brief.md to begin the workflow.

Files (bmad-module-skill-forge)
  • assets
    • compile-assembly-rules.md 25.1 KB
      # SKILL.md Compilation Assembly Rules
      
      ## Frontmatter (agentskills.io compliance)
      
      ```yaml
      ---
      name: {brief.name}
      description: >
        {Trigger-optimized description from brief and extraction data.
        Include what it does, when to use it, and what NOT to use it for.
        1-1024 chars, optimized for agent discovery.}
      ---
      ```
      
      **Frontmatter rules:**
      
      - `name`: lowercase alphanumeric + hyphens only, must match the skill output directory name. Prefer gerund form (`processing-pdfs`, `analyzing-spreadsheets`) for clarity.
      - `description`: non-empty, max 1024 chars, optimized for agent discovery. Use third-person voice ("Processes Excel files..." not "I can help you..." or "You can use this to...") and include a trigger phrase so agents know when to match. **The description must contain a literal `Use when` clause somewhere** — `skill-check`'s `description.use_when_phrase` rule tests for that exact phrase anywhere in the string, so `Triggers on …` or `Reach for this when …` on their own score below 100 with a `description should contain "Use when" phrasing` warning. The lead is still free: a descriptive opener followed by a `Use when …` clause (`Charts and visualizations powered by D3.js. Use when plotting data …`) passes cleanly, and the other two phrases are fine as *additional* text. When using `Use when `, follow with a **gerund** (`Use when building/processing/analyzing X…`) or a noun-phrase clause (`Use when the user requests to "X"`); never a bare indicative verb like `builds`/`processes`, because `skill-check --fix` will prepend a literal `Use when ` to a description missing the trigger phrase, producing ungrammatical output (`Use when builds X…`). Inconsistent point-of-view or a missing trigger phrase causes discovery problems since the description is injected into the system prompt. See `skf-brief-skill/assets/description-voice-examples.md` for the canonical voice palette.
      - **The `description` cannot contain angle brackets** — neither standalone placeholders like `<name>`, `<component>`, `<path>`, nor inline generics like `` `Array<T>` `` or `` `Meta<typeof X>` ``. Both `skill-check`'s `description_field` validator and `tessl`'s deterministic description check parse the frontmatter description as a raw string and reject any `<` or `>`, regardless of markdown context (backticks do not protect content here). A rejected description fails the review with 0% score. When the natural phrasing would use angle brackets:
        - Prefer the curly-brace form in prose: `{name}`, `{component-id}`, `{path}` — readable and tessl-safe.
        - Uppercase placeholders are also acceptable: `NAME`, `COMPONENT_ID`.
        - For code-ish fragments, use curly braces in place of angle brackets inside the backticks: `` `Meta{typeof X}` ``, `` `Array{T}` ``.
      
        Authors should not rely on remembering this — step 5 §2a enforces it via a pre-write sanitization pass that unconditionally replaces every `<` with `{` and every `>` with `}` in the frontmatter `description`. See step 5 §2a for the rule and `assets/tessl-dismissal-rules.md` (`description-xml-tags-guarded-upstream`) for the recovery path if a downstream tool rewrites the description after §2a has run.
      - Only `name` and `description` in frontmatter — `version` and `author` go in metadata.json
      - No other frontmatter fields for standard skills (only `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools` are permitted by spec)
      
      ## Two-Tier Assembly
      
      **Two-tier assembly.** SKILL.md must retain actionable inline content that survives `split-body` extraction. Assemble Tier 1 sections first (always inline), then Tier 2 sections (reference-eligible, may be extracted by split-body).
      
      ### Tier 1 — Always Inline (must survive split-body)
      
      These sections form the essential standalone body. Target: **under 300 lines total** for Tier 1. An agent loading only SKILL.md (without references) must get enough to act.
      
      **Section 1 — Overview (~10 lines):**
      - 1-line summary of what the library does
      - Source repo, version, branch
      - Forge tier used for compilation
      - Export count and confidence summary
      
      **Section 2 — Quick Start (~30 lines):**
      - Select the 3-5 most commonly used functions (by import frequency or documentation prominence)
      - One runnable code example showing a typical end-to-end flow (e.g., `add → process → search`)
      - Minimal usage examples — ONLY from source tests or official docs
      - If no examples exist in source, show signature-only quick start
      - Provenance citation for each function
      
      **Section 3 — Common Workflows (~30 lines):**
      - 4-5 patterns showing typical function call sequences
      - Each pattern: 1-line bold description + function call chain with key params
      - Focus on the most common developer tasks, not exhaustive coverage
      - Format example:
        ```
        **Add and process data:**
        `await cognee.add(data) → await cognee.cognify() → await cognee.search(query)`
        ```
      
      **Section 4 — Key API Summary (~20 lines):**
      - Table of top 10-15 functions: name, purpose, key parameters
      - One row per function — no full signatures, just enough for discovery
      - Provenance citation per function
      
      **Section 4b — Migration & Deprecation Warnings (~10 lines, Deep tier only):**
      - Emit the heading verbatim as `## Migration & Deprecation Warnings`. Downstream gates match that exact text, so a naturally-retitled section (`## Deprecations & pre-1.0 hazards`, `## v2 → v3 breaking changes`) is reported as *missing* while its bullets are re-emitted as duplicate `## CORRECTION` blocks.
      - Only populated when step 4 enrichment produced **T2-future** annotations (deprecation warnings, breaking changes, planned renames)
      - List each warning as a single-line bullet: function name, what changed or will change, source citation
      - Max 10 lines — just the actionable warnings, not full context
      - Link to Tier 2 Full API Reference for details: "See Full API Reference for migration details."
      - **Skip entirely** for Quick/Forge tiers or when no T2-future annotations exist — do not emit an empty section
      - This section survives split-body, ensuring agents always see critical migration context
      
      **Section 5 — Key Types (~20 lines):**
      - Most important enum/type definitions inline (e.g., SearchType values, config options)
      - Only types that appear in Quick Start or Common Workflows
      - Full type details go in Tier 2
      
      **Section 6 — Architecture at a Glance (~10 lines):**
      - Bullet list of major subsystem categories (e.g., "Graph DBs: Neo4j, Kuzu, Neptune")
      - Adapter/driver overview — what's available, not how it works
      - Skip for Quick tier or small libraries with < 5 modules
      
      **Section 7 — CLI (~10 lines, if applicable):**
      - Basic CLI commands if the library has a CLI interface
      - Skip if no CLI exists
      
      **Section 7b — Scripts & Assets (~10 lines, if applicable):**
      - Manifest table of included scripts with filename, one-line purpose, and provenance citation
      - Manifest table of included assets with filename, one-line purpose, and provenance citation
      - Each entry: `scripts/{filename}` or `assets/{filename}`, purpose, `[SRC:{source_path}:L1]`
      - Include a note: "Load scripts from `scripts/` and assets from `assets/` when directed by the instructions above."
      - **Skip entirely** when no scripts or assets detected in extraction inventory — do not emit an empty section
      - Like Sections 4b and 7, parsers must treat this section as optional
      
      **Section 8 — Manual Sections:**
      - Seed empty `<!-- [MANUAL] -->` markers:
      ```markdown
      <!-- [MANUAL:additional-notes] -->
      <!-- Add custom notes here. This section is preserved during skill updates. -->
      <!-- [/MANUAL:additional-notes] -->
      ```
      - Place after Quick Start and after Key API Summary sections
      
      ### Tier 2 — Reference-Eligible (can be extracted by split-body)
      
      Assemble Sections 9-11 (Full API Reference, Full Type Definitions, Full Integration Patterns) as defined in the skill-sections data file. These contain full detail and are split into `references/` when the body exceeds 500 lines. Include T2 annotations from enrichment in the Full API Reference (Deep tier only).
      
      **Tier 2 differentiation from Tier 1:** Tier 2 Full API Reference must contain content that is not present in Tier 1's Key API Summary. Specifically:
      
      - **Full parameter tables** with types, defaults, and required/optional flags (Tier 1 only lists key params)
      - **Return value details** including structure, types, and error conditions
      - **T2 temporal annotations** — migration notes, deprecation details, breaking change context (Deep tier)
      - **Usage examples** from source tests or documentation (Tier 1 has signature-only references)
      - **Edge cases and constraints** — parameter validation rules, size limits, behavioral notes
      
      Do not repeat Tier 1's name/purpose/key-params table format in Tier 2. Tier 2 is a deep reference, not a reformatted summary. This distinction prevents conciseness scorers from flagging the two-tier design as redundancy.
      
      ### Component Library Assembly Overrides
      
      When `scope.type: "component-library"` in the brief AND `component_catalog[]` is available in context, apply these overrides to the standard assembly. All other sections remain unchanged.
      
      **Section 2 (Quick Start) — CLI-first override:**
      
      Replace the standard function-based Quick Start with CLI installation:
      
      ```markdown
      ## Quick Start
      
      Install a component:
      `npx {cli-name} add {top-component-id}`
      
      {If provider wrapping detected in source:}
      Set up providers:
      \`\`\`tsx
      import { ThemeProvider, UILibraryProvider } from "{primary-package}";
      
      <ThemeProvider>
        <UILibraryProvider>
          <YourApp />
        </UILibraryProvider>
      </ThemeProvider>
      \`\`\`
      ```
      
      Detect CLI name from: `package.json` `bin` field, README usage examples, or `registry_path` context. If no CLI detected, fall back to standard import-based Quick Start.
      
      **Section 4 (Key API Summary) — Component Catalog override:**
      
      Replace the function table with a Component Catalog organized by category:
      
      ```markdown
      ## Component Catalog
      
      | Category | Count | Key Components |
      |----------|-------|---------------|
      | {category} | {count} | {top 3-5 component names, comma-separated} |
      
      **Design system variants:** {variant list with primary marked}
      **Total components:** {unique count} | {Per variant: **With {name}:** {count}}
      ```
      
      Source: `component_catalog[]` from step 3d. Group by `category` field. Provenance: cite the registry file.
      
      **Section 5 (Key Types) — Props-focused override:**
      
      Replace generic types with the top 5 most-used Props interfaces (by component count or prominence):
      
      ```markdown
      ## Key Types
      
      ### {ComponentName}Props
      | Prop | Type | Default | Required |
      |------|------|---------|----------|
      | {prop} | {type} | {default or —} | {yes/no} |
      ```
      
      Show only the 5 most important Props interfaces inline. Full Props details go in Tier 2.
      
      **Tier 2 (Full API Reference) — Props Reference override:**
      
      Organize by component (not by function). Per component:
      
      ```markdown
      ### {ComponentName}
      
      **Install:** `npx {cli} add {component-id}`
      **Available in:** {variant list}
      **Props:** `{ComponentName}Props`
      
      | Prop | Type | Default | Description |
      |------|------|---------|-------------|
      | {prop} | {type} | {default} | {JSDoc description or —} |
      ```
      
      **context-snippet.md — Component Library format:**
      
      ```markdown
      [{name} v{version}]|root: skills/{name}/
      |IMPORTANT: {name} v{version} — read SKILL.md before writing {name} code. Do NOT rely on training data.
      |install: npx {cli} add <component-id>
      |catalog:{SKILL.md#component-catalog} — {N} components: {category(count), ...}
      |variants: {variant list} — {provider wrapping note if applicable}
      |key-props:{SKILL.md#key-types} — {top props interfaces with key fields}
      |gotchas: {detected gotchas}
      ```
      
      **metadata.json — Component Library stats:**
      
      When `scope.type: "component-library"`, add these fields to `stats`:
      
      ```json
      {
        "stats": {
          "components_registered": 0,
          "components_documented": 0,
          "props_interfaces_extracted": 0,
          "components_unique": 0,
          "demo_files_excluded": 0,
          "design_variants": {}
        }
      }
      ```
      
      These are in addition to the standard stats fields (exports_documented, etc.).
      
      ### Reference-App Assembly Overrides
      
      When `scope.type: "reference-app"` in the brief, apply these overrides to the standard assembly. The skill's value is an integration pattern (wiring), not a library surface — the default library-export template would force assemblers to remap wiring onto export slots and produce fuzzy counts. These overrides give that value a first-class home.
      
      **Section 4 (Key API Summary) — Pattern Surface override:**
      
      Replace the per-function table with an ordered list of wiring steps. Each step names a file, a surface (decorator, build field, lifecycle hook, config key), and a one-line description of what the user does there — not a function signature. The goal is that a reader copying the pattern can follow the list in order.
      
      ```markdown
      ## Pattern Surface
      
      | # | File | Surface | Purpose |
      |---|------|---------|---------|
      | 1 | {src/main.py} | {@app.startup decorator} | {brief wiring purpose} |
      | 2 | {electron.vite.config.ts} | {build.copy field} | {brief wiring purpose} |
      | … | … | … | … |
      ```
      
      Source: the authored pattern surface from extraction + brief `scope.tier_a_include` (when present) or the curated provenance-map entries (when absent). Provenance: cite the originating source files.
      
      **Section 3 (Common Workflows) — Adoption Steps override:**
      
      Replace the function-call-chain format with copy-this-wiring narrative. Prefer numbered imperative steps that the user can execute in order, not an API-call sequence. Keep each step's code snippet minimal (5–15 lines) — full wiring lives in Tier 2.
      
      ```markdown
      ## Adoption Steps
      
      1. **{Step name}** — {one-line description}
         ```{language}
         {minimal wiring snippet}
         ```
      2. **{Step name}** — {one-line description}
         …
      ```
      
      **Tier 2 (Full API Reference) — pattern-oriented override:**
      
      Replace per-function subsections with `references/pattern-*.md` groupings: one reference file per coherent wiring concern (e.g. `pattern-lifecycle.md`, `pattern-build-config.md`, `pattern-ipc.md`). Each reference file covers every file/surface touched by that concern with full code snippets, gotchas, and provenance. Tier 1 (Pattern Surface) stays index-like; Tier 2 carries the copy-paste-able depth.
      
      **metadata.json — Reference-App stats semantics:**
      
      `stats.exports_documented` is a **library concept** and does not carry over cleanly. When `scope.type: "reference-app"`:
      
      - `stats.exports_documented` MAY be set to the Pattern Surface row count as a proxy, but its semantics change: it counts authored pattern surfaces, not public exports. Emit an adjacent `stats.pattern_surfaces_documented` with the same integer so downstream consumers (test-skill, feasibility, discovery) can discriminate.
      - `exports_public_api` / `exports_internal` / `exports_total` are not meaningful for a reference app — omit them, or set all three to the Pattern Surface count with a note in `stats.notes`: `"reference-app: export counts are pattern-surface proxies, not library exports"`.
      - Do not fabricate signature / type-coverage data from pattern surfaces. skf-test-skill will skip those categories when metadata flags a reference-app (same skip path as `stackSkill` once that flag is wired — see scoring-rules.md).
      - Do not emit `effective_denominator` for reference-app scope, even when the source is a monorepo. `effective_denominator` counts public exports from the authoring-surface barrel — a library concept that `pattern_surfaces_documented` already replaces here. A reference-app-in-monorepo literally satisfies `compile.md` §4's emit-conditions (condition 1 arm (a) + non-`full-library` + stratified `scope.notes`), so this carve-out takes precedence: pattern-surface coverage is the reference-app basis, and skf-test-skill skips library-export coverage for it.
      
      **Language / spec-reference sub-shape:** A reference app that documents an engine- or spec-versioned **language** — a query language, grammar, or DSL (e.g. SurrealQL @ SurrealDB) whose value is construct idioms rather than wiring or exports — is a recognized reference-app sub-shape. Keep `scope.type: "reference-app"` (there is no separate enum value), so every carve-out above applies unchanged (`pattern_surfaces_documented` proxy, no `effective_denominator`, test-skill signature/type skip). Adapt the three overrides to the language's surface instead of app wiring:
      
      - **Section 4 (Pattern Surface)** becomes a **construct-area map**, not a wiring list. Each row is a language construct area, where it lives in the source, and what the user writes there:
      
        ```markdown
        ## Pattern Surface
      
        | # | Construct Area | Where in Source | Purpose |
        |---|----------------|-----------------|---------|
        | 1 | {statements / DDL} | {sql::statements} | {what the user writes} |
        | 2 | {operators} | {sql::operator} | {…} |
        | … | … | … | … |
        ```
      
      - **Section 3 (Adoption Steps)** becomes **production-task workflows** — ordered tasks a user performs *in the language* (define a schema, write a query, call a built-in function), each with a minimal snippet — not a copy-this-wiring narrative.
      - **Tier 2** groups `references/*.md` by **language concern** (e.g. `statements.md`, `functions.md`, `types.md`) — one file per construct family — instead of `pattern-*.md` wiring concerns.
      - **metadata.json:** `pattern_surfaces_documented` counts the documented construct areas; the no-`effective_denominator` carve-out applies (a language has no export barrel). The brief's `language` field records the **documented** language (e.g. `surrealql`), which may differ from the source language it was extracted from (e.g. `rust`) — see `skf-analyze-source/assets/skill-brief-schema.md`.
      
      **When this clause does not apply:** `full-library`, `specific-modules`, `public-api`, `component-library`, or `docs-only`. Those scope types have their own assembly semantics and export-count conventions — do not mix.
      
      ### Whole-Language Reference Assembly Overrides
      
      A **whole-language reference** is a compiler/interpreter repo (rustc, CPython, the Go toolchain, TypeScript) enriched with the language's canonical prose — the guide/Book and the standard/library docs. It maps to `scope.type: full-library`, but its value to a skill consumer is the **language**, not the compiler's internal exports. The standard library-export layout above would foreground compiler-internal signatures and bury the prose; these overrides invert that.
      
      **GATE — apply ONLY when this is a whole-language reference.** The brief carries ≥1 `doc_urls` entry with `source: language-registry` (equivalently, `skf-derive-assembly-shape.py` returns `assembly_shape: "whole-language-reference"`). This is a structured, schema-validated signal — not a `scope.notes` substring — so an ordinary `full-library` library, a parser *library* (pest/lalrpop — its code IS the product, so §6b seeds no corpora), a component-library, a reference-app (including the language/spec-reference DSL sub-shape, which stays `scope.type: reference-app` and is handled by the reference-app override above), and a docs-only brief all fail the gate. When the gate does not fire, **skip this entire section** — the standard Tier 1 (sections 1–8) and Tier 2 (9–11) layout and the signature-fidelity rule run unchanged, so non-whole-language skills assemble byte-identically.
      
      When the gate fires, the assembler has a `language_guide[]` artifact from step 3c §4a (the retained corpora prose, carved out of the T3-vs-T1 conflict rule). Apply these overrides:
      
      **Empty-guide guard (check first):** If `language_guide[]` is absent or every entry's `prose` is null (all registry corpora failed to fetch), do not emit a thin Language Guide above a full internals section — fall back to the standard layout and record a warning in `evidence-report.md`: "Whole-language reference gated but no Language-Guide prose was retained — assembled as a standard library skill." This prevents the inverse-value outcome (prose section empty, compiler internals dominant).
      
      **New Section 1b — Language Guide (Tier 1, foregrounded):** Immediately after Section 1 (Overview) and BEFORE Quick Start, emit the skill's primary content from `language_guide[]`. One subsection per corpus (`{label}`), carrying the retained prose — language concepts, idioms, and usage examples — each block cited `[EXT:{url}]`. This is the section an agent reads to learn the language; it survives split-body as Tier 1.
      
      **Section 2 (Quick Start) — language-usage override:** Build the runnable examples from the Language-Guide prose (writing and running code *in* the language — a minimal program, a common idiom), cited `[EXT:{url}]`, not from compiler-internal export call chains. Fall back to the standard signature-only Quick Start only if the guide yields no examples.
      
      **Section 3 (Common Workflows) — "Writing {language}" override:** Replace function-call-chain workflows over compiler exports with common language tasks (define a type, handle errors, organize a module), each a short idiom from the guide.
      
      **Section 4 (Key API Summary) — demote compiler internals:** Replace the top-of-body compiler-export function table with a one-paragraph "Standard Library & Language Surface" pointer into the Language Guide. Move the AST/compiler-internal exports into a late-ordered Tier 2 subsection `### Compiler Internals (reference only)` with a one-line note: "Internal implementation surface of the {language} toolchain — most consumers want the Language Guide above, not these."
      
      **Signature fidelity is preserved (not relaxed):** this is an ordering/prominence change only. Any compiler signatures that DO appear (in Compiler Internals) keep their T1/AST-authoritative params and return types per Section 1b of `compile.md` and rule 7 below. The override never substitutes prose-derived signatures for AST-extracted ones; it changes which content leads, not which tier wins a per-export signature conflict.
      
      **metadata.json:** the skill stays `scope.type: full-library`; export-count stats are still emitted. (A whole-language skill's low public-API coverage versus the full compiler surface is expected — coverage-gate semantics for this shape are tracked separately and out of scope here.)
      
      ### Assembly Rules
      
      1. Assemble all Tier 1 sections first — these form the essential standalone body
      2. Assemble all Tier 2 sections after — these are progressive disclosure detail
      3. Tier 1 content stays under 300 lines (excluding frontmatter)
      4. If Tier 1 alone exceeds 300 lines, reduce Key API Summary and Architecture at a Glance
      5. Tier 1 sections are kept short enough that `split-body` targets the larger Tier 2 sections (`## Full ...` headings) instead
      6. After split-body, SKILL.md must still contain all Tier 1 sections with actionable content
      7. **Signature fidelity:** When populating function entries from the extraction inventory, always use the `params` and `return_type` fields from the provenance-map entry (T1/AST-backed). Do not substitute parameter names, types, or return types from documentation, README examples, or enrichment annotations. T2/T3 sources may enrich descriptions and add usage notes, but structural signature data from AST extraction is authoritative.
      
      ### Reference File Rules
      
      - **Table of contents required** for any reference file exceeding 100 lines — include a `## Contents` section at the top listing all sub-sections. This ensures agents can see the full scope even when previewing with partial reads.
      - One file per major function group or type — group by module, file, or functional area
      - Name files descriptively: `form_validation_rules.md`, not `doc2.md`
      
      ### Content Quality Rules
      
      These SKF-specific rules apply to all content assembled in SKILL.md and reference files. Generic authoring craft — matching instruction freedom to task fragility, consistent terminology, avoiding time-sensitive instructions, progress checklists for multi-step workflows, and the plan-validate-execute pattern for batch/destructive operations — is assumed and not re-taught here. Only the two rules below encode a non-obvious SKF constraint or downstream-validator quirk that an author would not otherwise infer.
      
      **Zero-hallucination examples:** Include input/output or usage examples ONLY when concrete pairs exist in source tests or official docs — 2-3 such pairs convey the desired style and detail more clearly than a description alone. If no examples exist in source, note the gap rather than fabricating pairs — zero hallucination applies.
      
      **Generic-plus-signature code spans:** When documenting a generic class constructor or factory signature, do not place the generic brackets and the parameter list inside a single inline code span. `skill-check`'s `links.local_markdown_resolves` validator parses `` `ClassName[T](key: str)` `` as a broken markdown link (`[T]` becomes the link text, `(key: str)` becomes the URL) and emits a `broken local link` warning on the Links axis, regardless of the surrounding backticks. This applies to Tier 1 Key Types, Tier 2 Full API Reference, and reference files. Safe alternatives:
      - Split into two code spans: `` `ClassName[T]` — dataclass with fields `(key: str, value: int)` ``
      - Drop the explicit constructor and describe fields in prose: `` `ClassName[T]` — generic container parameterized by `T`, with field `key: str` ``
      - Use the curly-brace substitution used for frontmatter: `` `ClassName{T}(key: str)` `` (readable, avoids both markdown-link and angle-bracket parsing)
      
    • skill-sections.md 15.6 KB
      # SKILL.md Section Structure
      
      ## agentskills.io Compliant Format
      
      ### Frontmatter
      
      The authoritative frontmatter contract — permitted fields, the `name`/`description` rules, the trigger-phrase/voice requirements, and the no-angle-brackets rule — is owned by `assets/compile-assembly-rules.md` (## Frontmatter). This catalog does not restate it (both files load together at compile.md §1).
      
      ### Two-Tier Section Structure
      
      SKILL.md uses a two-tier structure to ensure actionable content survives `split-body` extraction.
      
      #### Tier 1 — Always Inline (survives split-body, target <300 lines)
      
      | #  | Section                              | Budget    | Purpose                                                         |
      |----|--------------------------------------|-----------|-----------------------------------------------------------------|
      | 1  | **Overview**                         | ~10 lines | What the library does, source repo, version, tier, export count |
      | 2  | **Quick Start**                      | ~30 lines | 3-5 core functions with one runnable end-to-end example         |
      | 3  | **Common Workflows**                 | ~30 lines | 4-5 typical function call sequences for common tasks            |
      | 4  | **Key API Summary**                  | ~20 lines | Table of top 10-15 functions (name, purpose, key params)        |
      | 4b | **Migration & Deprecation Warnings** | ~10 lines | T2-future warnings inline (Deep tier only, skip if none)        |
      | 5  | **Key Types**                        | ~20 lines | Most important enum/type values inline                          |
      | 6  | **Architecture at a Glance**         | ~10 lines | Bullet list of subsystem categories                             |
      | 7  | **CLI**                              | ~10 lines | Basic CLI commands (skip if no CLI)                             |
      | 7b | **Scripts & Assets**                 | ~10 lines | Manifest of included scripts and assets (skip if none detected) |
      | 8  | **Manual Sections**                  | ~5 lines  | `<!-- [MANUAL] -->` markers for update-skill                    |
      
      #### Tier 2 — Reference-Eligible (extracted by split-body into references/)
      
      | # | Section | Purpose |
      |---|---------|---------|
      | 9 | **Full API Reference** | Complete signatures, parameter tables, return types, examples, citations |
      | 10 | **Full Type Definitions** | All types, interfaces, enums with full field details |
      | 11 | **Full Integration Patterns** | Co-import patterns, adapter details, pipeline internals (Forge/Deep only) |
      
      #### Section Conditionality
      
      - **Section 4b (Migration & Deprecation Warnings)** is conditional: only emitted for Deep tier when T2-future annotations exist. Quick/Forge/Forge+ tiers and Deep tiers without T2-future annotations omit it entirely (no empty section). Parsers and validators must treat this section as optional.
      - **Section 7b (Scripts & Assets)** is conditional: only emitted when `scripts_inventory` or `assets_inventory` is non-empty. Omitted entirely when no scripts or assets are detected. Parsers and validators must treat this section as optional.
      
      Assembly ordering, the under-300-line Tier-1 budget, and split-body targeting are owned by `assets/compile-assembly-rules.md` (### Assembly Rules).
      
      ### Component Library Section Overrides
      
      The `component-library` section formats — Component Catalog (§4), Key Props (§5), the Props Reference Tier 2 layout, and the component-library `context-snippet.md` format — are owned by `assets/compile-assembly-rules.md` (### Component Library Assembly Overrides), alongside the `reference-app` and whole-language override sets. This catalog does not restate them (both files load together at compile.md §1).
      
      ### Provenance Citation Format
      
      | Tier            | Format                     | Example                       |
      |-----------------|----------------------------|-------------------------------|
      | T1 (AST)        | `[AST:{file}:L{line}]`     | `[AST:src/auth/index.ts:L42]` |
      | T1-low (Source) | `[SRC:{file}:L{line}]`     | `[SRC:src/auth/index.ts:L42]` |
      | T2 (QMD)        | `[QMD:{collection}:{doc}]` | `[QMD:project:CHANGELOG.md]`  |
      | T3 (External)   | `[EXT:{url}]`              | `[EXT:docs.example.com/api]`  |
      
      ### [MANUAL] Section Markers
      
      Seed empty manual sections for future update-skill compatibility:
      
      ```markdown
      <!-- [MANUAL:additional-notes] -->
      <!-- Add custom notes here. This section is preserved during skill updates. -->
      <!-- [/MANUAL:additional-notes] -->
      ```
      
      Place after Quick Start and after API Reference sections.
      
      ---
      
      ## context-snippet.md Format (Vercel-aligned indexed format)
      
      Indexed pipe-delimited format for CLAUDE.md managed section (~80-120 tokens per skill). Aligned with Vercel's research finding that retrieval instructions + indexed file maps + inline gotchas dramatically improve agent performance.
      
      ```markdown
      [{skill-name} v{version}]|root: skills/{skill-name}/
      |IMPORTANT: {skill-name} v{version} — read SKILL.md before writing {skill-name} code. Do NOT rely on training data.
      |quick-start:{SKILL.md#quick-start}
      |api: {top exports with () for functions, comma-separated}
      |key-types:{SKILL.md#key-types} — {inline summary of most important type values}
      |gotchas: {2-3 most critical pitfalls or breaking changes, inline}
      ```
      
      ### Format rules
      
      - **Line 1:** Skill name + version + root path — version signals training data staleness
      - **Line 2 (IMPORTANT):** Retrieval instruction — always present, tells agent to read SKILL.md before acting
      - **Lines 3+:** Pipe-delimited index mapping topics to SKILL.md sections (with `#anchor` pointers)
      - **Inline content:** Each index line includes a brief inline summary (~10 words) so the agent can decide whether to read the full section
      - **gotchas line:** 2-3 most critical pitfalls inline — prevents mistakes without requiring a file read
      - **Token budget:** ~80-120 tokens per skill (justified by Vercel's finding that indexed format maintains performance at 80% compression)
      - **T1-now content only** — no T2 annotations in the snippet
      - **Section anchors** (`#quick-start`, `#key-types`) must match actual SKILL.md heading slugs
      - **Version** comes from source detection, not brief default
      
      ---
      
      ## metadata.json Structure
      
      ```json
      {
        "name": "{skill-name}",
        "version": "{source-version}",
        "skill_type": "single",
        "scope_type": "{full-library|specific-modules|public-api|component-library|reference-app|docs-only}",
        "source_authority": "{official|community|internal}",
        "source_repo": "{github-url}",
        "source_root": "{resolved-source-path}",
        "source_commit": "{commit-hash}",
        "source_ref": "{source_ref or null}",
        "confidence_tier": "{Quick|Forge|Forge+|Deep}",
        "spec_version": "1.3",
        "generation_date": "{ISO-8601}",
        "description": "{SKILL.md frontmatter description}",
        "language": "{primary-source-language}",
        "ast_node_count": "{number-or-omitted-if-no-ast}",
        "exports": [],
        "tool_versions": {
          "ast_grep": "{version-or-null}",
          "qmd": "{version-or-null}",
          "skf": "{skf_version}"
        },
        "confidence_distribution": {
          "t1": 0,
          "t1_low": 0,
          "t2": 0,
          "t3": 0
        },
        "stats": {
          "exports_documented": 0,
          "exports_public_api": 0,
          "exports_internal": 0,
          "exports_total": 0,
          "public_api_coverage": 0.0,
          "total_coverage": 0.0,
          "scripts_count": 0,
          "assets_count": 0
          // "effective_denominator": 0  // optional — emitted for the curated-subset shapes in compile.md §4 condition 1; test-skill uses this as the coverage denominator when present. See compile.md §4 emit rules.
        },
        // scripts[] and assets[] — include ONLY when inventories are non-empty; omit entirely otherwise
        // "scripts": [{ "file": "scripts/{name}", "purpose": "{description}", "source_file": "{source-path}", "confidence": "T1-low" }],
        // "assets": [{ "file": "assets/{name}", "purpose": "{description}", "source_file": "{source-path}", "confidence": "T1-low" }],
        // doc_sources[] — include ONLY when doc detection ran; omit if detection was skipped entirely
        // "doc_sources": [{ "url": "https://...", "detected_via": "homepageUrl|readme_link|pages_api|docs_folder|readme_always|brief_doc_urls", "content_hash": "sha256:{hex}|null", "recorded_at": "ISO-8601" }],
        // detected_via "brief_doc_urls" = docs-only skills: the brief's doc_urls (plus subpages step 3c fetched), hashed by `skf-detect-docs.py hash-urls`
        "generated_by": "{quick-skill|create-skill}",
        "dependencies": [],
        "compatibility": "{semver-range}"
      }
      ```
      
      ---
      
      ## references/ Directory Structure
      
      One file per major function group or type:
      
      ```
      references/
      ├── {function-group-a}.md    — Detailed reference with full examples
      ├── {function-group-b}.md    — Detailed reference with full examples
      └── {type-name}.md           — Type definition details
      ```
      
      Each reference file includes:
      - Full function signatures
      - Detailed parameter descriptions
      - Return value details
      - Complete usage examples
      - Related functions cross-references
      - Temporal annotations (Deep tier: T2-past, T2-future)
      - **Table of contents** (required for files exceeding 100 lines) — a `## Contents` section at the top listing all sub-sections for agent discoverability
      
      ---
      
      ## provenance-map.json Structure
      
      ```json
      {
        "provenance_version": "2.0",
        "skill_name": "{name}",
        "skill_type": "single|stack",
        "source_repo": "{url or [urls] for multi-source}",
        "source_commit": "{hash or {repo: hash} for multi-source}",
        "source_ref": "v0.5.0",
        "generated_at": "{ISO-8601}",
        "entries": [
          {
            "export_name": "getToken",
            "export_type": "function",
            "source_library": "{library-name — defaults to skill name for single skills}",
            "params": ["userId: string", "options?: TokenOptions"],
            "return_type": "Token",
            "source_file": "src/auth/index.ts",
            "source_line": 42,
            "confidence": "T1",
            "extraction_method": "ast-grep",
            "ast_node_type": "export_function_declaration",
            "signature_source": "T1"
          }
        ],
        "integrations": [
          {
            "libraries": ["lib-a", "lib-b"],
            "pattern_type": "provider-consumer",
            "detection_method": "co-import grep|architecture_co_mention|inferred_from_shared_domain",
            "co_import_files": [{"file": "src/app.ts", "line": 5}],
            "confidence": "T1|T2|T3"
          }
        ],
        "constituents": [
          {
            "skill_name": "{constituent-skill-name}",
            "skill_path": "skills/{skill-dir}/",
            "version": "{version at compose time}",
            "composed_at": "{ISO-8601}",
            "metadata_hash": "sha256:{hash of constituent metadata.json at compose time}"
          }
        ],
        "file_entries": [
          {
            "file_name": "scripts/{name}",
            "file_type": "script",
            "source_file": "{source-path}",
            "confidence": "T1-low",
            "extraction_method": "file-copy",
            "content_hash": "sha256:{hash}"
          }
        ]
      }
      ```
      
      - `provenance_version`: Schema version. `"2.0"` enables unified single/stack support. Audit reads both v1 (no version field) and v2.
      - `source_library`: Identifies which library an export belongs to. For single skills, defaults to the skill/package name. For stack skills, identifies the constituent library.
      - `integrations`: Stack-only. Describes cross-library integration patterns with their own schema — not shoehorned into entries.
      - `constituents`: Stack-only (compose-mode). Tracks the compose-time snapshot of each source skill for staleness detection. `metadata_hash` enables audit to detect constituent drift without re-reading all constituent files.
      - `file_entries` remains optional — omit when no scripts, assets, or promoted docs exist.
      - Single skills omit `integrations` and `constituents` arrays entirely (not empty arrays).
      
      **`file_type` enum — canonical values:**
      
      | Value | Source inventory | Copied to skill package? | `extraction_method` | Purpose |
      |---|---|---|---|---|
      | `"script"` | `scripts_inventory` | yes → `scripts/` | `"file-copy"` | Runnable scripts (CLI tools, validation scripts, etc.) bundled into the skill for execution. |
      | `"asset"` | `assets_inventory` | yes → `assets/` | `"file-copy"` | Data files (templates, JSON schemas, config fixtures) bundled for reference. |
      | `"doc"` | `promoted_docs` (from step 3 §2a) | **no** | `"promoted-authoritative"` | Authoritative AI documentation files (`llms.txt`, `AGENTS.md`, etc.) promoted into scope by the Discovered Authoritative Files Protocol. The source file is tracked for drift detection but not bundled into the skill — its content influences compile-time decisions (description, Quick Start) and is watched for upstream changes. |
      
      When `file_type: "doc"`:
      
      - `source_file` is the relative path from the source root to the authoritative doc.
      - `content_hash` is computed at promotion time (step 3 §2a) and persisted here so update-skill Category D can detect drift via hash comparison.
      - `file_name` convention: use the source-relative path prefixed with `docs/authoritative/` to namespace doc entries away from script/asset filenames in tools that iterate `file_entries[]` by `file_name`. Example: `docs/authoritative/apps/docs/public/llms.txt`. Step-07 does not write this path — it is a synthetic namespace used only within the provenance map.
      - `confidence` is `"T1-low"` (the promotion decision is source-level but the content was not AST-parsed).
      
      ---
      
      ## evidence-report.md Structure
      
      ```markdown
      ---
      skill_name: {skill-name}
      generated: {date}
      forge_tier: {tier}
      t2_future_count: {N}
      ---
      
      # Evidence Report: {skill-name}
      
      **Generated:** {date}
      **Forge Tier:** {tier}
      **Source:** {repo} @ {commit}
      
      ## Tool Versions
      - ast-grep: {version}
      - QMD: {version}
      - SKF: {skf_version}
      
      ## Extraction Summary
      - Files scanned: {count}
      - Exports found: {count}
      - Confidence: T1={n}, T1-low={n}, T2={n}, T3={n}
      
      ## Validation Results
      - Schema: {pass/fail} (quality score: {score}/100)
      - Frontmatter: {pass/fail}
      - Body: {pass/fail} {split-body applied if applicable}
      - Security: {pass/warn/skipped}
      - Content Quality (tessl): {pass/warn/skipped} (score: {score}%)
      - Metadata: {pass/fail}
      
      ## Quality Score Breakdown
      - Frontmatter (30%): {score} | Description (30%): {score} | Body (20%): {score} | Links (10%): {score} | File (10%): {score}
      
      ## Description Guard
      - Restored: {true/false}
      - Triggering tool: {tool_name or —}
      - Original description preserved: {true/false}
      - Notes: {one-sentence detail or —}
      
      ## Auto-Fixed Issues
      - {list of issues automatically corrected by --fix}
      
      ## Remaining Warnings
      - {any warnings from extraction or validation}
      ```
      
      **Frontmatter — pinned detection contract:** the `t2_future_count` field is the authoritative forward-looking-annotation count for downstream gate checks (e.g. skf-test-skill §2b migration-section rule). Emit **always**, even when 0 — omission is indistinguishable from "no T2-future data" and silently flips the gate into Case 2/3 for a Case-1 skill. `generated` and `forge_tier` mirror the narrative header for consumers that read only the frontmatter. Downstream gate rules parse `t2_future_count` from frontmatter, not prose — prose drift (heading renames, alternate phrasings like "forward-looking annotations") silently breaks grep-based detection.
      
      **Description Guard slot:** populated by step 6 §0 (create-skill) and §0 (update-skill) when the guard protocol fires. `Restored: true` indicates that an external tool (typically `skill-check --fix` or `split-body`) rewrote the frontmatter `description` and the guard restored the pre-tool value. When `Restored: false`, leave `Triggering tool`, `Original description preserved`, and `Notes` as `—`. When `Restored: true`, fill all four fields: tool name, whether the original was successfully written back, and a one-sentence note describing what the tool had changed (e.g., "replaced with generic summary", "truncated at 80 chars", "angle-bracket tokens re-introduced"). Downstream test-skill assertions can grep for `Restored: true` to detect unintended tool rewrites without parsing free-form warning prose.
      
    • tessl-dismissal-rules.md 11.8 KB
      # tessl Dismissal Rules
      
      This file is the **single source of truth** for tessl review findings that Skill Forge expects and must dismiss. Step-06 §6 loads this file, parses the `tessl skill review` JSON output, and cross-references each finding against the rules below. Matches are dismissed with their rationale logged to the evidence report; non-matches surface to the user via §6b. Never embed dismissal logic in step files — update the rules here first and reference them from step 6.
      
      ---
      
      ## Score Thresholds
      
      When parsing tessl output, check these fields against the thresholds below. Violations trigger warnings or errors as specified.
      
      | Field | Threshold | Severity on violation | Rationale |
      |---|---|---|---|
      | `review_score` (overall) | `>= 60` | warn | Two-tier SKF skills trade some conciseness score for progressive disclosure. 60 is the floor below which something is genuinely wrong. |
      | `description_field` validator finding | none expected | **error — recover, then halt** | The **deterministic** `description_field` validator surfaces as a `findings[]` entry (not as the judge percentage). It parses the frontmatter `description` as a raw string and rejects angle-bracket / XML-tag content. step 5 §2a unconditionally replaces `<` with `{` and `>` with `}` before validation, so a `description_field` finding means the sanitizer was bypassed — a downstream tool rewrote the description. Attempt recovery per rule `description-xml-tags-guarded-upstream` (re-apply the §2a substitution in place on the staging SKILL.md frontmatter and re-run tessl). Halt only if recovery fails. |
      | `description_score` (LLM judge %) | `>= 60` | warn | The judge's 0–100 discoverability score, composed of `trigger_term_quality`, `specificity`, `completeness`, and `distinctiveness` sub-scores. A value below 100 while the deterministic `description_field` validator PASSES is a soft discoverability hint (e.g. jargon density), **not** a sanitizer bypass — record a warning and continue. Never halt on the judge percentage alone; the authored brief is the source of truth for the description. |
      | `content_score` | `>= 60` | warn | Two-tier design deliberately duplicates key API data across Tier 1 and Tier 2. Conciseness scorer penalizes this. 60 is the acceptable floor. |
      
      **Recovery is gated on the deterministic validator, not the judge percentage.** If tessl emits a `findings[]` entry with rule ID `description_field` (angle-bracket / XML-tag rejection), **attempt recovery first** per rule `description-xml-tags-guarded-upstream` below: re-apply step 5 §2a's `<` → `{` / `>` → `}` substitution in place on the staging SKILL.md frontmatter `description`, then re-run `npx -y tessl skill review <staging-skill-dir>` once. If the re-run clears the `description_field` finding, log `description-recovery: applied ({count} substitutions)` in the evidence report and proceed to normal suggestion handling. If recovery fails, halt with: "Description sanitization recovery failed — step 5 §2a's `<`/`>` → `{`/`}` replacement did not resolve the tessl finding. Investigate the staging SKILL.md frontmatter for non-angle-bracket content that tessl is still rejecting, and patch §2a accordingly. Do not edit the description manually." Do not proceed to §6b user prompt on an unrecovered failure.
      
      A `description_score` (LLM judge %) below 100 **without** an accompanying `description_field` finding is **not** a recovery trigger and **not** a halt condition: record `description_judge_score: {n}% (deterministic description_field validator PASSED)` as a warning in the evidence report and continue to suggestion handling. The recover-then-halt path is reserved for the deterministic validator.
      
      ---
      
      ## Suggestion Dismissal Rules
      
      Each rule below describes a tessl suggestion that Skill Forge expects and dismisses automatically. For each `judge_suggestion` in tessl output, check it against these rules in order. If any rule matches, record the dismissal in the evidence report (`rule_id`, `rationale`) and do not apply the suggestion. If no rule matches, the suggestion is novel and must surface to the user in §6b.
      
      ### Rule: `remove-manual-markers`
      
      - **Scorer:** any
      - **Match criteria (any of):**
        - Suggestion text contains `[MANUAL]` AND one of: `remove`, `delete`, `strip`, `clean up`
        - Suggestion text contains `manual marker` AND one of: `remove`, `delete`, `unnecessary`
      - **Rationale:** `<!-- [MANUAL] -->` markers delineate author-added content that `skf-update-skill` preserves during merge operations. Removing them would cause update-skill to overwrite author edits on the next run. The markers are a load-bearing contract between create-skill, test-skill, and update-skill.
      - **Action:** dismiss.
      
      ### Rule: `move-full-api-reference`
      
      - **Scorer:** any
      - **Match criteria (all of):**
        - Suggestion text mentions `Full API Reference` OR a `## Full ...` section heading
        - Suggestion text contains one of: `move`, `relocate`, `extract`, `split out`, `separate file`, `external file`
      - **Rationale:** The two-tier design (Tier 1 inline + Tier 2 inline) keeps Tier 2 sections inline by default. `skill-check split-body` relocates Tier 2 to `references/` only when the body exceeds size limits — see step 6 §4. Preemptive relocation destroys the standalone SKILL.md that the cited 100% task accuracy (vs. 79% for on-demand retrieval) depends on.
      - **Action:** dismiss.
      
      ### Rule: `consolidate-duplicate-parameters`
      
      - **Scorer:** `conciseness` or any
      - **Match criteria (any of):**
        - Suggestion mentions `duplicate parameter`, `redundant parameter`, `consolidate parameter`
        - Suggestion mentions both `Key API` / `Key API Summary` AND `Full API Reference` and suggests merging them
      - **Rationale:** Tier 1 Key API Summary intentionally lists only key parameters for standalone discoverability. Tier 2 Full API Reference provides complete parameter tables, types, defaults, and edge cases. This is progressive disclosure, not duplication. Consolidating would force agents to scroll to Tier 2 for the most common function calls, defeating Tier 1's purpose and breaking the split-body Tier 1 preservation contract in step 6 §4.
      - **Action:** dismiss.
      
      ### Rule: `conciseness-redundancy-between-tiers`
      
      - **Scorer:** `conciseness`
      - **Match criteria (all of):**
        - Scorer is `conciseness`
        - Suggestion or score rationale references redundancy between Tier 1 and Tier 2 sections (typically `Key API` ↔ `Full API` or `Quick Start` ↔ `Full Reference`)
      - **Expected score:** 2/3 is normal. 3/3 is only achievable by collapsing the two-tier design.
      - **Rationale:** Progressive disclosure is the intentional SKF design. The conciseness scorer has no concept of Tier 1 / Tier 2 and flags the structure as redundancy. Collapsing the two tiers would eliminate standalone discoverability and break split-body behavior.
      - **Action:** dismiss. Do not attempt to raise the score above 2/3.
      
      ### Rule: `provenance-citations-required`
      
      - **Scorer:** `conciseness`, `actionability`, or any
      - **Match criteria (any of):**
        - Suggestion text references `[SRC:`, `[AST:`, `[QMD:`, or `[EXT:` annotations AND one of: `remove`, `strip`, `omit`, `drop`
        - Suggestion mentions `provenance annotations`, `source citations`, or `source markers` AND one of: `remove`, `unnecessary`, `token bloat`, `token cost`
      - **Expected score:** `conciseness` 1/3 is normal while inline provenance annotations are present. Raising it would require stripping the annotations.
      - **Rationale:** SKF's zero-hallucination policy requires every claim in SKILL.md to carry a provenance citation — see the "Signature fidelity" rule in `assets/compile-assembly-rules.md` and the "Provenance Citation Format" table in `assets/skill-sections.md`. Removing them would break the provenance audit trail and the trust contract that lets `skf-update-skill` detect source drift and `skf-audit-skill` verify claims against source.
      - **Action:** dismiss.
      
      ### Rule: `description-xml-tags-guarded-upstream`
      
      - **Scorer:** `description_field` (deterministic validator)
      - **Match criteria (any of):**
        - Finding rule ID is `description_field` AND message mentions `XML tag`, `must not contain`, or angle-bracket-related text
      - **Expected occurrence:** zero. step 5 §2a unconditionally replaces `<` with `{` and `>` with `}` in the frontmatter description before SKILL.md is written.
      - **Action:** **Attempt recovery, then halt if recovery fails.** This is not a dismissal — the finding represents a real sanitizer bypass that step 6 must resolve before proceeding.
        1. **Re-apply §2a in place.** Read the current `description` from the on-disk staging SKILL.md frontmatter, replace every `<` with `{` and every `>` with `}`, and write the result back to the frontmatter. Re-sync the in-context copy to match. Count the substitutions; if zero, the description is already clean and the tessl finding points at something other than angle brackets — skip to the halt branch below.
        2. **Re-run tessl once.** Execute `npx -y tessl skill review <staging-skill-dir>` a second time and re-parse the JSON output.
        3. **On success** (the `description_field` finding is absent from the re-run): log `description-recovery: applied ({count} substitutions)` in the evidence report under "Dismissed tessl suggestions", then continue §6 with the recovered review result (proceed to normal suggestion handling against the rules below). The rerun's `judge_suggestions[]` replaces the original. A residual judge `description_score < 100` on the re-run is a warning, not a failure — recovery success is keyed on the deterministic finding clearing, not on the judge percentage reaching 100.
        4. **On failure** (the `description_field` finding persists after re-sanitization): halt with: "Description sanitization recovery failed — step 5 §2a's `<`/`>` → `{`/`}` replacement did not resolve the tessl finding. Investigate the staging SKILL.md frontmatter for non-angle-bracket content that tessl is still rejecting, and patch §2a accordingly. Do not edit the description manually."
      
      The recovery path makes the skill shippable when a downstream tool (`skill-check --fix`, `split-body`, or a future validator) re-introduces angle brackets into the description after §2a has run. The Description Guard Protocol in step 6 §0 is the first line of defense against such rewrites; this rule is the second line, active when the guard also missed.
      
      ---
      
      ## How Step-06 §6 Uses This File
      
      1. Load this file completely at the start of §6.
      2. Run `npx -y tessl skill review <staging-skill-dir>` and parse JSON output.
      3. Check score thresholds. For a deterministic `description_field` finding (angle-bracket / XML-tag rejection): follow the recovery-then-halt path described in the threshold table above and the `description-xml-tags-guarded-upstream` rule below — re-apply §2a in place, re-run tessl once, and continue on recovery success (finding cleared) or halt on recovery failure. A judge `description_score` below 100 while the `description_field` validator passes is a warning, not a halt. For other warns: continue, log warnings to evidence report.
      4. For each `judge_suggestions[]` entry in the output:
         a. Iterate the rules above in order.
         b. If a rule's match criteria are satisfied, record `{rule_id, rationale, suggestion_text}` in the evidence report under "Dismissed tessl suggestions" and move to the next suggestion.
         c. If no rule matches, add the suggestion to the "Novel tessl suggestions" list that §6b surfaces to the user.
      5. Proceed to §6b if any novel suggestions exist, or auto-proceed if all suggestions were dismissed.
      
      ---
      
      ## Evolving This List
      
      When tessl changes or an SKF design decision shifts, add a new rule section above (`Rule:` / `Scorer:` / `Match criteria` / `Rationale` / `Action`), link each rationale to the concrete SKF design principle it protects (two-tier design, MANUAL markers, sanitization, split-body preservation — a rationale without such a link is a smell), and keep score thresholds at or above current production floors — never lower them to suppress real regressions.
      
  • references
    • sub
      • ccc-discover.md 4.3 KB
        ---
        nextStepFile: '../extract.md'
        ---
        
        <!-- Config: communicate in {communication_language}. -->
        
        # Step 2b: CCC Semantic Discovery
        
        ## STEP GOAL:
        
        If tier is Forge+ or Deep AND ccc is available, perform a semantic discovery pass over the source code to identify the most relevant files for the skill being created. Store ranked discovery results in context to pre-rank the file extraction queue in step 3.
        
        For Quick and Forge tiers, or when ccc is unavailable, skip silently and proceed.
        
        ## Rules
        
        - Focus only on running ccc semantic search and storing results — do not extract exports
        - Do not block the workflow if ccc fails
        - Quick and Forge tiers: skip this step entirely and silently
        
        ## MANDATORY SEQUENCE
        
        ### 1. Check Tier Eligibility
        
        **If tier is Quick or Forge:**
        
        Set `{ccc_discovery: []}` in context. Auto-proceed silently. Display no message. Immediately load, read entire file, then execute `{nextStepFile}`.
        
        **If tier is Forge+ or Deep:**
        
        Check `tools.ccc` from forge-tier.yaml. If `tools.ccc` is false, set `{ccc_discovery: []}` in context and auto-proceed to section 5.
        
        If `tools.ccc` is true, check the remote source guard **before** proceeding to section 2:
        
        **Remote source guard:** If `source_root` is a remote URL (GitHub repository — workspace clone or ephemeral clone happens in step 3), CCC cannot operate yet. Set `{ccc_discovery: []}` and display: "CCC discovery deferred — remote source will be indexed after clone in step 3." Auto-proceed to section 5 (step completion). Step-03 will detect the deferred scenario and run CCC discovery on the resolved clone (workspace or ephemeral) before AST extraction begins.
        
        If `source_root` is a local path, continue to section 2.
        
        ### 2. Check CCC Index State
        
        Read `ccc_index` from forge-tier.yaml:
        
        - If `ccc_index.status` is `"fresh"` or `"created"`: continue to section 3.
        - If `ccc_index.status` is `"stale"`: display brief note — "CCC index is stale — discovery results may miss recent changes." Continue to section 3.
        - If `ccc_index.status` is `"none"` or `"failed"`: attempt lazy indexing via `ccc_bridge.ensure_index(source_root)`. If indexing succeeds, continue to section 3. If indexing fails, set `{ccc_discovery: []}` and auto-proceed to section 5.
        
        **Tool resolution for ccc_bridge.ensure_index:** Use `/ccc` skill indexing (Claude Code), ccc MCP server (Cursor), or `cd {source_root} && ccc init` + `ccc index` (CLI). Note: `ccc init` takes no positional arguments — it initializes the index for the current working directory. See `knowledge/tool-resolution.md`.
        
        ### 3. Construct Semantic Query
        
        Build the discovery query from the brief data:
        
        **Primary query:** `"{brief.name} {brief.scope}"`
        
        Where:
        - `brief.name` is the skill name from the brief
        - `brief.scope` is the scope field (e.g., "Full library", "Public API", or specific module names)
        
        **Query length cap:** Truncate to 80 characters if longer — ccc semantic search is sensitive to overly long queries. When truncating, keep the full skill name and trim `brief.scope` from the end. If `brief.scope` is very short (< 10 chars), append terms from `brief.description` to fill the remaining space.
        
        ### 4. Execute CCC Semantic Search
        
        Run `ccc_bridge.search(query, source_root, top_k=20)`:
        
        **Tool resolution for ccc_bridge.search:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `cd {source_root} && ccc search --limit 20 "{query}"` (CLI). Note: `ccc search` operates on the index in the current working directory — there is no flag to specify a project directory. See `knowledge/tool-resolution.md`.
        
        **If search succeeds:**
        
        Store results as `{ccc_discovery: [{file, score, snippet}]}` in context.
        
        Display brief discovery summary:
        
        "**CCC semantic discovery: {N} relevant regions identified across {M} unique files.**"
        
        Where:
        - `{N}` is the total result count
        - `{M}` is the count of unique file paths in results
        
        **If search fails (any error):**
        
        Set `{ccc_discovery: []}` in context.
        
        Display: "CCC discovery unavailable — proceeding with standard extraction."
        
        Do not halt. This is not an error.
        
        **If search returns empty results:**
        
        Set `{ccc_discovery: []}` in context.
        
        No message needed — empty results are normal for small or highly focused libraries.
        
        ### 5. Auto-Proceed
        
        No user interaction. Load `{nextStepFile}`, read it fully, then execute it. CCC failures degrade and proceed — they never halt.
        
        
      • fetch-docs.md 16.2 KB
        ---
        nextStepFile: '../enrich.md'
        # Resolve `{atomicWriteHelper}` by probing `{atomicWriteProbeOrder}` in order
        # (installed SKF module path first, src/ dev-checkout fallback); first existing
        # path wins. HALT if neither resolves.
        atomicWriteProbeOrder:
          - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
          - '{project-root}/src/shared/scripts/skf-atomic-write.py'
        # Resolve `{deriveAssemblyShapeHelper}` the same way — used in §1 to decide
        # whether this is a whole-language reference (registry-corpora prose retained
        # as a Language Guide) or a standard skill (unchanged behaviour).
        deriveAssemblyShapeProbeOrder:
          - '{project-root}/_bmad/skf/shared/scripts/skf-derive-assembly-shape.py'
          - '{project-root}/src/shared/scripts/skf-derive-assembly-shape.py'
        ---
        
        <!-- Config: communicate in {communication_language}. -->
        
        # Step 3c: Fetch Remote Documentation
        
        ## STEP GOAL:
        
        Fetch remote documentation from brief-specified URLs using whatever web fetching capability is available in the agent's environment, extract API information, and add T3-confidence content to the extraction inventory. Tool-agnostic — the agent uses Firecrawl, WebFetch, web-reader, curl, or any available web tool.
        
        ## Rules
        
        - No tier gate — runs at any tier when `doc_urls` are present in the brief
        - Tool-agnostic — use whatever web fetching capability is available
        - Do not halt the workflow if web fetching is unavailable or fails
        - Do not override existing T1, T1-low, or T2 extraction data with T3 content
        - Never delete the staging directory of a `docs-only` skill — the fetched pages are its only source corpus
        
        ## MANDATORY SEQUENCE
        
        ### 1. Check Eligibility
        
        Evaluate the following conditions. **If the condition fails, skip silently to section 7 (auto-proceed) with no output:**
        
        1. **`doc_urls` is present in the brief data:** Check that `doc_urls` contains at least one URL entry from step 1 context. If `doc_urls` is absent or empty, skip silently.
        
        No tier gate — if `doc_urls` are present, this step runs at Quick, Forge, and Deep tiers alike.
        
        **Determine assembly shape (whole-language gate).** Resolve `{deriveAssemblyShapeHelper}` from `{deriveAssemblyShapeProbeOrder}` (first existing path wins) and run it on the brief:
        
        ```bash
        uv run {deriveAssemblyShapeHelper} {brief_path}
        ```
        
        If the result's `assembly_shape` is `whole-language-reference` (the brief carries ≥1 `doc_urls` entry with `source: language-registry` — a compiler/interpreter repo enriched with the language's canonical prose), set the in-context flag `whole_language_reference: true`. This changes ONLY how the registry-sourced corpora are handled below (§4a): their prose is retained as a **Language Guide** rather than shredded into per-export items. For every other brief the flag is false and this step behaves exactly as before — no change to ordinary skills.
        
        ### 2. Security Notice
        
        Display an informational notice (not a gate — the user already approved these URLs in the brief):
        
        "**Documentation fetch:** The following external URLs will be fetched:
        {for each URL: `- {label}: {url}`}
        
        Content fetched from external URLs is classified as **T3** (external, untrusted) and cited as `[EXT:{url}]`."
        
        ### 3. Fetch Documentation
        
        **Discover available web fetching capability.** Try tools in any order — use whatever is accessible in the current environment (e.g., Firecrawl scrape, WebFetch, web-reader, MCP fetch, curl, browser tools). If no web fetching capability can be found:
        
        - Log warning: "No web fetching capability available in this environment. Skipping documentation fetch."
        - Skip to section 7 (auto-proceed).
        
        **For each URL in `doc_urls`:**
        
        - Fetch the content at `{url}` as clean markdown using the discovered web tool.
        - **If fetch succeeds:** Store the markdown content with the URL as provenance source.
        - **If fetch fails:** Log warning: "Failed to fetch {url}: {reason}. Skipping." Continue with remaining URLs.
        
        **Subpage discovery (root URL detection):**
        
        After fetching a URL, apply the following heuristic to detect documentation root pages that contain no useful API content. This is common with modern documentation sites (Mintlify, Docusaurus, ReadTheDocs, GitBook) that render API content on subpages.
        
        **Root page detection — apply only when the URL path ends in `/`, `/index`, `/index.html`, has no path component (bare domain), or has 1 path segment (e.g., `/docs`). For deeper URL paths (2+ segments like `/api/reference`), skip this heuristic and keep the content as-is.**
        
        Subpage discovery is triggered if **either** of the following independent triggers fires:
        
        **Trigger 1 — Content-based (both conditions must be true):**
        
        1. **Zero API content indicators:** The fetched markdown contains none of: fenced code blocks (`` ``` ``), parameter tables (`|---|`), or function signature patterns (`def `, `function `, `fn `, `func `, `export `).
        2. **High link density:** More than 70% of non-empty lines are markdown links (matching `[text](url)` with no other substantive content on the line).
        
        **Trigger 2 — URL-based (independent of content analysis):**
        
        The URL matches the path criteria above (ends in `/`, bare domain, or 1 segment) AND the fetched content is under **2000 words**. Short content on root-like URLs almost certainly indicates a navigation hub or landing page, even if it contains introductory code examples that would prevent Trigger 1 from firing. This handles modern doc sites (Mintlify, Docusaurus, GitBook) that include hero sections with code snippets on their root pages.
        
        If neither trigger fires, keep the page content as-is and do not trigger subpage discovery.
        
        **If a root URL with minimal content is detected:**
        
        1. **Attempt sitemap/map discovery:** Use whatever discovery tool is available:
           - Firecrawl: `firecrawl_map({url})` to discover all subpages
           - Manual: try fetching `{url}/sitemap.xml` and parsing URLs from it
           - Crawl: if a crawl tool is available, use it with depth=1 on the root URL
           - If no discovery tool is available, keep the root page content as-is and continue
        
        2. **Filter discovered URLs by relevance and origin:** Restrict candidates to the same **registrable domain** as the root URL — strip the URL down to its eTLD+1 (e.g., for root `https://docs.example.com/intro`, accept any subdomain of `example.com` such as `api.example.com` or `docs.example.com`, but reject `example.org` or `cdn.partner.io`). Cross-origin links must be discarded before any fetch. The same-registrable-domain rule prevents Mintlify/Docusaurus link clouds from pulling in tracking pixels, doc-site CDNs, or third-party embeds as if they were canonical docs. From the surviving same-domain candidates, select the most relevant pages by searching for API-related terms in the URL path or title (e.g., `api`, `reference`, `quickstart`, `setup`, `config`, `getting-started`, `guide`, `sdk`, `methods`, `functions`). Exclude pages that are clearly non-API content (e.g., `blog`, `changelog`, `pricing`, `about`, `careers`).
        
        3. **Fetch top subpages (in parallel):** Fetch up to **10** of the most relevant subpages **concurrently** — subpage fetches are independent and network-bound, so wall-clock benefits substantially from parallel execution. Bound concurrency to **4 in flight** at a time to stay polite to documentation hosts (Mintlify/Docusaurus typically allow more, but conservatism here protects against unrecognized rate limits).
        
           The parallel pattern depends on the fetch tool:
        
           - **LLM-driven tools** (Firecrawl `firecrawl_scrape`, `WebFetch`, MCP fetch, browser tools): issue up to 4 tool calls **in a single message**. The agent runtime executes parallel tool calls concurrently; collect results from the batch before issuing the next set of up to 4. Repeat until all up-to-10 subpages have been attempted or rate limiting halts the batch.
           - **Bash-driven tools** (`curl`, `wget`): use `xargs -P 4 -n 1` to fan out from a newline-separated subpage list. Example:
        
             ```bash
             printf '%s\n' "${subpages[@]}" | xargs -P 4 -n 1 -I {} bash -c '
               url="{}"
               safe=$(echo -n "$url" | sha256sum | cut -c1-12)
               curl -sSL --max-time 30 "$url" > "{staging}/subpage-$safe.md" \
                 || echo "fetch failed: $url" > "{staging}/subpage-$safe.md"
             '
             ```
        
           For each subpage (regardless of tool):
           - Use the same web fetching tool as the root URL
           - Store with the subpage URL as provenance: `[EXT:{subpage-url}]`
           - If a subpage fetch fails, skip it and continue with the rest of the batch — do not halt the whole stage
        
        4. **Rate limiting:** If rate limiting (HTTP 429) is encountered during subpage fetching, stop discovery for this root URL. Keep results collected so far. Log: "Subpage discovery stopped due to rate limiting." For the parallel-tool-call pattern, drop any not-yet-issued tool calls from subsequent batches; for the `xargs` pattern, interrupt the pipeline (set `--max-procs 0` is **not** a graceful stop — the simplest stop is to kill the xargs PID and let in-flight writers complete naturally).
        
        **If ALL URLs fail (including any subpage fetches):** Log warning: "No documentation could be fetched. Proceeding without T3 content." Skip to section 7 (auto-proceed).
        
        ### 4. Extract API Information from Fetched Content
        
        Parse the successfully fetched markdown for:
        
        - **Function/method signatures** and their parameters
        - **Return types** and data structures
        - **Configuration options** and their defaults
        - **Usage examples** and code snippets
        
        **Citation rule:** Every extracted item gets a T3 confidence citation: `[EXT:{url}]` where `{url}` is the source URL the item was extracted from.
        
        **No hallucination:** If information cannot be found in the fetched content, exclude it. Do not infer or fabricate API details.
        
        **Whole-language references — retain prose, do not shred (`whole_language_reference: true`):** For a whole-language reference the registry-sourced corpora (the guide/Book, the standard/library docs) ARE the product, not the compiler's internal exports. Reducing that prose to per-export signature items and then discarding it under the §5 "T3 never overrides T1" rule (the compiler's AST already owns names like `Vec`, `Option`, `HashMap`) would gut exactly the content the skill exists to teach. So for these briefs, skip §4a below for the registry corpora.
        
        ### 4a. Retain the Language Guide (whole-language references only)
        
        **Skip this section entirely unless `whole_language_reference: true`.** When it is true, for each `doc_urls` entry whose `source` is `language-registry`:
        
        - Do not reduce its fetched markdown to per-export items. Instead retain the cleaned prose as a Language-Guide entry `{url, label, prose}`, where `prose` is the substantive body (narrative, idioms, usage examples, conceptual reference) lightly trimmed of navigation/boilerplate, each block cited `[EXT:{url}]`.
        - Collect these into a `language_guide[]` context artifact, in `doc_urls` order.
        
        This artifact is a **distinct** carrier — it is not merged into the extraction inventory and is not subject to the §5 conflict rule, so the canonical prose survives intact into step 5 (compile), which foregrounds it as the skill's Language Guide. Non-registry docs (README-detected, homepage, Pages, docs-folder) still flow through §4's normal per-export extraction and the §5 merge unchanged.
        
        **If a registry corpus could not be fetched** (network failure), record it in `language_guide[]` as `{url, label, prose: null}` and warn — step 5 surfaces the gap rather than emitting a thin guide silently.
        
        ### 5. Build Doc-Fetch Inventory
        
        **Mode determines merge behavior:**
        
        - **`source_type: "docs-only"`** — The doc-fetch inventory IS the extraction inventory. It replaces the empty inventory from step 3, since there was no source code to extract from.
        - **`source_type: "source"` (supplemental mode)** — Merge T3 items into the existing extraction inventory from step 3.
        
        **Conflict rule:** T3 items never override existing T1, T1-low, or T2 items for the same export. When an export already has a higher-confidence entry, the T3 item is discarded — T3 has the lowest priority.
        
        **Language-Guide carve-out:** the `language_guide[]` artifact from §4a (whole-language references) is not part of the export inventory and is therefore not subject to this conflict rule — it carries no export key, so it cannot collide with a T1 compiler export and can never be pruned. It is passed separately into step 5, which renders it as the foregrounded Language Guide section. Only the per-export T3 items participate in the T1/T2/T3 merge.
        
        **Edge case — T1-zero supplemental mode:** If T1 extraction produced zero results and `doc_urls` are present in supplemental mode, T3 items should be used as the primary inventory since no T1 data exists to conflict with.
        
        **Aggregate totals for reporting:**
        - URLs fetched successfully vs. total
        - URLs that failed
        - T3 items extracted
        
        ### 5b. Index into QMD (Deep Tier Only)
        
        **If tier is not Deep:** Skip this section silently.
        
        **If tier is Deep and at least one URL was fetched successfully:**
        
        1. Write fetched markdown files to a staging directory: `_bmad-output/{skill-name}-docs/` — clear any previous contents first, so a corpus retained from an earlier docs-only run (step 5 below) does not mix with this run's pages.
        2. Index into QMD with atomic replace + rollback: if a `{skill-name}-docs` collection already exists, run `qmd collection remove {skill-name}-docs` first, then `qmd collection add {project-root}/_bmad-output/{skill-name}-docs/ --name {skill-name}-docs --mask "*.md"`. **If `qmd collection add` fails after a successful `remove`:** remove any matching `{skill-name}-docs` entry from `forge-tier.yaml` → `qmd_collections[]` to keep the registry consistent with QMD's actual state, warn in evidence-report, and skip the embed — docs enrichment degrades gracefully.
        3. Generate embeddings scoped to this collection (only if step 2 `add` succeeded): `qmd embed --collection {skill-name}-docs` (required for semantic `type:'vec'` and HyDE `type:'hyde'` sub-queries within the QMD `query` tool). If the installed `qmd` CLI does not accept `--collection`, gate the embed behind a freshness check: skip re-embedding if the existing `{skill-name}-docs` registry entry is within 24 hours, and log the skip in the evidence report to prevent unbounded batch-mode re-embedding.
        4. Register in forge-tier.yaml `qmd_collections` array — **acquire an exclusive `flock` on `{sidecar_path}/forge-tier.yaml.lock` for the read-modify-write** (see the locking pattern documented in step 3b §4). Write via `python3 {atomicWriteHelper} write --target {sidecar_path}/forge-tier.yaml`. If `flock` is unavailable, fall back to read-CAS-by-mtime (capture `st_mtime` before, re-check after; refuse to clobber if a concurrent run wrote in between).
        
        ```yaml
        - name: "{skill-name}-docs"
          type: "docs"
          source_workflow: "create-skill"
          skill_name: "{skill-name}"
          created_at: "{current ISO date}"
        ```
        
        5. Clean up the staging directory after indexing — **only when `source_type` is `"source"`**: `rm -rf {project-root}/_bmad-output/{skill-name}-docs/`. Note that this directory is the source path of the `{skill-name}-docs` collection registered in step 4; removing it is accepted for supplemental docs, whose T3 items already live in the extraction inventory. **When `source_type` is `"docs-only"`, keep the directory.** The fetched pages are the skill's only source corpus — there is no code tree — so deleting them would leave the just-registered collection with nothing to refresh from and nothing to verify citations against. Record the retained path in the evidence report.
        
        **If QMD indexing fails:** Warn: "QMD indexing of fetched docs failed. T3 items are still in the extraction inventory — enrichment will proceed without QMD-indexed docs." Continue.
        
        ### 6. Report
        
        Display:
        
        "**Documentation fetch complete.**
        **URLs processed:** {fetched}/{total}
        **T3 items extracted:** {count}
        **Confidence:** All doc-fetched items are T3 — `[EXT:{url}]` citations applied.
        {If docs-only mode: '**Mode:** Docs-only — all skill content is T3. source_authority: community'}
        {If docs-only mode AND tier is Deep: '**Docs corpus retained:** `_bmad-output/{skill-name}-docs/`{if the `{skill-name}-docs` collection was registered in §5b: ' — source path of QMD collection `{skill-name}-docs`'}'}
        
        Proceeding to enrichment..."
        
        ### 7. Auto-Proceed
        
        No user interaction. After the fetch completes or is skipped for any reason, load `{nextStepFile}`, read it fully, then execute it.
        
      • fetch-temporal.md 16.3 KB
        ---
        nextStepFile: 'fetch-docs.md'
        # Resolve `{atomicWriteHelper}` by probing `{atomicWriteProbeOrder}` in order
        # (installed SKF module path first, src/ dev-checkout fallback); first existing
        # path wins. HALT if neither resolves.
        atomicWriteProbeOrder:
          - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
          - '{project-root}/src/shared/scripts/skf-atomic-write.py'
        ---
        
        <!-- Config: communicate in {communication_language}. -->
        
        # Step 3b: Fetch Temporal Context
        
        ## STEP GOAL:
        
        To fetch temporal context (issues, PRs, changelogs, release notes) from the source repository and index it into a QMD collection for Deep tier enrichment. This ensures step 4 has historical data to search when annotating extracted functions with T2 provenance.
        
        ## Rules
        
        - Deep tier only — Quick, Forge, and Forge+ tiers skip this step entirely and silently
        - GitHub repositories only — other source types degrade gracefully
        - Do not halt the workflow if fetching or indexing fails
        - Do not modify extraction data from step 3 — this step only creates QMD collections
        
        ## MANDATORY SEQUENCE
        
        ### 1. Check Eligibility
        
        Evaluate the following conditions sequentially. **If ANY condition fails, skip silently to section 5 (auto-proceed) with no output:**
        
        1. **Tier is Deep:** If tier is Quick, Forge, or Forge+, skip silently.
        2. **Source is GitHub:** Verify `source_repo` is a GitHub URL (`https://github.com/...`) or `owner/repo` format. If the source is a local path, a non-GitHub URL, or any other format, attempt GitHub remote detection (section 1b) before skipping.
        3. **`gh` CLI is available:** Run `timeout 10s gh auth status` to verify the CLI is installed and authenticated (the short timeout protects against a misconfigured network or hung auth helper blocking the workflow). If it fails or times out, skip silently.
        
        All three conditions must pass to proceed to section 2.
        
        ### 1b. GitHub Remote Detection for Local Sources
        
        **Only runs when condition 2 above fails because `source_repo` is a local path.**
        
        Local repositories that are clones of GitHub repos contain temporal context (issues, PRs, releases) accessible via `gh`. Detect this automatically:
        
        1. Check if the local path is a git repository: `git -C "{source_repo}" rev-parse --is-inside-work-tree`
        2. If not a git repo: skip silently to section 5 (current behavior).
        3. Extract the origin remote: `git -C "{source_repo}" remote get-url origin`
        4. If the remote URL contains `github.com`:
           - Extract `owner/repo` from the remote URL (strip `.git` suffix, handle both HTTPS and SSH formats)
           - Log: "**Local source with GitHub remote detected:** {owner}/{repo} — fetching temporal context."
           - Use the extracted `owner/repo` for all `gh` API calls in sections 3-4. Continue to condition 3 (gh CLI check).
        5. If no remote, or remote is not GitHub: skip silently to section 5 (current behavior).
        
        ### 2. Check Cache (Skip If Fresh)
        
        Read `forge-tier.yaml` from the sidecar path.
        
        - Look for a `qmd_collections` entry where `skill_name` matches the current brief AND `type` is `"temporal"`.
        - If found AND `created_at` is within the last **7 days** (rationale: temporal context — issues, PRs, changelogs — rarely changes meaningfully on shorter horizons; a 7-day window balances freshness against re-fetch cost and GitHub rate limits): the temporal collection is fresh. Display:
        
        "**Temporal context: cached.** Collection `{skill-name}-temporal` is fresh ({days} days old). Skipping re-fetch."
        
        Skip to section 5 (auto-proceed).
        
        - If not found OR `created_at` is older than 7 days: continue to section 3.
        
        ### 3. Fetch Temporal Context
        
        Create a staging directory: `_bmad-output/{skill-name}-temporal/`
        
        Resolve the `owner` and `repo` from `source_repo` (e.g., `acme/toolkit` from `https://github.com/acme/toolkit`).
        
        Execute the four fetches below **in parallel** — they are independent and the network round-trips dominate wall-clock. Background each, then `wait` for the batch. **If any individual fetch fails, log a warning and continue with the others.** The 4-concurrent fan-out is well under GitHub's authenticated REST rate limit (5000/hr); no bounded-concurrency guard is needed for this set.
        
        ```bash
        mkdir -p {staging}
        # 1. Issues (last 100)
        ( gh issue list -R {owner}/{repo} --state all --limit 100 \
            --json number,title,state,labels,createdAt,closedAt,body \
            | jq -r '...' > {staging}/issues.md ) &
        # 2. Merged PRs (last 100)
        ( gh pr list -R {owner}/{repo} --state merged --limit 100 \
            --json number,title,mergedAt,labels,body \
            | jq -r '...' > {staging}/prs.md ) &
        # 3. Release tags only (the per-tag fetch loop runs sequentially below to
        #    preserve append-immediately crash-resume semantics for releases.md)
        ( gh release list -R {owner}/{repo} --limit 10 \
            --json tagName,name,publishedAt > {staging}/.release-tags.json ) &
        # 4. Changelog (404 is silent skip — note `set +e` so the subshell doesn't
        #    propagate `gh api`'s non-zero exit on missing file)
        ( set +e
          gh api repos/{owner}/{repo}/contents/CHANGELOG.md --jq '.content' \
            | base64 -d > {staging}/changelog.md 2>/dev/null
          [ -s {staging}/changelog.md ] || rm -f {staging}/changelog.md ) &
        wait
        ```
        
        Per-call rationale:
        
        1. **Issues (last 100):** 100 is `gh issue list`'s default max-per-page; one paginated call captures recent activity without extra round trips or rate-limit pressure. Output → `{staging}/issues.md` formatted as a markdown document with one section per issue.
        
        2. **Merged PRs (last 100):** Same 100-per-page convention as issues — captures the most recent merges in one API call. Output → `{staging}/prs.md`.
        
        3. **Release tags + per-release fetches (last 10):** Release notes accumulate slowly relative to issues/PRs; the most recent 10 tags cover roughly the last 6-18 months of changelog-relevant history for typical OSS projects, which is enough context for T2-past annotations without fanning out to dozens of `gh release view` calls.
        
           **Note:** `gh release list --json` does **not** support the `body` field. The parallel block above fetches tags only (Step 1). After `wait`, run **Step 2 sequentially** to preserve the append-immediately crash-resume contract:
        
           If `{staging}/.release-tags.json` is empty (no releases), skip Step 2 and omit the releases section entirely. Otherwise:
        
           ```bash
           # Sequential per-tag loop. Iterate the JSON tag array verbatim so a
           # crash mid-loop leaves a partial-but-well-formed releases.md on disk.
           echo "# Releases (partial if interrupted)" > {staging}/releases.md
           ERR_FILE="{staging}/.gh-release-err"  # per-run stderr capture inside staging
           jq -r '.[].tagName' {staging}/.release-tags.json | while IFS= read -r tag; do
             if gh release view "$tag" -R {owner}/{repo} \
                  --json tagName,name,publishedAt,body 2>"$ERR_FILE"; then
               jq -r '...' >> {staging}/releases.md  # one ## {tag} block per release
             else
               echo "## $tag — fetch failed: $(cat "$ERR_FILE")" \
                 >> {staging}/releases.md
             fi
           done
           rm -f "$ERR_FILE"
           ```
        
           Failed individual fetches get a one-line placeholder; the loop continues with remaining tags. If a rate limit (HTTP 429) is hit, stop the release loop, keep the partial `releases.md` file in place (do not delete it), and log: "Release fetch stopped at tag {N}/{total} due to rate limiting — partial releases.md retained."
        
           **Why sequential here when the rest is parallel:** the append-per-release pattern guarantees that a mid-loop abort (rate limit, network drop, user interrupt) leaves a partial but well-formed `releases.md` with every release fetched so far. Parallel writers appending to the same file would need file locking and per-writer ordering — the simpler sequential loop is robust for free, and 10 release fetches contribute only ~5-10s of the total wall-clock.
        
        4. **Changelog:** `CHANGELOG.md` or `RELEASES.md` at the repository root. The parallel block above writes to `{staging}/changelog.md` only when the file exists; non-existence (404 from `gh api`) leaves no file behind.
        
        #### 3b. Targeted Function Searches (Uses Extraction Inventory)
        
        After the generic fetches above, perform **targeted searches** using the top-level public API function names from `extraction_inventory.top_exports[]`. This produces high-signal results that generic list fetches miss.
        
        **Short-circuit on empty `top_exports`:** If `extraction_inventory.top_exports` is missing or `== []` (docs-only mode, or a source extraction that produced zero public exports), skip this sub-section entirely with a one-line log: "No exports in inventory — skipping targeted function searches." The generic fetches from §3 remain in place and continue to provide baseline temporal context.
        
        **Limit:** Search the top **10 function names** maximum to control API call volume and avoid `gh` rate limiting. (rationale: 10 targeted searches + generic fetches from §3 stays well under GitHub's unauthenticated search rate limit of 10 requests/minute and authenticated 30/minute; matches the `top_exports[]` size emitted by step 3 §5 so every tracked export gets one search.)
        
        For each function name in `top_exports[]` (up to 10), **sanitize first**: strip every character that is not in `[A-Za-z0-9_]` from `function_name` to produce `safe_name`. This prevents shell injection and `gh` query parser errors when an export name contains punctuation (e.g., `<T>`, `.method`, `::namespace`, quotes). If `safe_name` is empty after sanitization (the original was entirely punctuation — rare but possible for symbol exports), fall back to piping the original name through stdin via `--query-from-file -`-style indirection if your `gh` version supports it; otherwise skip that one entry with a log line — never substitute the unsanitized name back into the shell command.
        
        **Parallel fan-out (bounded concurrency = 5):** sanitized searches are independent and benefit from parallelism. Use `xargs -P 5` so at most 5 `gh search` calls are in flight at once — this stays well under GitHub's authenticated search rate limit (30/min) while saving ~5-8s of wall-clock on a top-10 export list compared to fully sequential.
        
        ```bash
        # 1. Sanitize and write one safe_name per line; skip empties.
        #    Python is the canonical sanitizer because awk/sed regex semantics
        #    vary across platforms.
        jq -r '.top_exports[]' {extraction_inventory.json} \
          | python3 -c 'import sys,re
        for line in sys.stdin:
          s = re.sub(r"[^A-Za-z0-9_]", "", line.strip())
          if s: print(s)' > {staging}/.safe-names.txt
        
        # 2. Fan out to gh search, 5 in flight. Each writer emits a self-contained
        #    section to its own per-name file; we concatenate after the wait.
        # --limit 5: top-5 issues per function keeps signal-to-noise high and caps
        #    total response size across 10 fan-outs at 50 issues.
        mkdir -p {staging}/targeted
        cat {staging}/.safe-names.txt | xargs -P 5 -I {} bash -c '
          gh search issues --repo {owner}/{repo} "{}" --limit 5 \
              --json number,title,state,body 2>/dev/null \
            | jq -r ". | \"## {}\\n\" + (...)" > {staging}/targeted/{}.md \
          || echo "## {} — fetch failed" > {staging}/targeted/{}.md
        '
        
        # 3. Concatenate in stable order (alpha by safe_name) into the single
        #    aggregated file the rest of the workflow expects.
        sort {staging}/.safe-names.txt | while IFS= read -r name; do
          cat "{staging}/targeted/$name.md"
        done > {staging}/targeted-issues.md
        ```
        
        Aggregate all targeted search results into a single file: `{staging}/targeted-issues.md`. The per-name temp directory `{staging}/targeted/` can be removed after concat — it exists only to give each parallel writer an isolated output stream.
        
        **If `gh search` is unavailable** (older `gh` CLI versions): skip targeted searches silently. The generic fetches from section 3 still provide baseline temporal context.
        
        **If rate limiting occurs** (HTTP 429 or similar): stop targeted searches immediately, keep results collected so far. Log: "Targeted search stopped at function {N}/{total} due to rate limiting."
        
        **After all fetching,** verify at least one file was written to the staging directory. If the staging directory is empty (all fetches failed), log a warning and skip to section 5.
        
        ### 4. Index Into QMD & Register
        
        **Index the staging directory:**
        
        If a `{skill-name}-temporal` collection already exists, remove and recreate for atomic replace. **Wrap the remove + add pair with rollback on `add` failure** — a `remove` that succeeds followed by an `add` that fails must not leave the registry claiming a collection that no longer exists in QMD:
        
        ```bash
        qmd collection remove {skill-name}-temporal
        if ! qmd collection add {project-root}/_bmad-output/{skill-name}-temporal/ --name {skill-name}-temporal --mask "*.md"; then
          # add failed after remove succeeded — the collection is gone from QMD. Clean the registry too.
          # Remove any {skill-name}-temporal entry from forge-tier.yaml qmd_collections[].
          # Warn the user, do not fail the workflow (temporal enrichment degrades gracefully).
          echo "WARN: qmd add failed after remove — registry entry for {skill-name}-temporal removed to keep forge-tier.yaml consistent with QMD state."
          # [skip the embed step]
        else
          qmd embed --collection {skill-name}-temporal
        fi
        ```
        
        **Rollback rule:** if the `qmd collection add` step fails (non-zero exit, network error, parse error) after the prior `remove` succeeded, remove the canonical registry entry in `forge-tier.yaml` to match QMD's actual state. A dangling registry entry that points at a non-existent QMD collection poisons subsequent cache-hit checks in §2. Emit a warning in evidence-report and skip the embed — enrichment degrades to no-QMD for this run.
        
        **Scope the embed:** Always pass `--collection {skill-name}-temporal` to `qmd embed`. An unscoped `qmd embed` re-embeds every collection in the QMD store, which can take minutes per run in batch mode and generates wasteful GPU/API cost. If the installed `qmd` CLI does not accept `--collection` (older upstream versions), gate the embed behind a per-skill check: if a previous `{skill-name}-temporal` entry already exists in `qmd_collections` and its `created_at` is within 24 hours, skip the embed entirely and warn "qmd embed skipped — upstream qmd lacks --collection scope; re-embedding all collections would be wasteful in batch mode". Log the skip in the evidence report.
        
        **Note:** `qmd embed` generates vector embeddings required for semantic (`type:'vec'`) and HyDE (`type:'hyde'`) sub-queries inside the QMD `query` tool. Without embeddings, only BM25 (`type:'lex'`) keyword search works. Run `qmd embed` after every `qmd collection add`.
        
        **Update the registry** in `forge-tier.yaml` under a file lock to prevent concurrent batch runs from clobbering each other's entries:
        
        1. Acquire an exclusive `flock` on `{sidecar_path}/forge-tier.yaml.lock` (create the lock file if absent). Use `flock -x {lockfile} -c "..."` or an equivalent `fcntl.flock(LOCK_EX)` guard.
        2. Read the current `forge-tier.yaml`, capturing its `st_mtime` as `mtime_before`.
        3. Perform the read-modify-write below.
        4. Write via `python3 {atomicWriteHelper} write --target {sidecar_path}/forge-tier.yaml`.
        5. Release the flock.
        
        **Fallback when `flock` is unavailable:** re-stat the file after the write; if the on-disk `st_mtime` is newer than `mtime_before` by more than this run's own write timestamp, halt with "forge-tier.yaml modified mid-update by another process — refusing to clobber. Re-run after the other run completes." This read-CAS-by-mtime is the belt-and-braces safety net for environments without `flock`.
        
        If an entry with `name: "{skill-name}-temporal"` already exists in `qmd_collections`, replace it. Otherwise, append:
        
        ```yaml
          - name: "{skill-name}-temporal"
            type: "temporal"
            source_workflow: "create-skill"
            skill_name: "{skill-name}"
            created_at: "{current ISO date}"
        ```
        
        **Clean up** the staging directory after successful indexing:
        
        ```bash
        rm -rf {project-root}/_bmad-output/{skill-name}-temporal/
        ```
        
        **Error handling:**
        
        - If QMD indexing fails: log the error, note that temporal enrichment will be unavailable. Do not fail the workflow.
        - If registry update fails: log the error, continue. The collection may exist in QMD even if the registry entry failed.
        - If cleanup fails: log a warning and continue.
        
        Display brief confirmation:
        
        "**Temporal context indexed.** Collection `{skill-name}-temporal` created ({file_count} files: {list files}). Proceeding to enrichment..."
        
        ### 5. Auto-Proceed
        
        No user interaction. After temporal context is fetched and indexed (or skipped for any reason — non-Deep tier, non-GitHub source, cache hit, or any failure), load `{nextStepFile}`, read it fully, then execute it.
        
        
    • authoritative-files-protocol.md 10.5 KB
      # Authoritative Files Protocol
      
      ## Overview
      
      Loaded on demand when step 3's `### 2a. Discovered Authoritative Files Protocol` sub-step runs.
      
      **Skip this protocol entirely if `source_type: "docs-only"`** — there is no source tree to scan.
      
      Before resolving source access for extraction, scan the source tree for **authoritative AI documentation files** that the brief's scope filters excluded. Project authors increasingly add files specifically written to steer AI assistants (`llms.txt`, `AGENTS.md`, `.cursorrules`, etc.), and these files often contain the **canonical** install command, quick-start, or architecture summary — information that nowhere else in the source tree provides. A brief authored from a scan of `src/**` will frequently exclude these files without the author realizing they exist.
      
      This protocol detects such files, prompts the user, and records the decision in the brief so future runs (re-create, update, audit) honor it.
      
      **Heuristic scan list (handled by the helper — listed here for reference):** case-insensitive basename match, any directory depth, on `llms.txt`, `llms-full.txt`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `COPILOT.md`, `.cursorrules`, `.windsurfrules`, `.clinerules`.
      
      ## Procedure
      
      1. **Resolve, scan, classify, and load previews in one call.** The helper handles the source-tree walk (pruning `node_modules`, `dist`, `.git`, etc.), case-insensitive basename matching, scope-filter diff (with `**`-recursive-glob support), amendment reconciliation (most-recent action wins), preview load (first 20 lines), and SHA-256 hashing:
      
         ```bash
         uv run {resolveAuthoritativeFilesHelper} resolve \
             --source-root {source_root} \
             --brief {forge_data_folder}/{skill_name}/skill-brief.yaml \
             [--preview-lines 20]
         ```
      
         The helper emits one envelope with three buckets:
      
         ```json
         {
           "status": "no-candidates" | "candidates-found",
           "summary": {
             "candidates_total": N, "already_in_scope_count": N,
             "pre_decided_count": N, "unresolved_count": N
           },
           "already_in_scope": [
             {"path": "...", "heuristic": "...", "size_bytes": N,
              "line_count": N, "content_hash": "sha256:..."}
           ],
           "pre_decided": [
             {"path": "...", "heuristic": "...",
              "prior_action": "promoted"|"skipped",
              "should_add_to_promoted_docs": <bool>,
              "size_bytes": N|null, "line_count": N|null,
              "content_hash": "sha256:..."|null}
           ],
           "unresolved": [
             {"path": "...", "heuristic": "...", "size_bytes": N,
              "line_count": N, "content_hash": "sha256:...",
              "preview": "<first N lines>",
              "excluded_by_pattern": "<glob>"|"not matched by any scope.include"}
           ]
         }
         ```
      
      2. **Apply the helper's classification:**
      
         - **`already_in_scope[]`** — file is in scope and not skipped. **Remove the path from §2's filtered file list** and append the record to in-context `promoted_docs[]`. No prompt — authoritative docs must never reach §4 code extraction even when scope.include matches.
         - **`pre_decided[]` with `prior_action: "promoted"`** — amendment says promoted; **append to `promoted_docs[]`** using the helper-supplied hash/size/lines. Deterministic replay path.
         - **`pre_decided[]` with `prior_action: "skipped"`** — user previously declined. Do nothing. Move on.
         - **`unresolved[]`** — proceed to step 3 below (user prompt).
      
      3. **Prompt.** Present each `unresolved[]` candidate to the user. Use the helper's `preview`, `size_bytes`, `line_count`, and `excluded_by_pattern` fields verbatim so the prompt reports facts rather than recomputing them:
      
         ```
         **Discovered authoritative file excluded by brief scope**
      
         Path: {relative_path_from_source_root}
         Size: {line_count} lines, {bytes} bytes
         Matched heuristic: {basename}
         Excluded by pattern: {matching_exclude_pattern or "not matched by any scope.include"}
      
         First 20 lines:
         {inline preview}
      
         This file is typically authored for AI assistants and may contain canonical usage information not present elsewhere in the source. How should extraction handle it?
      
         [P] Promote — include in this extraction run AND amend brief for future runs
         [S] Skip    — honor the brief exclusion AND record skip in amendments (no re-prompt)
         [U] Update  — halt this run and return to skf-brief-skill to refine scope
         ```
      
      4. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate. Record amendment entries with `action: "skipped"` and `reason: "headless: no user to prompt"`. A non-interactive run must never silently promote files into scope — the decision requires a human.
      
      5. **Apply decision:**
      
         - **[P] Promote:**
           1. **Do not add the path to the filtered file list from §2.** Authoritative documentation files are not code — they must not go through the AST extraction pipeline in §4, which would silently produce no exports (ghost entries). Instead, add the path to a new in-context list `promoted_docs[]` with `{path, heuristic, size_bytes, line_count, content_hash}`. Compute the SHA-256 content hash of the file now.
           2. Append to `brief.scope.include`: add the exact `candidate.path` as a literal glob (no wildcards — the amendment targets this specific file). This write ensures that a re-run of `skf-create-skill` against the amended brief sees the path in scope and skips re-prompting.
           3. Append to `brief.scope.amendments[]` a new entry with `action: "promoted"`, `path: candidate.path`, `reason: {user-provided one-sentence reason or auto-generated "authoritative AI docs — matched heuristic {basename}"}`, `heuristic: {basename}`, `date: {today ISO}`, `workflow: "skf-create-skill"`.
           4. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Immediate write (not deferred to step 7) ensures a crashed run still leaves the amendment recorded. Preserve all other brief fields and formatting. **Use atomic write + backup:** before writing, copy the original brief to `{forge_data_folder}/{skill_name}/skill-brief.yaml.bak` (overwriting any prior `.bak` — the most recent pre-amendment snapshot is the useful one). Then pipe the amended YAML through the shared atomic writer so a crash mid-write cannot corrupt the brief:
      
              ```bash
              # 1. Backup
              cp {forge_data_folder}/{skill_name}/skill-brief.yaml \
                 {forge_data_folder}/{skill_name}/skill-brief.yaml.bak
      
              # 2. Atomic write (stdin → tmp → fsync → rename)
              cat <<'AMENDED_YAML' | python3 {atomicWriteHelper} write \
                  --target {forge_data_folder}/{skill_name}/skill-brief.yaml
              {amended brief YAML}
              AMENDED_YAML
              ```
      
              The helper stages into `{brief}.skf-tmp`, fsyncs, then `os.replace()`s — readers never see a half-written brief.
           5. Display: "**Promoted `{path}`** — tracked as documentation file, amendment recorded."
      
         - **[S] Skip:**
           1. Do not modify `scope.include` or `scope.exclude`.
           2. Append to `brief.scope.amendments[]` a new entry with `action: "skipped"`, `path: candidate.path`, `reason: {user-provided reason or auto-generated "user declined promotion at create-skill §2a"}`, `heuristic: {basename}`, `date: {today ISO}`, `workflow: "skf-create-skill"`.
           3. **Write the amended brief back to disk** so future runs do not re-prompt. Use the same backup-then-atomic-write pattern as the [P] Promote path (copy to `skill-brief.yaml.bak` first, then pipe through `skf-atomic-write.py write --target {brief_path}`).
           4. Display: "**Skipped `{path}`** — decision recorded in amendments."
      
         - **[U] Update:**
           1. Halt the workflow immediately.
           2. Display: "**Halting create-skill.** Re-run `skf-brief-skill` to refine the scope filters for `{skill_name}`, then re-run `skf-create-skill`. Decisions for previously prompted candidates were already persisted to the brief; the current candidate was not written."
           3. Exit with status `halted-for-brief-refinement`.
      
      6. **Summary.** After all candidates are resolved (or none were found), display a one-line summary:
      
         - `"Authoritative files scan: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments."`
         - If N = 0: `"Authoritative files scan: no candidates."`
      
      **Record for evidence report:** `authoritative_files_scan: {candidates: N, promoted: P, skipped: S, pre_decided: A, decisions: [{path, action, heuristic, reason}]}` — step 7 includes this in `evidence-report.md`.
      
      ## How promoted docs reach the provenance map
      
      Promoted docs do not flow through §4 code extraction. Instead:
      
      1. §2a populates the in-context `promoted_docs[]` list with content hashes.
      2. **Step-05 §6** (provenance-map assembly) reads `promoted_docs[]` and emits one `file_entries[]` entry per promoted doc with `file_type: "doc"`, `extraction_method: "promoted-authoritative"`, `confidence: "T1-low"`, and the pre-computed `content_hash`.
      3. **Step-07 §2** does not copy doc files into the skill package (unlike scripts and assets). The source file remains at its original path; only the provenance map tracks it. Future audit and update workflows compare against this tracking entry via content hash — no file copy is required because the intent is drift detection on the *source*, not bundling documentation into the skill output.
      
      **Re-running `skf-create-skill`** reads the amended brief. Files with `action: "promoted"` amendments already appear in `scope.include`, but §2a still runs — it detects the file is in scope AND has an existing amendment, and takes the "pre-decided" silent path. The `promoted_docs[]` list is rebuilt on each run by scanning amendments with `action: "promoted"` (this is the deterministic replay path).
      
      ## Downstream workflow consumption
      
      Zero code changes required in consumer workflows:
      
      - **`skf-update-skill`** reads `provenance-map.json`. Promoted docs appear as `file_entries[]` entries. Update-skill Category D (script/asset file changes) iterates `file_entries` and compares content hashes — this works identically for `file_type: "doc"` entries, giving drift detection for free.
      - **`skf-audit-skill`** scans files from `provenance-map.json`. The re-index builds its list from `entries[].source_file ∪ file_entries[].source_file`, so promoted doc paths are naturally included in the audit scan.
      
      The brief is the single source of truth for authored scope intent. The provenance map is the single source of truth for extracted state. `scope.amendments[]` is the bridge that records when those two intentionally diverged. `promoted_docs[]` is the in-memory handoff from §2a to step 5 §6; it is not persisted — the persisted form is the `file_entries[]` list in provenance-map.json.
      
    • compile.md 22.9 KB
      ---
      nextStepFile: 'step-doc-sources.md'
      skillSectionsData: 'assets/skill-sections.md'
      assemblyRulesData: 'assets/compile-assembly-rules.md'
      # Resolve `{renderMetadataStatsHelper}` by probing `{renderMetadataStatsProbeOrder}`
      # in order (installed SKF module path first, src/ dev-checkout fallback); first
      # existing path wins. HALT if neither resolves — the metadata `stats` block and
      # `confidence_distribution` are computed values that must not be hand-binned
      # (the historical 147 ≠ 59 miscount lived exactly here).
      renderMetadataStatsProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-render-metadata-stats.py'
        - '{project-root}/src/shared/scripts/skf-render-metadata-stats.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5: Compile
      
      ## STEP GOAL:
      
      To assemble the complete skill content from the extraction inventory and enrichment annotations — building SKILL.md sections, context-snippet.md, metadata.json, and references/ content according to the agentskills.io format.
      
      ## Rules
      
      - Focus only on assembling content from extraction inventory + enrichment
      - Do not include any content without a provenance citation
      - Write all compiled artifacts to the staging directory `_bmad-output/{skill-name}/`. Do not write to `skills/` or `forge-data/` — step 7 promotes staged artifacts to their final versioned locations.
      - Do not fabricate examples not found in source tests or docs
      - Seed `<!-- [MANUAL] -->` markers for future update-skill compatibility
      
      ## MANDATORY SEQUENCE
      
      ### 1. Load Data Files
      
      Load `{skillSectionsData}` and `{assemblyRulesData}` completely. These define the agentskills.io-compliant format and detailed assembly rules for all output artifacts.
      
      ### 1a. Create Staging Directory
      
      Create `_bmad-output/{skill-name}/` (and `_bmad-output/{skill-name}/references/`). All artifacts produced in sections 2–7 below are written here:
      
      - `SKILL.md`
      - `context-snippet.md`
      - `metadata.json`
      - `references/*.md`
      - `provenance-map.json`
      - `evidence-report.md`
      
      This is the `<staging-skill-dir>` referenced by step 6 (`npx skill-check check`, `npx -y tessl skill review`). Step-07 reads from the in-context copies (resynced by step 6 after any `--fix` modifications) and writes to the final versioned layout.
      
      ### 1b. Signature Fidelity Rule
      
      **When assembling function signatures, parameter lists, and return types in any SKILL.md section or reference file:**
      
      - **T1 provenance-map entries (AST-extracted) are authoritative** for: function name, parameter names, parameter types, parameter order, return type, and optionality markers (e.g., `?`, `Optional`, `= default`).
      - **T2 (QMD-enriched) and T3 (doc-derived) sources may add** contextual descriptions, usage notes, behavioral documentation, and examples to function entries, but must not replace structural signature data from T1 entries.
      - **On conflict:** If a T2/T3 source provides a different signature than the T1 extraction for the same export (e.g., different parameter count, different types, missing `Partial<>` wrapper), keep the T1 signature and log a warning in the evidence report: "Signature conflict for `{export_name}`: T1 shows `{t1_signature}`, T2/T3 shows `{other_signature}`. T1 used as authoritative."
      - **`signature_source` field:** Record `signature_source: "T1" | "T1-low" | "T2" | "T3"` in each provenance-map entry to indicate the highest-confidence tier that contributed the structural signature data (params, return_type). This enables test-skill to verify signature provenance.
      
      This rule applies to every section including Tier 1 Key API Summary, Tier 2 Full API Reference, and Section 4b Migration & Deprecation Warnings.
      
      ### 2. Build SKILL.md Content
      
      Assemble each section in order using the assembly rules data file (`{assemblyRulesData}`). The data file specifies frontmatter format, Tier 1 section details (Sections 1-8, including conditional Section 7b for scripts/assets), Tier 2 section details (Sections 9-11), and assembly ordering rules. Follow it exactly. Assemble Section 7b (Scripts & Assets) only if `scripts_inventory` or `assets_inventory` is non-empty.
      
      **Shape-specific overrides:** the assembly-rules file defines gated override blocks for `component-library`, `reference-app`, and **whole-language reference** skills. Apply the override whose gate matches this brief. A whole-language reference is gated on `assembly_shape: "whole-language-reference"` (a `doc_urls` entry with `source: language-registry`, as step 3c §1 determined and as `skf-derive-assembly-shape.py` reports) and uses the `language_guide[]` artifact step 3c §4a retained — it foregrounds the Language Guide and demotes compiler internals. When no override gate matches, assemble the standard library-export layout unchanged.
      
      ### 2a. Description Sanitization Pass
      
      **Before writing SKILL.md frontmatter to disk**, sanitize the assembled `description` string by replacing every `<` with `{` and every `>` with `}`. Apply this pass unconditionally to the final assembled description in context, then write the result to `SKILL.md`.
      
      **Why unconditional?** Both `skill-check`'s `description_field` validator and `tessl`'s deterministic description check parse the frontmatter `description` as a raw string — they reject any `<` or `>` regardless of whether the content is inside a backtick span or a generic expression. The previous rule exempted backticked content on the assumption that backticks protect from XML-tag parsing, but that assumption is false for these validators: a backticked TypeScript generic like `` `Meta<typeof X>` `` still fails tessl's check because tessl reads the raw string before markdown parsing. Unconditional replacement guarantees no angle brackets reach either validator.
      
      **Scope:** This rule applies **only** to the frontmatter `description` field. Body content, code examples, reference files, and assembly-rule documents retain their original angle brackets — they are parsed through the markdown AST where backticks do protect content.
      
      Record the count of substitutions in context as `description_sanitizations: {count}` for the evidence report.
      
      If a downstream tool re-introduces angle brackets into the description, step 6 §6 recovers via `description-xml-tags-guarded-upstream` in `assets/tessl-dismissal-rules.md`.
      
      ### 3. Build context-snippet.md Content
      
      Vercel-aligned indexed format for CLAUDE.md managed section (~80-120 tokens):
      
      ```markdown
      [{skill-name} v{version}]|root: skills/{skill-name}/
      |IMPORTANT: {skill-name} v{version} — read SKILL.md before writing {skill-name} code. Do NOT rely on training data.
      |quick-start:{SKILL.md#quick-start}
      |api: {top exports with () for functions, comma-separated}
      |key-types:{SKILL.md#key-types} — {inline summary of most important type values}
      |gotchas: {2-3 most critical pitfalls or breaking changes, inline}
      ```
      
      **Derivation rules:**
      
      - **version**: From source detection (reconciled in step 3), not brief default
      - **api**: Top 10 exports from extraction inventory, append `()` to function names
      - **key-types**: Inline summary of most important enum/type values from Key Types section
      - **gotchas**: Derived from T2-future annotations (breaking changes), async requirements, version-specific behavior changes. If no gotchas available, omit the gotchas line.
      - **Section anchors** (`#quick-start`, `#key-types`): Must match actual heading slugs in the assembled SKILL.md
      
      ### 4. Build metadata.json Content
      
      Following the structure from the skill-sections data file:
      - Populate all fields from brief_data, extraction inventory, and tier
      - Set `generation_date` to current ISO-8601 timestamp
      - Set `source_commit` from resolved source (if available)
      - Set `source_ref` from resolved source ref (tag name, branch, or `HEAD`; null if unavailable)
      - Set `scope_type` from the brief's `scope.type` value verbatim (`full-library`, `specific-modules`, `public-api`, `component-library`, `reference-app`, or `docs-only`). **Always emit this field.** `skf-test-skill` keys reference-app handling on `metadata.json.scope_type == "reference-app"` — both the scoring redistribution (Signature Accuracy / Type Coverage marked N/A) and the coverage-check §4b count-coherence skip. Omitting it silently mis-scores a reference-app skill as a library (zero-export barrel HALT or false metadata-drift findings). The value is informational for the other scope types (consumers only branch on `reference-app`), but emit it for all so the producer/consumer contract holds.
      - **Compute the `stats` block and `confidence_distribution` deterministically** with `{renderMetadataStatsHelper}` (resolve from `{renderMetadataStatsProbeOrder}`; first existing path wins). The helper owns all the arithmetic — binning each provenance entry once by its `signature_source` tier, summing the bins, and computing every coverage ratio — and returns the finished `stats` + `confidence_distribution` objects to write into `metadata.json` verbatim. Run `uv run {renderMetadataStatsHelper} --help` for the full contract. You supply only the judgment payload below.
      
        **Judgment payload (what you decide — passed as JSON on stdin):**
        - `exports_public_api`: count of exports from public entry points (`__init__.py`, `index.ts`, `lib.rs`, or equivalent) — derive this from step 3's entry-point validation (section 4b), not from the provenance-map entry count (which may be incomplete if extraction patterns missed some export types)
        - `exports_internal`: count of all other non-underscore-prefixed exports (internal modules, helpers, adapters)
        - `scripts` / `assets`: the `scripts_inventory` / `assets_inventory` arrays (or `[]` when empty) — the helper sets `stats.scripts_count` / `stats.assets_count` from their lengths
        - `effective_denominator` (**optional** — include in the helper's judgment payload only for the curated-subset shapes enumerated in condition 1 below): the count of public exports from files matched by the brief's authoring-surface globs, filtered by `scope.exclude`, resolved against `source_path`. **Prefer `scope.tier_a_include` when the brief supplies it** — that narrow list represents the authoring surface the brief intends to document; resolve its globs across `source_path` and count the union of **named exports** — items reachable from the language's public entry-point barrel (`lib.rs` `pub use`, `index.ts` / `index.js` re-exports, `__init__.py` exports), where a type counts **once** with its methods and impl-block members rolling up under it (count a `pub fn` only when it is a free function reachable from the barrel, never a method on an already-counted type). **Otherwise use `scope.include`** — the coarse list. This counting unit matches what `skf-test-skill` re-derives on the consumer side (`coverage-check.md` §2c excludes `kind: "method"` from `documented_set`), so the producer- and test-side denominators stay aligned; without it, a type-heavy API (a few handle types carrying hundreds of methods) inflates the denominator by an order of magnitude and auto-fails the coverage gate. This is the coverage denominator `skf-test-skill` uses when the brief documents a curated subset rather than a package's whole barrel, so it must match the brief's authoring intent. Compute when ALL of the following hold:
          1. The skill's coverage denominator is resolved by a `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition clause that actually consumes `effective_denominator` — **any one** of:
             - **(a) stratified-scope:** the source is a monorepo (detected via `packages/` layout, `workspaces` field in root `package.json`, `lerna.json`, `rush.json`, `nx.json`, or Cargo `[workspace]`);
             - **(b) single-crate curated subset:** `scope.type` is `specific-modules` on a non-monorepo repo;
             - **(c) multi-entry (exports-map):** the in-scope `package.json` declares an `exports` map with more than one non-root subpath, and the repo carries no monorepo markers.
      
             Arms (b) and (c) are not a widening for its own sake — both consumer clauses already say "prefer `metadata.json.stats.effective_denominator`", and a monorepo-only emit condition makes those two rungs unreachable, so a non-monorepo curated scope has no way to lock its denominator.
          2. `scope.type` is not `full-library` (and not `reference-app` — see carve-out below), AND the resolved include list (`tier_a_include` if present, else `scope.include`) lists a curated file/directory subset rather than the full workspace.
          3. `scope.notes` is present and documents the stratification strategy (e.g., a tiered A/B/C plan) — this serves as the intent marker confirming the subset is by design.
      
          Otherwise omit it from the payload entirely — when absent, `skf-test-skill` falls back to `exports_public_api`. See `skf-test-skill` `references/source-access-protocol.md` §Source API Surface Definition ("Stratified-scope monorepo packages", "Single-crate curated subset", "Multi-entry (exports-map) packages") for the test-side consumption rules.
      
          **A `public-api` scope on a single-package repo with no multi-subpath `exports` map matches none of the three arms** — that shape falls to the standard root-barrel rule, which honors neither `effective_denominator` nor `tier_a_include`. Emitting the field there has no effect on the test-side denominator, so omit it.
      
          **That shape has no brief-side denominator lever, and `scope.exclude` is not one.** The root-barrel rule counts the named exports of a single entry-point file (`index.ts` / `__init__.py` / `lib.rs`), so excluding *files* cannot remove exports from the barrel's own export list — the denominator is unchanged. When the brief genuinely documents a curated subset of a single package's surface (citation-only files alongside a narrow `tier_a_include`), express that with `scope.type: "specific-modules"` rather than `public-api`: the single-crate curated-subset clause in `skf-test-skill` `references/source-access-protocol.md` filters the barrel by `scope.include` / `scope.exclude` **and** honors `effective_denominator`, so condition 1 arm (b) applies and the denominator matches the authoring intent. Reserve `public-api` for the case where the whole root barrel is the intended surface.
      
          **Reference-app carve-out:** never emit `effective_denominator` for `scope.type: "reference-app"`, even when the three conditions above are literally satisfied (a reference-app-in-monorepo matches all of them). A reference app's coverage basis is `pattern_surfaces_documented`, not library exports — see the Reference-App stats semantics in `assets/compile-assembly-rules.md`.
      - **Shape:** pass `--shape reference-app` or `--shape stack` when the matching assembly-rules override applies (default `library`). For a reference app, also put `pattern_surfaces_documented` (the Pattern Surface row count) in the payload — the helper uses it as `exports_documented`, emits `stats.pattern_surfaces_documented`, and refuses to emit `effective_denominator`; the distribution then sums to the per-citation count, not to `exports_documented`. For a stack, put the own-barrel `exports_documented` (usually `0`) in the payload; the distribution sums to the cited-constituent count.
      - **Invoke** — stage `provenance-map.json` (§6) first, since the helper reads its `entries[]`:
      
        ```bash
        echo '{"exports_public_api": {N}, "exports_internal": {M}, "scripts": {scripts_inventory-or-[]}, "assets": {assets_inventory-or-[]}}' \
          | uv run {renderMetadataStatsHelper} <staging-skill-dir>/provenance-map.json
        ```
      
        Write the returned `stats` and `confidence_distribution` verbatim. The helper derives `exports_documented` (the documented-export count = provenance `entries[]` count), `exports_total` = `exports_public_api` + `exports_internal`, `public_api_coverage` = documented / public_api (`null` if public_api is 0), `total_coverage` = documented / total (`null` if total is 0), and bins `confidence_distribution.{t1, t1_low, t2, t3}` by each entry's `signature_source` — **each entry counted exactly once**, so the four bins provably sum to the documented-export count. This structurally prevents the recurring miscount where binning ~8 T2 annotations + ~80 T3 doc items on top of 59 exports produced a distribution summing to 147 ≠ 59 (`skf-test-skill` coverage-check §4b flags that sum as an internal-consistency defect). If the helper reports `coherence.ok: false`, some provenance entries carry a missing/unrecognized `signature_source` (or a file-entry count disagrees) — fix the provenance map, do not hand-edit the stats.
      - Set `description` from the SKILL.md frontmatter `description` field (already assembled in section 2)
      - Set `language` from source analysis (e.g., `"typescript"`, `"python"`) — use the primary language of the entry point file
      - Set `ast_node_count` from extraction stats if ast-grep was used (Forge/Deep tier), otherwise omit
      - Set `tool_versions` based on tier and available tools. Resolve `{skf_version}` using this resolution chain (try each in order, use the first that succeeds):
        1. `{project-root}/_bmad/skf/package.json` → read `.version` field
        2. `node -p "require('./node_modules/bmad-module-skill-forge/package.json').version"`
        3. `{project-root}/_bmad/skf/VERSION` → read plain text file (single line containing version string, written by the SKF installer)
        4. `"unknown"` (final fallback — add a warning to the evidence report)
        Never hardcode the version.
      - Resolve `{ast_grep_version}` using this resolution chain (try each in order, use the first that succeeds):
        1. `ast-grep --version` → parse version string from output (e.g., `ast-grep 0.41.1` → `"0.41.1"`)
        2. `mcp__ast-grep__find_code` tool metadata (if version is exposed by the MCP server)
        3. `"unknown"` (final fallback — add a warning to the evidence report)
      - Resolve `{qmd_version}` using this resolution chain (try each in order, use the first that succeeds):
        1. `qmd --version` → parse version string from output (e.g., `qmd 2.0.1` → `"2.0.1"`)
        2. `mcp__plugin_qmd-plugin_qmd__status` → parse version if exposed in status output
        3. `"unknown"` (final fallback — add a warning to the evidence report)
        Note: QMD is a Bun/Node package (`@tobilu/qmd`). Install via `bun install -g @tobilu/qmd`.
      - Store `commit_short` = first 8 characters of `source_commit` (or `"unknown"` if unavailable) for use in step 8 report.
      - If `scripts_inventory` is non-empty, populate the `scripts[]` array (per-file `{file, purpose, source_file, confidence}` rows). If `assets_inventory` is non-empty, populate the `assets[]` array. Omit the `scripts[]` / `assets[]` arrays entirely when their inventory is empty — `stats.scripts_count` / `stats.assets_count` were already set by `{renderMetadataStatsHelper}` from the `scripts` / `assets` payload you passed it (0 when empty).
      
      ### 5. Build references/ Content
      
      Create one reference file per major function group or type:
      - Full function signatures with detailed parameter descriptions
      - Complete usage examples (from source only)
      - Related functions cross-references
      - Temporal annotations (Deep tier: T2-past, T2-future)
      
      Group functions logically by module, file, or functional area.
      
      ### 6. Build provenance-map.json Content
      
      One entry per extracted export: export_name, export_type, params[] (typed strings), return_type, source_file, source_line, confidence tier (T1/T1-low/T2), extraction_method, ast_node_type, signature_source ("T1"|"T1-low"|"T2"|"T3" — indicates which tier contributed the structural signature).
      
      **File entries** — emit one `file_entries[]` row per tracked non-code file when any of these inventories are non-empty:
      
      - `scripts_inventory` → `file_type: "script"`, `extraction_method: "file-copy"`, stored in `{skill_package}/scripts/` by step 7
      - `assets_inventory` → `file_type: "asset"`, `extraction_method: "file-copy"`, stored in `{skill_package}/assets/` by step 7
      - `promoted_docs` (from step 3 §2a) → `file_type: "doc"`, `extraction_method: "promoted-authoritative"`, **not** copied into the skill package by step 7. The source file stays at its original path; only the provenance tracking entry is written. `content_hash` was pre-computed by §2a.
      
      Each `file_entries[]` row has the same shape regardless of `file_type`: `{file_name, file_type, source_file, content_hash, confidence, extraction_method}`. See `{skillSectionsData}` for full schema and the canonical list of `file_type` values.
      
      ### 7. Build evidence-report.md Content
      
      Compilation audit trail: generation date, forge tier, source info, tool versions, extraction summary (files/exports/confidence), warnings. For validation-specific fields (Schema, Body, Security, Content Quality, tessl, Metadata), insert the placeholder text `[PENDING — populated by step 6]`. Step-06 will replace these placeholders with actual results. See `{skillSectionsData}` for full template. Use the same `{skf_version}` value resolved in section 4 when populating the Tool Versions block.
      
      **Frontmatter — pinned fields:** emit YAML frontmatter at the top of `evidence-report.md` with at minimum `skill_name`, `generated`, `forge_tier`, and `t2_future_count`. Compute `t2_future_count` as the count of forward-looking (T2-future) temporal annotations in the enrichment data produced by step 4 (`qmd query` + temporal classification). **Emit `t2_future_count: 0` when no T2-future annotations exist** — omission is indistinguishable from "no data" for downstream consumers and would silently flip the skf-test-skill §2b/§5b migration-section gate into Case 2/3 for a Case-1 skill. This frontmatter is the authoritative detection contract — `migration-section-rules.md` Case Rules parse it deterministically rather than grepping prose.
      
      **Auto-Decisions section (render from the durable sink):** render the `## Auto-Decisions` section into `evidence-report.md` from the **union of the on-disk auto-decision sink** (`{sidecar_path}/auto-decisions.jsonl`, established at step 1 §3 and appended to on every gate landing) **and the in-context `headless_decisions[]` buffer** — read the sink's JSON lines, union them with the buffer keyed on `step`+`gate` so nothing is duplicated or dropped, and emit one row per entry in the table format documented in step 6 §8. The sink is authoritative: on a long component-library run the in-context buffer may have compacted across the token-heavy step 3→5 extraction window, but the sink carries every decision written as it landed (step 1 tier-override, step 2 ecosystem gate, step 3 zero-exports, step 3d component-extraction gates). This step is the first point `evidence-report.md` is written to disk (staging dir, §8 below). Exactly one gate can still fire after this step — step 6 §6b (the tessl-suggestions gate) — and step 6 §8 re-reconciles idempotently. If the union is empty (no decision has fired), emit the section with the single line `No auto-decisions — workflow ran interactively (or all gates had no match to auto-resolve).` — step 6 §8 replaces that line if a later decision lands.
      
      ### 8. Auto-Proceed
      
      No user interaction. Once all content is assembled in context and written to the staging directory `_bmad-output/{skill-name}/` (no final files in `skills/` or `forge-data/` yet), load `{nextStepFile}`, read it fully, then execute it. `extraction-rules.yaml` is generated by step 7 from extraction data — do not create it here.
      
      
    • component-extraction.md 12.4 KB
      ---
      returnToStep: 'extract.md'
      extractionPatternsData: 'references/extraction-patterns.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 3d: Component Library Extraction
      
      ## STEP GOAL:
      
      When `scope.type: "component-library"`, perform specialized extraction that treats the component registry as the primary API surface and props interfaces as API contracts. This step replaces the standard AST extraction flow (step 3 sections 4-4c) and returns control to step 3 section 5 (Build Extraction Inventory).
      
      ## Rules
      
      - Focus only on extracting component catalog, props interfaces, and shared types
      - Do not compile SKILL.md content (Step 05) or write output files
      - Every extracted item must have a provenance citation: `[AST:{file}:L{line}]` or `[SRC:{file}:L{line}]`
      
      ## MANDATORY SEQUENCE
      
      **Prerequisite — §2a already ran.** Step-03 executes `§2a Discovered Authoritative Files Protocol` before delegating to this file. Any promoted authoritative files (`llms.txt`, `AGENTS.md`, etc.) are tracked in the `promoted_docs[]` context list and will be written to `file_entries[]` with `file_type: "doc"` by step 5 §6 — they do not appear in the filtered file list that was passed into this step, so Phase 1 demo exclusion below only operates on code files. No special handling is required in this file for promoted docs. See step 3 §2a for the full flow.
      
      ### Phase 1: Demo/Example Exclusion
      
      Before extraction, identify and exclude demo/example files to avoid inflating export counts.
      
      **Note:** For remote sources, the file list from step 3 section 2 was built from the API tree. When scanning for demo directories, scan the local workspace/clone path (`remote_clone_path`) instead of the API-derived file list.
      
      **If `scope.demo_patterns` is specified in the brief:** Use those patterns directly.
      
      **Otherwise, auto-detect:**
      
      1. Scan the filtered file list for directories matching: `demo/`, `demos/`, `stories/`, `examples/`, `__stories__/`, `storybook/`
      2. Scan for file patterns matching: `*.stories.*`, `*.story.*`, `*.example.*`, `*.demo.*`
      3. Count matches per pattern category
      
      **User confirmation required:**
      
      "**Auto-detected {N} demo/example files** in {M} directories matching these patterns:
      {list detected patterns with counts}
      
      Confirm exclusion? [Y/n] Or adjust patterns:"
      
      **GATE [default: Y]** — if `{headless_mode}` is true: auto-confirm the auto-detected exclusion patterns, log "headless: auto-confirm demo/example exclusion ({N} files, {M} directories)", and append `{step: "component-extraction", gate: "demo-exclusion", decision: "Y", value: "{N} files / {M} dirs", rationale: "headless mode — auto-detected demo patterns accepted", timestamp: {ISO}}` to `headless_decisions[]` and, the moment it lands, append the same object as a JSON line to the durable audit sink `{sidecar_path}/auto-decisions.jsonl` (the on-landing append established at step 1 §3) — the sink feeds the evidence-report `## Auto-Decisions` table at step 5 §7 and step 6 §8. Then proceed without waiting.
      
      Wait for user response (interactive only). Apply confirmed patterns to the exclude list. Record `demo_files_excluded: {count}` in context.
      
      Update the filtered file list by removing excluded demo files before proceeding.
      
      ### Phase 2: Registry Detection
      
      The component registry is the primary API surface for component libraries.
      
      **If `scope.registry_path` is specified in the brief:**
      
      Read the specified file directly. Parse it as described below.
      
      **Otherwise, auto-detect:**
      
      1. Search the filtered file list for files matching these patterns (in priority order):
         - Files named `registry.ts`, `registry.tsx`, `registry.js` in any directory
         - Files named `components.ts`, `components.tsx` in `registry/`, `catalog/`, or `components/` directories
         - Files named `index.ts`, `index.tsx` in `registry/` or `catalog/` directories
      
      2. For each candidate, read the file and look for:
         - Arrays of objects with fields like `id`, `name`, `component`, `category`
         - Type annotations containing `Component[]` or similar array types
         - 10+ entries to qualify as a registry (reject small arrays)
      
      3. **Confidence scoring** for each candidate:
         - +3 points: Contains `id` field per entry
         - +2 points: Contains `name` or `component` field per entry
         - +2 points: Contains `category` or `tags` field per entry
         - +1 point: Located in a `registry/` or `catalog/` directory
         - +1 point: Has 20+ entries
         - Minimum threshold: 5 points to qualify
      
      4. **User confirmation required:**
      
      "**Registry candidate detected** at `{path}` (confidence: {score}/9, {count} entries).
      
      Sample entries:
      {show 3-5 sample entries with id, name, category}
      
      Is this the component registry? [Y/n] Or provide the correct path:"
      
      **GATE [default: Y]** — if `{headless_mode}` is true AND `score >= 7` (high-confidence candidate): auto-confirm the registry candidate, log "headless: auto-confirm registry candidate `{path}` (score {score}/9)", and append `{step: "component-extraction", gate: "registry-confirm", decision: "Y", value: "{path} score={score}/9", rationale: "headless mode — high-confidence registry candidate auto-accepted", timestamp: {ISO}}` to `headless_decisions[]` and, the moment it lands, append the same object as a JSON line to the durable audit sink `{sidecar_path}/auto-decisions.jsonl` (the on-landing append established at step 1 §3). If `score < 7` in headless mode, auto-reject the candidate (treat as "no registry found"), log "headless: auto-reject low-confidence registry candidate (score {score}/9) — below auto-accept threshold 7", record the decision the same way (buffer + sink, `decision: "reject-low-score"`), and fall through to the "no registry found" branch below. Confidence threshold 7 matches the minimum score the heuristic considers "probable enough to risk" without human eyes.
      
      Wait for user response (interactive only).
      
      **If no registry found and no `registry_path` in brief:**
      
      "**No component registry detected.** Component-library extraction works best with a registry file. Options:
      - **[P]** Provide the registry file path
      - **[S]** Skip registry — proceed with standard props-first extraction only"
      
      **GATE [default: S]** — if `{headless_mode}` is true: auto-select [S] Skip (props-first extraction without registry), log "headless: no registry detected, auto-skip to props-first extraction (no path was provided in brief.scope.registry_path)", and append `{step: "component-extraction", gate: "provide-or-skip-registry", decision: "S", rationale: "headless mode — no human to provide registry path", timestamp: {ISO}}` to `headless_decisions[]` and, the moment it lands, append the same object as a JSON line to the durable audit sink `{sidecar_path}/auto-decisions.jsonl` (the on-landing append established at step 1 §3). The default is `[S]` rather than `[P]` because providing a path requires user input that headless cannot supply; skipping degrades gracefully to a smaller but valid extraction.
      
      Wait for user response (interactive only).
      
      ### Phase 3: Parse Registry
      
      If a registry was confirmed, parse it into `component_catalog[]`:
      
      **Per-component entry:**
      
      - `id` — registry key (used for CLI `add` command)
      - `name` — display name (PascalCase)
      - `description` — from registry or empty
      - `category` — from registry field or directory structure
      - `available_in[]` — which design system variants include this component
      - `code_paths[]` — source file path for each variant
      - `tags[]` — from registry or empty
      - Provenance citation: `[SRC:{registry_file}:L{line}]`
      
      Display: "**Parsed component catalog: {N} components across {M} categories.**"
      
      **If no registry:** Set `component_catalog: []` and proceed to Phase 4.
      
      ### Phase 4: Props-First Extraction
      
      Extract props interfaces as the primary API contracts, then link to components.
      
      **Step 1 — Extract Props interfaces:**
      
      Load `{extractionPatternsData}` and use the component-library-specific patterns.
      
      Using AST tools (Forge/Deep) or source reading (Quick):
      
      ```yaml
      # React/TypeScript props interfaces
      id: react-props-interfaces
      language: typescript
      rule:
        pattern: 'export interface $NAME { $$$ }'
      constraints:
        NAME:
          regex: '.*Props$'
      ```
      
      For each `*Props` interface found:
      - Extract all fields with types, optionality, and default values
      - Extract JSDoc descriptions per field (if present)
      - Record: interface name, fields[], source file, line number
      - Provenance: `[AST:{file}:L{line}]` or `[SRC:{file}:L{line}]`
      
      **Step 2 — Extract component exports:**
      
      ```yaml
      # React component exports (PascalCase)
      id: react-component-exports
      language: tsx  # Use 'tsx' for .tsx files, 'typescript' for .ts files
      rule:
        pattern: 'export function $NAME($$$PARAMS)'
      constraints:
        NAME:
          regex: '^[A-Z]'
      ```
      
      Also run `export const $NAME` patterns for arrow function components.
      
      For each component export: record name, source file, line number. Do not document the function signature in detail (it's always `(props: XProps) => JSX.Element`).
      
      **Step 3 — Link Props to Components:**
      
      Use a 3-level fallback chain:
      
      1. **Naming convention (primary):** Match `FooProps` → `Foo` component by stripping the `Props` suffix
      2. **File co-location (fallback):** If naming doesn't match, check if a Props interface and a component are defined in the same file
      3. **Generic parameter (deep fallback):** Look for `ComponentProps<typeof Foo>` or similar generic patterns that reference the component
      
      For each linked pair, record the association. For unlinked Props interfaces, include them as standalone type exports.
      
      **Step 4 — Extract shared types:**
      
      Extract non-Props type exports using standard AST patterns (same as Forge tier):
      - `export type $NAME = $VALUE`
      - `export enum $NAME { $$$ }`
      - `export interface $NAME { $$$ }` (excluding `*Props` already captured)
      
      ### Phase 5: Variant Consolidation
      
      **Skip this phase if `scope.ui_variants` is not specified and no variant directories detected.**
      
      When multiple design system variants exist:
      
      1. **Group components by registry `id`** (not by filename — registry is source of truth):
         - For each `id` in `component_catalog[]`, collect all variant paths from `available_in[]` and `code_paths[]`
      
      2. **Select canonical props definition:**
         - Use the primary variant (first in `scope.ui_variants` list) as canonical
         - If primary variant's props are unavailable, use the first available variant
      
      3. **Detect props differences between variants:**
         - For components available in 2+ variants, compare Props interfaces
         - Record any variant-specific props as notes (e.g., "Base UI variant adds `slots` prop")
      
      4. **Deduplicate export counts:**
         - Count unique components (by registry `id`), not total files across variants
         - Record: `components_unique: {N}`, `components_total_with_variants: {M}`
      
      Display: "**Variant consolidation: {unique} unique components across {variant_count} variants** ({total} total including variants). Primary variant: {primary_name}."
      
      ### Phase 6: Build Component Extraction Results
      
      Compile all extracted data into the format expected by step 3 section 5:
      
      **Per-export entry (for Props interfaces — primary API):**
      
      - Interface name (e.g., `NativeLiquidButtonProps`)
      - Full interface with all fields and types
      - Parameters: each field as name, type, required/optional, default
      - Linked component name (e.g., `NativeLiquidButton`)
      - Source file and line number
      - Provenance citation
      - Confidence tier (T1 or T1-low)
      
      **Per-export entry (for component functions):**
      
      - Component name
      - Linked Props interface (if found)
      - Source file and line number
      - Provenance citation
      - Confidence tier
      
      **Per-export entry (for shared types):**
      
      - Same as standard extraction format
      
      **Component library aggregate counts:**
      
      - `components_registered`: count from registry (or 0)
      - `components_documented`: count of components with linked Props
      - `props_interfaces_extracted`: count of `*Props` interfaces
      - `components_unique`: deduplicated count (after variant consolidation)
      - `demo_files_excluded`: count from Phase 1
      - `design_variants`: map of variant name → component count (if variants exist)
      
      **Store `component_catalog[]` in context** — this is consumed by step 5 for the Component Catalog section.
      
      Display: "**Component extraction complete.** Returning to main extraction flow."
      
      ## RETURN PROTOCOL
      
      After Phase 6 completes, return control to step 3 section 5 (Build Extraction Inventory). The extraction results from this step are merged into the standard extraction inventory format. Step-03 continues with its normal Gate 2 summary and confirmation.
      
      Do not load `{returnToStep}` — the calling step (step 3) continues from where it delegated.
      
    • ecosystem-check.md 3.9 KB
      ---
      nextStepFile: 'sub/ccc-discover.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 2: Ecosystem Check
      
      ## STEP GOAL:
      
      To search the agentskills.io ecosystem for an existing official skill matching the brief, advise the user if one is found, and allow them to decide whether to proceed with compilation or install the existing skill.
      
      ## Rules
      
      - Focus only on ecosystem search and presenting findings — do not begin extraction
      - Do not halt the workflow if the ecosystem check fails or times out (5-second timeout)
      - If a match is found, present it factually — let the user decide
      
      ## MANDATORY SEQUENCE
      
      ### 1. Check Ecosystem for Existing Skill
      
      **Note:** Ecosystem lookup requires the agentskills.io registry API, which is not yet available. The `skill-check` CLI validates local skills but does not query a remote registry.
      
      **If agentskills.io registry API is available:**
      
      Query the ecosystem using the skill name from the brief:
      - Call the registry API with brief.name — check if an official skill already exists
      - Enforce 5-second timeout — if the query does not return within 5 seconds, treat as no match. Rationale: ecosystem check is an opportunistic advisory; a slow or degraded registry must not stall the compilation pipeline, and 5s is well beyond any healthy registry's p99 latency.
      - Cache results for 24 hours (if re-running same skill). Rationale: the agentskills.io registry publishes new official skills in daily batches; a 24-hour TTL balances freshness against redundant network calls during iterative brief refinement.
      
      **If registry API is not available (current default):**
      
      Skip completely and silently. Do not output any message about API unavailability or the ecosystem check being skipped. Emit zero text to the console. Proceed exactly as if no match was found.
      
      ### 2. Evaluate Results
      
      **If match found (official skill exists):**
      
      Present the finding to the user:
      
      "**Ecosystem match found.**
      
      **Existing skill:** {matched_skill_name} v{matched_version}
      **Source:** {matched_source}
      **Authority:** {official/community}
      **Compatibility:** {compatibility_notes}
      
      An existing skill already covers this source. You can:
      - **[P] Proceed** — compile your own version anyway (useful for customization or different scope)
      - **[I] Install** — install the existing skill instead of compiling
      - **[A] Abort** — cancel this compilation"
      
      **If no match found:**
      
      Auto-proceed silently to next step. Do not display any message about the ecosystem check — absence of a match is the expected case.
      
      **If timeout or error:**
      
      Auto-proceed silently. Log a note in context: "Ecosystem check skipped (timeout/error) — proceeding with compilation."
      
      ### 3. Gate — Ecosystem Match (conditional)
      
      This gate fires only when §2 found a match; on no match, timeout, or unavailable tool the step already auto-proceeded to `{nextStepFile}` with no menu.
      
      **GATE [default: P]** — under `{headless_mode}` with a match, auto-proceed as [P], log "headless: ecosystem match found, auto-proceeding", and append `{step: "ecosystem-check", gate: "ecosystem-match", decision: "P", rationale: "headless mode — match found, auto-proceed with user's own compilation", timestamp: {ISO}}` to `headless_decisions[]` and, the moment it lands, append the same object as a JSON line to the durable audit sink `{sidecar_path}/auto-decisions.jsonl` (the on-landing append established at step 1 §3) — the sink feeds the evidence-report `## Auto-Decisions` table at step 5 §7 and step 6 §8.
      
      Interactively, halt and wait for the user's choice on the §2 menu:
      
      - **P** — proceed despite the existing skill; load `{nextStepFile}`, read it fully, then execute it.
      - **I** — display "Install the existing skill using: `[SF] Setup Forge → install {matched_skill_name}`", then halt (no extraction).
      - **A** — display "Compilation aborted. Return to Ferris menu to select another action.", then halt.
      - Any other question — answer it, then redisplay the menu.
      
      
    • enrich.md 6.3 KB
      ---
      nextStepFile: 'compile.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 4: Enrich
      
      ## STEP GOAL:
      
      To enrich the extraction inventory with temporal context from QMD knowledge searches — issues, PRs, changelogs, and migration notes that add T2-confidence annotations to extracted functions. Deep tier only; Quick, Forge, and Forge+ tiers skip this step entirely.
      
      ## Rules
      
      - Focus only on QMD searches to annotate extracted functions — enrichment is additive only
      - Do not begin compilation (Step 05)
      - QMD failures degrade gracefully — continue without enrichment
      - Quick, Forge, and Forge+ tiers: skip this step entirely, auto-proceed
      
      ## MANDATORY SEQUENCE
      
      ### 1. Check Tier Eligibility
      
      **If tier is Quick, Forge, or Forge+:**
      
      Auto-proceed silently. Display no message. Immediately load, read entire file, then execute `{nextStepFile}`.
      
      **If tier is Deep:**
      
      Continue to step 2.
      
      ### 2. Collection Inventory Pre-check (Deep Tier Only)
      
      Before searching, check which QMD collections are available:
      
      1. Read the sidecar `forge-tier.yaml` to get registered `qmd_collections` entries
      2. Identify which collections contain enrichment context — collections with `type` equal to `"temporal"` (issues, PRs, changelogs) or `"docs"` (fetched external documentation). Do not include `"extraction"` or `"brief"` type collections.
      3. **If no enrichment collections exist** (no `"temporal"` or `"docs"` type collections): report this and auto-proceed. Display:
      
      "**Enrichment: no enrichment collections available.**
      Only brief-type or extraction-type QMD collections found — no temporal context (issues, PRs, changelogs) or docs collections indexed. {IF extraction_mode is 'docs-only' AND T3 items exist in extraction_inventory: 'T3 documentation content is available in extraction inventory — enrichment via QMD is skipped but T3 content will be compiled.'} {ELSE: 'Enrichment skipped (expected for first-run skill creation). Enrichment becomes available when temporal or docs context is indexed into QMD collections.'}
      
      Proceeding to compilation..."
      
      Then immediately load, read entire file, then execute `{nextStepFile}`.
      
      4. **If temporal collections exist:** Continue to step 3.
      
      ### 3. QMD Enrichment Searches (Deep Tier Only)
      
      For each major exported function — limited to the **top-level public API surface** (typically 10-20 functions that will appear in the context-snippet), not all extracted exports — search the temporal QMD collections for context:
      
      **Search query construction:**
      
      For each function, derive the **module context** from the extraction inventory's source file path (e.g., `src/graph/neo4j/index.ts` → module context `graph neo4j`). This context improves search relevance by scoping results to the function's subsystem without adding extra queries.
      
      **Primary searches (BM25 — always runs, no GPU/VRAM dependency):**
      
      1. **Issues/PRs:** `qmd_bridge.query(searches=[{type:'lex', query:'{module_context} {function_name}', intent:'issues-prs'}])` — find related discussions scoped by module context
      2. **Changelog entries:** `qmd_bridge.query(searches=[{type:'lex', query:'{function_name} changelog', intent:'changelog'}])` — find version history, breaking changes (kept generic — changelogs rarely use module paths)
      3. **Migration notes:** `qmd_bridge.query(searches=[{type:'lex', query:'{function_name} migration deprecated breaking', intent:'migration'}])` — find deprecation or migration context using exact keyword matching
      
      **Supplemental search (best-effort — requires GPU/VRAM, may fail):**
      
      4. **Semantic migration context:** `qmd_bridge.query(searches=[{type:'vec', query:'{module_context} {function_name} migration deprecated', intent:'semantic-migration'}])` — adds semantic matches (synonyms, paraphrases) that BM25 keyword search may miss. Merge results with search #3, deduplicating by document ID. If the `vec`-type search fails (VRAM, GPU driver, model loading), discard silently — the BM25 (`lex`) results from search #3 provide baseline coverage.
      
      **Tool resolution for qmd_bridge:** QMD MCP exposes a single `query` tool that accepts a `searches[]` array. Each entry has a `type` field — `'lex'` for BM25 keyword search, `'vec'` for semantic vector search, `'hyde'` for hypothetical-document retrieval — plus `query` and `intent` fields. Claude Code: `mcp__plugin_qmd-plugin_qmd__query`. Cursor: qmd MCP server. CLI: `qmd search "{query}"` (BM25) / `qmd vector-search "{query}"` (semantic). See `knowledge/tool-resolution.md`.
      
      **Tool probe (graceful degradation):** If any tool-not-found error surfaces while invoking `query` (e.g., legacy `vector_search` still expected by an older client, or a bridge returns "tool not registered"), treat it as a non-fatal failure for that function's enrichment and continue with the remaining functions. Do not retry against the stale `vector_search` tool name — that name was removed from the QMD MCP server. Record the degradation in context for the evidence report.
      
      **For each QMD result:**
      
      - Create a T2 annotation: `[QMD:{collection}:{doc}]`
      - Classify temporal relevance:
        - **T2-past:** Historical context (why it was built, what it replaced)
        - **T2-future:** Forward-looking context (planned changes, deprecation warnings)
      
      **Handling QMD failures:**
      
      - If qmd_bridge is unavailable: skip all enrichment, note in context
      - If individual search fails: skip that function's enrichment, continue with others
      - If QMD returns no results for a function: no annotation added (absence is normal)
      
      ### 4. Annotate Extraction Inventory
      
      Add enrichment annotations to the extraction inventory without modifying extraction data:
      
      **Per-function enrichment (if found):**
      - Related issues/PRs with summary
      - Changelog history (version changes, breaking changes)
      - Migration/deprecation context
      - T2 provenance citation for each annotation
      
      **Enrichment summary counts:**
      - Functions enriched: {count} of {total}
      - T2 annotations added: {count}
      - T2-past annotations: {count}
      - T2-future annotations: {count}
      
      ### 5. Report Enrichment (Deep Tier Only)
      
      Display brief enrichment summary:
      
      "**Enrichment complete.**
      
      **Functions enriched:** {enriched_count} of {total_count}
      **T2 annotations:** {t2_count} ({t2_past} historical, {t2_future} forward-looking)
      
      Proceeding to compilation..."
      
      ### 6. Auto-Proceed
      
      No user interaction. After enrichment completes (or is skipped for non-Deep tiers), load `{nextStepFile}`, read it fully, then execute it.
      
      
    • extract.md 24.7 KB
      ---
      nextStepFile: 'sub/fetch-temporal.md'
      componentExtractionStepFile: 'component-extraction.md'
      extractionPatternsData: 'references/extraction-patterns.md'
      extractionPatternsTracingData: 'references/extraction-patterns-tracing.md'
      tierDegradationRulesData: 'references/tier-degradation-rules.md'
      sourceResolutionData: 'references/source-resolution-protocols.md'
      authoritativeFilesProtocol: 'references/authoritative-files-protocol.md'
      # Probe installed SKF module path first, src/ dev-checkout fallback. At first
      # use below, resolve `{atomicWriteHelper}` to the first existing path; HALT if
      # neither candidate exists — losing atomic-write guarantees is not an option.
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      # Resolve `{detectScriptsAssetsHelper}` to the first existing path; HALT if
      # neither candidate exists. §4c relies on the helper for deterministic
      # script/asset detection (file walk, SHA-256 hashing, header-comment purpose
      # extraction); falling back to prose-driven detection would lose hash stability.
      detectScriptsAssetsProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-detect-scripts-assets.py'
        - '{project-root}/src/shared/scripts/skf-detect-scripts-assets.py'
      # Resolve `{resolveAuthoritativeFilesHelper}` to the first existing path;
      # HALT if neither exists. §2a uses it to scan the source tree for
      # authoritative AI documentation files, classify each against scope
      # filters + amendments, and load previews + content hashes — all five
      # deterministic phases in one call. Falling back to prose-driven file
      # walking + glob matching + hashing would let the LLM drift on the
      # heuristic list and miss auth-doc files at deeper directory depths.
      resolveAuthoritativeFilesProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-resolve-authoritative-files.py'
        - '{project-root}/src/shared/scripts/skf-resolve-authoritative-files.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 3: Extract
      
      ## STEP GOAL:
      
      To extract all public exports, function signatures, type definitions, and co-import patterns from the source code using tier-appropriate tools, building a complete extraction inventory with confidence-tiered provenance citations.
      
      ## Rules
      
      - Focus only on extracting exports, signatures, types from source code — do not compile SKILL.md
      - Do not write any output files — extraction stays in context
      - Every extracted item must have a provenance citation: `[AST:{file}:L{line}]` or `[SRC:{file}:L{line}]`
      
      ## MANDATORY SEQUENCE
      
      ### 1. Load Extraction Patterns
      
      Load `{extractionPatternsData}` completely. Identify the strategy for the current forge tier.
      
      ### 2. Apply Scope Filters
      
      From the brief, apply scope and pattern filters:
      
      - `scope.type` — determines what to extract (e.g., `full-library`, `specific-modules`, `public-api`, `component-library`, `reference-app`, `docs-only`). Use `reference-app` when the source is a whole app and the skill's value is wiring patterns rather than public exports (embedded-sidecar reference apps, CLI-demo repos, integration-pattern demonstrators). `reference-app` triggers the compile-assembly overrides in `assets/compile-assembly-rules.md` that replace "Key API Summary" with a "Pattern Surface" section and make `stats.exports_documented` semantics pattern-oriented. Do not pick `full-library` for reference apps — downstream assembly will remap wiring onto export slots, producing fuzzy counts and an awkward SKILL.md.
      - `scope.include` — file globs to include
      - `scope.exclude` — file globs to exclude
      
      Build the filtered file list from the source tree resolved in step 1. Record the result: "**Filtered file count: {N} files in scope**" — this count is the input to the AST Extraction Protocol decision tree in the extraction patterns data file.
      
      ### 2a. Discovered Authoritative Files Protocol
      
      **Skip this section entirely if `source_type: "docs-only"`** — there is no source tree to scan.
      
      Load `{authoritativeFilesProtocol}` and execute it. The full protocol (heuristic scan list, helper invocation, classification dispatch, prompt flow, P/S/U decision-apply, summary, provenance-map handoff, downstream consumption) lives there.
      
      Briefly: scan the source tree for authoritative AI documentation files (`llms.txt`, `AGENTS.md`, `.cursorrules`, etc.) that the brief's scope filters may have excluded. The `{resolveAuthoritativeFilesHelper}` helper does the deterministic work (walk, scope diff, amendment reconcile, preview load, hashing); the LLM applies the resulting `unresolved[]` prompt loop. Promoted decisions are persisted to the brief immediately so re-runs replay deterministically.
      
      ### 2b. Resolve Source Access
      
      **If `source_type: "docs-only"`:** skip §2b entirely — there is no source to resolve. Proceed directly to §2c (component library delegation, which is itself skipped for docs-only) and then §3 (Check for Docs-Only Mode). Tag resolution, remote/workspace cloning, source-commit capture, version reconciliation, and deferred CCC discovery all require a source tree and have nothing to do in docs-only mode.
      
      Load `{sourceResolutionData}` completely. Follow these protocols in order:
      1. **Tag Resolution** — run the explicit variant when `brief.target_version` is set, or the implicit variant when only `brief.version` is set (Forge/Deep remote sources only). This sets `source_ref` before any clone happens. Quick tier remote sources skip this.
      2. **Remote Source Resolution** — workspace or ephemeral clone, cleanup (Forge/Deep tiers).
      3. **Source Commit Capture** — all tiers.
      4. **Version Reconciliation** — all tiers.
      
      This ensures source code is accessible regardless of which extraction path is taken below (standard, component-library, or docs-only).
      
      **Deferred CCC Discovery (Forge+ and Deep — remote sources only):**
      
      If ALL of these conditions are true:
      - `tools.ccc` is true in forge-tier.yaml
      - `{ccc_discovery}` is empty (step 2b deferred because source was remote)
      - `remote_clone_path` is set (source resolution succeeded for a remote URL)
      - Tier is Forge+ or Deep
      
      Then run CCC indexing and discovery on the resolved clone (workspace or ephemeral):
      
      1. **Check existing index:** If `{remote_clone_path}/.cocoindex_code/` already exists (workspace repo with a persisted CCC index), skip steps 2-3 and proceed directly to step 4 using `ccc search --refresh` instead of plain `ccc search`. The `--refresh` flag tells CCC to re-index if files have changed since the last index, then search. This is the fast path for workspace repos that have been indexed before. **Note:** If `--refresh` is not supported by the installed ccc version, omit the flag — ccc will use the existing index. Before trusting a reused index, run the same `ccc status` `Languages:`-breakdown integrity check as step 3: a persisted index can be degraded (source language absent), and `--refresh` re-indexes changed files but does not repair degraded settings — if the source language is missing, fall through to a clean rebuild (`ccc init -f` + `ccc index`) rather than searching it.
      
      2. **Initialize index (first time only):** Run `cd {remote_clone_path} && ccc init`. If init exits non-zero with `A parent directory has a project marker` — the common case when the clone is nested under a ccc-indexed project (e.g. a `.forge-sources/` checkout inside this repo) — re-run as `cd {remote_clone_path} && ccc init -f` to initialize at the subtree anyway (same handling as step 7 §6b). If init fails for any other reason, or the `-f` retry also fails, set `{ccc_discovery: []}` and continue — this is not an error.
      
         **Apply standard exclusions:** After `ccc init`, apply generic build/dependency exclusions to `{remote_clone_path}/.cocoindex_code/settings.yml`. These are standard artifact patterns, not SKF-specific paths (the workspace checkout is a source repo, not an SKF project):
      
         ```
         node_modules/, dist/, build/, .git/, vendor/, __pycache__/, .cache/, .next/, .nuxt/, target/, out/, .venv/, .tox/
         ```
      
         Read `settings.yml`, append any patterns not already present to the `exclude_patterns` array, write back. **Reuse check:** if an existing `.cocoindex_code/settings.yml` was already present (workspace hit), read its `exclude_patterns` first and diff against the standard-exclusion list above. If ANY standard entry is missing from the existing list, append only the missing entries (preserving any user-added patterns) AND force a re-index by running `ccc reset --force` (discards the stale databases) followed by `ccc index` — `ccc index` itself takes no rebuild flag, so re-running it alone would reuse the existing index. If every standard entry is already present, skip the write and skip the forced re-index — the existing index is valid. Record `ccc_exclusions_augmented: {count}` in context for the evidence report.
      
         **Note:** Brief-specific `include_patterns` and `exclude_patterns` are not written to `settings.yml`. The CCC index is general-purpose — it indexes everything (minus standard artifacts). Brief-specific filtering happens at search result time, not index time. This allows a single workspace CCC index to serve multiple briefs with different scope filters.
      
      3. **Index the clone:** Run `cd {remote_clone_path} && ccc index` with an extended timeout or in background mode. Indexing can take several minutes on large codebases (1000+ files). Verify completion with `ccc status`, then **verify integrity**: a non-zero `Chunks`/`Files` total is not sufficient — read the `Languages:` breakdown and confirm the source's primary language (`{brief.language}`) reports a non-trivial chunk count. An index dominated by `markdown`/config chunks with the source language absent or near-zero means `ccc index` ran against degraded settings (a stale `.cocoindex_code/` inherited from a parent, or a no-op init over a prior partial index) and the source code was never indexed — searches over it return nothing useful. When the source language is absent, rebuild from a clean init: `cd {remote_clone_path} && ccc init -f` then `ccc index`, and re-check the `Languages:` breakdown. If indexing fails, or the source language is still missing after a forced re-init, set `{ccc_discovery: []}` and continue — this is not an error.
      
      4. **Construct semantic query:** Build from brief data: `"{brief.name} {brief.scope}"`. Truncate to 80 characters — keep the full skill name and trim `brief.scope` from the end. If `brief.scope` is very short (< 10 chars), append terms from `brief.description` to fill the remaining space.
      
      5. **Execute search:** Run `ccc_bridge.search(query, remote_clone_path, top_k=20)`:
         - **If existing index was found (step 1):** Use `cd {remote_clone_path} && ccc search --refresh --limit 20 "{query}"` — this re-indexes if files changed, then searches. If `--refresh` is not supported by the installed ccc version, omit the flag — ccc will use the existing index.
         - **Otherwise:** Use `cd {remote_clone_path} && ccc search --limit 20 "{query}"` after indexing in step 3.
         - **Tool resolution:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or CLI. Note: `ccc search` operates on the index in the current working directory. See `knowledge/tool-resolution.md`.
      
      6. **Store results:** If search succeeds, store as `{ccc_discovery: [{file, score, snippet}]}`. Display: "**CCC semantic discovery: {N} relevant regions identified across {M} unique files.**"
      
         If `remote_clone_type == "workspace"` and an existing index was reused, append: "(reused workspace index)"
      
      7. **On failure:** Set `{ccc_discovery: []}`. Display: "CCC discovery unavailable — proceeding with standard extraction." Do not halt.
      
      **CCC Discovery Integration (Forge+ and Deep with ccc only):**
      
      If `{ccc_discovery}` is in context and non-empty (populated by step 2b or deferred discovery above):
      - Sort the filtered file list by CCC relevance score: files appearing in `{ccc_discovery}` results move to the front of the extraction queue, sorted by their relevance score descending
      - Files not in CCC results remain in the queue after ranked files — they are not excluded, only deprioritized
      - Display: "**CCC discovery: {N} files pre-ranked by semantic relevance** — extraction will prioritize these first."
      
      If `{ccc_discovery}` is empty or not in context: proceed with existing file ordering (no change to current behavior).
      
      ### 2c. Component Library Delegation
      
      **Skip this section if `source_type` is `"docs-only"` — docs-only skills do not use component extraction.**
      
      **If `scope.type: "component-library"` in the brief:**
      
      "**Component library detected.** Delegating to specialized extraction strategy for registry-first, props-focused extraction."
      
      Load and execute `{componentExtractionStepFile}` completely. When that step completes, it returns control here. Resume at section 5 (Build Extraction Inventory) with the enriched extraction data and `component_catalog[]` from the component extraction step.
      
      **Otherwise:** Continue with standard extraction below.
      
      ### 3. Check for Docs-Only Mode
      
      **If `source_type: "docs-only"` in the brief data:**
      
      "**Docs-only mode:** No source code to extract. Documentation content will be fetched from `doc_urls` in step 3c."
      
      Build an empty extraction inventory with zero exports. **Set `top_exports = []` explicitly in context** — downstream steps (notably §3b targeted searches and step 4 enrichment fan-out) must see an empty list rather than an undefined/missing field so they can short-circuit deterministically. Set `extraction_mode: "docs-only"` in context. Auto-proceed through Gate 2 (section 6) — display the empty inventory and note that T3 content will be produced by the doc-fetcher step.
      
      **If `source_type: "source"` (default):** Continue with extraction below.
      
      ### 4. Execute Tier-Dependent Extraction
      
      Source resolution, version reconciliation, and CCC discovery were completed in section 2b. Proceed with the tier-specific extraction strategy below.
      
      **Quick Tier (No AST tools):**
      
      1. Use `gh_bridge.list_tree(owner, repo, branch)` to map source structure (if remote)
      2. Identify entry points: index files, main exports, public modules
      3. Use `gh_bridge.read_file(owner, repo, path)` to read each entry point
      4. Extract from source text: exported function names, parameter lists, return types
      5. Infer types from JSDoc, docstrings, type annotations
      6. Confidence: All results T1-low — `[SRC:{file}:L{line}]`
      
      **Tool resolution for gh_bridge:** Use `gh api repos/{owner}/{repo}/git/trees/{branch}?recursive=1` for list_tree, `gh api repos/{owner}/{repo}/contents/{path}` for read_file. If source is local, use direct file listing/reading instead. See `knowledge/tool-resolution.md`.
      
      **Forge/Forge+/Deep Tier (AST available):**
      
      Before executing AST extraction, load the **AST Extraction Protocol** section from `{extractionPatternsData}`. Follow the decision tree based on the file count from step 1's file tree — it determines whether to use the MCP tool, scoped YAML rules, or CLI streaming. Do not use `ast-grep --json` (without `=stream`) — it loads the entire result set into memory and fails on large codebases; use the explicit `run` subcommand with streaming: `ast-grep run -p '{pattern}' --json=stream`.
      
      1. Detect language from brief or file extensions
      2. Follow the AST Extraction Protocol decision tree from `{extractionPatternsData}`:
         - ≤100 files: use `find_code()` MCP tool with `max_results` and `output_format="text"`
         - ≤500 files: use `find_code_by_rule()` MCP tool with scoped YAML rules
         - >500 files: use CLI `--json=stream` with line-by-line streaming Python — inject the brief's `scope.exclude` patterns into the Python filter's `EXCLUDES` list (use `[]` if absent) so excluded files are discarded before consuming `head -N` slots (see template in extraction patterns data)
      3. For each export: extract function name, full signature, parameter types, return type, line number
      4. Use `ast_bridge.detect_co_imports(path, libraries[])` to find integration points
      5. Build extraction rules YAML data for reproducibility
      6. Confidence: All results T1 — `[AST:{file}:L{line}]`
      
      **Tool resolution for ast_bridge:** Use ast-grep MCP tools (`mcp__ast-grep__find_code`, `mcp__ast-grep__find_code_by_rule`) as specified in the AST Extraction Protocol above, or `ast-grep` CLI. For `detect_co_imports`, use `find_code_by_rule` with a co-import YAML rule scoped to the libraries list. See `knowledge/tool-resolution.md`.
      
      **If AST tool is unavailable at Forge/Deep tier** (see `{tierDegradationRulesData}` for full rules):
      
      ⚠️ **Warn the user explicitly:** "AST tools are unavailable — extraction will use source reading (T1-low). Run [SF] Setup Forge to detect and configure AST tools for T1 confidence."
      
      Degrade to Quick tier extraction. Note the degradation reason in context for the evidence report.
      
      **For each file — handle failures gracefully:**
      
      - If a file cannot be read: log warning, skip file, continue with remaining files
      - If AST parsing fails on a file: fall back to source reading for that file, continue
      
      **Re-export tracing (Forge/Deep only):** After the initial AST scan, check for unresolved public exports from entry points (`__init__.py`, `index.ts`, `lib.rs`). Follow the **Re-Export Tracing** protocol in `{extractionPatternsTracingData}` to resolve them to their definition files.
      
      ### 4b. Validate Exports Against Package Entry Point
      
      After extraction, validate the collected exports against the package's actual public API surface:
      
      - **Python:** Read `{source_root}/__init__.py` — extract imports to build the public export list. Compare against AST results:
        - In AST but not entry point → mark as internal (exclude from `metadata.json` exports)
        - In entry point but not AST → flag as extraction gap (trace via re-export protocol)
      - **TypeScript/JS:** Read `index.ts`/`index.js` — same comparison logic.
      - **Rust:** Read `lib.rs` — extract `pub use` items. Same logic. **Go:** Scan for exported (capitalized) identifiers.
      
      **Multi-entry packages (`exports` map / declaration-file entry points).** A single per-language entry-point read misses public surface that a package ships through its `package.json` `exports` map — especially committed `.d.ts` / `.d.mts` declaration files that resolve **outside** the conventional source dir (e.g. a monorepo package whose `./macro` subpath maps to `macro/index.d.mts`, listed in `files[]` but not under `src/`). When the in-scope package declares an `exports` map:
      
      - Resolve each `exports` subpath to its target file and treat that file — and any committed `.d.ts` / `.d.mts` declaration it resolves to — as an authoritative public entry point, reading it the same way as the primary barrel above even when it lives outside `src/`.
      - If a resolved `exports` subpath target falls **outside** the brief's `scope.include` globs, surface a note: `"warn: public entry point {path} (exports subpath '{subpath}') resolves outside scope.include — widen scope.include before extraction, or this surface stays undocumented and excluded from the coverage denominator."` Widening `scope.include` here keeps the documented surface aligned with the `effective_denominator` that compile.md §4 derives from those same globs, without mid-run scope surgery.
      
      Use the entry point as the authoritative source for `metadata.json`'s `exports[]` array.
      
      **If entry point is missing or unreadable:** Skip validation with a warning.
      
      ### 4c. Detect and Inventory Scripts/Assets
      
      **Default resolution:** If `scripts_intent` is absent from the brief, treat as `"detect"` (auto-detection). If `assets_intent` is absent, treat as `"detect"`. Only an explicit `"none"` value disables detection.
      
      Invoke the deterministic detector — it implements the heuristics from `{extractionPatternsTracingData}` (directory conventions, shebang signals, `package.json` `bin` entry-points, asset filename patterns, binary-extension exclusion, generated-path pruning) so this stage doesn't re-derive them per-run:
      
      ```bash
      uv run {detectScriptsAssetsHelper} detect <source-root> \
          --scripts-intent <scripts_intent> \
          --assets-intent <assets_intent> \
          [--scope-include "<glob1>,<glob2>,..."] \
          [--max-lines 500]
      ```
      
      The helper emits JSON on stdout:
      
      ```json
      {
        "scripts_inventory": [ {name, source_file, purpose, language, content_hash, confidence, lines, size_flag}, ... ],
        "assets_inventory":  [ {name, source_file, purpose, type,     content_hash, confidence, lines, size_flag}, ... ],
        "scripts_skipped": <bool>,
        "assets_skipped":  <bool>,
        "stats": { "scripts_found": N, "assets_found": M, "files_scanned": K }
      }
      ```
      
      Merge `scripts_inventory[]` and `assets_inventory[]` into the running extraction inventory verbatim — entries already carry `confidence: "T1-low"` and `content_hash` (sha256:...). Records with `size_flag: "oversized"` should be surfaced in §6 (Extraction Summary) so the user can confirm before bundling. If both `scripts_skipped` and `assets_skipped` are true, the helper performs no walk and §4c is effectively a no-op.
      
      ### 5. Build Extraction Inventory
      
      Compile all extracted data into a structured inventory:
      
      **Per-export entry:**
      - Function/type name
      - Full signature with types
      - Parameters (name, type, required/optional)
      - Return type
      - Source file and line number
      - Provenance citation (`[AST:...]` or `[SRC:...]`)
      - Confidence tier (T1 or T1-low)
      
      **Aggregate counts:**
      - Total files scanned
      - Total exports found
      - Exports by type (functions, types/interfaces, constants)
      - Confidence breakdown (T1 count, T1-low count)
      - `top_exports[]` — sorted list of the top 10-20 public API function names by prominence (import frequency or documentation position). This named field is consumed by step 3b for targeted temporal fetching and cache fingerprinting.
      
      **Script/asset counts (when detected):**
      - `scripts_found`: count of scripts detected
      - `assets_found`: count of assets detected
      
      **Co-import patterns (Forge/Deep only):**
      - Libraries commonly imported alongside extracted exports
      - Integration point suggestions
      
      ### 6. Present Extraction Summary (Gate 2)
      
      **Docs-only note:** If `docs_only_mode` is active (`extraction_mode: "docs-only"`), display a brief note explaining that T3 content will be added by the doc-fetcher step (step 3c), then auto-proceed past this gate. Example: "Docs-only mode: extraction inventory is empty. Documentation content will be fetched from `doc_urls` in step 3c. Auto-proceeding."
      
      **Zero-export sanity check (source mode):** If `extraction_mode != "docs-only"` AND `export_count == 0` AND the brief declares no `doc_urls`, an empty extraction is almost always an error — a wrong branch/tag, an over-narrow `scope.include`, or a failed AST run — not a valid empty surface. Do not let this sail through to a green report. Surface a distinct warning at the gate:
      
      "**⚠️ Zero public exports extracted.** A source-type brief produced no documented surface and declares no `doc_urls`. This usually means a scope/branch/tag mismatch (wrong `target_version`, over-narrow `scope.include`) or a failed AST run — the compiled skill would document nothing. Verify the brief's source ref and scope before continuing.
      
      **[C] Continue anyway** — compile an empty surface (default)"
      
      Under `{headless_mode}`, do not auto-pass silently: set `status: "partial"` and `summary.warning: "zero-exports"` on the result contract (carried to step 8's record), log `"headless: zero public exports extracted — likely scope/branch/tag mismatch, continuing"`, append a `headless_decisions[]` entry `{step: "extract", gate: "zero-exports", decision: "C", rationale: "headless mode — zero exports, no human to confirm scope/ref", timestamp: {ISO}}` and, the moment it lands, append the same object as a JSON line to the durable audit sink `{sidecar_path}/auto-decisions.jsonl` (the on-landing append established at step 1 §3), and proceed. The distinct warning string surfaces the worst kind of failure (looks green, isn't) where a human or automator can act on it.
      
      Display the extraction findings for user confirmation:
      
      "**Extraction complete.**
      
      **Files scanned:** {file_count}
      **Exports found:** {export_count} ({function_count} functions, {type_count} types, {constant_count} constants)
      **Confidence:** {t1_count} T1 (AST-verified), {t1_low_count} T1-low (source reading)
      **Tier used:** {tier}
      **Co-import patterns:** {pattern_count} detected
      {if scripts_found > 0: **Scripts detected:** {scripts_found}}
      {if assets_found > 0: **Assets detected:** {assets_found}}
      
      **Top exports:**
      {list top 10 exports with signatures}
      
      {warnings if any files skipped or degraded}
      
      Review the extraction summary above, then confirm to continue."
      
      ### 7. Gate 2 — Confirm Extraction
      
      Docs-only mode (`extraction_mode: "docs-only"`) needs no confirmation — auto-proceed to `{nextStepFile}`.
      
      Otherwise this is a confirmation gate: halt after the §6 summary and wait for the user to continue (they may ask about the results first). **GATE [default: continue]** — under `{headless_mode}`, auto-proceed and log "headless: auto-approve extraction summary". On continue, load `{nextStepFile}`, read it fully, then execute it.
      
      
    • extraction-patterns-tracing.md 6.6 KB
      # Extraction Patterns: Tracing and File-Level Extraction
      
      This file covers re-export tracing protocols and script/asset file-level extraction patterns. For core tier strategies and AST extraction protocol, see `extraction-patterns.md`.
      
      ---
      
      ## Re-Export Tracing
      
      After initial AST extraction, some top-level exports may resolve to **module imports** rather than direct function definitions. This is common in Python libraries that use `__init__.py` re-exports for a clean public API.
      
      **Detection heuristic:** For each top-level export from `__init__.py` (or equivalent entry point), check if the import path resolves to a directory (contains `__init__.py`) rather than a `.py` file with a matching `def` or `class`. If the initial AST scan found no function/class definition for a known public export, it is likely a module re-export.
      
      **Tracing protocol:**
      
      1. Read the entry point file (e.g., `{package}/__init__.py`) and extract all `from .X import Y` statements
      2. For each import where Y was not found by the initial AST scan:
         - Check if the import path resolves to a directory (e.g., `{package}/api/v1/delete/` exists with `__init__.py`)
         - If directory: read its `__init__.py` to find the actual re-exported symbol
         - **Handle aliases:** Check for `from .module import A as B` patterns in the intermediate `__init__.py`. If the parent imports `B`, trace through to `A` in `.module`. If the parent imports `A` but the `__init__.py` only exports it as `B` (via `from .module import A as B`), match by original name `A` and note the alias
         - Trace the symbol to its definition file and run AST extraction on that file
      3. Cite the actual definition location: `[AST:{definition_file}:L{line}]`
      
      **Examples:**
      
      ```python
      # Module re-export — follow required
      from .api.v1.delete import delete    # delete/ is a directory → read delete/__init__.py
      
      # Direct function import — no follow needed
      from .api.v1.add.add import add      # add.py exists with def add()
      
      # Aliased re-export — follow through alias
      # In cognee/api/v1/visualize/__init__.py:
      #   from .start_visualization_server import visualization_server
      # In cognee/__init__.py:
      #   from .api.v1.visualize import start_visualization_server
      # → Match start_visualization_server against both definition names AND alias names
      #   in the intermediate __init__.py to resolve the chain
      ```
      
      **Unresolvable imports:** If the import statement is a star-import (`from .X import *`) or a conditional import (`try`/`except`), the symbol cannot be reliably traced via this protocol. Record it with `[SRC:{package}/__init__.py:L{line}]` (T1-low) and a note: "star/conditional import — manual trace required."
      
      **Scope limit:** Only trace re-exports for symbols listed in the top-level entry point's public API. Do not recursively trace beyond one level of `__init__.py` indirection. If a re-export cannot be resolved after one level, record it with a `[SRC:{package}/__init__.py:L{line}]` citation (T1-low) from the import statement itself.
      
      **Other languages:** JS/TS barrel files (`index.ts` with `export { X } from './module'`) follow the same principle — trace the re-export to the definition file. Rust `pub use` and Go package-level re-exports are less common but follow the same heuristic when encountered.
      
      ---
      
      ## Script/Asset Extraction Patterns
      
      Scripts and assets are file-level artifacts, not code exports. They follow the **file-copy extraction method** — detected in source, copied with provenance citations.
      
      ### Detection Heuristics
      
      **Script directories:** `scripts/`, `bin/`, `tools/`, `cli/`
      **Asset directories:** `assets/`, `templates/`, `schemas/`, `configs/`, `examples/`
      
      **Script file signals:**
      
      | Signal                  | Strength | Pattern                                                                              |
      |-------------------------|----------|--------------------------------------------------------------------------------------|
      | Entry point declaration | Strong   | `package.json` `bin` field, Cargo.toml `[[bin]]`, pyproject.toml `[project.scripts]` |
      | Shebang + executable    | Strong   | `#!/bin/bash`, `#!/usr/bin/env python`, `#!/usr/bin/env node`                        |
      | CLI argument parser     | Moderate | `argparse`, `yargs`, `commander`, `cobra`, `clap` imports in file                    |
      | Directory convention    | Moderate | File in `scripts/`, `bin/`, `tools/` directory                                       |
      | CI/CD reference         | Moderate | Script referenced in `.github/workflows/*.yml`                                       |
      
      **Asset file signals:**
      
      | Signal           | Strength | Pattern                                             |
      |------------------|----------|-----------------------------------------------------|
      | JSON Schema      | Strong   | `*.schema.json`, file contains `"$schema"` key      |
      | Config template  | Strong   | `*.example`, `*.template.*`, `*.sample` extension   |
      | Official example | Moderate | File in `examples/` directory, referenced in README |
      | OpenAPI/GraphQL  | Moderate | `openapi.json`, `*.graphql`, `swagger.yaml`         |
      | Design tokens    | Weak     | `tokens.json`, `theme.json` in `assets/`            |
      
      ### Inclusion Rules
      
      - Only include files within brief's `scope.include` patterns (or auto-detected directories)
      - Exclude binary files (check extension: `.so`, `.dll`, `.jar`, `.wasm`, `.exe`)
      - Exclude generated files (`dist/`, `build/`, `.webpack/` output)
      - Exclude vendored/third-party files
      - Flag files >500 lines for user confirmation (may be too large for skill package)
      - If `scripts_intent` is absent from the brief, treat as `"detect"` (auto-detection is the default). If `scripts_intent` is explicitly `"none"`, skip scripts detection. Same rule applies to `assets_intent`.
      
      ### Provenance and Hashing
      
      Each extracted file receives:
      - Citation: `[SRC:{source_path}:L1]` (T1-low — file verified to exist but content not AST-analyzed)
      - Content hash: SHA-256 of file content (for drift detection in audit-skill)
      - Extraction method: `"file-copy"` (distinct from `"ast_bridge"` for code exports)
      
      ### Inventory Structure
      
      **Script inventory entry:**
      - `name`: filename (e.g., `validate-config.sh`)
      - `source_file`: path relative to source root
      - `purpose`: extracted from file header comments or README reference (if none found, use filename)
      - `language`: detected from extension or shebang
      - `content_hash`: SHA-256
      - `confidence`: T1-low
      
      **Asset inventory entry:**
      - `name`: filename (e.g., `config-schema.json`)
      - `source_file`: path relative to source root
      - `purpose`: extracted from file header or schema `title`/`description` field
      - `type`: `template`, `schema`, `config`, `example`
      - `content_hash`: SHA-256
      - `confidence`: T1-low
      
    • extraction-patterns.md 24.9 KB
      # Extraction Patterns by Tier
      
      ## Quick Tier (No AST)
      
      Source reading via gh_bridge — infer exports from file structure and content.
      
      ### Strategy
      1. `gh_bridge.list_tree(owner, repo, branch)` — map source structure
      2. Identify entry points: index files, main exports, public modules
      3. `gh_bridge.read_file(owner, repo, path)` — read each entry point
      4. Extract: exported function names, parameter lists, return types (from signatures)
      5. Infer types from JSDoc, docstrings, type annotations in source
      
      ### Confidence
      - All results: T1-low (source reading without structural verification)
      - No co-import detection available
      - No AST-backed line numbers
      
      ### Supported Patterns
      - `export function name(...)` / `export const name = ...` (JS/TS)
      - `pub fn name(...)` (Rust)
      - `def name(...)` with `__all__` (Python)
      - `func Name(...)` (Go, capitalized = exported)
      
      ---
      
      ## Forge Tier (AST Available)
      
      Structural extraction via ast-grep — verified exports with line-level citations.
      
      > **Note:** `ast_bridge.*`, `qmd_bridge.*`, and `ccc_bridge.*` references below are **conceptual interfaces**, not callable functions. Resolve them as follows:
      > - `ast_bridge.*` → ast-grep MCP tools (`mcp__ast-grep__find_code`, `mcp__ast-grep__find_code_by_rule`) or `ast-grep` CLI
      > - `qmd_bridge.*` → QMD MCP `query` tool (`mcp__plugin_qmd-plugin_qmd__query`) taking `searches=[{type:'lex'|'vec'|'hyde', query, intent}]`, or `qmd` CLI (`qmd search` / `qmd vector-search`). The legacy `vector_search` MCP tool has been removed; if a client surfaces a tool-not-found error, degrade gracefully per the QMD step 4 tool-probe note — do not retry the stale name.
      > - `ccc_bridge.*` → `/ccc` skill (Claude Code), ccc MCP server (Cursor), or `ccc` CLI
      > - `gh_bridge.*` → `gh api` commands or direct file I/O for local sources
      >
      > See `knowledge/tool-resolution.md` for the complete resolution table. Also see the AST Extraction Protocol section below and the TOOL/SUBPROCESS FALLBACK rule for dispatch details.
      
      ### Strategy
      
      1. Detect language from brief or file extensions
      2. Use ast-grep to extract all exports from `path` for the given `language` (scan definitions)
      3. For each export: function name, full signature, parameter types, return type, line number
      4. Use ast-grep to detect co-imported symbols in `path` for the given `libraries[]`
      5. Build extraction rules YAML for reproducibility
      
      ### Confidence
      - Exported functions with full signatures: T1 (AST-verified)
      - Type definitions and interfaces: T1
      - Co-import patterns: T1
      - Internal/private functions: excluded (not part of public API)
      
      ### ast-grep Patterns
      - JS/TS: `export function $NAME($$$PARAMS): $RET` / `export const $NAME = ($$$PARAMS) => $BODY` / `export const $NAME` / `export class $NAME`
      - Rust: `pub fn $NAME($$$PARAMS) -> $RET`
      - Python: function definitions within `__all__` list
      - Go: capitalized function definitions
      
      ---
      
      ## Forge+ Tier (AST + CCC)
      
      Identical extraction to Forge tier. CCC adds an upstream semantic discovery step that pre-ranks the file extraction queue.
      
      ### When CCC Pre-Discovery Applies
      
      CCC pre-discovery runs in ccc-discover (before this extraction step) when ALL of the following are true:
      - Tier is Forge+ or Deep
      - `tools.ccc: true` in forge-tier.yaml
      - `ccc_index.status` is `"fresh"`, `"stale"`, `"created"`, or `"none"`/`"failed"` (step 2b attempts lazy indexing for the latter two)
      
      The discovery step stores `{ccc_discovery: [{file, score, snippet}]}` in context. This extraction step consumes those results to pre-rank the file list.
      
      ### CCC Pre-Ranking Strategy
      
      When `{ccc_discovery}` is present and non-empty:
      
      1. Files appearing in `{ccc_discovery}` results move to the front of the extraction queue, sorted by relevance score descending
      2. Files not in CCC results remain in the queue — they are not excluded, only deprioritized
      3. If the CCC intersection with scoped files produces <10 files: include all scoped files (CCC results too narrow)
      4. Proceed with the AST Extraction Protocol on the pre-ranked list
      
      ### ast-grep Patterns
      
      Same patterns as Forge tier — see Forge tier section above. CCC pre-ranking does not change which AST patterns are used, only which files are processed first.
      
      ### Confidence
      
      All results: T1 (AST-verified) — identical to Forge tier. CCC is upstream discovery only and is invisible in the output artifact.
      
      ### Important
      
      CCC pre-discovery failures (ccc unavailable, command error, empty results) always result in standard Forge extraction behavior. This is not reported to the user as a problem — it is normal behavior when ccc has no relevant results for the skill's scope.
      
      ---
      
      ## Deep Tier (AST + QMD)
      
      Same extraction as Forge tier. Deep tier adds enrichment in step 4, not extraction.
      
      ### Strategy
      - Identical to Forge tier extraction
      - QMD enrichment happens in the next step (enrich)
      - Extraction results carry forward unchanged
      
      ### Confidence
      - Extraction: same as Forge (T1)
      - Enrichment annotations added in step 4: T2
      
      ---
      
      ## AST Extraction Protocol
      
      When AST tools are available (Forge/Deep tier), follow this deterministic protocol to prevent output overflow on large codebases.
      
      **"Files in scope"** = files remaining after applying `include_patterns` and `exclude_patterns` from the brief, filtered by the target language extension. This is not the total repository file count from step 1's tree listing. Use the filtered count from step 3 section 2 as the decision tree input.
      
      ### Decision Tree
      
      Apply the first matching condition:
      
      ```
      Files in scope ≤ 100
        → Use ast-grep MCP tool: find_code(pattern, max_results=100, output_format="text")
        → Parse compact text output directly into extraction inventory
      
      Files in scope 101–500
        → Use ast-grep MCP tool: find_code_by_rule(yaml, max_results=150, output_format="text")
        → Use scoped YAML rules (see recipes below) to filter at the AST level
        → Parse compact text output into extraction inventory
      
      Files in scope > 500
        → CLI streaming fallback: ast-grep run --json=stream + line-by-line Python processing
        → Process in directory batches, cap per-batch output
        → Merge batch results into extraction inventory
      ```
      
      ### Safety Valve
      
      If any ast-grep operation (MCP or CLI) visibly causes a timeout, returns an error related to output size, or produces unexpectedly large output: immediately switch to the CLI streaming fallback with `--json=stream`. Do not retry the same approach. When falling back to the CLI streaming template, inject the brief's `scope.exclude` patterns into the `EXCLUDES` list (use `[]` if absent) — this applies regardless of which path triggered the fallback. Note: `max_results` in the MCP tool and `| head -N` in the CLI path provide hard caps, but this safety valve covers cases where the upstream tool itself fails before returning results (e.g., OOM during JSON serialization).
      
      ### MCP Tool Usage (Preferred)
      
      **Simple pattern search:**
      
      ```
      find_code(
        project_folder="{source_path}",
        pattern="async def $NAME($$$PARAMS)",
        language="python",
        max_results=100,
        output_format="text"
      )
      ```
      
      **Scoped YAML rule search (for larger repos):**
      
      ```
      find_code_by_rule(
        project_folder="{source_path}",
        yaml="id: public-api\nlanguage: python\nrule:\n  pattern: 'def $NAME($$$PARAMS)'\n  inside:\n    kind: module\n    stopBy: end\nconstraints:\n  NAME:\n    regex: '^[^_]'",
        max_results=150,
        output_format="text"
      )
      ```
      
      ### CLI Streaming Fallback
      
      When MCP tools are unavailable or the repo exceeds 500 files in scope, use `--json=stream` (not `--json` or `--json=pretty`, which load the whole result set into memory) with line-by-line Python processing:
      
      **Head cap selection:** The `| head -N` cap at the end of the pipeline controls how many exports are captured. Select `N` based on scope and tier:
      - **Default (Quick/Forge, any scope):** `N = 200`
      - **Forge+/Deep with `scope.type: "full-library"`:** `N = 500`
      - **Forge+/Deep with `scope.type: "component-library"`:** `N = 300` (components have fewer but richer exports; props interfaces are the primary API surface)
      
      For full-library skills at higher tiers, the larger cap prevents silently dropping internal module exports that maintainers need. The cap is applied AFTER exclude-pattern filtering, so useful results are not wasted on excluded files.
      
      ```bash
      # Note: use $$$ for variadic params in ast-grep patterns (e.g., 'def $NAME($$$PARAMS)')
      # {exclude_patterns} = Python list from brief's scope.exclude, e.g. ['tests/**', '**/test_*']
      # If scope.exclude is absent or empty in the brief, inject [] as the default.
      # Patterns are matched against the full file path as emitted by ast-grep.
      # Ensure paths are relative to the same root as the patterns (strip ./ prefix if needed).
      # {HEAD_CAP} = 200 (default) or 500 (Forge+/Deep full-library) — see head cap selection above.
      # IMPORTANT: The explicit 'run' subcommand is required for --json=stream to work.
      ast-grep run -p '{pattern}' -l {language} --json=stream {path} | python3 -c "
      import sys, json, fnmatch, signal
      signal.signal(signal.SIGPIPE, signal.SIG_DFL)
      
      EXCLUDES = {exclude_patterns}
      
      for line in sys.stdin:
          try:
              m = json.loads(line)
              f = m.get('file','')
              if EXCLUDES and any(fnmatch.fnmatch(f, pat) for pat in EXCLUDES):
                  continue
              v = m.get('metaVariables',{})
              name = v.get('single',{}).get('NAME',{}).get('text','')
              if name and not name.startswith('_'):
                  ln = m.get('range',{}).get('start',{}).get('line',0)+1
                  sig = m.get('text','').split(chr(10))[0].strip()
                  print(f'[AST:{f}:L{ln}] {sig}')
          except: pass
      " | head -{HEAD_CAP}
      ```
      
      **Streaming constraints (these prevent OOM on large result sets):**
      
      - Use `--json=stream`, not `--json` — the latter loads the entire array into memory
      - Process line-by-line (`for line in sys.stdin`), not `json.load(sys.stdin)`
      - Cap output with `| head -N` as a safety valve
      - For repos > 500 files, process in directory batches of 20-50 files each: split by top-level source directory, run the CLI streaming template per batch with the same head cap, then merge results and deduplicate by export name (keep the first occurrence if duplicates exist across batches)
      
      ### YAML Rule Recipes by Language
      
      **Python — public functions:**
      
      ```yaml
      id: python-public-functions
      language: python
      rule:
        pattern: 'def $NAME($$$PARAMS)'
        inside:
          kind: module
          stopBy: end
      constraints:
        NAME:
          regex: '^[^_]'
      ```
      
      **Python — public classes:**
      
      ```yaml
      id: python-public-classes
      language: python
      rule:
        pattern: 'class $NAME'
        kind: class_definition
        inside:
          kind: module
          stopBy: end
      constraints:
        NAME:
          regex: '^[^_]'
      ```
      
      > **Pattern note:** The minimal `class $NAME` pattern (with `kind: class_definition` to disambiguate the AST node) works on ast-grep 0.42.x via both MCP `find_code_by_rule` and CLI `--json=stream`. The previously documented `class $NAME($$$BASES)` and `class $NAME($$$BASES):` variants are known-broken on 0.42.x — see Known Limitations #7 below. For simple CLI extraction without a YAML rule, use `ast-grep run -p 'class $NAME' -l python --json=stream {path}` and post-filter names via the `^[^_]` regex in the Python processing step of the CLI streaming template.
      
      **JavaScript/TypeScript — exported functions:**
      
      > **Language selection:** Use `language: typescript` for `.ts` files and `language: tsx` for `.tsx` files. Patterns that work with `typescript` may return zero results with `tsx` and vice versa — they use different tree-sitter parsers. For mixed codebases, run each pattern twice (once per language) and merge results. Note: the `export function $NAME($$$PARAMS)` pattern returns **zero** with `tsx` on ast-grep 0.41.x (see Known Limitation #5) **and** with plain `typescript` on 0.42.x (see Known Limitation #9) — use source reading as the fallback for `export function` on both.
      
      ```yaml
      id: js-exported-functions
      language: typescript  # Use 'tsx' for .tsx files — see language selection note above
      rule:
        pattern: 'export function $NAME($$$PARAMS)'
      ```
      
      **JavaScript/TypeScript — exported constants:**
      
      ```yaml
      id: js-exported-constants
      language: typescript
      rule:
        pattern: 'export const $NAME = $VALUE'
      ```
      
      **JavaScript/TypeScript — exported arrow functions:**
      
      ```yaml
      id: js-exported-arrow-functions
      language: typescript
      rule:
        pattern: 'export const $NAME = ($$$PARAMS) => $BODY'
      ```
      
      > **JS/TS Pattern Merging:** Modern TypeScript codebases often use `export const` exclusively for all exports (arrow functions, objects, constants). Run ALL four JS/TS patterns (functions, arrow functions, constants, classes) and merge results by `$NAME`. Priority when deduplicating: arrow function match > function declaration match > constant match. Arrow function matches capture parameters directly; constant matches require inspecting `$VALUE` to extract signatures.
      
      **JavaScript/TypeScript — exported classes:**
      
      ```yaml
      id: js-exported-classes
      language: typescript
      rule:
        pattern: 'export class $NAME { $$$ }'
      ```
      
      > **Important:** The body (`{ $$$ }`) is required on ast-grep 0.42.x. The bare `export class $NAME` pattern returns zero matches — and emits a `Pattern contains an ERROR node` warning — through **both** `find_code()` and the CLI, because an incomplete class declaration does not parse as a complete statement (see Known Limitation #9). With the body present, the simple `find_code()` pattern detects class exports reliably — it matches non-generic classes only; generic (`export class $NAME<T>`) and generic-extends (`export class $NAME<T> extends $BASE`) forms are skipped, so source-read them via `^export (abstract )?class` and merge by name+file (see Known Limitation #10). `find_code_by_rule` would additionally require an explicit AST `kind` rule.
      
      **JavaScript/TypeScript — re-export detection (use `find_code`):**
      
      Use `find_code()` with pattern `export { $$$NAMES } from $SOURCE` for re-export detection. Note: this pattern may produce multiple AST node matches. Post-process results to split comma-separated names from `$$$NAMES`. For complex re-export chains (aliased exports, default re-exports, namespace re-exports), fall back to the Re-Export Tracing protocol in `extraction-patterns-tracing.md`.
      
      **Rust — public functions:**
      
      ```yaml
      id: rust-public-functions
      language: rust
      rule:
        any:
          - pattern: 'pub fn $NAME($$$PARAMS) -> $RET'
          - pattern: 'pub fn $NAME($$$PARAMS)'
      ```
      
      **Go — exported functions (capitalized):**
      
      ```yaml
      id: go-exported-functions
      language: go
      rule:
        any:
          - pattern: 'func $NAME($$$PARAMS) $RET'
          - pattern: 'func $NAME($$$PARAMS)'
      constraints:
        NAME:
          regex: '^[A-Z]'
      ```
      
      ### Component Library YAML Rule Recipes
      
      These patterns are used by `component-extraction.md` when `scope.type: "component-library"`. They prioritize Props interfaces and PascalCase component exports.
      
      **React/TypeScript — Props interfaces (primary API contracts):**
      
      ```yaml
      id: react-props-interfaces
      language: typescript  # Use 'tsx' for .tsx files
      rule:
        pattern: 'export interface $NAME { $$$ }'
      constraints:
        NAME:
          regex: '.*Props$'
      ```
      
      **React/TypeScript — Component function exports (PascalCase):**
      
      > **Language note:** Use `language: tsx` for `.tsx` files. The `export function` pattern may fail with tsx on ast-grep 0.41.x (see Known Limitations #5). Use `export const` patterns as primary and fall back to source reading for `export function` in tsx files.
      
      ```yaml
      id: react-component-functions
      language: tsx
      rule:
        pattern: 'export function $NAME($$$PARAMS)'
      constraints:
        NAME:
          regex: '^[A-Z]'
      ```
      
      **React/TypeScript — Component arrow function exports:**
      
      ```yaml
      id: react-component-arrow-functions
      language: typescript
      rule:
        pattern: 'export const $NAME = ($$$PARAMS) => $BODY'
      constraints:
        NAME:
          regex: '^[A-Z]'
      ```
      
      **Vue — defineProps extraction:**
      
      ```yaml
      id: vue-define-props
      language: typescript
      rule:
        pattern: 'defineProps<$TYPE>()'
      ```
      
      **Props-to-Component linking strategy:**
      
      After extracting Props interfaces and component exports, link them using this 3-level fallback chain:
      
      1. **Naming convention (primary):** Strip `Props` suffix from interface name → match to component export (e.g., `NativeLiquidButtonProps` → `NativeLiquidButton`)
      2. **File co-location (fallback):** If naming doesn't match, check if a Props interface and a PascalCase export function are defined in the same file — link them
      3. **Generic parameter (deep fallback):** Search for `ComponentProps<typeof $NAME>` or `React.ComponentProps<typeof $NAME>` patterns that reference the component by name
      
      Unlinked Props interfaces are included as standalone type exports. Unlinked component exports are included with a note that no Props interface was found (signature-only, T1-low confidence for API contract).
      
      ### Known ast-grep Limitations
      
      When using ast-grep for extraction, be aware of these documented limitations:
      
      1. **`export class $NAME` needs a body on 0.42.x; `find_code_by_rule` needs explicit `kind`:** The bare `export class $NAME` pattern returns zero through **both** `find_code()` and the CLI on ast-grep 0.42.x — add the body, `export class $NAME { $$$ }` (see #9). With `find_code_by_rule`, a class export additionally needs a `kind` rule for the tree-sitter node type; the simpler `find_code()` with the body-form pattern is the lighter path.
      
      2. **Re-export patterns produce multiple AST nodes:** `export { A, B, C } from './module'` decomposes into multiple metavariable bindings for `$$$NAMES`. Results require post-processing to split comma-separated names.
      
      3. **Default anonymous exports capture no name:** `export default function $NAME` works, but `export default $EXPR` (anonymous default export) captures no name in `$NAME`. Fall back to source reading (T1-low) for anonymous defaults.
      
      4. **Fallback protocol:** If an ast-grep pattern returns errors or zero results when results are expected:
         - First: retry with `find_code()` using a simpler pattern (drop type annotations, use broader match)
         - Second: if `find_code()` also fails, fall back to source reading for that pattern category (T1-low confidence)
         - Never silently accept zero results for a pattern category that the source language commonly uses
      
      5. **TSX `export function` pattern failure:** The `export function $NAME($$$PARAMS)` pattern may return zero results in TSX files with ast-grep 0.41.x. This affects both MCP tools and CLI. `export const` and `export type` patterns are unaffected. **Workaround:** For TSX files, use `export const` patterns first (which work), then fall back to source reading (grep/file read) for `export function` declarations. When a TSX codebase shows zero `export function` matches but source files clearly contain them, this is a known ast-grep tree-sitter tsx parser limitation — not an extraction error. Log it in the evidence report and proceed with T1-low confidence for those exports.
      
      6. **CLI `--json=stream` may produce no output:** On ast-grep 0.41.x, `--json=stream` may produce empty output for certain patterns. The `--json=stream` flag requires the explicit `run` subcommand: use `ast-grep run -p '{pattern}' --json=stream` (not `ast-grep -p '{pattern}' --json=stream`). If streaming still produces no output, fall back to the MCP tool or source reading.
      
      7. **Python class patterns with bases/colon return zero (ast-grep 0.42.x):** The patterns `class $NAME($$$BASES)` and `class $NAME($$$BASES):` return zero matches on real Python sources with ast-grep 0.42.0, even on files containing dozens of subclassed public classes. `find_code_by_rule` also rejects the bare inline rule without `kind` as `Rule must specify a set of AST kinds to match. Try adding \`kind\` rule.` **Workaround:** Use the minimal `class $NAME` pattern with `kind: class_definition` (YAML) or `ast-grep run -p 'class $NAME' -l python --json=stream` (CLI), then post-filter names via the `^[^_]` regex. The `^[^_]` constraint enforces the "public" filter since ast-grep's base-match rule is what's broken, not the name-match rule. See the Python — public classes recipe above.
      
      8. **Rust `pub fn` any-pattern returns zero; bare `pub fn $NAME` over-captures (ast-grep 0.42.x):** The `rust-public-functions` recipe's `any:` of `pub fn $NAME($$$PARAMS) -> $RET` / `pub fn $NAME($$$PARAMS)` returns "No matches found" on real Rust sources with ast-grep 0.42.2, even on crates containing 200+ public functions. Dropping to the bare `pub fn $NAME` pattern matches, but over-captures restricted-visibility functions such as `pub(crate) fn` / `pub(super) fn`, which are **not** public API. **Workaround:** Prefer a visibility-constrained source grep — `rg '^\s*pub fn ' <src>` filtered to exclude lines beginning `pub(` — cross-checked against the AN-verified public surface, at T1-low confidence. Never silently accept zero results for Rust public functions, and never treat a bare `pub fn $NAME` match set as the public API without stripping `pub(...)`-restricted items. See the Rust — public functions recipe above.
      
      9. **Plain `language: typescript` declaration patterns without a body return zero (ast-grep 0.42.x):** For `language: typescript` on ast-grep 0.42.2, the incomplete-statement patterns `export class $NAME`, `export function $NAME($$$PARAMS)`, `export type $NAME`, and `export enum $NAME` all return **zero** matches against real `.ts` sources — `export class` / `export type` / `export enum` additionally print `Pattern contains an ERROR node`. This affects the CLI (`ast-grep run -p ... -l typescript`) and the MCP `find_code()` API **identically** — `find_code()` is not a workaround. Only `export const $NAME = $VALUE` matches as documented. The cause is that a declaration pattern missing its body/initializer does not parse as a complete statement. **Workarounds (verified on 0.42.2 via both CLI and `find_code`):**
         - **class:** add the body — `export class $NAME { $$$ }` matches.
         - **enum:** add the body — `export enum $NAME { $$$ }` matches.
         - **type alias:** drop `export` and include the initializer — `type $NAME = $T` matches (and captures both `export type` and bare `type` declarations).
         - **interface:** `export interface $NAME { $$$ }` already carries a body and **works** for plain interfaces — but it misses generic (`interface $NAME<T>`) and `extends` forms; source-read those at T1-low.
         - **function:** the body form `export function $NAME($$$PARAMS) { $$$ }` matches only functions with no return-type annotation and no `async` modifier, so it is unreliable. Prefer a source-read fallback (or a barrel cross-check) at T1-low for `export function`, mirroring the tsx guidance in #5.
      
         Never silently accept zero results for a declaration form the source language commonly uses.
      
      10. **Generic class declarations do not match non-generic class patterns (ast-grep 0.42.x):** The non-generic patterns `export class $NAME { $$$ }` and `export class $NAME extends $BASE { $$$ }` silently skip every generic form on ast-grep 0.42.2 — verified via both the CLI (`ast-grep run -p ... -l typescript`) and `find_code()`, the first matched only a plain `Plain` and the second only a plain `PlainExtends` on a fixture of `Plain` / `Generic<T>` / `GenericExtends<T> extends Base<T>` / `abstract AbstractGeneric<T>` / `PlainExtends`. The per-shape generic patterns `export class $NAME<$$$P> { $$$ }`, `export class $NAME<$$$P> extends $BASE { $$$ }`, and `export abstract class $NAME<$$$P> { $$$ }` each match exactly **one** shape — no single pattern covers all of them. **Workaround:** Always run a source-read fallback `^export (abstract )?class` over the in-scope `.ts` sources (T1-low) and merge by name+file with the AST results, mirroring the `export function` guidance in #9. The bare `class $NAME` pattern catches every form but over-captures non-exported classes, so it still needs the source-read pass to re-impose the exported-only filter — this is the opposite trade from the Python #7 `^[^_]` re-filter, since TS has no name-prefix convention for exports. Never accept a class inventory that omits generic classes when the source uses them.
      
      ### Component Library Demo/Example Auto-Exclusion
      
      When `scope.type: "component-library"`, auto-detect and propose demo/example exclusions before extraction begins. **User confirmation is required before applying** — some `examples/` directories contain API-level code.
      
      **Auto-detect directory patterns:**
      - `**/demo/**`, `**/demos/**`
      - `**/stories/**`, `**/__stories__/**`, `**/storybook/**`
      - `**/examples/**`, `**/example/**`
      
      **Auto-detect file patterns:**
      - `**/*.stories.*`, `**/*.story.*`
      - `**/*.example.*`, `**/*.demo.*`
      
      If `demo_patterns` is specified in the brief, use those instead of auto-detection.
      
      **Procedure:**
      1. Scan the scoped file tree for matching directories and files
      2. Count matches per pattern category
      3. Present to user: "**Auto-detected {N} demo/example files** in {M} directories matching these patterns: {list}. Confirm exclusion? [Y/n] Or adjust patterns:"
      4. Apply confirmed patterns to the exclude list before AST extraction
      5. Record in extraction inventory: `demo_files_excluded: {count}`
      
      ### Re-Export Tracing and Script/Asset Extraction
      
      See `extraction-patterns-tracing.md` for:
      - **Re-export tracing protocol** — resolving module imports through `__init__.py`, barrel files, `pub use`
      - **Script/asset extraction patterns** — detection heuristics, inclusion rules, provenance, inventory structure
      
      
    • generate-artifacts.md 14 KB
      ---
      nextStepFile: 'report.md'
      forgeTierConfig: '{sidecar_path}/forge-tier.yaml'
      # Resolve `{atomicWriteHelper}` by probing `{atomicWriteProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. HALT if neither resolves — the active-symlink flip and registry
      # writes below go through the atomic helper for concurrency safety.
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      # Resolve `{forgeTierRwHelper}` by probing `{forgeTierRwProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. HALT if neither resolves — §6b's ccc-index registry round-trip is
      # comment-preserving and has no prose fallback.
      forgeTierRwProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-forge-tier-rw.py'
        - '{project-root}/src/shared/scripts/skf-forge-tier-rw.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 7: Generate Artifacts
      
      ## STEP GOAL:
      
      To write all compiled content to disk — 4 deliverable files to `{skill_package}` and 3 workspace artifacts to `{forge_version}`, creating directories as needed. Then create or update the `active` symlink.
      
      ## Rules
      
      - Focus only on writing files from compiled content — do not modify content during writing
      - All base artifact types must be written (4 deliverables + 3 workspace files + N reference files)
      - Create directories before writing files
      
      ## MANDATORY SEQUENCE
      
      ### 1. Create Directory Structure
      
      Resolve `{version}` from the skill brief's `version` field. Create the following directories:
      
      ```
      {skill_group}                          # {skills_output_folder}/{name}/
      {skill_package}                        # {skills_output_folder}/{name}/{version}/{name}/
      {skill_package}/references/
      {forge_version}                        # {forge_data_folder}/{name}/{version}/
      ```
      
      If `scripts_inventory` is non-empty, also create: `{skill_package}/scripts/`
      If `assets_inventory` is non-empty, also create: `{skill_package}/assets/`
      
      Where `{name}` is the skill name from the brief (kebab-case) and `{version}` is the semver version from the brief (with build metadata stripped per `knowledge/version-paths.md`).
      
      If directories already exist, do not error — proceed with file writing (overwrites existing files).
      
      ### 2. Write Deliverables to {skill_package}
      
      Write these 4 files from the compiled content:
      
      **File 1:** `{skill_package}/SKILL.md`
      - The complete compiled skill document
      - agentskills.io-compliant format with all sections
      - [MANUAL] markers seeded
      
      **File 2:** `{skill_package}/context-snippet.md`
      - Compressed 2-line format for CLAUDE.md integration
      
      **File 3:** `{skill_package}/metadata.json`
      - Machine-readable birth certificate with stats and provenance
      
      **File 4:** `{skill_package}/references/*.md`
      - One file per function group or type
      - Progressive disclosure detail files
      
      **Files 4b (conditional):** `{skill_package}/scripts/*`
      - One file per detected script, copied from source with content preserved
      - Only created when `scripts_inventory` is non-empty
      
      **Files 4c (conditional):** `{skill_package}/assets/*`
      - One file per detected asset, copied from source with content preserved
      - Only created when `assets_inventory` is non-empty
      
      **Note on `file_type: "doc"` entries** (promoted authoritative docs from step 3 §2a):
      
      Promoted docs are tracked in `file_entries[]` with `file_type: "doc"` for drift detection but are **not** copied into the skill package. The source file remains at its original location outside `{skill_package}`. Step-07 must skip any `file_entries[]` row where `file_type == "doc"` when iterating for file copy — these entries exist only for provenance tracking, not bundling. Step-07 verification (§5) also does not check for doc files in the skill package.
      
      ### 3. Write Workspace Artifacts to {forge_version}
      
      Write these 3 files from the compiled content:
      
      **File 5:** `{forge_version}/provenance-map.json`
      - Per-claim source map with AST bindings and confidence tiers
      
      **File 6:** `{forge_version}/evidence-report.md`
      - Build artifact with extraction summary, validation results, warnings
      
      **File 7:** `{forge_version}/extraction-rules.yaml`
      - Language and ast-grep schema used for this extraction (for reproducibility)
      - Note: This file is generated here from extraction data collected during steps 3-4, not assembled in step 5
      
      ### 4. Create Active Symlink (atomic flip)
      
      Create or update the `active` symlink at `{skill_group}/active` pointing to `{version}` using the shared atomic-flip helper. The helper holds an `flock` on `{skill_group}/active.skf-lock`, refuses to replace a non-symlink at `{skill_group}/active` (protecting against accidental rm-rf of a real directory), and uses a rename-over-symlink pattern so the update is atomic from a concurrent reader's perspective:
      
      ```bash
      python3 {atomicWriteHelper} flip-link \
        --link {skill_group}/active \
        --target {version}
      ```
      
      The helper returns non-zero (exit 2) if `{skill_group}/active` already exists as a real directory or file rather than a symlink — in that case, halt with: "Refusing to flip `{skill_group}/active` — existing path is not a symlink. Investigate manually; expected a symlink pointing at a version directory."
      
      Do not `rm` + `ln -s` the active link by hand. The bare-rm pattern has two failure modes: (1) a concurrent reader sees a missing `active` mid-flip, and (2) a bug or typo that replaces `{skill_group}/active` with a plain directory turns the next manual `rm -rf {skill_group}/active` into data loss. The helper encapsulates both guards.
      
      ### 5. Verify Write Completion
      
      After all files are written, verify:
      - All 4 deliverable artifact types exist (SKILL.md, context-snippet.md, metadata.json, **and** either at least one file in `references/` **or** `references/` is empty AND Tier-2 content is inline in SKILL.md — see "Empty `references/` exception" below), all 3 workspace artifacts exist (provenance-map.json, evidence-report.md, extraction-rules.yaml), plus scripts/ and assets/ files when inventories are non-empty
      - The `active` symlink at `{skill_group}/active` resolves to `{version}`
      - Store `ref_count` = count of files written to `references/` for use in step 8 report
      - List each file with its path and size
      
      **Empty `references/` exception (Tier-2 inline):** `ref_count == 0` is a valid completion state when step 6 kept Tier-2 content inline in SKILL.md — e.g., the body was already under the size limit, or `skill-check` was unavailable and the manual fallback (step 6 §3) skipped the split. In that case, append a single line to `{forge_version}/evidence-report.md` recording the inline state so downstream tooling and audits can distinguish "inline by design" from "split-body skipped due to error":
      
      ```
      ref_count: 0  # Tier-2 kept inline in SKILL.md (no split performed in step 6)
      ```
      
      When `ref_count > 0` is expected (because step 6 ran a split) but no files were written, halt with: "Split-body produced zero reference files. Investigate step 6 output before retrying — empty `references/` after a split is never a valid state."
      
      **If any write failed:**
      Halt with: "Artifact generation failed: could not write `{file_path}`. Check permissions and disk space."
      
      **If all writes succeeded:**
      Display brief confirmation:
      
      "**Artifacts generated.**
      
      **Deliverables ({skill_package}):**
      - SKILL.md
      - context-snippet.md
      - metadata.json
      {if scripts: - scripts/ ({scripts_count} files)}
      {if assets: - assets/ ({assets_count} files)}
      - references/ ({reference_count} files)
      
      **Workspace ({forge_version}):**
      - provenance-map.json
      - evidence-report.md
      - extraction-rules.yaml
      
      **Symlink:** {skill_group}/active -> {version}
      
      Proceeding to compilation report..."
      
      ### 6. QMD Collection Registration (Deep Tier Only)
      
      **IF forge tier is Deep AND QMD tool is available:**
      
      Index the generated skill artifacts into a QMD collection so that audit-skill and update-skill can perform high-signal searches against curated extraction data instead of raw source files.
      
      **Collection creation:** Create (or replace) a QMD collection from the skill artifacts:
      ```bash
      qmd collection remove {name}-extraction 2>/dev/null  # no-op if new
      qmd collection add {skill_package} --name {name}-extraction --mask "**/*"
      qmd embed --collection {name}-extraction  # generates vector embeddings for semantic (vec) and HyDE query sub-types; scope to this collection to avoid re-embedding others. If the installed qmd CLI lacks --collection, gate the embed behind a per-skill freshness check (skip when the existing {name}-extraction entry is within 24 hours — rationale: an unscoped embed re-runs over every collection, which in a populated QMD store can cost minutes of GPU time per create-skill run; 24 hours is long enough to absorb rapid re-forges from the same brief without losing meaningful content freshness) and warn in evidence-report.
      ```
      
      **Registry update:**
      
      Read `{forgeTierConfig}` and update the `qmd_collections` array **under an exclusive `flock` on `{sidecar_path}/forge-tier.yaml.lock`** (see step 3b §4 for the full pattern — acquire lock → read → modify → atomic write via `skf-atomic-write.py write` → release). If `flock` is unavailable, fall back to read-CAS-by-mtime.
      
      If an entry with `name: "{name}-extraction"` already exists, replace it. Otherwise, append:
      
      ```yaml
        - name: "{name}-extraction"
          type: "extraction"
          source_workflow: "create-skill"
          skill_name: "{name}"
          created_at: "{current ISO date}"
      ```
      
      Write the updated forge-tier.yaml.
      
      **Error handling:**
      - If QMD collection creation fails: log the error, note that indexing can be retried via [SF] setup. Do not fail the workflow.
      - If forge-tier.yaml update fails: log the error, continue. The collection exists in QMD even if the registry entry failed.
      
      **IF forge tier is not Deep:** Skip this section silently. No messaging.
      
      ### 6b. CCC Index Registry Registration (Forge+ and Deep with ccc)
      
      **IF `tools.ccc` is true in forge-tier.yaml (Forge+ or Deep with ccc available):**
      
      Ensure the source path used for extraction is indexed by ccc and registered in the `ccc_index_registry` array.
      
      **Index verification:**
      
      **Working-directory contract:** `ccc init`, `ccc index`, and `ccc status` take **no positional path argument** — each operates on the current working directory, and passing a path makes them exit non-zero with `Got unexpected extra argument`. Run them from `{source_root}` (`cd {source_root} && ccc …`, the same form `extract.md` step 2b and `knowledge/ccc-bridge.md` use), then return to `{project-root}` before the registry update below, which resolves `{forgeTierConfig}` relative to the project.
      
      `ccc index` requires the directory to be initialized first. Run `cd {source_root} && ccc init` (idempotent — a no-op once initialized) before `cd {source_root} && ccc index`, or use the ccc MCP tool. `ccc_bridge.ensure_index` is a conceptual interface, not a callable function. Indexing is a no-op if the source was already indexed during setup or step 2b.
      
      **Nested project marker:** when `{source_root}` is a subtree of a repo that already carries a project marker (e.g. a cloned source tree under `.forge-sources/`, or a `.cocoindex_code` / VCS marker in a parent), `ccc init` exits non-zero with `A parent directory has a project marker`. This is expected — re-run as `cd {source_root} && ccc init -f` to initialize at the subtree anyway, then `ccc index`. Do not treat the parent-marker warning as fatal.
      
      **Verify the index is not degraded:** after `ccc index`, run `cd {source_root} && ccc status` and read the `Languages:` breakdown — a non-zero `Chunks`/`Files` total is not sufficient. Confirm the source's primary language (`{brief.language}`) reports a non-trivial chunk count. An index dominated by `markdown`/config chunks with the source language absent or near-zero means `ccc index` ran against degraded settings (a stale `.cocoindex_code/` inherited from a parent project, or a no-op `ccc init` over a prior partial index that returned `Project already initialized.`) — the source code was never indexed, which silently cripples later `skf-audit-skill` / `skf-update-skill` searches with no error surfaced. When the source language is absent, rebuild from a clean init (`cd {source_root} && ccc init -f` then `ccc index`) and re-check the breakdown. Note that `-f` only skips the parent-marker warning — it does not clear a stale `.cocoindex_code/`; when the degraded settings survive the forced re-init, `ccc reset` (run from `{source_root}`) is what discards the existing databases. If the source language is still missing after a forced re-init, log it and continue per the error handling below — a degraded index is not workflow-fatal, but it must be recorded so the gap is visible.
      
      **Registry update:**
      
      **Resolve `{forgeTierRwHelper}`** from `{forgeTierRwProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      Register the indexed source path via `register-ccc-index` — a comment-preserving round-trip on `forge-tier.yaml`. This differs from §6, which mutates the registry with an inline read→modify→atomic-write under `flock`; here the helper owns the read→modify→write internally so the `ccc_index_registry` deduplication logic stays in the script. Acquire an exclusive `flock` on `{sidecar_path}/forge-tier.yaml.lock`, then:
      
      ```bash
      echo '{"source_repo":"{brief.source_repo}","path":"{source_root}","skill_name":"{name}","indexed_at":"{current ISO date}","source_workflow":"create-skill"}' \
        | uv run {forgeTierRwHelper} register-ccc-index --target {forgeTierConfig}
      ```
      
      Deduplicates by `source_repo` + `skill_name` (not local `path`, which may be ephemeral). Release the lock after the command completes. If `flock` is unavailable, fall back to read-CAS-by-mtime.
      
      **Error handling:** If ccc indexing or registry update fails, log and continue — do not fail the workflow.
      
      **IF `tools.ccc` is false:** Skip this section silently.
      
      ### 7. Auto-Proceed
      
      No user interaction. Once all 7 files are written and verified (and optionally indexed into QMD), load `{nextStepFile}`, read it fully, then execute it. A QMD-indexing failure does not block; a file-write failure halts (§5) rather than proceeding with partial output.
      
      
    • health-check.md 1 KB
      ---
      # `shared/health-check.md` resolves relative to the SKF module root
      # (`{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during
      # development), not relative to this step file.
      nextStepFile: 'shared/health-check.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 9: Workflow Health Check
      
      ## STEP GOAL:
      
      Chain to the shared workflow self-improvement health check at `{nextStepFile}`. This is the terminal step of create-skill — after the shared health check completes, the workflow is fully done.
      
      ## Rules
      
      - No user-facing reports, file writes, or result contracts in this step — those belong in step 8
      - Delegate directly to `{nextStepFile}` with no additional commentary
      - In batch mode, this step is only reached after the final brief — step 8 loops back to load-brief for remaining briefs and skips chaining here
      - Do not attempt any other action between loading this step and executing `{nextStepFile}`
      
      ## MANDATORY SEQUENCE
      
      Load `{nextStepFile}`, read it fully, then execute it.
      
    • load-brief.md 10.3 KB
      ---
      nextStepFile: 'ecosystem-check.md'
      forgeTierFile: '{sidecar_path}/forge-tier.yaml'
      preferencesFile: '{sidecar_path}/preferences.yaml'
      # Resolve `{validateBriefSchemaHelper}` to the first existing path; HALT if
      # neither candidate exists. §3 relies on the helper for deterministic
      # schema-conformance checks (required fields, regex patterns, enum
      # membership, docs-only conditional rules) so this stage does not re-run
      # those checks in prose.
      validateBriefSchemaProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
        - '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 1: Load Brief
      
      ## STEP GOAL:
      
      To load and validate the skill-brief.yaml compilation config, resolve the source code location, and load the forge tier from sidecar to determine available capabilities for the compilation pipeline.
      
      ## Rules
      
      - Focus only on loading brief, resolving source, and determining tier — do not begin extraction or compilation
      - Do not write any output files — this step only loads and validates
      
      ## MANDATORY SEQUENCE
      
      ### 1. Load Forge Tier
      
      Load `{forgeTierFile}` completely.
      
      **If file does not exist:**
      Halt with: "Forge halted: No forge configuration found. Run [SF] Setup Forge first to detect tools and set your tier."
      
      **If file exists:**
      Extract and report:
      - `tier`: Quick, Forge, Forge+, or Deep
      - `tools`: which tools are available (gh, ast-grep, ccc, qmd)
      - `ccc_index`: ccc index state (status, indexed_path, last_indexed) — needed by step 2b
      
      **Apply tier override:** Read `{preferencesFile}`. If `tier_override` is set and is one of the exact valid tier values (`Quick`, `Forge`, `Forge+`, `Deep`), use it instead of the detected tier. **If `tier_override` is set but is not one of those four values:** log a warning — "Unknown tier_override `{value}` in preferences.yaml; falling back to detected tier `{detected_tier}`. Valid values: Quick, Forge, Forge+, Deep." — and use the detected tier. Never silently apply an unknown override value, and never map it heuristically to a tier.
      
      **Record the decision:** append an entry to the in-context `headless_decisions[]` buffer (initialize to `[]` at the start of this step if absent) whenever a non-interactive choice is made automatically — both the valid-override path AND the rejected-override path:
      
      - Valid override applied: `{step: "load-brief", gate: "tier-override", decision: "apply", value: "{tier_override}", rationale: "explicit preferences.yaml tier_override", timestamp: {ISO}}`
      - Invalid override rejected: `{step: "load-brief", gate: "tier-override", decision: "reject-invalid", value: "{tier_override}", fallback: "{detected_tier}", rationale: "tier_override not in {Quick,Forge,Forge+,Deep}", timestamp: {ISO}}`
      
      These entries stay in the in-context buffer until §3 establishes the on-disk auto-decision sink and seeds it with the buffer-so-far; from then on every later gate's decision is appended to the sink as it lands (per the headless Workflow Rule). Step 5 §7 renders the sink into the evidence-report `## Auto-Decisions` table and step 6 §8 reconciles it, so reviewers can audit every silent choice even on a run long enough to compact the in-context buffer before step 5.
      
      ### 2. Discover Skill Brief
      
      **If user provided a specific brief path or skill name:**
      - If the value looks like a file path (starts with `/`, `./`, `~`, or contains path separators): treat it as a direct file path and load it
      - Otherwise, treat it as a skill name and search `{forge_data_folder}/{skill-name}/skill-brief.yaml`
      - If found, load it completely
      
      **If user invoked with --batch flag:**
      - Check `{sidecar_path}/batch-state.yaml` for an active batch checkpoint:
        - If `batch_active: true`: validate the checkpoint before trusting it — both conditions below must hold:
          1. `0 <= current_index < len(brief_list)` — the index points inside the recorded list.
          2. `os.path.exists(brief_list[current_index])` — the brief file is still on disk.
          If both hold, load the brief at `brief_list[current_index]` (resuming a batch loop from step 8). If **either** check fails, the checkpoint is stale (briefs renamed, moved, or deleted between runs; index off the end after a partial failure). Log a warning — "Stale batch checkpoint — current_index={i}, brief_list length={n}, brief_exists={bool}. Resetting and re-discovering." — then set `batch_active: false` in `batch-state.yaml` and fall through to the no-checkpoint branch below.
        - If no checkpoint exists or `batch_active` is false: search specified directory for all `skill-brief.yaml` files, list discovered briefs with skill names, store list for batch loop processing, and load the FIRST brief
      
      **If no brief found:**
      Halt with: "No skill brief found. Run [BS] Brief Skill to create one, or use [QS] Quick Skill for brief-less generation."
      
      ### 3. Validate Brief Structure
      
      Run the deterministic schema validator — it checks required fields, regex patterns (`name`, `version`), enum membership (`source_type`, `source_authority`, `forge_tier`, `scope.type`), type correctness, the docs-only conditional rule (`doc_urls` ≥ 1 when `source_type == "docs-only"`), and the version-non-empty-or-whitespace rule:
      
      ```bash
      uv run {validateBriefSchemaHelper} <path-to-skill-brief.yaml>
      ```
      
      The helper emits:
      
      ```json
      {
        "valid": <bool>,
        "errors":   [{"field": "...", "message": "Brief validation failed: ..."}, ...],
        "warnings": [{"field": "...", "message": "..."}, ...],
        "halt_reason": "brief-missing" | "brief-malformed" | "brief-invalid" | null,
        "brief": { ...parsed YAML when loadable... }
      }
      ```
      
      **If `valid` is false:** HALT and display the first error's `message` field verbatim — the helper already formats messages in the "Brief validation failed: ..." form the user expects. For halt-reasons:
      
      - `brief-missing` — the brief path doesn't exist. Display the helper's message (it includes the `Run [BS] Brief Skill` redirect).
      - `brief-malformed` — the YAML failed to parse. Display the helper's message.
      - `brief-invalid` — schema or conditional-rule violation. Display the first `errors[].message`. Multiple errors may appear; the user typically fixes one source and re-runs.
      
      **If `valid` is true:** continue with `brief` (the parsed object) for downstream sections. Surface any `warnings[]` to the user but do not halt.
      
      **Establish the auto-decision sink (durable headless audit).** The brief is now confirmed, so create the on-disk audit sink at `{sidecar_path}/auto-decisions.jsonl` — one compact JSON object per line — and seed it with whatever is already in the in-context `headless_decisions[]` buffer (the §1 tier-override row, or nothing if none fired). This truncates any stale sink a prior brief left behind:
      
      ```bash
      : > {sidecar_path}/auto-decisions.jsonl          # truncate / create empty
      # then, for each entry already in headless_decisions[], append its compact JSON:
      printf '%s\n' '{decision-json}' >> {sidecar_path}/auto-decisions.jsonl
      ```
      
      From here on, **every time a later gate appends an auto-decision to `headless_decisions[]`, append that same object as a line to this sink the moment it lands** (the on-landing append the headless Workflow Rule mandates). The array is tiny, so the append costs nothing and keeps a complete on-disk copy across the token-heavy step 3→5 extraction window — the point where a long component-library run can compact the in-context buffer. Step 5 §7 renders this sink into the evidence-report `## Auto-Decisions` table and step 6 §8 reconciles it, so the audit table and `auto_decision_count` stay complete even when the buffer is lost.
      
      **Field reference (for human readers):**
      
      The complete contract — required fields, optional fields, types, and rules — lives in `src/shared/scripts/schemas/skill-brief.v1.json` and the prose mirror at `src/skf-brief-skill/assets/skill-brief-schema.md`. Read those if you need to explain a specific field; do not restate the rules here.
      
      ### 4. Resolve Source Code Location
      
      **If `source_type: "docs-only"`:** Skip source resolution. Set `source_root: null` in context. Proceed directly to section 5 (Report Initialization) — docs-only skills have no source to resolve.
      
      **If source_repo is a GitHub URL or owner/repo format:**
      - Verify repository exists via `gh_bridge.list_tree(owner, repo, branch)` — **Tool resolution:** `gh api repos/{owner}/{repo}/git/trees/{branch}?recursive=1` or direct file listing if local; see `knowledge/tool-resolution.md`
      - If branch not specified, detect default branch
      - Store resolved: owner, repo, branch, file tree — note: `source_root` for remote repos is initially set to the remote URL (for detection and API access purposes) and then updated to the local workspace/clone path during step 3 source resolution
      - **Version-to-tag pinning intent:** If `brief.target_version` is absent but `brief.version` is present, record the intent to apply **implicit tag resolution** from `brief.version` when step 3 resolves the source. Do not resolve the tag here — tag resolution runs in step 3 alongside the clone. This step only notes the pinning intent so step 3 knows to attempt it. See `references/source-resolution-protocols.md` → "Implicit Tag Resolution".
      
      **If source_repo is a local path:**
      - Verify path exists and contains source files
      - Store resolved: local path as `source_root`, file listing
      
      **If source cannot be resolved:**
      Halt with: "Source not found: `{source_repo}`. Verify the repository exists and is accessible."
      
      ### 5. Report Initialization
      
      Display initialization summary:
      
      "**Forge initialized.**
      
      **Skill:** {name} v{version}
      **Source:** {source_repo} @ {branch}
      **Language:** {language}
      **Scope:** {scope}
      **Tier:** {tier} — {tier_description}
      **Tools:** {available_tools_list}
      
      Proceeding to ecosystem check..."
      
      Where tier_description follows positive capability framing:
      - Quick: "Source reading and spec validation"
      - Forge: "AST-backed structural extraction"
      - Forge+: "Semantic-guided precision — ccc pre-ranks files before AST extraction"
      - Deep: "Full intelligence — structural + contextual + QMD knowledge synthesis"
      
      ### 6. Auto-Proceed
      
      No user interaction. After initialization completes and all data is loaded (including `target_version` if present), load `{nextStepFile}`, read it fully, then execute it. A failed prerequisite check above has already halted with an actionable error rather than reaching here.
      
      
    • report.md 10.4 KB
      ---
      nextStepFile: 'health-check.md'
      # Resolve `{atomicWriteHelper}` by probing `{atomicWriteProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. HALT if neither resolves.
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 8: Report
      
      ## STEP GOAL:
      
      To display the final compilation summary — skill name, version, source, export count, confidence distribution, tier used, file list, and any warnings — and suggest next steps for the user.
      
      ## Rules
      
      - Focus only on reporting compilation results — do not modify any files
      - Deliver structured report with confidence breakdown
      - Chains to the local health-check step via `{nextStepFile}` after completion (non-batch mode, or after the final batch brief) — the user-facing report is not the terminal step
      
      ## MANDATORY SEQUENCE
      
      ### 1. Display Forge Completion Banner
      
      "**Skill forged: {name} v{version} — {export_count} functions, {primary_confidence} confidence.**"
      
      Where `{primary_confidence}` is the predominant confidence tier (T1 if Forge/Deep, T1-low if Quick).
      
      ### 2. Display Compilation Summary
      
      "**Compilation Summary**
      
      | Field | Value |
      |-------|-------|
      | **Skill** | {name} v{version} |
      | **Source** | {source_repo} @ {branch} ({commit_short}) |
      | **Language** | {language} |
      | **Forge Tier** | {tier} — {tier_description} |
      | **Files Scanned** | {file_count} |
      | **Exports Documented** | {documented_count} public API ({public_api_coverage}%) / {total_count} total ({total_coverage}%) |
      
      **Confidence Distribution:**
      | Tier | Count | Description |
      |------|-------|-------------|
      | T1 (AST) | {t1_count} | Structurally verified via ast-grep |
      | T1-low (Source) | {t1_low_count} | Inferred from source reading |
      | T2 (QMD) | {t2_count} | QMD-enriched semantic context |
      | T3 (External) | {t3_count} | Sourced from external documentation URLs |
      
      **Output Files:**
      - `{skill_package}/SKILL.md` — Active skill with trigger-based usage
      - `{skill_package}/context-snippet.md` — Passive context snippet (used by export-skill)
      - `{skill_package}/metadata.json` — Machine-readable birth certificate
      - `{skill_package}/references/` — Progressive disclosure ({ref_count} files)
      - `{forge_version}/provenance-map.json` — Source map with AST bindings
      - `{forge_version}/evidence-report.md` — Build audit trail
      - `{forge_version}/extraction-rules.yaml` — Reproducible extraction schema
      - `{skill_group}/active` -> `{version}` — Symlink to current version"
      
      ### 3. Display Warnings (If Any)
      
      If there were warnings from extraction, validation, or enrichment, display them:
      
      "**Warnings:**
      - {warning_1}
      - {warning_2}
      - ..."
      
      If no warnings, omit this section entirely.
      
      ### 4. Suggest Next Steps
      
      "**Recommended next steps:**
      - **[TS] Test Skill** — verify completeness and accuracy before export
      - **[EX] Export Skill** — publish to your skill library or agentskills.io
      - **[US] Update Skill** — edit specific sections or add manual content
      
      To use this skill immediately, add the context snippet to your CLAUDE.md:
      ```
      {context_snippet_content}
      ```"
      
      ### 5. Batch Mode Status (If Applicable)
      
      **If running in --batch mode:**
      
      "**Batch progress:** {completed_count} of {total_count} skills compiled.
      
      {If more remaining:} Proceeding to next brief: {next_skill_name}..."
      
      Update the batch checkpoint in `{sidecar_path}/batch-state.yaml` with:
      
      ```yaml
      batch_active: true
      brief_list: [{full list of brief paths}]
      current_index: {index of next brief to process, 0-based}
      completed: [{list of completed skill names}]
      last_updated: {ISO timestamp}
      ```
      
      **Before writing:** validate the same two invariants that step 1 re-checks on resume — `0 <= current_index < len(brief_list)` AND `os.path.exists(brief_list[current_index])`. If either fails (e.g., the next brief file was deleted mid-batch, or arithmetic pushed the index off the end), set `batch_active: false` and write `batch_halt_reason: "invalid checkpoint at write time — index or file missing"` instead of the active record. The next run will re-discover rather than resume a broken index.
      
      Then load and execute `references/load-brief.md` for the next brief. Step-01 detects an active batch via `batch-state.yaml` and loads the brief at `current_index` only after re-validating the same invariants (belt and braces — the checkpoint may have been edited between runs).
      
      **If all batch briefs complete:**
      
      Set `batch_active: false` in `{sidecar_path}/batch-state.yaml` to prevent stale state. Display: "Batch complete. {completed_count} skills compiled."
      
      **If not batch mode:**
      
      End workflow. No further steps.
      
      ### Result Contract
      
      **If not batch mode (or all batch briefs complete):**
      
      **Resolve the schema reference:** before writing, verify that `{project-root}/src/shared/references/output-contract-schema.md` exists and is readable. Try in order: `{project-root}/src/shared/references/output-contract-schema.md`, then `{project-root}/_bmad/skf/shared/references/output-contract-schema.md` (installed-forge path).
      
      - **If resolved:** write the result contract per the schema — the per-run record at `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_version}/create-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include `SKILL.md`, `context-snippet.md`, `metadata.json`, **and `{forge_version}/evidence-report.md`** paths in `outputs` (the evidence report carries the `## Auto-Decisions` audit table where every silent auto-decision is recorded — pipeline consumers follow this path to audit the run) and confidence distribution in `summary`. Also set `summary.auto_decision_count` to the number of decision rows in the reconciled evidence-report `## Auto-Decisions` table (0 when the run was interactive and the section holds only the no-auto-decisions line) so a consumer can tell from the result JSON alone whether any gate auto-resolved. Count the persisted rows — the durable audit record step 6 §8 reconciled from disk — rather than the in-context `headless_decisions[]` length, so the count matches the table even if a long headless run compacted the buffer. Use `python3 {atomicWriteHelper} write --target {forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` (stdin-piped JSON) for the per-run record, then the same helper for the `-latest.json` copy.
      
      - **If neither candidate path resolves:** skip the result-contract write entirely. Append a warning to `evidence-report.md`: "Result contract skipped — `shared/references/output-contract-schema.md` could not be resolved at either candidate path." Then set `validation_status: 'schema-unavailable'` in `metadata.json` (and re-write metadata.json via `skf-atomic-write.py write`). Pipeline consumers will observe the missing `-latest.json` and the metadata flag.
      
      **Post-completion hook (optional).** After the result JSON and `metadata.json` are finalized, if `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 from `workflow.on_complete`), invoke it:
      
      ```bash
      {onCompleteCommand} --result-path={forge_version}/create-skill-result-latest.json
      ```
      
      Log success or failure to `workflow_warnings[]` but never fail the workflow on a hook error — the skill is already written and the result contract is final. The hook runs last so a git-add, registry registration, notifier, or downstream-skill chain sees a complete package. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
      
      ### Result Contract on HARD HALT
      
      The success-variant contract above is only reached at step 8. The ~10 HARD HALT conditions in steps 1–7 (forge-config missing, no brief, brief invalid, source not found, prerequisite failure in load-brief; atomic/detect/auth helper unresolved and the Tier-1 split-count drop in extract/validate; the non-symlink active-link refusal in generate-artifacts) otherwise print a human string and exit with **no machine-readable outcome** — a pipeline polling `create-skill-result-latest.json` cannot distinguish "halted at brief-invalid" from "still running" from "crashed". Mirror skf-quick-skill: **whenever `{headless_mode}` is true, every HARD HALT must surface an error-variant result before exiting.**
      
      **For every HARD HALT under `{headless_mode}` (regardless of phase)** — emit a single line on **stderr** (one line, no pretty-print; matches the prefix-and-envelope convention used by `skf-emit-result-envelope.py`):
      
      ```
      SKF_CREATE_SKILL_RESULT_JSON: {"status":"failed","phase":"<step-slug>","outputs":{},"summary":{"halt_reason":"<short class>","evidence_report":"<path-or-null>"},"skill_package":"<path-or-null>"}
      ```
      
      Use `status: "partial"` instead of `"failed"` when artifacts were already staged or promoted before the HALT (i.e. the HALT fired at step 7 generate-artifacts after some files were written); use `"failed"` for HALTs before any artifact exists on disk (steps 1–6).
      
      **Additionally, when `{forge_version}` is resolved** (HALT at step 7 onward, where the staging tree has been promoted) — write the same JSON object (without the `SKF_CREATE_SKILL_RESULT_JSON: ` prefix) to disk at `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` and a copy at `{forge_version}/create-skill-result-latest.json` (copy, not symlink) via `python3 {atomicWriteHelper} write`, so consumers that hardcode the `-latest.json` path see a deterministic file even on failed runs. Set `summary.evidence_report` to `{forge_version}/evidence-report.md` whenever that file exists, so the consumer can still reach the `## Auto-Decisions` audit on a failed run. HALTs before step 7 cannot write to disk because `{forge_version}` is only created at step 7 §1; for those, the stderr envelope is the contract and `summary.evidence_report` is `null`.
      
      When `{headless_mode}` is false, HARD HALTs display their human message only — no envelope is emitted.
      
      ### 6. Chain to Health Check
      
      **If not batch mode (or all batch briefs complete):**
      
      Only after the compilation report, warnings (if any), recommended next steps, and result contract have been handled do you load `{nextStepFile}`, read it fully, and execute it. The health-check step is the true terminal step — do not stop here even though the report reads as final.
      
      **If batch mode with remaining briefs:** Skip the health-check chain — load and execute `references/load-brief.md` for the next brief instead. The health check runs only after the final brief in the batch.
      
      
    • source-resolution-protocols.md 19 KB
      # Source Resolution Protocols
      
      ## Shell Path Quoting
      
      Every shell snippet in this document uses `{...}` placeholders for paths. **Always wrap path interpolations in double quotes** when emitting the actual command — `git -C "{workspace_repo_path}"`, `rm -rf "{temp_path}"`, `cd "{project-root}"`. SKF's supported platforms are Linux and macOS; user home directories on macOS frequently contain spaces, which break unquoted shell. WSL2 users see the same. Native Windows is untested but the quoting convention is also required there.
      
      ## Tag Resolution
      
      Tag resolution maps a declared version in the brief onto a concrete git ref before cloning, so the skill is built from code matching its declared version. Three signals can drive it, in priority order: an explicit `brief.target_ref` (a ref the user states verbatim — highest priority), an **explicit** `brief.target_version` (deliberate user intent), or an **implicit** `brief.version` (auto-populated hint from `brief-skill`). All apply only when `source_repo` is a remote URL.
      
      **Explicit ref override (when `target_ref` is set):** When `brief.target_ref` is present AND `source_repo` is a remote URL, use its value verbatim as `source_ref` and skip all version-to-tag matching below. This is the escape hatch for ref conventions the matching heuristics don't cover — notably monorepo crate tags whose prefix differs from the skill name (e.g. skill `livekit-rust` built from tag `livekit/v0.7.42`). Confirm it resolves first: `git ls-remote "{source_repo}" "{target_ref}"` (matches a tag or branch) — if it returns nothing, ⚠️ warn "`target_ref` ({target_ref}) does not resolve in {source_repo}; falling back to version matching" and continue with the version-based matching below.
      
      **When none of `brief.target_ref`, `brief.target_version`, or `brief.version` is set:** skip tag resolution entirely. Set `source_ref` to `HEAD` (default branch).
      
      ### Explicit Tag Resolution (when target_version is set)
      
      When `brief.target_version` is present AND `source_repo` is a remote URL, resolve the target version to a git tag before cloning:
      
      1. **List available tags:**
         - `gh api repos/{owner}/{repo}/tags --paginate --jq '.[].name'`
         - Fallback: `git ls-remote --tags "{source_repo}" | sed 's|.*refs/tags/||'`
      
      2. **Match `target_version` against tags** in priority order:
         - **Exact match:** `{target_version}` (e.g., `0.5.0`)
         - **With `v` prefix:** `v{target_version}` (e.g., `v0.5.0`)
         - **With package scope (monorepos):** `{brief.name}@{target_version}` or `@{scope}/{brief.name}@{target_version}`
         - **With crate/package-directory prefix (monorepos):** `{brief.name}/v{target_version}`, `{brief.name}/{target_version}`, or `{brief.name}-v{target_version}` (e.g. `tokio/v1.0.0`). Covers monorepos whose tags are prefixed by the crate/package directory **when that directory equals the skill name**. When the directory differs from the skill name (e.g. crate `livekit` for skill `livekit-rust`, tag `livekit/v0.7.42`), this heuristic can't infer it — set `target_ref` explicitly instead.
      
      3. **Resolution outcomes:**
         - **Single match:** Store the matched tag as `source_ref`. Use it as `{branch}` in all subsequent clone/API commands.
         - **Multiple matches:** Present the matching tags to the user — "Multiple tags match version {target_version}: {list}. Which one should I use?" Wait for selection.
         - **Zero matches:** ⚠️ Warn: "No git tag found matching version {target_version}. Closest available tags: {list 5 nearest by semver sort}. Falling back to default branch — **extracted code may not match target version.**" Set `source_ref` to `HEAD` and proceed with default branch.
      
      4. **Store `source_ref`** in context. This value is written to metadata.json and provenance-map.json for downstream workflows (update-skill, audit-skill) to re-clone from the same ref.
      
      ### Implicit Tag Resolution (when only brief.version is set)
      
      When `brief.target_version` is absent but `brief.version` is present AND `source_repo` is a remote URL, treat `brief.version` as an **implicit** target version and attempt tag resolution before cloning. This matches `brief-skill`'s behavior, which auto-populates `brief.version` from the latest non-prerelease release tag — so a tag matching `brief.version` is the common case, and silently cloning HEAD would produce a skill labeled with `brief.version` but built from an unrelated default-branch commit.
      
      1. **List available tags** exactly as in Explicit Tag Resolution above.
      
      2. **Match `brief.version` against tags** in this reduced priority order. Package-scoped monorepo variants are **not** tried — those require deliberate user intent via `target_version`, since implicit matching against a monorepo tag like `{brief.name}@{version}` could silently select a sibling package's ref:
         - **Exact match:** `{brief.version}` (e.g., `0.3.37`)
         - **With `v` prefix:** `v{brief.version}` (e.g., `v0.3.37`)
      
      3. **Resolution outcomes:**
         - **Single match:** Store the matched tag as `source_ref`. Use it as `{branch}` in all subsequent clone/API commands. Do not warn — this is the expected path.
         - **Multiple matches:** Present the matching tags to the user — "Multiple tags match `brief.version` ({brief.version}): {list}. Which one should I use, or fall back to HEAD?" Wait for selection.
         - **Zero matches:** ⚠️ Warn: "No git tag found matching `brief.version` ({brief.version}). Falling back to default branch — **extracted code may not match the declared version.** If you intended to pin a specific version, set `target_version` explicitly in the brief." Set `source_ref` to `HEAD` and proceed with default branch. Append `tag_resolution: {status: "fallback-head", requested: "{brief.version}", reason: "no-matching-tag"}` to the in-context evidence-report payload so step 5 §7 surfaces the fallback in the evidence report. This turns the warning into a persistent audit trail a reviewer can grep later, not just a one-shot stderr line.
      
      4. **Do not halt on zero matches.** Unlike the explicit path, implicit resolution never blocks compilation — `brief.version` is an auto-populated hint, and some repositories simply do not tag releases. The warning is sufficient notice; the evidence report in step 8 will surface the HEAD fallback for reviewers.
      
      5. **Store `source_ref`** in context exactly as in the explicit path. It flows through to metadata.json and provenance-map.json so downstream workflows (update-skill, audit-skill) can re-clone from the same ref.
      
      **Interaction with Version Reconciliation (below):** When implicit tag resolution succeeds, the clone's source files should carry the same version as `brief.version` — so the Version Reconciliation section's source-vs-brief mismatch warning will not fire. When implicit resolution falls back to HEAD, Version Reconciliation runs normally against the default branch's version file and may produce its own mismatch warning.
      
      ### Local Source Warning
      
      When `brief.target_version` is set AND `source_repo` is a local path:
      
      ⚠️ "**Local source may not match target version {target_version}.** Ensure you've checked out the correct version locally, or use a remote GitHub URL so SKF can clone from the git tag automatically."
      
      Proceed with local files as-is. Set `source_ref` to `"local"`.
      
      Implicit resolution via `brief.version` is **not applied to local sources** — local paths reflect whatever the user has checked out, and rewriting them from a tag would be out of scope for a local-source workflow.
      
      ---
      
      ## Remote Source Resolution
      
      **Note:** Quick-tier remote sources do not use the workspace/clone protocol described below. Quick tier accesses remote files via the `gh_bridge.read_file` path described in step 3 section 4.
      
      If `source_repo` is a local path: proceed with the tier-appropriate strategy as normal.
      
      If `source_repo` is a remote URL (GitHub URL or owner/repo format) AND tier is Forge, Forge+, or Deep:
      
      1. **Check `git` availability:** Verify `git` is functional (`git --version`). If `git` is not available, skip to the fallback warning below.
      
      2. **Compute workspace path:** Derive a persistent local path from the remote URL:
      
         - **Parse the URL** to extract `{host}`, `{owner}`, `{repo}`:
           - `https://github.com/facebook/react` or `https://github.com/facebook/react.git` → `github.com/facebook/react`
           - `git@github.com:facebook/react.git` → `github.com/facebook/react`
           - `facebook/react` (owner/repo shorthand) → `github.com/facebook/react`
         - **Resolve workspace root:** Use environment variable `SKF_WORKSPACE` if set, otherwise `~/.skf/workspace/` (where `~` is the user's home directory on all platforms)
         - **Workspace repo path:** `{workspace_root}/repos/{host}/{owner}/{repo}/`
      
      3. **Workspace check — resolve the source locally:**
      
         **Concurrency guard:** all of the operations below (fetch, checkout, rev-parse, and the extraction read that follows in step 3) must be wrapped in an exclusive `flock` on `{workspace_repo_path}/.skf-workspace.lock`. Acquire the lock before the workspace-hit check, hold it across fetch + checkout + rev-parse, AND keep holding it through the extraction-time read of the working tree. Two concurrent batch runs that target the same workspace clone but different `source_ref` values would otherwise race — one would `checkout` while the other was reading files mid-extraction, corrupting the inventory. The lock makes the per-workspace-repo unit of work serial. Use `flock -x {lockfile} -c "..."` or `fcntl.flock(LOCK_EX)`. If `flock` is unavailable, log a warning ("Concurrency guard unavailable — concurrent forges against the same workspace repo may produce inconsistent extraction inventories") and proceed.
      
         **If `{workspace_repo_path}/.git/` exists (workspace hit):**
      
         The repo was previously cloned into the workspace. Fetch updates and checkout the requested ref.
      
         **Detect tag vs branch for `source_ref`** (skipped when `source_ref` is `HEAD` — in that case fetch default branch without a ref argument):
      
         ```
         # Ask the remote whether source_ref exists as a tag
         git -C "{workspace_repo_path}" ls-remote --tags origin {source_ref} | grep -q "refs/tags/{source_ref}$" && ref_kind=tag || ref_kind=branch
         ```
      
         Fetch using the ref-kind-appropriate invocation so tag refs are written into `refs/tags/*` rather than being dropped by a branch-only fetch:
      
         ```
         if ref_kind == tag:
           git -C "{workspace_repo_path}" fetch origin tag {source_ref}
         else:
           git -C "{workspace_repo_path}" fetch origin {source_ref}
         ```
      
         Check if checkout is needed — skip if the requested ref is already checked out:
      
         ```
         current_head = git -C "{workspace_repo_path}" rev-parse HEAD
         fetched_head = git -C "{workspace_repo_path}" rev-parse FETCH_HEAD
         ```
      
         If `current_head != fetched_head`:
         ```
         git -C "{workspace_repo_path}" -c advice.detachedHead=false checkout FETCH_HEAD
         ```
      
         If fetch or checkout fails, proceed to the **ephemeral fallback** (step 5).
      
         **If `{workspace_repo_path}/.git/` does not exist (workspace miss):**
      
         Clone the repository into the workspace for persistent reuse. Create the parent directory first (`{workspace_root}/repos/{host}/{owner}/`):
      
         ```
         mkdir -p "{workspace_root}/repos/{host}/{owner}/"
         ```
      
         Clone with the appropriate branch flag — `--branch` is only valid for real branch/tag names, not for `HEAD`. **Do not pass `--single-branch`** here: workspace clones are persistent and re-used for future forges with different `source_ref` values (a later run may target a different tag or branch). A single-branch workspace clone would force every re-forge with a new ref to fall through to ephemeral cloning, defeating the workspace cache:
      
         ```
         # If source_ref is a real branch or tag (not HEAD/null):
         git clone --depth 1 --branch {source_ref} "{source_repo}" "{workspace_repo_path}"
      
         # If source_ref is HEAD or not set (default branch):
         git clone --depth 1 "{source_repo}" "{workspace_repo_path}"
         ```
      
         **Note:** No `--filter=blob:none` — blobs for the current tree are needed for indexing and the cost is amortized across all future forges. No sparse-checkout — a full checkout serves all consumers (different briefs with different include/exclude patterns) without configuration conflicts. `--single-branch` is reserved for ephemeral clones (step 5); workspace clones keep all branches available so re-forges against different refs can fetch + checkout without re-cloning.
      
         If this is the **first repo** in the workspace (workspace root was just created), print an informational message:
      
         "Caching source at `{workspace_repo_path}` (saves time on re-forges). Override location with `SKF_WORKSPACE` env var."
      
         If clone fails, proceed to the **ephemeral fallback** (step 5).
      
      4. **If workspace resolution succeeds:** Set `source_root = {workspace_repo_path}` — this updates the working source path for all subsequent operations (AST extraction, CCC indexing, artifact generation). Capture the source commit: `git -C "{workspace_repo_path}" rev-parse HEAD` — store as `source_commit` in context. Proceed with the **Forge/Deep Tier** extraction strategy below. Set context:
         - `source_root = {workspace_repo_path}`
         - `remote_clone_path = {workspace_repo_path}`
         - `remote_clone_type = "workspace"`
      
         **Scope filtering:** Since the workspace uses a full checkout (no sparse-checkout), apply `include_patterns` and `exclude_patterns` from the brief as **file-level filters** when building the extraction file list. Always-included root files (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `build.gradle.kts`, `Package.swift`, `setup.py`, `setup.cfg`, `VERSION`) are exempt from pattern filtering.
      
      5. **Ephemeral fallback (on any workspace failure):**
      
         If workspace clone or fetch fails for any reason (network error, auth failure, disk full, timeout), fall back to ephemeral cloning — the pre-workspace behavior that always works:
      
         ```
         temp_path = {system_temp}/skf-ephemeral-{skill-name}-{timestamp}/
      
         # If source_ref is a real branch or tag (not HEAD/null):
         git clone --depth 1 --branch {source_ref} --single-branch --filter=blob:none "{source_repo}" "{temp_path}"
      
         # If source_ref is HEAD or not set (default branch):
         git clone --depth 1 --single-branch --filter=blob:none "{source_repo}" "{temp_path}"
         ```
      
         If ephemeral clone succeeds: Set `source_root = {temp_path}`. Capture `source_commit`. Set context:
         - `source_root = {temp_path}`
         - `remote_clone_path = {temp_path}`
         - `remote_clone_type = "ephemeral"`
      
         Apply `include_patterns` and `exclude_patterns` from the brief as file-level filters when building the extraction file list.
      
      6. **If all cloning fails (workspace AND ephemeral):**
      
         ⚠️ **Warn the user explicitly:**
      
         "Clone of `{source_repo}` failed: {error}. Degrading to source reading (T1-low) for this run. For T1 (AST-verified) confidence, clone the repository locally and update `source_repo` in your brief to the local path."
      
         Proceed with Quick tier extraction strategy below. Note the degradation reason in context for the evidence report.
      
      **Remote clone cleanup:** After extraction is complete for all files in scope (whether successful or partially failed), before presenting the Gate 2 summary (Section 6):
      
      - **If `remote_clone_type == "ephemeral"`:** Cleanup is required.
        1. **Reset working directory first:** Run `cd "{project-root}"` using the **absolute path** captured at workflow start.
        2. **Delete the clone:** `rm -rf "{temp_path}"`
        3. **Log:** "Ephemeral source clone cleaned up."
      
        This ensures cleanup runs even if some extractions failed. If any error halts the step before Gate 2, cleanup must still occur.
      
      - **If `remote_clone_type == "workspace"`:** No cleanup. The workspace checkout persists for future forges.
      
      ---
      
      ## Source Commit Capture (all tiers, source mode only)
      
      **If `source_type: "docs-only"`:** skip — set `source_commit: null`.
      
      After the source path is accessible, capture the current commit hash for provenance tracking:
      
      - **Local path:** `git -C "{source_root}" rev-parse HEAD` — if the path is a git repo
      - **Ephemeral clone (Forge/Deep):** already captured during clone (step 3 above)
      - **Quick tier (remote, no clone):** `gh api repos/{owner}/{repo}/commits/{source_ref} --jq '.sha'`
      
      Store the result as `source_commit` in context. If capture fails (not a git repo, API unavailable), set `source_commit: null` — this is not an error.
      
      Also store `source_ref` in context (from tag resolution above, or `HEAD` if no tag was resolved, or `"local"` for local sources). This value is persisted to metadata.json and provenance-map.json so downstream workflows (update-skill, audit-skill) can re-access the same source ref.
      
      ---
      
      ## Version Reconciliation (all tiers, source mode only)
      
      **Target version override:** If `brief.target_version` is present, use it as the authoritative version for the skill. Do not warn about a brief-vs-source version mismatch — the user intentionally specified this version. Set the working version to `brief.target_version` and skip the rest of this reconciliation section. The `target_version` field indicates deliberate user intent (e.g., targeting an older version, or providing the version for a docs-only skill).
      
      **If `source_type: "docs-only"`:** skip this section — no source files exist to reconcile.
      
      After the source path is accessible (local path from step 1, or workspace/ephemeral clone from above), check whether the source contains a version identifier and reconcile it with `brief.version`. Look for the first matching version file in the resolved source path:
      
      - Python: `pyproject.toml` (`[project] version`), `setup.py` (`version=`), `__version__` in `__init__.py`
      - JavaScript/TypeScript: `package.json` (`"version"`). **Monorepo resolution:** When multiple `package.json` files exist (workspace root + packages), resolve version using this priority:
        1. Package whose `name` field matches `brief.name` (e.g., the skill's target library name)
        2. Package with a `bin` field (CLI entry point — represents the published version)
        3. Root workspace `package.json` version (if present)
        4. Fall back to `brief.version` if no version found. For monorepos using workspace protocols (pnpm, yarn, npm workspaces), the root `package.json` often has no `version` field — this is expected, not an error.
      - Rust: `Cargo.toml` (`[package] version`)
      - Go: `go.mod` (module version if tagged)
      
      **If a source version is found AND it differs from `brief.version`:**
      
      ⚠️ Warn the user: "Brief version ({brief.version}) differs from source version ({source_version}). Using source version ({source_version})."
      
      Update the working version in context to the source version. Record the mismatch in context for the evidence report (step 8).
      
      **If no version file is found or version cannot be extracted:** keep `brief.version` as-is. No warning needed.
      
      **If source is remote and accessed via Quick tier (gh_bridge, no local files):** attempt to read the version file via `gh_bridge.read_file(owner, repo, "{version_file}")` — resolved as `gh api repos/{owner}/{repo}/contents/{version_file}` or direct file read if local (see `knowledge/tool-resolution.md`) — for the primary version file of the detected language. If the read fails, keep `brief.version`.
      
    • step-auto-shard.md 7.2 KB
      ---
      nextStepFile: 'step-doc-rot.md'
      # Resolve `{shardBodyHelper}` by probing `{shardBodyProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. HALT if neither resolves — hand line-counting is the least reliable
      # deterministic op and would silently ship an over-budget body or wrongly HALT.
      shardBodyProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-shard-body.py'
        - '{project-root}/src/shared/scripts/skf-shard-body.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5b: Auto-Shard
      
      ## STEP GOAL:
      
      Proactively reduce oversized SKILL.md bodies to under 400 lines by extracting Tier 2 sections (`## Full` headings) to `references/`, providing 100 lines of headroom below the 500-line `body.max_lines` ceiling. Tier 1 sections always remain inline. If the body is already within budget, skip cleanly.
      
      ## Rules
      
      - Auto-proceed step — no user interaction required
      - Graceful skip — if body is under threshold, proceed without modification
      - Only extract Tier 2 sections (identified by `## Full` heading prefix)
      - Tier 1 sections stay inline — moving one to references/ would break the standalone SKILL.md the two-tier design guarantees
      - Do not modify frontmatter — only body content and references/ directory
      - Do not invoke `npx skill-check split-body` — this step uses direct extraction
      - Do not invoke the Description Guard Protocol — frontmatter is untouched
      
      ## MANDATORY SEQUENCE
      
      ### §0. Run the Shard Script (primary path)
      
      The counting, boundary detection, size-sort, file writes, and blockquote replacement described in §1–§5 are fully deterministic and run every invocation — so a script owns them, not the model. Do not count body lines or extract sections by hand when the script ran.
      
      **Resolve `{shardBodyHelper}`** from `{shardBodyProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      Run:
      
      ```bash
      uv run {shardBodyHelper} <staging-skill-dir>/SKILL.md --budget 400
      ```
      
      The script performs everything §1–§5 document — it counts the body between the frontmatter close and EOF, enumerates the `## Full` Tier 2 sections, extracts the largest first to `references/` until the body fits, writes the extracted files and the trimmed SKILL.md through the atomic-write helper, rewrites each section as a cross-reference blockquote, and checks Tier 1 preservation and cross-reference integrity. Read its JSON and set context directly:
      
      - **`action: "skip"`** → the body was already within budget. Log `"auto-shard: skipped (body {body_lines_before} lines)"`, set `auto_shard_triggered: false`, `sections_extracted: []`, `body_lines_before`/`body_lines_after` from the report, then skip to §6.
      - **`action: "shard"`** → set `auto_shard_triggered: true` and copy `sections_extracted`, `body_lines_before`, `body_lines_after` straight from the report.
      - **HALT** if `tier1_preserved` is false: `"Auto-shard removed Tier 1 section(s) {tier1_missing}. Aborting."`
      - **HALT** if `xref_ok` is false: `"Auto-shard cross-references did not resolve. Aborting."`
      - If `under_budget` is false, selective Tier 2 extraction alone could not bring the body under budget (rare — Tier 1 itself exceeds ~300 lines). Apply the §4 editing-judgment trim, then continue.
      
      Log: `"auto-shard: {N} sections extracted, body reduced from {body_lines_before} to {body_lines_after} lines"`, then proceed to §6.
      
      **Manual fallback (only when `uv`/Python is unavailable):** perform §1–§5 by hand as documented below — they describe exactly what the script does.
      
      ### §1. Count Body Lines
      
      The script counts all lines in the staging SKILL.md between the frontmatter closing `---` and EOF, excluding trailing blank lines, and reports the total as `body_lines_before`.
      
      ```
      body_lines_before = body_line_count
      ```
      
      **IF `body_line_count` <= 400:** the script emits `action: "skip"` and writes nothing —
      - Log: `"auto-shard: skipped (body {body_line_count} lines)"`
      - Set context: `auto_shard_triggered: false`, `sections_extracted: []`, `body_lines_before: {body_line_count}`, `body_lines_after: {body_line_count}`
      - Skip to §6 (Auto-Proceed)
      
      **ELSE:** the script proceeds to §2.
      
      ### §2. Selective Shard — Tier 2 Extraction
      
      The script identifies Tier 2 sections by their `## Full` heading prefix:
      - `## Full API Reference` → `references/full-api-reference.md`
      - `## Full Type Definitions` → `references/full-type-definitions.md`
      - `## Full Integration Patterns` → `references/full-integration-patterns.md`
      
      Sections are sorted by line count descending (largest first).
      
      **FOR EACH Tier 2 section (largest first):**
      
      1. Extract the full section content (from `## Full` heading to the next `##` heading or EOF)
      2. Derive the reference filename from the heading: kebab-case (as shown above)
      3. Write the extracted content (preserving the `##` heading) to `<staging-skill-dir>/references/{filename}` via the atomic-write helper
      4. Replace the extracted section in SKILL.md with a cross-reference blockquote:
         ```markdown
         > See [Full API Reference](references/full-api-reference.md)
         ```
      5. Re-count body lines
      6. **IF `body_line_count` <= 400:** stop extracting, proceed to §3
      
      The report's `sections_extracted: [{heading, file, lines}]` records exactly which sections were pulled.
      
      ### §3. Tier 1 Preservation Check
      
      The script verifies ALL Tier 1 sections that were inline before extraction remain inline in SKILL.md afterward. These headings:
      
      - `## Overview`
      - `## Quick Start`
      - `## Common Workflows`
      - `## Key API Summary` (or `## Component Catalog` for component-library scope)
      - `## Migration & Deprecation Warnings` (conditional — only checked if it was present before extraction)
      - `## Key Types`
      - `## Architecture at a Glance`
      - `## CLI` (conditional — only checked if present before extraction)
      - `## Scripts & Assets` (conditional — only checked if present before extraction)
      - `## Manual Sections` (conditional — only checked if present before extraction)
      
      The result surfaces as `tier1_preserved` (with any pulled headings in `tier1_missing`).
      
      **IF `tier1_preserved` is false:**
      HALT: `"Auto-shard removed Tier 1 section {name}. Aborting."`
      
      ### §4. Post-Shard Validation
      
      The report's `body_lines_after` is the recount after all extraction. `under_budget` is true when the body now fits.
      
      **IF `under_budget` is false** (body still > 400 after all Tier 2 sections extracted — this is the one genuine editing-judgment step):
      - Trim oversized Tier 1 sections: reduce `## Key API Summary` and `## Architecture at a Glance` content to fit within the 400-line budget
      - Do not move any Tier 1 section to references/
      - Re-run `{shardBodyHelper}` (or re-count) and update `body_lines_after`
      
      ### §5. Cross-Reference Integrity
      
      The script verifies, for each extracted reference file, that the file exists at `<staging-skill-dir>/references/{filename}` and that the cross-reference blockquote in SKILL.md links to it — reported as `xref_ok`. HALT if `xref_ok` is false.
      
      Log: `"auto-shard: {N} sections extracted, body reduced from {body_lines_before} to {body_lines_after} lines"`
      
      Set context:
      - `auto_shard_triggered: true`
      - `sections_extracted: [{heading names}]`
      - `body_lines_before: {before}`
      - `body_lines_after: {after}`
      
      ### §6. Auto-Proceed
      
      Load, read the entire file, then execute `{nextStepFile}`.
      
    • step-doc-rot.md 9.7 KB
      ---
      nextStepFile: 'validate.md'
      scanDocRotHelper: 'scripts/scan-doc-rot.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5c: Doc-Rot
      
      ## STEP GOAL:
      
      Scan feeder artifacts for doc-rot correction indicators and annotate the compiled SKILL.md with `## CORRECTION` blocks. Matching is a deterministic substring grep performed by `{scanDocRotHelper}` (`scripts/scan-doc-rot.py`) — no AI judgment is used for detection. The prompt keeps only the genuine judgment the script cannot make: enriching each match's `affected` symbol (§2) and choosing where each `## CORRECTION` block goes (§3).
      
      ## Rules
      
      - Auto-proceed step — no user interaction required
      - Graceful skip — if no corrections are found in any feeder artifact, proceed without modification
      - Only modify the compiled SKILL.md (correction block insertion) and references (if corrections target referenced content)
      - Do not modify feeder artifacts (evidence-report.md, provenance-map.json, metadata.json) — this step READS them only
      - Do not modify frontmatter — correction blocks are body content only
      - Matching is case-insensitive substring grep — no semantic or AI-based assessment
      
      ## MANDATORY SEQUENCE
      
      ### §1. Locate Feeder Artifacts
      
      Identify the feeder artifacts in the **staging directory** for the current skill. This step (5c) runs **before** step 7 promotes the staging tree to `{forge_data_folder}/{skill-name}/{version}/`, so the feeder artifacts only exist under the staging path compile (step 5 §1a) wrote — reading the not-yet-promoted `{forge_data_folder}` path would make every match a no-op:
      
      1. **Evidence report:** `_bmad-output/{skill-name}/evidence-report.md`
      2. **Provenance map:** `_bmad-output/{skill-name}/provenance-map.json` — focus on T2/T3 entries with temporal annotations
      3. **Temporal context:** changelogs, migration guides, and issue/PR data fetched by step 3b and enriched by step 4 (available in workflow context)
      4. **Compiled SKILL.md:** the staged `_bmad-output/{skill-name}/SKILL.md` itself — check for `[QMD:...]` or `[DOC:...]` annotations referencing corrections. **Do not treat its own self-authored regions as correction sources:** compile already wrote the `## Migration & Deprecation Warnings` section (step 5 §4b) and the frontmatter `description` (step 5 §2) from the same T2-future annotations, so both restate already-surfaced corrections — §2 discards matches that land in either.
      
      For each artifact, attempt to load its content. If an artifact does not exist or is empty, skip it — this is not an error.
      
      Store: `feeder_artifacts_scanned: [{list of artifacts that were loaded}]`
      
      ### §2. Grep for Correction Indicators
      
      The scan is deterministic plumbing — a fixed-table substring grep with one correct answer per input — so it runs in `{scanDocRotHelper}`, **not** in-prompt. Do not hand-grep the feeder artifacts: identical feeders must yield identical `correction_matches`, and only the script guarantees that across multi-KB inputs. Pass the `## Migration & Deprecation Warnings`-bearing compiled SKILL.md (feeder #4) as `--skill-md` and every other loaded feeder as a positional argument:
      
      ```bash
      uv run {scanDocRotHelper} \
        --skill-md _bmad-output/{skill-name}/SKILL.md \
        --max-corrections 10 \
        _bmad-output/{skill-name}/evidence-report.md \
        _bmad-output/{skill-name}/provenance-map.json \
        _bmad-output/{skill-name}-temporal/*.md
      ```
      
      (Pass whichever of the §1 feeder paths actually exist — the script skips any that are missing or empty, and reports the loaded set in `scanned`.)
      
      The script emits `{scanned: [...], matches: [...], match_count, excluded_count, deduped_count, capped_count, cap}` on stdout. Each entry in `matches` is a match record with the deterministic fields the script owns:
      
      - `source`: the feeder artifact path where the match was found
      - `pattern`: the specific pattern string that matched
      - `category`: the category label
      - `context_line`: the line containing the match
      - `line_number`: the 1-indexed line the match sits on — a representative occurrence when duplicates were collapsed
      - `occurrences`: how many feeder lines collapsed into this record (1 when it is unique)
      - `duplicate_of`: the `{source, line_number}` of every collapsed occurrence (empty when it is unique)
      
      Read `matches` into `correction_matches: [{match records}]`. Then add the one judgment field the script cannot infer:
      
      - `affected`: the function name, API, or section the correction relates to — enrich each record from surrounding context (use the `[QMD:...]`/`[DOC:...]` annotations and nearby symbols); if not identifiable, set to `"unknown"`.
      
      **What the script does (the contract it implements — keep this table and the script's `PATTERN_TABLE` in lockstep):** it matches every feeder line against the following correction patterns. All matches are **case-insensitive substring matches** — no regex interpretation, no semantic analysis.
      
      | Pattern | Category |
      |---------|----------|
      | `deprecated` | Deprecation |
      | `@deprecated` | Deprecation |
      | `breaking change` | Breaking change |
      | `BREAKING` | Breaking change |
      | `removed in` | Removal |
      | `was removed` | Removal |
      | `renamed to` | Rename |
      | `renamed from` | Rename |
      | `superseded by` | Supersession |
      | `replaced by` | Supersession |
      | `no longer supported` | End of life |
      | `migration required` | Migration |
      | `signature changed` | Signature change |
      
      **Exclusion — drop the compiled SKILL.md's self-authored content (deterministic, no AI judgment, applied inside the script before it emits):** the script discards any match whose `source` is the compiled SKILL.md (feeder #4) **and** whose `line_number` sits in either of two positional windows. `excluded_count` reports the combined total.
      
      1. **Its own `## Migration & Deprecation Warnings` section** — a line at or after the heading and before the next `##` heading. Compile (step 5 §4b) authored that section from the same T2-future annotations, so re-emitting its bullets as `## CORRECTION` blocks in §3 would duplicate already-surfaced content verbatim.
      2. **Its own YAML frontmatter** — a line at or inside the leading `---` … `---` fences. Compile (step 5 §2) writes the frontmatter `description` from the same annotations, and it routinely restates the release's headline breaking change, so a match there is already-surfaced too. The frontmatter is a closed key set of pipeline scalars, so it can never carry an upstream correction the body has missed — and §3 forbids annotating it in any case, which would leave such a match with nowhere legitimate to go.
      
      Both are positional boundary checks on text already loaded, not semantic assessments. The heading search starts after the frontmatter, so an indented restatement of the heading inside a folded `description:` cannot anchor the window in the wrong place. Set `feeder_artifacts_scanned` from the script's `scanned` list.
      
      **Bounding — collapse duplicates, then cap (deterministic, applied inside the script after the exclusions):** the temporal feeder above is a verbatim upstream changelog, so a single run can match hundreds of "breaking"/"deprecated" lines drawn from years of release history. Two bounds keep §3's output proportionate:
      
      - **Duplicate collapse.** Matches sharing a `category` and the same whitespace-normalized, lowercased `context_line` collapse into their first occurrence, which carries `occurrences` and `duplicate_of`. `deduped_count` reports how many collapsed. A changelog that restates one deprecation across many releases yields one block, not one per release.
      - **Cap.** At most `--max-corrections` matches survive (default 10; pass `0` for unlimited). `capped_count` reports how many were dropped and `cap` the limit in force. Selection prefers the compiled SKILL.md's own annotations over other feeders, then scan order. Ten blocks at ~7 lines each fits step 5b's 400-line body budget with headroom.
      
      Neither bound is a judgment call, and neither is silent — §4 logs both counts.
      
      **IF `correction_matches` is empty** (the script returned `match_count: 0`, or the run had no feeder files to pass)**:**
      - Log: `"doc-rot: skipped (no correction indicators found in feeder artifacts)"`
      - Set context: `doc_rot_triggered: false`, `corrections_added: 0`
      - Skip to §5 (Auto-Proceed)
      
      **ELSE:** Proceed to §3.
      
      ### §3. Annotate SKILL.md with Correction Blocks
      
      For each entry in `correction_matches`, insert a `## CORRECTION` block into the compiled SKILL.md. The list is already collapsed and capped by §2's bounding, so this loop is bounded by construction — do not re-expand it from `duplicate_of`, and do not write the drop counts into the artifact (they belong in the §4 log, not in a third-party skill).
      
      **Block format:**
      
      ```markdown
      ## CORRECTION
      
      **Source:** {source}
      **Pattern:** {pattern}
      **Affected:** {affected}
      **Detail:** {context_line}
      ```
      
      **Insertion rules:**
      - **After the relevant API section** in SKILL.md if the `affected` function or section can be identified and located in the document
      - **At the end of SKILL.md body** (before any trailing sections like `## Manual Sections`) if the affected section cannot be determined
      - **Never inside frontmatter** — body content only
      - Each correction block is self-contained with its own source citation
      - Multiple corrections produce multiple `## CORRECTION` blocks
      
      Store: `corrections_added: {count of blocks inserted}`
      
      ### §4. Log Results
      
      Log: `"doc-rot: {corrections_added} correction blocks added from {feeder_artifacts_scanned_count} feeder artifacts ({deduped_count} duplicates collapsed, {capped_count} dropped over cap {cap})"`
      
      Set context:
      - `doc_rot_triggered: true`
      - `corrections_added: {count}`
      - `corrections_deduped: {deduped_count}`
      - `corrections_capped: {capped_count}`
      - `feeder_artifacts_scanned: [{list}]`
      - `correction_matches: [{match records}]`
      
      ### §5. Auto-Proceed
      
      Load, read the entire file, then execute `{nextStepFile}`.
      
    • step-doc-sources.md 7.5 KB
      ---
      nextStepFile: 'step-auto-shard.md'
      # Resolve `{detectDocsHelper}` by probing `{detectDocsProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. HALT if neither resolves — §2 has no prose fallback for doc-source
      # detection (Pages-API walk, docs/ folder scan, content hashing), and §2a none
      # for docs-only URL hashing.
      detectDocsProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-detect-docs.py'
        - '{project-root}/src/shared/scripts/skf-detect-docs.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5a: Doc Sources
      
      ## STEP GOAL:
      
      Record detected documentation pages and README — or, for a docs-only skill, the brief's documentation URLs — with content hashes in metadata.json so that downstream audit (analyze-skill) can detect when upstream docs have changed since the skill was compiled.
      
      ## Rules
      
      - Auto-proceed step — no user interaction required
      - Graceful failure — if doc detection fails, skip with a warning and proceed to validate
      - Do not modify any compiled artifact other than `metadata.json`
      - Do not block the pipeline on any doc detection error
      
      ## MANDATORY SEQUENCE
      
      ### 1. Check for Upstream Doc Detection Results
      
      Check if `doc_detection_results` is already populated in the workflow context (set by BS auto-brief in the forge-auto pipeline).
      
      - **If upstream results exist:** use them directly, skip to step 3.
      - **If no upstream results:** continue to step 2.
      
      ### 2. Run Doc Detection (if needed)
      
      **If `source_type` is `"docs-only"`:** skip detection — `source_repo` is the documentation site, not a GitHub repository, so `{detectDocsHelper} --repo-url` would exit 2 with `INVALID_URL` and leave `doc_sources[]` empty. The brief's `doc_urls` are the doc sources; continue to §2a.
      
      Otherwise check that `{source_repo}` is available from the skill brief.
      
      **If `{source_repo}` is not available:**
      - Set `doc_detection_results` to an empty array `[]`
      - Add evidence note: `"Doc sources: detection skipped — no source_repo in brief"`
      - Skip to step 3.
      
      **If `{source_repo}` is available:**
      
      **Resolve `{detectDocsHelper}`** from `{detectDocsProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      Invoke the detect-docs script:
      
      ```bash
      uv run {detectDocsHelper} \
        --repo-url {source_repo} \
        [--local-path {source_path}] \
        [--skip-pages-api]
      ```
      
      Pass `--local-path {source_path}` when a local clone exists from the extraction step to avoid redundant cloning for `docs/` folder detection.
      
      **Handle exit codes:**
      - **Exit 0** (found ≥1 doc): parse JSON stdout as `doc_detection_results`
      - **Exit 1** (none found): set `doc_detection_results` to empty array `[]`
      - **Exit 2** (error): set `doc_detection_results` to empty array `[]`, add evidence note: `"Doc sources: detection partial — skf-detect-docs.py error (exit 2)"`
      
      ### 2a. Docs-Only: Hash the Brief's Documentation URLs
      
      Runs only when `source_type` is `"docs-only"` (§2 routed here). Build the URL list: every `doc_urls[].url` from the brief, in brief order, followed by every subpage URL that step 3c (fetch-docs) actually fetched through subpage discovery — the `[EXT:{url}]` provenance set of the doc-fetch inventory. Include a brief URL even if step 3c could not fetch it; the helper fetches independently of the agent's web tools.
      
      **Resolve `{detectDocsHelper}`** from `{detectDocsProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      Pipe the list to the `hash-urls` subcommand. It fetches each URL and hashes the raw response bytes with the same primitive `skf-audit-skill` step 5a (`compare-hashes`) uses, so a hash recorded here compares byte-for-byte at audit time. Do not hash the markdown a web-fetch tool rendered in step 3c — the audit re-fetches raw bytes, and a rendered-markdown hash would report every page as drifted.
      
      ```bash
      printf '%s' '["{url-1}", "{url-2}", "..."]' | uv run {detectDocsHelper} hash-urls -
      ```
      
      The input may also be the brief's `doc_urls` block verbatim — an array of `{url, label}` objects, or an object with a `doc_urls` array.
      
      **Handle exit codes:**
      - **Exit 0:** parse the JSON object. Set `doc_detection_results` ← its `doc_sources` array — entries are already in the §4 schema (`url`, `detected_via: "brief_doc_urls"`, `content_hash` — `null` for a URL that could not be fetched — and `recorded_at`). Keep `fetch_failed[]` and `stats` for the §5 evidence note.
      - **Exit 2** (malformed input, unreadable file): set `doc_detection_results` to empty array `[]`, add evidence note: `"Doc sources: detection partial — skf-detect-docs.py hash-urls error (exit 2)"`
      
      Skip §3 — a docs-only brief has no repository README to track. Continue at §4.
      
      ### 3. Ensure README Entry
      
      **If `source_type` is `"docs-only"`:** skip this section — `source_repo` is the documentation site, not a repository, so there is no README to track and `{source_repo}/blob/main/README.md` would be a fabricated URL. Continue at §4.
      
      After obtaining detection results, check if any entry has a URL matching `*/README.md` or `*/readme.md`.
      
      **If a README entry already exists** from detection (e.g., `detected_via: "docs_folder"` found a README): keep it as-is.
      
      **If no README entry exists,** add one:
      
      - `url`: Construct from `{source_repo}/blob/main/README.md`
      - `detected_via`: `"readme_always"`
      - `content_hash`: Hash the README content using `sha256:{hexdigest}` convention:
        - If `{source_path}` is available: read `{source_path}/README.md` locally with `encoding="utf-8"`, compute `sha256:{hexdigest}`
        - Else: fetch via `gh api repos/{owner}/{repo}/readme --jq '.content'`, base64-decode, hash
        - If README cannot be found or fetched: set `content_hash` to `null`
      - `recorded_at`: current ISO-8601 timestamp with timezone
      
      ### 4. Build doc_sources Array
      
      Map each detection result to the `doc_sources` schema:
      
      ```json
      {
        "url": "{url from detection result}",
        "detected_via": "{detected_via from detection result}",
        "content_hash": "{content_hash from detection result — sha256:{hexdigest} or null}",
        "recorded_at": "{current ISO-8601 timestamp with timezone}"
      }
      ```
      
      Field mapping from `skf-detect-docs.py` output:
      - `url` ← `url` (direct copy)
      - `detected_via` ← `detected_via` (direct copy; or `"readme_always"` for the mandatory README entry)
      - `content_hash` ← `content_hash` (direct copy, already in `sha256:{hexdigest}` format)
      - `recorded_at` ← generated at step execution time (ISO-8601 with timezone)
      
      Note: `content_type` from detect-docs output is not carried into `doc_sources`.
      
      For a docs-only skill the `hash-urls` output from §2a is already in this shape (`detected_via: "brief_doc_urls"`, `recorded_at` stamped by the helper) — copy its `doc_sources` entries verbatim.
      
      ### 5. Update metadata.json
      
      Read the staging `_bmad-output/{skill-name}/metadata.json` that compile (step 5) wrote.
      
      **If the staging metadata.json is unreadable:** HALT — this indicates compile failed (critical, not doc-detection-related).
      
      **Replace** the `doc_sources` field entirely (do not merge or append to stale data from prior compiles):
      
      ```python
      metadata["doc_sources"] = new_doc_sources  # full replacement
      ```
      
      Write the updated metadata.json back to the staging directory.
      
      Add evidence note summarizing the result:
      - Success: `"Doc sources: {N} detected, README tracked"`
      - Skip: `"Doc sources: detection skipped — {reason}"`
      - Partial: `"Doc sources: detection partial — {N} found, {errors}"`
      - Docs-only: `"Doc sources: {stats.hashed}/{stats.total} brief doc URL(s) hashed, {stats.fetch_failed} fetch failed"`
      
      ### 6. Auto-Proceed
      
      Load, read the entire file, then execute `{nextStepFile}`.
      
    • tier-degradation-rules.md 3.1 KB
      # Tier Degradation Rules
      
      ## Remote Source at Forge/Deep Tier
      
      When `source_repo` is a remote URL (GitHub URL or owner/repo format) and the tier is Forge or Deep:
      
      - **ast-grep requires local files** — it cannot operate on remote URLs
      
      **Workspace-first clone strategy (preferred):**
      
      1. Check `git` availability (`git --version`). `git` is effectively guaranteed at Deep tier (via `gh` dependency) but not guaranteed at Forge tier.
      2. If `git` is available: check for an existing workspace checkout at `{workspace_root}/repos/{host}/{owner}/{repo}/`. If found, `git fetch` to update. If not found, clone into the workspace path with `--depth 1 --single-branch`. See `source-resolution-protocols.md` for the full workspace resolution algorithm.
      3. The workspace uses a full checkout (no sparse-checkout). Brief `include_patterns` and `exclude_patterns` are applied as file-level filters at extraction time, not at the git level. This allows a single workspace checkout to serve multiple briefs with different scope filters.
      4. For update-skill: `changed_files_from_manifest` scoping is applied as file-level filters at extraction time on the full workspace checkout.
      5. If workspace clone/fetch succeeds: use the workspace path for AST extraction. All results are T1 with `[AST:...]` citations.
      6. If workspace fails: fall back to ephemeral clone (`{system_temp}/skf-ephemeral-{skill-name}-{timestamp}/`). If ephemeral succeeds, use it. Ephemeral clone is deleted after extraction.
      7. Workspace checkouts persist across forges — CCC indexes, tool outputs, and the checkout itself are reused.
      
      **Fallback (clone fails or `git` unavailable):**
      
      - The extraction step warns the user explicitly before degrading — a silent drop from AST (T1) to source reading (T1-low) would leave them trusting a lower-confidence result without knowing it changed
      - **create-skill:** the warning includes actionable guidance — clone locally and update `source_repo` in the brief to the local path
      - **update-skill:** the warning includes actionable guidance — clone locally, re-run [CS] Create Skill with the local path to regenerate provenance data, then re-run the update
      - Extraction proceeds using Quick tier strategy (source reading via gh_bridge — resolved as `gh api` commands or direct file I/O; see `knowledge/tool-resolution.md`)
      - All results labeled T1-low with `[SRC:...]` citations
      - The degradation reason is recorded in the evidence report
      
      ## AST Tool Unavailable at Forge/Deep Tier
      
      When the tier is Forge or Deep but ast-grep is not functional:
      
      - The extraction step warns the user explicitly before degrading
      - The warning includes actionable guidance: run [SF] Setup Forge to detect tools
      - Extraction proceeds using Quick tier strategy
      - All results labeled T1-low
      - The degradation reason is recorded in the evidence report
      
      ## Per-File AST Failure
      
      When ast-grep fails on an individual file (parse error, unsupported syntax):
      
      - Fall back to source reading for **that file only**
      - Other files continue with AST extraction
      - The affected file's results are labeled T1-low; unaffected files retain T1
      - Log a warning noting which file degraded and why
      
    • validate.md 28.2 KB
      ---
      nextStepFile: 'generate-artifacts.md'
      tesslDismissalData: 'assets/tessl-dismissal-rules.md'
      descriptionGuardProtocol: '{project-root}/src/shared/references/description-guard-protocol.md'
      # Resolve `{atomicWriteHelper}` by probing `{atomicWriteProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. HALT if neither resolves — losing atomic-write guarantees is not
      # an option for the staging-directory artifacts this step produces.
      atomicWriteProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
        - '{project-root}/src/shared/scripts/skf-atomic-write.py'
      # Resolve `{descriptionGuardHelper}` by probing `{descriptionGuardProbeOrder}`
      # in order (installed SKF module path first, src/ dev-checkout fallback);
      # first existing path wins. HALT if neither resolves — letting an external
      # tool's rewrite of the description field stand would silently regress
      # discovery quality.
      descriptionGuardProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-description-guard.py'
        - '{project-root}/src/shared/scripts/skf-description-guard.py'
      # Resolve `{frontmatterValidator}` by probing `{frontmatterValidatorProbeOrder}`
      # in order (installed SKF module path first, src/ dev-checkout fallback); first
      # existing path wins. §0's post-restore re-validation hook uses it; an installed
      # module has no src/ tree, so a bare src/ path would silently fail the hook.
      frontmatterValidatorProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-validate-frontmatter.py'
        - '{project-root}/src/shared/scripts/skf-validate-frontmatter.py'
      # Resolve `{shardBodyHelper}` by probing `{shardBodyProbeOrder}` in order
      # (installed SKF module path first, src/ dev-checkout fallback); first existing
      # path wins. §4 uses it as the deterministic selective splitter and reads its
      # `tier1_preserved` field instead of counting Tier-1 headings pre/post by hand.
      shardBodyProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-shard-body.py'
        - '{project-root}/src/shared/scripts/skf-shard-body.py'
      # Resolve `{renderMetadataStatsHelper}` by probing `{renderMetadataStatsProbeOrder}`
      # in order (installed SKF module path first, src/ dev-checkout fallback); first
      # existing path wins. §7 uses it in --check mode to re-derive the metadata
      # `stats` / `confidence_distribution` instead of re-doing the arithmetic by hand.
      renderMetadataStatsProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-render-metadata-stats.py'
        - '{project-root}/src/shared/scripts/skf-render-metadata-stats.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 6: Validate
      
      ## STEP GOAL:
      
      To validate the compiled SKILL.md content against the agentskills.io specification using skill-check, auto-fix any validation failures, and confirm spec compliance before artifact generation.
      
      ## Rules
      
      - Focus only on validating compiled content against spec — only fix spec compliance issues
      - Validation and auto-fix modify files in the staging directory
      - `<staging-skill-dir>` resolves to `_bmad-output/{skill-name}/` as created by step 5. The directory name must match the skill's frontmatter `name` field exactly — `skill-check`'s `frontmatter.name_matches_directory` rule rejects any suffix.
      - If skill-check unavailable: skip validation, add warning to evidence report
      - Ignore non-zero exit codes from skill-check if JSON output shows 0 errors
      
      ## MANDATORY SEQUENCE
      
      ### 0. Description Guard Protocol
      
      **Used by:** §2 (`skill-check check --fix`), §4 (`split-body`), and any future tool invocation that may modify SKILL.md.
      
      Load `{descriptionGuardProtocol}` for the full prose explanation of the four-phase guard (why it exists, what counts as divergence, why token-stream comparison is the right shape). The deterministic phases are executed via `{descriptionGuardHelper}` — the calling sections (§2 and §4) invoke the helper at the capture and verify-restore points. `verify-restore` refuses an empty `--captured-description` (exit 1, file untouched); when that happens, follow the protocol's empty-snapshot rule instead of re-running with the empty value.
      
      **This skill's post-restore re-validation hook:** after `{descriptionGuardHelper}` reports `restored: true`, resolve `{frontmatterValidator}` from `{frontmatterValidatorProbeOrder}` (first existing path wins), run `uv run {frontmatterValidator} <staging-skill-dir>/SKILL.md` and capture `schema_revalidation_result` in context. If the validator exits non-zero OR reports failure for the `description` field, flip the Schema result back to `FAIL` in the evidence report (overriding any prior PASS/WARN from §2), record `description_guard_revalidation: FAIL` with the validator's diagnostic message, and continue — do not halt (step 9 health-check and result contract still need to run so the failure is surfaced through the normal artifact path).
      
      ### 1. Check Tool Availability
      
      Run: `timeout 30s npx skill-check -h` — the short timeout protects against a cold `npx` download blocking the workflow indefinitely on a slow network.
      
      - If succeeds: Continue to automated validation (section 2)
      - If fails or times out: Perform manual fallback (section 3); add note to evidence-report: "Spec validation performed manually — skill-check tool unavailable". Also set `metadata.validation_status: 'manual-only'` in `metadata.json` (write via `python3 {atomicWriteHelper} write --target <staging-skill-dir>/metadata.json`), and in the evidence-report's `Validation Results` section mark Security, Body, and Content Quality (tessl) rows explicitly as `skipped — skill-check unavailable`. Downstream consumers (pipeline, forger, test-skill) check `validation_status` to decide how much weight to put on the artifact; leaving it unset would make a manual-only run look equivalent to a fully automated PASS.
      
      **Important:** Do not assume availability — empirical check required.
      
      ### 2. Validate & Auto-Fix (skill-check check --fix)
      
      Run the external skill-check tool against the compiled skill staging directory.
      
      **Flag probe (run once, cache the result for §4 and §5 re-invocations):**
      
      ```bash
      npx skill-check check --help 2>/dev/null | grep -- --no-security-scan
      ```
      
      - If the probe matches `--no-security-scan`: set `{security_scan_flag} = "--no-security-scan"`.
      - Else run a second probe — `npx skill-check check --help 2>/dev/null | grep -- --skip-security` — and if it matches, set `{security_scan_flag} = "--skip-security"`.
      - If neither flag exists: set `{security_scan_flag} = ""` (empty) AND set `{skill_check_flag_fallback} = true`. Skip §2 and §4 automated flows entirely — fall through to §3 manual frontmatter validation. Record in evidence-report: `skill_check_flag_probe: neither --no-security-scan nor --skip-security supported by installed skill-check; validation performed manually`.
      
      **If a security-scan-disable flag was resolved (probe succeeded):**
      
      ```bash
      npx skill-check check <staging-skill-dir> --fix --format json {security_scan_flag}
      ```
      
      This performs frontmatter validation, description quality checks, body limit enforcement, local link resolution, file formatting, auto-fix of deterministic issues, and quality scoring (0-100) across five weighted categories.
      
      **Parse the JSON output** for: `scores[].score` (0-100 — match the entry by `relativePath`/`skillId`; falls back to a top-level `qualityScore` on older skill-check builds), `diagnostics[]` (remaining issues), `fixed[]` (auto-corrected issues).
      
      **Description Guard Protocol:** This invocation may modify SKILL.md (especially when `fixed[]` is non-empty). Wrap the `skill-check check --fix` call in the four-phase guard defined in §0 by invoking `{descriptionGuardHelper}` at the capture and verify-restore points:
      
      ```bash
      # Phase 1 — capture before the tool call
      uv run {descriptionGuardHelper} capture <staging-skill-dir>/SKILL.md
      # stash the returned `description` as `guarded_description` in workflow context
      
      # Phase 2 — run skill-check (see command block above)
      
      # Phases 3+4 — verify and restore after the tool call
      uv run {descriptionGuardHelper} verify-restore <staging-skill-dir>/SKILL.md \
          --captured-description "{guarded_description}"
      ```
      
      If `restored: true` in the verify-restore output, apply §0's post-restore re-validation hook. If `fixed[]` was non-empty in the skill-check output, also re-read the modified SKILL.md to sync the in-context copy before proceeding — this prevents silent divergence between the in-context and on-disk versions that step 7 will use for artifact generation.
      
      **Note:** `skill-check` may return non-zero exit code even when `summary.errorCount` is 0. Always rely on parsed JSON, not the shell exit code.
      
      - **Score ≥ 70:** Record "Schema: PASS (score: {score}/100)" in evidence-report
      - **Score < 70:** Log remaining diagnostics as warnings, record "Schema: WARN — score {score}/100, {count} remaining issues", proceed
      - **Unfixable errors:** Record specific rule IDs and suggestions, proceed with warnings
      
      ### 3. Validate Frontmatter (Fallback)
      
      **If skill-check was available:** Skip — already validated in step 2.
      
      **If skill-check not available (fallback):** Perform manual frontmatter compliance check:
      
      - [ ] Frontmatter present — file starts with `---` and has closing `---`
      - [ ] `name` field — present, non-empty, lowercase alphanumeric + hyphens only, 1-64 chars
      - [ ] `name` matches skill output directory name
      - [ ] `description` field — present, non-empty, 1-1024 characters
      - [ ] No unknown fields — only `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools` permitted
      - [ ] `version` and `author` are not in frontmatter (they belong in metadata.json)
      
      If fails: auto-fix (deterministic), re-validate once, record result. If passes: record "Frontmatter: PASS".
      
      ### 4. Split Oversized Body (if needed)
      
      **If step 2 reported `body.max_lines` failure:**
      
      **Description Guard Protocol:** Split operations may rewrite the frontmatter. Wrap the split invocation in the four-phase guard defined in §0:
      
      ```bash
      # Phase 1 — capture before the split
      uv run {descriptionGuardHelper} capture <staging-skill-dir>/SKILL.md
      # stash returned `description` as `guarded_description`
      
      # Phase 2 — run the split (selective extraction or, last-resort, split-body --write)
      
      # Phases 3+4 — verify and restore after the split
      uv run {descriptionGuardHelper} verify-restore <staging-skill-dir>/SKILL.md \
          --captured-description "{guarded_description}"
      ```
      
      If `restored: true` in the verify-restore output, apply §0's post-restore re-validation hook.
      
      **Mandatory approach — selective split:** Identify Tier 2 sections by their `## Full` heading prefix (e.g., `## Full API Reference`, `## Full Type Definitions`, `## Full Integration Patterns`). Extract ONLY those sections to `references/`, starting with the largest. Keep ALL Tier 1 content and any smaller sections inline. Inline passive context achieves 100% task accuracy vs 79% for on-demand retrieval (per Vercel research).
      
      This selective split is deterministic — run `{shardBodyHelper}` (the same splitter step 5b auto-shard uses) rather than counting and extracting by hand. **Resolve `{shardBodyHelper}`** from `{shardBodyProbeOrder}`; first existing path wins.
      
      ```bash
      uv run {shardBodyHelper} <staging-skill-dir>/SKILL.md --budget 400
      ```
      
      It extracts the largest `## Full` sections to `references/` until the body fits, rewrites each as a cross-reference blockquote through the atomic-write helper, and reports `sections_extracted`, `body_lines_after`, `tier1_preserved`, `xref_ok`, and `under_budget`.
      
      **Do not run `npx skill-check split-body --write` before selective extraction.** It extracts every `##` section top-to-bottom, destroying the Tier 1 inline content the two-tier design depends on — a last resort used only after selective split has been attempted and proven insufficient.
      
      **If selective split alone does not bring body under the limit** (the splitter reports `under_budget: false` — rare, typically only when Tier 1 itself exceeds 300 lines): reduce Tier 1 Key API Summary and Architecture at a Glance sections to fit within limits. Do not fall back to automated `split-body --write` to solve a Tier 1 sizing problem.
      
      **Tier 1 preservation check:** After any split operation, verify that all of the following Tier 1 sections remain inline in SKILL.md (not moved to references/): Overview, Quick Start, Common Workflows, Key API Summary, Migration & Deprecation Warnings (if present), Key Types, Architecture at a Glance, CLI (if present), Scripts & Assets (if present), Manual Sections. If any was moved to references/, restore it immediately and re-split targeting only Tier 2 sections.
      
      **Post-split Tier-1 count check (mandatory):** do not recount Tier-1 headings by hand — consume the splitter's `tier1_preserved` field. `{shardBodyHelper}` compares the Tier-1 headings inline before extraction against those inline afterward and reports `tier1_preserved` (with any pulled headings in `tier1_missing`). Read it from the invocation above, or re-check any split's result with `uv run {shardBodyHelper} <staging-skill-dir>/SKILL.md --dry-run`. **HALT** if `tier1_preserved` is false with: "Split reduced Tier-1 section count (missing {tier1_missing}). Tier-1 sections must remain inline. Restoring from staging backup and aborting body split — manual review required." Do not proceed past §4 — Tier-1 preservation is a hard invariant and a `tier1_preserved: false` result means the splitter pulled an inline section into references/ regardless of the section-list check above (e.g., heading-text variation, capitalization, or the splitter's own heuristics).
      
      **Anchor validation and remediation:** After any split, verify that context-snippet section anchors (`#quick-start`, `#key-types`) still resolve to headings in SKILL.md. If an anchor no longer resolves (section was split out), restore that section to SKILL.md inline content — the context-snippet must always reference sections that exist in the main file.
      
      Then re-validate: `npx skill-check check <staging-skill-dir> --format json {security_scan_flag}` — use the flag cached from §2's probe. If `{skill_check_flag_fallback}` is true, skip re-validation and rely on the §3 manual check.
      
      **If skill-check unavailable or no body size issue:** Skip.
      
      ### 5. Security Scan
      
      **If skill-check available:**
      
      ```bash
      npx skill-check check <staging-skill-dir> --format json
      ```
      
      (Security scan enabled by default when `--no-security-scan` omitted. The scan uses [Snyk](https://docs.snyk.io/) to check for prompt injection risks, sensitive data exposure, and unsafe tool permissions.)
      
      Record any security warnings in evidence-report. Security findings are advisory — they do not block artifact generation. If the full validation re-run produces a different quality score than section 2, update the evidence-report with the newer score.
      
      **If security scan fails due to missing SNYK_TOKEN:**
      
      Display: "Security scan requires a Snyk Enterprise API token ([docs](https://docs.snyk.io/snyk-api/authentication-for-api)). Set `SNYK_TOKEN=your-token` in environment or `.env`, then re-run [SF] Setup Forge. Without Enterprise, use `--no-security-scan` to skip. Security scanning is optional and does not block skill compilation."
      
      Record: "Security scan skipped — SNYK_TOKEN not configured"
      
      **If skill-check unavailable:** Skip with note: "Security scan skipped — skill-check tool unavailable"
      
      ### 6. Content Quality Review (tessl)
      
      **If tessl available**, run: `timeout 120s npx -y tessl skill review <staging-skill-dir>` — the 120s cap matches skf-test-skill's tessl invocation guard and prevents a stalled LLM call in tessl from blocking compilation. On timeout, treat the step as unavailable and record `tessl: timeout — content quality review skipped` in the evidence report.
      
      Parse output for: `description_score`, `content_score`, `review_score`, `validation_result`, `judge_suggestions[]`.
      
      **Load dismissal rules:** Before interpreting any findings, load `{tesslDismissalData}` completely. This file is the single source of truth for tessl findings that SKF expects and must dismiss. It defines score thresholds, suggestion dismissal patterns, and the action to take when each rule matches.
      
      **Apply dismissal rules** in this order:
      
      1. **Check score thresholds** against the "Score Thresholds" table in `{tesslDismissalData}`. Most importantly:
         - If tessl's output contains a `findings[]` entry with rule ID `description_field` (the deterministic angle-bracket / XML-tag validator): follow the **recover-then-halt** path defined by the `description-xml-tags-guarded-upstream` rule in `{tesslDismissalData}`. Re-apply step 5 §2a's `<`/`>` → `{`/`}` substitution in place on the staging SKILL.md frontmatter `description`, re-sync the in-context copy, and re-run `npx -y tessl skill review <staging-skill-dir>` once. **Re-run gate:** treat the `description_field` finding being **absent** from the re-run as the only successful recovery outcome. If the finding persists on the re-run — whether the re-substitution improved the description or not — that counts as recovery failure: halt with the original `description-xml-tags-guarded-upstream` failure message from `{tesslDismissalData}`, do not proceed to §6b, and do not downgrade the recovery to a warning. On successful recovery (finding cleared), log `description-recovery: applied ({count} substitutions)` in the evidence report under "Dismissed tessl suggestions" and continue suggestion iteration against the rerun's `judge_suggestions[]`.
         - If the LLM-judge `description_score` is below 100 **but no `description_field` finding is present** (the deterministic validator PASSED): this is a soft discoverability signal (jargon density, trigger-term phrasing) from the judge's sub-scores, not a sanitizer bypass. Record `description_judge_score: {n}% (deterministic description_field validator PASSED)` as a warning in the evidence report and continue — do not trigger the recover-then-halt path and do not halt.
         - If `review_score < 60` or `content_score < 60`: record warnings in the evidence report, continue.
      2. **Iterate `judge_suggestions[]`.** For each suggestion:
         - Cross-reference against the rules in `{tesslDismissalData}` in order.
         - If a rule matches: record `{rule_id, rationale, suggestion_text}` under "Dismissed tessl suggestions" in the evidence report. Do not apply.
         - If no rule matches: add to the "Novel tessl suggestions" list for §6b to surface to the user.
      3. **Short-circuit when empty.** If every suggestion was dismissed (no novel suggestions), §6b has nothing to show — auto-proceed to §7.
      
      - **Unavailable:** Skip with note: "Content quality review skipped — tessl tool unavailable"
      
      tessl installs automatically via `npx`. A missing tool is not an error — graceful skip.
      
      #### 6b. User Decision Gate (conditional)
      
      **If §6 produced no novel suggestions (all dismissed via `{tesslDismissalData}`) OR tessl was unavailable:** Skip this gate — auto-proceed.
      
      **GATE [default: S]** — If `{headless_mode}` is true AND §6 produced novel suggestions: auto-select [S] Skip (a headless run has no human to triage novel suggestions), record `"tessl suggestions: {N} novel suggestion(s) auto-skipped (headless)"` in the evidence report under "Dismissed tessl suggestions", log `"headless: auto-skip {N} novel tessl suggestion(s)"`, and append `{step: "validate", gate: "tessl-suggestions", decision: "S", value: "{N} novel auto-skipped", rationale: "headless mode — no human to triage novel tessl suggestions", timestamp: {ISO}}` to the in-context `headless_decisions[]` list and, the moment it lands, append the same object as a JSON line to the durable audit sink `{sidecar_path}/auto-decisions.jsonl` (the on-landing append established at step 1 §3). §8 below reconciles the sink into the evidence-report `## Auto-Decisions` table — this is the last gate to fire; the earlier gates' rows are already in the sink and were rendered into the staged report at step 5 §7. This is the one consequential auto-decision that drops human-relevant feedback, so it must leave an audit row rather than vanishing. Then auto-proceed to §7 — do not present the menu below.
      
      **If §6 produced novel suggestions** (ones not matched by any dismissal rule) AND `{headless_mode}` is false, present them to the user:
      
      "**Content quality review: {score}%**
      
      tessl suggestions (novel — not matched by `{tesslDismissalData}`):
      {numbered list of novel suggestions}
      
      **Select an option:**
      - **[S] Skip** — proceed with current content as-is (default)
      - **[A] Apply structural fixes** — apply only structural suggestions (split sections, consolidate duplicates). No new content generated.
      - **[R] Review all** — show each suggestion with proposed changes before applying"
      
      #### Gate Rules:
      
      - **Structural suggestions** (split reference section, consolidate duplicates, reorder sections) can be applied without zero-hallucination risk — they restructure existing content
      - **Semantic suggestions** (add examples, add error handling, add validation checkpoints) introduce content not verified from source code. If the user chooses to apply these:
        - Warn: "This adds content not verified from source code."
        - Mark applied content with `<!-- [TESSL:auto-fix] -->` markers
        - Cite as `[TESSL:suggestion]` in the provenance map with `confidence: "TESSL"` (below T3)
        - Record in evidence report: "TESSL-suggested content applied: {count} items (unverified)"
      - **If user selects [S]:** Record "tessl suggestions: skipped by user" in evidence report. Proceed to section 7.
      - **If user selects [A]:** Apply structural fixes only, re-run tessl to capture updated score, record results. Proceed to section 7.
      - **If user selects [R]:** Show each suggestion with the proposed change. For each, user confirms or skips. Apply confirmed changes, record results. Proceed to section 7.
      
      ### 7. Validate metadata.json
      
      **Re-derive the computed fields with `{renderMetadataStatsHelper}` in check mode** rather than re-doing the arithmetic by hand. Resolve `{renderMetadataStatsHelper}` from `{renderMetadataStatsProbeOrder}` (first existing path wins; HALT if neither resolves), then run it against the staged provenance-map and metadata.json:
      
      ```bash
      uv run {renderMetadataStatsHelper} <staging-skill-dir>/provenance-map.json \
          --check <staging-skill-dir>/metadata.json
      ```
      
      The helper re-bins `entries[]` by `signature_source`, recomputes `exports_documented`, `exports_total`, and `public_api_coverage` / `total_coverage` (null when the denominator is 0), and cross-checks `stats.scripts_count` / `stats.assets_count` against the `scripts[]` / `assets[]` array lengths and the provenance-map `file_entries` counts. It takes the judgment values (`exports_public_api`, `exports_internal`, `effective_denominator`) from `metadata.json` itself and infers the shape from `scope_type` / `skill_type` (pass `--shape` to override). Parse the emitted JSON (rely on the JSON, not the exit code):
      
      - **`coherence.ok: true`** — the computed fields are internally consistent; record "Metadata: PASS".
      - **`coherence.ok: false`** — each `violations[]` entry is `{field, expected, actual}` where `expected` is the correct value. **Auto-fix each computed-value violation** (`field` starting `stats.` or `confidence_distribution.`) by setting that field in `metadata.json` to `expected` (write via `python3 {atomicWriteHelper} write --target <staging-skill-dir>/metadata.json`), leaving every other stats field — e.g. `stats.notes` on a reference app — untouched. Record "Metadata: auto-fixed {N} computed-value discrepanc(y|ies)" listing the fields. These are computed values, so the helper is authoritative — a `confidence_distribution` violation is the per-entry mis-binning compile.md §4 describes (T2 annotations + T3 doc items counted on top of the per-export tiers); the helper's per-entry counts replace them. Carve-outs are handled by `--shape`: a **stack** distribution sums to the constituent count and a **reference-app** distribution to the per-citation count, so those are consistent states, not violations. A `provenance.file_entries.*` violation is not a computed metadata field — it means the provenance-map `file_entries` and metadata counts disagree; record it as a warning for manual reconciliation rather than auto-editing the count.
      
      Then verify the two fields the helper does not own (genuine constants/contract):
      - `spec_version` is `"1.3"`.
      - `scope_type` is present and equals the brief's `scope.type` verbatim.
      
      ### 8. Update Evidence Report
      
      Add validation results to evidence-report content in context:
      
      ```markdown
      ## Validation Results
      - Schema: {pass/fail} (quality score: {score}/100)
      - Frontmatter: {pass/fail}
      - Body: {pass/fail} {split-body applied if applicable}
      - Security: {pass/warn/skipped}
      - Content Quality (tessl): {pass/warn/skipped} (score: {score}%)
      - Metadata: {pass/fail}
      
      ## Quality Score Breakdown
      - Frontmatter (30%): {score} | Description (30%): {score} | Body (20%): {score} | Links (10%): {score} | File (10%): {score}
      
      ## Description Guard
      - Restored: {true/false}
      - Triggering tool: {tool_name or —}
      - Original description preserved: {true/false}
      - Notes: {one-sentence detail or —}
      
      ## Auto-Fixed Issues
      - {list of issues automatically corrected by --fix}
      
      ## Remaining Warnings / Security Findings / Content Quality (tessl)
      - {warnings, security results, tessl scores and suggestions — or "skipped"}
      ```
      
      **Auto-Decisions table (reconcile from the durable sink — idempotent):** all gates have now fired — every one appended its row to the on-disk sink `{sidecar_path}/auto-decisions.jsonl` as it landed (including §6b's tessl-suggestions row), step 5 §7 rendered the step 1–3d rows into the staged `<staging-skill-dir>/evidence-report.md`, and steps 7–9 add none. Reconcile: read the sink's JSON lines (the authoritative durable record — it survives any compaction of the in-context buffer), union them with both the `## Auto-Decisions` rows already in `<staging-skill-dir>/evidence-report.md` and the in-context `headless_decisions[]` buffer, keyed on `step`+`gate` so no decision is duplicated or dropped, and re-render the section from that union. Because the rows are recovered from the sink rather than from the possibly-compacted buffer, the audit table stays complete on a long headless run. Emit one row per entry:
      
      ```
      ## Auto-Decisions
      
      | Step | Gate | Decision | Rationale | Timestamp |
      |------|------|----------|-----------|-----------|
      | {step} | {gate} | {decision}{value?} | {rationale} | {timestamp} |
      ```
      
      If the sink, the on-disk rows, and `headless_decisions[]` are all empty, keep the single line step 5 §7 emitted: `No auto-decisions — workflow ran interactively (or all gates had no match to auto-resolve).` This keeps the section always present so reviewers can tell "zero auto-decisions" apart from "section missing", and keeps the row count equal to `summary.auto_decision_count`.
      
      **Description Guard population:** if the §0 protocol fired during §2 (`skill-check --fix`) or §4 (`split-body`), fill the four Description Guard fields from context:
      
      - `Restored: true` when `description_guard_restored == true`, otherwise `false`.
      - `Triggering tool`: the tool name recorded by §0 (`skill-check --fix`, `skill-check split-body`, etc.), or `—` if the guard did not fire.
      - `Original description preserved`: `true` if the restore succeeded (on-disk now matches the pre-tool snapshot), `false` if restoration itself failed (rare — treat as a halt condition in a future version).
      - `Notes`: a one-sentence description of what the tool had changed. Typical values: `"replaced with generic summary"`, `"truncated at N chars"`, `"angle-bracket tokens re-introduced"`, `"field deleted entirely"`. If `Restored: false`, use `—`.
      
      When `Restored: false`, the three follow-up fields are all `—` — this is the clean-run expected state — with one exception: when `description_guard_refused == "empty-capture"` (the protocol's empty-snapshot rule — `verify-restore` exited 1 and no in-context copy allowed a re-run), the guard did fire and must not render as a clean run. Set `Restored: false`, `Triggering tool` to the recorded tool name, `Original description preserved: false`, and `Notes: guard refused — empty captured snapshot (empty-capture)`.
      
      ### 9. Auto-Proceed
      
      Conditional interaction: §6b halts for user input only when tessl produced novel suggestions; otherwise the step auto-proceeds. After validation completes (including any §6b decisions), load `{nextStepFile}`, read it fully, then execute it. Tool unavailability and validation failures are skips and warnings, never halts.
      
      
  • scripts
    • scan-doc-rot.py 16.5 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.10"
      # ///
      """Deterministic doc-rot correction-indicator scan (step-doc-rot.md §2).
      
      step 5c self-declares its correction scan "grep-based and deterministic — no AI
      judgment is used for detection." This helper *is* that grep: it walks the
      resolved feeder artifacts, matches every line against the fixed 13-row
      correction-pattern table with case-insensitive substring containment (no regex,
      no semantics), and emits the matches as JSON. It applies the positional filters
      the step documents — dropping matches that land inside the compiled SKILL.md's
      own YAML frontmatter or its own `## Migration & Deprecation Warnings` section
      (both are self-authored: compile §2 wrote the frontmatter `description` and
      compile §4b wrote the migration bullets, so re-emitting either would be
      circular) — and then bounds what survives, before it returns. Running the scan
      here (instead of in-prompt) makes the "deterministic, identical input →
      identical output" promise actually hold: the model no longer hand-greps
      multi-KB artifacts.
      
      Bounding matters because §2's own example command passes the raw temporal
      changelog as a feeder. That artifact is a verbatim upstream dump (thousands of
      lines of release history), so an unbounded run turns years of "breaking" /
      "deprecated" lines into hundreds of `## CORRECTION` blocks and blows the
      compiled body's line budget. Two deterministic bounds run after the exclusions:
      duplicate collapse (same category + same normalized line text) and a hard cap
      on how many blocks a single run may propose. Nothing is silently destroyed —
      a collapsed record carries `occurrences` and `duplicate_of`, and the counts of
      what was collapsed and capped are reported so the step can log them.
      
      The genuine judgment the step keeps in-prompt is untouched by this script:
      enriching each match's `affected` symbol from surrounding context (§2) and
      choosing where the `## CORRECTION` block goes (§3). The script emits every
      deterministic field (`source`, `pattern`, `category`, `context_line`,
      `line_number`); the prompt adds `affected`.
      
      The pattern table and category labels are the contract in step-doc-rot.md §2 —
      keep the two in lockstep.
      
      CLI usage:
        uv run scan-doc-rot.py --skill-md <staged SKILL.md> [FEEDER ...]
        uv run scan-doc-rot.py --feeder evidence-report.md --feeder provenance-map.json
      
        --skill-md         the compiled/staged SKILL.md feeder (feeder #4); matches
                           inside its YAML frontmatter or its `## Migration &
                           Deprecation Warnings` section are excluded. It is also
                           scanned like any other feeder.
        FEEDER             any other feeder artifact (evidence-report.md,
                           provenance-map.json, temporal-context files). Repeatable
                           positionally or via --feeder.
        --max-corrections  cap on emitted matches (default 10). 0 or negative means
                           unlimited.
      
        Missing or empty files are skipped silently (not an error), matching §1's
        "attempt to load; if it does not exist or is empty, skip it."
      
      Output (stdout, one object):
        {
          "scanned": ["<path>", ...],        # feeders that existed and were non-empty
          "matches": [                        # correction_matches[] (affected added in-prompt)
            {"source": "<path>", "pattern": "deprecated", "category": "Deprecation",
             "context_line": "<line text>", "line_number": <1-indexed int>,
             "occurrences": <int>,            # 1 unless duplicates collapsed into this record
             "duplicate_of": [{"source": "<path>", "line_number": <int>}, ...]},
            ...
          ],
          "match_count": <int>,               # len(matches), after exclusions/collapse/cap
          "excluded_count": <int>,            # SKILL.md matches dropped (frontmatter + §4b)
          "deduped_count": <int>,             # matches collapsed into a surviving record
          "capped_count": <int>,              # matches dropped because the cap was reached
          "cap": <int>                        # effective cap (0 = unlimited)
        }
      
      Exit codes:
        0  — scan emitted successfully (including the zero-match case)
        1  — invalid arguments (no feeders supplied)
      """
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      import sys
      from pathlib import Path
      
      # Fixed correction-pattern table — mirrors step-doc-rot.md §2. (pattern, category)
      # Order is the table order; scanning is per-pattern so overlapping patterns
      # (e.g. "deprecated" ⊂ "@deprecated") each record their own hit, exactly as the
      # 13-row table enumerates them.
      PATTERN_TABLE: list[tuple[str, str]] = [
          ("deprecated", "Deprecation"),
          ("@deprecated", "Deprecation"),
          ("breaking change", "Breaking change"),
          ("BREAKING", "Breaking change"),
          ("removed in", "Removal"),
          ("was removed", "Removal"),
          ("renamed to", "Rename"),
          ("renamed from", "Rename"),
          ("superseded by", "Supersession"),
          ("replaced by", "Supersession"),
          ("no longer supported", "End of life"),
          ("migration required", "Migration"),
          ("signature changed", "Signature change"),
      ]
      
      _MIGRATION_HEADING = re.compile(r"^\s*##\s+Migration\s*&\s*Deprecation Warnings", re.IGNORECASE)
      
      # Cap on how many `## CORRECTION` blocks one run may propose (step-doc-rot.md §3).
      # Step 5b budgets the compiled body at 400 lines and each block is ~7 lines, so
      # 10 blocks stays inside the budget with headroom.
      DEFAULT_MAX_CORRECTIONS = 10
      
      
      def scan_text(text: str, source: str) -> list[dict]:
          """Case-insensitive substring scan of `text` against PATTERN_TABLE.
      
          Returns one record per (line, pattern) hit, in file order then table order.
          line_number is 1-indexed. Pure — no I/O.
          """
          matches: list[dict] = []
          for idx, line in enumerate(text.splitlines(), start=1):
              lowered = line.lower()
              for pattern, category in PATTERN_TABLE:
                  if pattern.lower() in lowered:
                      matches.append(
                          {
                              "source": source,
                              "pattern": pattern,
                              "category": category,
                              "context_line": line.strip(),
                              "line_number": idx,
                          }
                      )
          return matches
      
      
      def frontmatter_range(text: str) -> tuple[int, int] | None:
          """1-indexed [start, end] inclusive window of the leading YAML frontmatter
          block (both `---` fences included), or None when the file has no frontmatter.
      
          Delimiter handling mirrors the established SKF convention (see
          `skf-shard-body.py` `split_frontmatter` and
          `skf-validate-feasibility-report.py` `split_frontmatter`): the opening fence
          must be the FIRST line and the closing fence is the next line that is
          exactly `---` once surrounding whitespace is stripped. A `---` horizontal
          rule further down the body therefore cannot open a block, and an unterminated
          opening fence yields None rather than swallowing the file.
          """
          lines = text.splitlines()
          if not lines or lines[0].strip() != "---":
              return None
          for idx in range(2, len(lines) + 1):
              if lines[idx - 1].strip() == "---":
                  return (1, idx)
          return None
      
      
      def migration_section_range(skill_md_text: str) -> tuple[int, int] | None:
          """1-indexed [start, end) line window of the `## Migration & Deprecation
          Warnings` section, or None if the section is absent.
      
          start = the heading line; end = the next level-2 (`## `) heading, or EOF.
          A `### ` subsection inside the section does NOT close it (only a sibling
          `## ` heading does), matching §2's "before the next `##` heading".
      
          The search starts after any YAML frontmatter. `_MIGRATION_HEADING` tolerates
          leading whitespace, so an indented restatement of the heading inside a folded
          `description:` would otherwise anchor the window in the frontmatter and leave
          the real body section unexcluded.
          """
          lines = skill_md_text.splitlines()
          front = frontmatter_range(skill_md_text)
          first_body_line = front[1] + 1 if front else 1
          start = None
          for idx in range(first_body_line, len(lines) + 1):
              if _MIGRATION_HEADING.match(lines[idx - 1]):
                  start = idx
                  break
          if start is None:
              return None
          end = len(lines) + 1
          for idx in range(start + 1, len(lines) + 1):
              if lines[idx - 1].lstrip().startswith("## "):
                  end = idx
                  break
          return (start, end)
      
      
      def apply_frontmatter_exclusion(
          matches: list[dict], skill_md_source: str | None, skill_md_text: str | None
      ) -> tuple[list[dict], int]:
          """Drop matches whose source is the compiled SKILL.md and whose line sits
          inside its YAML frontmatter. Returns (kept, dropped).
      
          Same circularity argument as the §4b exclusion below: compile (step 5 §2)
          authors the frontmatter `description` from the very annotations this scan
          looks for, so a `breaking change` / `deprecated` phrase there is already
          surfaced, not a new correction. The frontmatter is also a closed key set of
          pipeline scalars (`name`, `description`), so a match in it can never be an
          upstream correction the body has missed.
      
          Scoped to the compiled SKILL.md deliberately. evidence-report.md also carries
          frontmatter, but its frontmatter holds pinned counts rather than authored
          prose, so it cannot restate an upstream correction and is left in scope.
          """
          if skill_md_source is None or skill_md_text is None:
              return matches, 0
          window = frontmatter_range(skill_md_text)
          if window is None:
              return matches, 0
          start, end = window
          kept, dropped = [], 0
          for m in matches:
              if m["source"] == skill_md_source and start <= m["line_number"] <= end:
                  dropped += 1
                  continue
              kept.append(m)
          return kept, dropped
      
      
      def apply_migration_exclusion(
          matches: list[dict], skill_md_source: str | None, skill_md_text: str | None
      ) -> tuple[list[dict], int]:
          """Drop matches whose source is the compiled SKILL.md and whose line sits
          inside its Migration & Deprecation Warnings section. Returns (kept, dropped)."""
          if skill_md_source is None or skill_md_text is None:
              return matches, 0
          window = migration_section_range(skill_md_text)
          if window is None:
              return matches, 0
          start, end = window
          kept, dropped = [], 0
          for m in matches:
              if m["source"] == skill_md_source and start <= m["line_number"] < end:
                  dropped += 1
                  continue
              kept.append(m)
          return kept, dropped
      
      
      def collapse_duplicates(matches: list[dict]) -> tuple[list[dict], int]:
          """Collapse matches that repeat the same text under the same category.
      
          Returns (kept, collapsed). The dedup key is
          `(category, whitespace-normalized lowercased context_line)` — a temporal
          changelog restates the same deprecation across many releases, and each
          restatement would otherwise become its own `## CORRECTION` block saying the
          same thing.
      
          The first occurrence in scan order survives and gains two fields:
          `occurrences` (how many lines collapsed into it, including itself) and
          `duplicate_of` (the `{source, line_number}` of every later occurrence), so
          nothing is silently destroyed. Records are copies — the inputs are untouched.
          """
          seen: dict[tuple[str, str], dict] = {}
          kept: list[dict] = []
          for m in matches:
              key = (m["category"], " ".join(m["context_line"].split()).lower())
              survivor = seen.get(key)
              if survivor is not None:
                  survivor["occurrences"] += 1
                  survivor["duplicate_of"].append(
                      {"source": m["source"], "line_number": m["line_number"]}
                  )
                  continue
              record = dict(m, occurrences=1, duplicate_of=[])
              seen[key] = record
              kept.append(record)
          return kept, len(matches) - len(kept)
      
      
      def apply_cap(
          matches: list[dict], cap: int, skill_md_source: str | None
      ) -> tuple[list[dict], int]:
          """Keep at most `cap` matches. Returns (kept, dropped).
      
          A cap of 0 or less means unlimited. Selection prefers the compiled SKILL.md's
          own annotations over other feeders — a naive head-slice would drop them first,
          because scan order puts the skill-md feeder last — and falls back to scan
          order within each group. The kept records are returned in scan order, so a
          run that does not hit the cap is ordered exactly as it is today.
          """
          if cap <= 0 or len(matches) <= cap:
              return matches, 0
          priority = sorted(
              range(len(matches)),
              key=lambda i: (0 if matches[i]["source"] == skill_md_source else 1, i),
          )
          keep = sorted(priority[:cap])
          return [matches[i] for i in keep], len(matches) - cap
      
      
      def _load(path: str) -> str | None:
          """Read a feeder file as UTF-8. Returns None for missing/empty (skip, not
          an error). UTF-8 avoids cp1252 mojibake on Windows."""
          p = Path(path)
          if not p.is_file():
              return None
          text = p.read_text(encoding="utf-8")
          if not text.strip():
              return None
          return text
      
      
      def scan_files(
          feeders: list[str],
          skill_md: str | None,
          *,
          max_corrections: int = DEFAULT_MAX_CORRECTIONS,
      ) -> dict:
          """Scan every feeder + the skill-md feeder, apply the self-authorship
          exclusions, collapse duplicates, cap the survivors, and return the result
          object. Deterministic: feeders scanned in the given order."""
          scanned: list[str] = []
          matches: list[dict] = []
          skill_md_text = None
      
          ordered = list(feeders)
          if skill_md is not None:
              ordered.append(skill_md)
      
          for path in ordered:
              text = _load(path)
              if text is None:
                  continue
              scanned.append(path)
              if path == skill_md:
                  skill_md_text = text
              matches.extend(scan_text(text, path))
      
          # Frontmatter first, then the §4b section — the two windows are disjoint, so
          # the total is order-independent, but running them in file order keeps the
          # attribution obvious when debugging a run.
          matches, front_excluded = apply_frontmatter_exclusion(matches, skill_md, skill_md_text)
          matches, section_excluded = apply_migration_exclusion(matches, skill_md, skill_md_text)
          matches, deduped = collapse_duplicates(matches)
          matches, capped = apply_cap(matches, max_corrections, skill_md)
          return {
              "scanned": scanned,
              "matches": matches,
              "match_count": len(matches),
              "excluded_count": front_excluded + section_excluded,
              "deduped_count": deduped,
              "capped_count": capped,
              "cap": max(max_corrections, 0),
          }
      
      
      def _build_parser() -> argparse.ArgumentParser:
          parser = argparse.ArgumentParser(
              prog="scan-doc-rot",
              description=(
                  "Deterministic doc-rot correction-indicator scan (step-doc-rot.md §2): "
                  "case-insensitive substring match of feeder artifacts against the fixed "
                  "correction-pattern table, with the compiled SKILL.md's frontmatter and "
                  "Migration & Deprecation Warnings section excluded, duplicates collapsed, "
                  "and the survivors capped."
              ),
              formatter_class=argparse.RawDescriptionHelpFormatter,
          )
          parser.add_argument(
              "positional_feeders",
              nargs="*",
              metavar="FEEDER",
              help="Feeder artifact paths (evidence-report.md, provenance-map.json, temporal files).",
          )
          parser.add_argument(
              "--feeder",
              action="append",
              default=[],
              dest="feeders",
              help="Feeder artifact path (repeatable). Equivalent to a positional FEEDER.",
          )
          parser.add_argument(
              "--skill-md",
              dest="skill_md",
              default=None,
              help="Compiled/staged SKILL.md feeder; its frontmatter and its Migration & "
              "Deprecation Warnings section are excluded from matches.",
          )
          parser.add_argument(
              "--max-corrections",
              dest="max_corrections",
              type=int,
              default=DEFAULT_MAX_CORRECTIONS,
              help=(
                  f"Maximum matches to emit (default {DEFAULT_MAX_CORRECTIONS}). "
                  "0 or negative means unlimited."
              ),
          )
          return parser
      
      
      def main(argv=None) -> int:
          parser = _build_parser()
          args = parser.parse_args(argv)
          feeders = list(args.positional_feeders) + list(args.feeders)
          if not feeders and args.skill_md is None:
              parser.print_usage(file=sys.stderr)
              print("error: supply at least one feeder path or --skill-md", file=sys.stderr)
              return 1
          result = scan_files(feeders, args.skill_md, max_corrections=args.max_corrections)
          print(json.dumps(result, indent=2))
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • customize.toml 1.9 KB
    # DO NOT EDIT -- overwritten on every update.
    #
    # Workflow customization surface for skf-create-skill.
    # Team overrides:     _bmad/custom/skf-create-skill.toml (under {project-root})
    # Personal overrides: _bmad/custom/skf-create-skill.user.toml (under {project-root})
    
    [workflow]
    
    # --- Configurable below. Overrides merge per BMad structural rules: ---
    #   scalars: override wins • arrays (persistent_facts, activation_steps_*): append
    #   arrays-of-tables with `code`/`id`: replace matching items, append new ones.
    
    # Steps to run before the standard activation (uv probe, config load).
    # Overrides append. Use for org-wide pre-flight checks (auth, network,
    # compliance) that must precede any skill compilation work.
    
    activation_steps_prepend = []
    
    # Steps to run after activation but before the first stage executes.
    # Overrides append. Use for context loads or banner customization that
    # should run once activation completes successfully.
    
    activation_steps_append = []
    
    # Persistent facts the workflow keeps in mind for the whole run
    # (extraction style, compilation guardrails, tessl review preferences).
    # Overrides append.
    #
    # Each entry is either:
    #   - a literal sentence, e.g. "Skills must declare PEP 723 inline metadata."
    #   - a file reference prefixed with `file:`, e.g.
    #     "file:{project-root}/docs/create-policy.md" (globs supported; file
    #     contents are loaded and treated as facts).
    
    persistent_facts = [
      "file:{project-root}/**/project-context.md",
    ]
    
    # Pipeline-integration hook invoked after the final report (step 8) and the
    # result JSON / metadata.json are finalized. The command is called as:
    #   <on_complete> --result-path=<{forge_version}/create-skill-result-latest.json>
    # Useful for Slack notifications, dashboard ingest, CI hooks, or chaining a
    # downstream skill (TS test-skill, EX export). Failures are logged to
    # workflow_warnings[] but never fail the workflow.
    #
    # Empty string = no-op (default).
    
    on_complete = ""
    
  • SKILL.md 5.8 KB
    ---
    name: skf-create-skill
    description: Compile a skill from a brief. Supports --batch for multiple briefs. Use when the user requests to "create a skill" or "compile a skill."
    ---
    
    # Create Skill
    
    ## Overview
    
    Compiles a verified agent skill from a skill-brief.yaml and source code, producing an agentskills.io-compliant SKILL.md with provenance map, evidence report, and progressive disclosure references. The workflow is mostly autonomous with three interaction points — after ecosystem check (if match found), after source extraction (to confirm findings), and after content quality review (when tessl produces suggestions). Steps adapt behavior based on forge tier (Quick/Forge/Forge+/Deep). Zero hallucination tolerance: every instruction in the output must trace to source code with a confidence tier citation. A single run is not resumable — if it is interrupted mid-compile, re-run from the brief (only `--batch` checkpoints progress across briefs).
    
    ## Conventions
    
    - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
    - `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates.
    - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
    - `{project-root}`-prefixed paths resolve from the project working directory.
    - `{skill-name}` resolves to the skill directory's basename.
    
    ## Role
    
    You are operating in Ferris Architect mode — a skill compilation engine performing structural extraction and assembly. Apply zero hallucination tolerance: uncitable content is excluded, not guessed.
    
    ## Workflow Rules
    
    These rules apply to every step in this workflow:
    
    - Never include content in SKILL.md that cannot be cited to source code
    - Only load one step file at a time — never preload future steps
    - Always communicate in `{communication_language}`
    - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action, logging each auto-decision to the in-context `headless_decisions[]` buffer AND appending it as a JSON line to the on-disk auto-decision sink (established at step 1 §3) the moment it lands, so the audit trail survives context compaction before step 5 first writes the evidence report
    
    ## Stages
    
    | # | Step | File | Auto-proceed |
    |---|------|------|--------------|
    | 1 | Load Brief | references/load-brief.md | Yes |
    | 2 | Ecosystem Check | references/ecosystem-check.md | Conditional |
    | 2b | CCC Discover | references/sub/ccc-discover.md | Yes |
    | 3 | Extract | references/extract.md | No (confirm) |
    | 3b | Fetch Temporal | references/sub/fetch-temporal.md | Yes |
    | 3c | Fetch Docs | references/sub/fetch-docs.md | Yes |
    | 3d | Component Extraction | references/component-extraction.md | Conditional |
    | 4 | Enrich | references/enrich.md | Yes |
    | 5 | Compile | references/compile.md | Yes |
    | 5a | Doc Sources | references/step-doc-sources.md | Yes |
    | 5b | Auto-Shard | references/step-auto-shard.md | Yes |
    | 5c | Doc-Rot | references/step-doc-rot.md | Yes |
    | 6 | Validate | references/validate.md | Conditional |
    | 7 | Generate Artifacts | references/generate-artifacts.md | Yes |
    | 8 | Report | references/report.md | Yes |
    | 9 | Workflow Health Check | references/health-check.md | Yes |
    
    *Sub-steps under `references/sub/` are conditional branches (CCC discovery, temporal/doc enrichment) kept out of the top-level step count so main-line steps 1–9 drive the workflow. Step 3d (Component Extraction) stays top-level as an alternative main step that replaces the standard extraction path when `scope.type: "component-library"`.*
    
    ## Invocation Contract
    
    | Aspect | Detail |
    |--------|--------|
    | **Inputs** | brief_path (path to skill-brief.yaml) [required], --batch [optional] |
    | **Gates** | step 2: Choice Gate [P] (if match) | step 3: Review Gate [C] | step 6: Content-Quality Gate [C] (if novel tessl suggestions) |
    | **Outputs** | SKILL.md, context-snippet.md, metadata.json, provenance-map.json, evidence-report.md, references/ |
    | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
    
    ## On Activation
    
    1. Load config from `{project-root}/_bmad/skf/config.yaml` and resolve:
       - `output_folder`, `user_name`, `communication_language`, `document_output_language`, `sidecar_path`, `skills_output_folder`, `forge_data_folder`
    
    2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
    
    3. **Resolve workflow customization.** Run:
    
       ```bash
       python3 {project-root}/_bmad/scripts/resolve_customization.py \
           --skill {skill-root} --key workflow
       ```
    
       The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append): `{skill-root}/customize.toml` (bundled defaults), `_bmad/custom/skf-create-skill.toml` under `{project-root}` (team overrides, committed), and `_bmad/custom/skf-create-skill.user.toml` under `{project-root}` (personal overrides, gitignored). If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly.
    
       Apply the resolved values so the surface is not a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts); and stash `{onCompleteCommand}` ← `workflow.on_complete` (empty string = no-op) for the final stage to invoke after the result JSON and metadata.json are finalized. After activation completes, execute each entry in `workflow.activation_steps_append` in order.
    
    4. Load, read the full file, and then execute `references/load-brief.md` to begin the workflow.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related