Claude Skill

skf-update-skill

Smart regeneration preserving [MANUAL] sections after source changes. Use when the user requests to "update a skill" or "regenerate 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-update-skill-492e73e.zip · 63 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-update-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

Update Skill

Overview

Surgically updates existing skills when source code changes, preserving all [MANUAL] developer content while re-extracting only affected exports with full provenance tracking. Only changed exports are re-extracted — unchanged content is never touched. Every regenerated instruction must trace to code with file:line citations. Stack skills (skill_type: "stack" in metadata.json) are not supported by surgical update — use skf-create-stack-skill to re-compose from updated constituents. If a stack skill is provided, this workflow exits with a redirect message.

Conventions

  • Bare paths (e.g. references/<name>.md) resolve from the skill root.
  • Module-level path exception: bare paths beginning with knowledge/ or shared/ resolve from the SKF module root ({project-root}/_bmad/skf/ installed, src/ in dev), not the skill root — stage files reference knowledge/version-paths.md and knowledge/tool-resolution.md, and the terminal step chains to shared/health-check.md.
  • 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.
  • Cross-skill data coupling: stages in this workflow load four shared assets from skf-create-skill to keep extraction semantics aligned between create and update — re-extract.md pulls extraction-patterns.md, extraction-patterns-tracing.md, and tier-degradation-rules.md from skf-create-skill/references/; remote-source-resolution.md references source-resolution-protocols.md; write.md reads skill-sections.md from skf-create-skill/assets/. Update-skill assumes these files are present at install time and that their semantics are stable across the two skills' versions.

Role

You are a precision code analyst operating in Ferris Surgeon mode. This is a surgical operation, not an exploratory session. You bring AST-backed structural analysis and provenance-driven change detection expertise, while the source code provides the ground truth.

Workflow Rules

These rules apply to every step in this workflow:

  • Never hallucinate — every statement must have AST provenance
  • [MANUAL] sections survive regeneration with zero content loss
  • 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 and log each auto-decision

Stages

# Step File Auto-proceed
1 Initialize & Load references/init.md No (confirm)
2 Detect Changes references/detect-changes.md Yes
3 Re-Extract references/re-extract.md Yes
4 Merge references/merge.md Yes
5 Validate references/validate.md Yes
6 Write references/write.md Yes
7 Report references/report.md Yes
8 Workflow Health Check references/health-check.md Yes

Invocation Contract

Aspect Detail
Inputs skill_name [required]
Flags --headless / -H (auto-resolve all gates); --from-test-report (gap-driven mode); --allow-workspace-drift (gap-driven only — bypass §0.a pinning guard); --allow-degraded (headless only — pre-authorize the lossy degraded full re-extraction when the provenance map is missing, instead of halting blocked; init.md §4); --detect-only (run detect-changes only, exit before re-extract; envelope status="detect-only"); --dry-run (run detect-changes + re-extract, exit before merge/write; envelope status="dry-run" describes what would change). If both --detect-only and --dry-run are passed, --detect-only wins.
Gates step 1: Confirm Gate [C] step 4: Confirm Gate [C if clean merge, HALT if conflicts]
Outputs Updated SKILL.md, metadata.json, provenance-map.json, evidence-report.md (none when --detect-only or --dry-run is set — those modes are read-only inspection paths)
Concurrency Two simultaneous real-update runs against the same skill would corrupt provenance. init.md §1b acquires a PID-file lock at {forge_data_folder}/{skill_name}/.skf-update.lock before any artifact read; live-PID collisions halt with status: "halted-for-concurrent-run". Stale locks (dead PID) are cleared silently with a warning. The lock is released by the terminal health-check step (step 8) on the normal path and by the two init-stage headless halts (§4/§6); mid-workflow halts leave it for the next run's stale-lock self-heal (see init.md §1b Release contract). Read-only modes (--detect-only, --dry-run) skip the lock entirely — they're safe alongside a concurrent real update.
Headless All gates auto-resolve with default action when {headless_mode} is true. Each auto-resolved gate appends a {gate, default_action, taken_action, reason, evidence?} entry to headless_decisions[], surfaced in step 7's SKF_UPDATE_RESULT_JSON envelope so non-interactive runs can be audited post-hoc. A HALT reached in headless mode emits its own SKF_UPDATE_RESULT_JSON at the halt site (the site's status code plus an error: {phase, path?, reason} object) and exits — halts do not fall through to step 7. Pipeline branches on the envelope's top-level status field (success, no-changes, detect-only, dry-run, or one of the documented halted-for-*/blocked codes). The first four are successful exits — pipelines treating non-success as failure must include them in the success set. The status enum is defined once in src/shared/scripts/schemas/skf-update-result-envelope.v1.json.

On Activation

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

    • project_name, output_folder, user_name, communication_language, document_output_language
    • skills_output_folder, forge_data_folder, sidecar_path
  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
    

    This merges the three layers per bmad-customize rules (scalars override, arrays append): {skill-root}/customize.toml (bundled defaults), _bmad/custom/<skill-name>.toml under {project-root} (team overrides), and _bmad/custom/<skill-name>.user.toml under {project-root} (personal overrides). If the script is missing or fails, read {skill-root}/customize.toml directly.

    Apply the resolved values so no surface is 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 — the bundled default loads any project-context.md under {project-root}); resolve {onCompleteCommand} ← workflow.on_complete if non-empty, else empty string, and stash it in workflow context (references/report.md §5b invokes it after the result contract is written; empty string = the hook is a no-op). After activation completes, execute each entry in workflow.activation_steps_append in order before init.md runs.

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

Files (bmad-module-skill-forge)
  • references
    • detect-changes.md 35.2 KB
      ---
      nextStepFile: 're-extract.md'
      noChangeReportFile: 'report.md'
      # Resolve `{hashContentHelper}` to the first existing path; HALT if neither
      # candidate exists — §1b and §Category D rely on the helper for deterministic
      # SHA-256 hashing (file-read + size + line-count) and provenance comparison
      # (UNCHANGED / MODIFIED_FILE / DELETED_FILE classification). Falling back to
      # prose-driven hashing would lose hash stability across runs.
      hashContentProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
        - '{project-root}/src/shared/scripts/skf-hash-content.py'
      # Resolve `{buildChangeManifestHelper}` to the first existing path; HALT if
      # neither candidate exists. §3 uses `build` to aggregate Category A/B/C/D
      # results into the unified manifest; §2.2 uses `deletion-ratio` to compute
      # the major-version trigger. Falling back to prose-driven count rollups
      # would let the LLM drift on manifest shape across runs.
      buildChangeManifestProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-build-change-manifest.py'
        - '{project-root}/src/shared/scripts/skf-build-change-manifest.py'
      # Resolve `{provenanceGapDispatchHelper}` to the first existing path; HALT
      # if neither exists. §1c uses it to discover the latest drift report, parse
      # Out-of-Scope candidates, and classify them against brief.scope.amendments[]
      # in one call. Falling back to prose-driven markdown parsing would let
      # report-format drift produce silent skips of out-of-scope candidates.
      provenanceGapDispatchProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-provenance-gap-dispatch.py'
        - '{project-root}/src/shared/scripts/skf-provenance-gap-dispatch.py'
      # Resolve `{detectScriptsAssetsHelper}` to the first existing path; HALT if
      # neither candidate exists. §Category D uses `detect` to walk the current
      # source tree for scripts/assets (directory conventions, shebang signals,
      # `package.json` `bin` entry-points, asset filename patterns) so NEW_FILE
      # detection mirrors create-skill §4c exactly. Falling back to prose-driven
      # file walking would let the LLM drift on the heuristic list and miss new
      # scripts/assets at deeper directory depths — and on installed modules
      # (no `src/` tree) the LLM would otherwise guess a non-existent path.
      detectScriptsAssetsProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-detect-scripts-assets.py'
        - '{project-root}/src/shared/scripts/skf-detect-scripts-assets.py'
      # `{newFileDiffHelper}` derives NEW_FILE (inventory source_files not in the
      # provenance map, minus [MANUAL] paths) — the set-difference §Category D would
      # otherwise ask the model to compute by hand. Per-skill helper, always bundled.
      newFileDiffHelper: 'scripts/skf-new-file-diff.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 2: Detect Changes
      
      ## STEP GOAL:
      
      Compare current source code state against the provenance map to produce a complete change manifest identifying every changed, added, deleted, moved, and renamed file and export since last extraction.
      
      ## Rules
      
      - Focus only on detecting and classifying changes — do not extract or merge
      - Use subprocess Pattern 4 (parallel) when available; if unavailable, compare sequentially
      
      ## Steps
      
      ### 0. Check for Test Report Input (Gap-Driven Mode)
      
      **If `update_mode == "gap-driven"` (set in step 1 via `--from-test-report`):**
      
      Load the test report at `{test_report_path}` and extract findings:
      
      1. Read the **Gap Report** section — each gap entry has severity, category, and description
      2. Read the **Coverage Analysis** section — each per-export row has documented/missing/mismatch status
      3. Translate findings into change manifest format:
      
      | Gap Severity | Gap Type | Change Category |
      |-------------|----------|-----------------|
      | Critical | Missing export documentation | NEW_EXPORT (undocumented public API) — unless the remediation says the export is internal / out of scope; then DELETED_EXPORT (rescope), see rule R1 |
      | High | Signature mismatch | MODIFIED_EXPORT (signature needs update) |
      | Medium | Missing type/interface docs | NEW_EXPORT (undocumented type) |
      | Medium | Stale documentation | MODIFIED_EXPORT (docs reference removed export) |
      | Critical/Medium | Coverage gap whose remediation is **removal** (export is internal, `#[doc(hidden)]`, or explicitly out of scope) | DELETED_EXPORT (rescope) — see rule R1 |
      | Medium/High | Structural / coherence drift (output file only, no source change) | STRUCTURAL_FIX — see rule R2 |
      | Medium | Export documented in SKILL.md/references but **missing from the provenance-map** | NEW_EXPORT (provenance-completeness) — see rule R3 |
      | Low | Missing metadata/examples | metadata update — see rule R4 |
      
      **Translation rules (referenced by the table above):**
      
      - **R1 — DELETED_EXPORT (rescope).** A coverage gap is a rescope only when its remediation text names removal (`rescope`, `remove from surface`, `out of scope`) or the export is upstream `#[doc(hidden)]` / internal. Default a bare "missing export documentation" gap to NEW_EXPORT (document it); choose rescope only on that explicit signal. **Interactive:** prompt per qualifying gap — "[D] Document the export / [R] Rescope (remove from the public surface)". **Headless:** default to **document** (NEW_EXPORT); choose rescope only when the remediation explicitly says removal *and* the export is internal/`#[doc(hidden)]`. A rescope is honest only if the reduction is expressed in the brief's scope: append a `scope.amendments[]` entry (`category: "scope-expansion"`, `action: "excluded"`) **and** add the export's source path to `brief.scope.exclude`, then route the entry to merge Priority 1 (removal) so step 4/6 remove it and recompute stats from the amended scope. Never close a coverage gap by editing `metadata.stats` to equal the documented count — that is denominator deflation and `skf-test-skill` will reject it.
      - **R2 — STRUCTURAL_FIX.** A coherence finding from `skf-scan-skill-md-structure.py` (e.g., `table_drift`, `unbalanced_fences`, a broken intra-skill anchor) that touches the generated output file only, with no source change and no provenance entry change. Carries `remediation` text describing the surgical markdown edit. Routes to merge Priority 8 (generated-markdown edit only); it never adds, modifies, or removes a provenance `entries[]` row.
      - **R3 — provenance-completeness.** An export documented in SKILL.md/`references/` but absent from the provenance-map. These are documented-and-known, so `unknown` is never correct: route to re-extract §0a source resolution **regardless of severity**, gated only on `source_root` being pinned and readable (see re-extract.md §0).
      - **R4 — metadata update.** A metadata-coherence patch that changes no export's source (e.g., reconcile a divergent `stats` count). Routes to merge Priority 8b and is applied by write.md §2 *before* the automatic stat recount (see merge.md §3 / write.md §2).
      
      4. Build the change manifest from translated gaps — no file-level timestamp comparison needed since source hasn't changed. For each manifest entry, propagate these fields from the test report finding so step 3 can resolve the export against live source:
      
         - **`severity`** — the Gap Report severity (`Critical`, `High`, `Medium`, `Low`, `Info`). Step-03 §0 and step 6 §3 gate the null-citation fallback on severity: Critical/High gaps must produce AST provenance, Medium/Low/Info gaps may degrade to `unknown`.
         - **`source_citation: {file, line}`** — populated only when the finding's `Source:` field is a `file:line` pair (e.g., a Gap Report row that cites `packages/utils/src/builder-utils.ts:33`). Step-03 §0 uses this field to perform a live spot-check against source rather than flagging the export as `unknown`. Omit when the `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) or missing.
         - **`remediation_paths: [path, ...]`** — path-like tokens extracted from the finding's `Remediation:` text: any substring matching a recognized source file extension (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.py`, `.rs`, `.go`, `.java`, `.rb`, `.c`, `.h`, `.cpp`), or a directory/glob fragment under the project's source root. Include every matching path verbatim. Step-03 §0a uses this list as the source set for its Targeted Re-Extraction Branch when `source_citation` is absent and severity is Critical/High. Omit the field when the Remediation text names no paths — the entry then falls through to `unknown` or to §0a's halt, depending on severity.
         - **`change_category`** — the Change Category resolved from the table above (`NEW_EXPORT`, `MODIFIED_EXPORT`, `DELETED_EXPORT`, `STRUCTURAL_FIX`, or `metadata update`). Step 3 §1a partitions on this field; merge.md §3 dispatches on it.
         - **`remediation`** — the finding's full `Remediation:` text, verbatim. Required for `STRUCTURAL_FIX` (the surgical markdown edit), `metadata update` (the patch description), and `DELETED_EXPORT` rescope (the removal rationale recorded in the `scope.amendments[]` entry). For `NEW_EXPORT` / `MODIFIED_EXPORT` it is informational.
         - **`provenance_completeness: true`** — set on a `NEW_EXPORT` entry whose gap is rule R3 (documented in SKILL.md/`references/` but absent from the provenance-map). Step 3 §0 routes these to §0a regardless of severity.
      5. Set `gap_count` from the total number of translated entries
      6. **Skip to section 5** (Display Change Summary) with the gap-derived manifest
      
      "**Gap-driven update mode.** Translating {gap_count} test report findings into change manifest — source drift detection skipped."
      
      **If normal mode:** Continue with source drift detection below.
      
      ### 1. Scan Current Source State
      
      Read the source directory at `{source_root}` and build a current file inventory:
      - For each source file: record path, file size, last modified timestamp
      - Focus on file types relevant to the skill (from provenance map file patterns)
      - Exclude non-source files (node_modules, build artifacts, etc.)
      
      ### 1b. Discovered Authoritative Files Protocol (Mirror)
      
      **Purpose:** mirror `skf-create-skill` §2a into update-skill. `skf-create-skill` §2a catches authoritative AI documentation files (`llms.txt`, `AGENTS.md`, `.cursorrules`, etc.) during **creation**, but a project may add these files *after* the skill was created. Without this mirror, update-skill would either miss the new file entirely (if it doesn't match the provenance map's file patterns) or classify it as a generic ADDED file in §2 Category A with no authoritative-file treatment. The mirror surfaces the discovery with the same P/S/U prompt create-skill uses, honoring any prior amendments.
      
      **Skip this section entirely if:**
      
      - `update_mode == "gap-driven"` (source hasn't drifted — we're verifying test report findings, not discovering new files), OR
      - `metadata.json.source_type == "docs-only"` (no source tree to scan)
      
      **Procedure (identical heuristics to create-skill §2a):**
      
      1. **Walk the source tree.** Match file basenames against the heuristic list case-insensitively:
         - `llms.txt`, `llms-full.txt`
         - `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `COPILOT.md`
         - `.cursorrules`, `.windsurfrules`, `.clinerules`
      
      2. **Cross-reference with provenance map.** For each match:
         - **Already in provenance map** (`entries[].source_file` or `file_entries[].source_file` contains this path): the file is already tracked. §2 will detect any drift in the normal flow. No action in §1b.
         - **Not in provenance map:** continue to amendment check.
      
      3. **Check brief amendments.** Load `brief.scope.amendments[]` from `{forge_data_folder}/{skill_name}/skill-brief.yaml`. For each candidate not in the provenance map:
         - **`action: "promoted"` for this path exists:** the brief says this file should be in scope, but it's missing from the provenance map. This means the file was promoted by a prior run but its `file_entries[]` row is missing (e.g. provenance-map was regenerated from source without re-reading amendments). Add the path to `promoted_docs_new[]` (see step 6 below) with its content hash so §4 merge writes a new `file_entries[]` row. No user prompt — the decision was already made. Display: `"Honoring prior amendment: promoted {path} scheduled for file_entries write."`
         - **`action: "skipped"` for this path exists:** user previously declined promotion. Honor the skip silently. No prompt, no action.
         - **No amendment for this path:** continue to user prompt.
      
      4. **Prompt.** For each unresolved candidate, present the same prompt as create-skill §2a:
      
         ```
         **New authoritative file discovered since skill creation**
      
         Path: {relative_path_from_source_root}
         Size: {line_count} lines, {bytes} bytes
         Matched heuristic: {basename}
         Provenance age: {days since skill creation}
      
         First 20 lines:
         {inline preview}
      
         This file was not present (or not in scope) when the skill was created. How should update-skill handle it?
      
         [P] Promote — extract in this update run AND amend brief for future runs
         [S] Skip    — leave out of scope AND record skip in amendments (no re-prompt)
         [U] Update  — halt this run and return to skf-brief-skill to refine scope
         ```
      
      5. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate — record `action: "skipped"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently add files to scope. **Also append one entry per candidate to in-context `headless_decisions[]`** (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "detect-changes.promoted-doc-prompt", default_action: "S", taken_action: "S", reason: "headless: no user to prompt", evidence: {path: "<candidate.path>"}}`.
      
      6. **Apply decision:**
      
         - **[P] Promote:**
           1. Append `candidate.path` to `brief.scope.include` as a literal glob.
           2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `path: candidate.path`, `reason: {user-provided or auto: "discovered post-creation — matched heuristic {basename}"}`, `heuristic: {basename}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
           3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
           4. **Hash the candidate** via `uv run {hashContentHelper} hash {candidate.path}` — emits `{content_hash, size_bytes, line_count}` as JSON. Combine with the existing context fields and append to the in-context `promoted_docs_new[]` list: `{path, heuristic, size_bytes, line_count, content_hash}`. This list is consumed by §4 merge Priority 7 to write new `file_entries[]` rows — promoted docs do NOT go through §3 code re-extraction, which would produce ghost entries on non-code files.
           5. Display: `"Promoted {path} — brief amended, scheduled as new file_entries row for file_type doc."`
      
         - **[S] Skip:**
           1. Do NOT modify `scope.include`.
           2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1b"}`, `heuristic: {basename}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
           3. **Write the amended brief back to disk** so neither update-skill nor create-skill will re-prompt in future runs.
           4. Display: `"Skipped {path} — decision recorded in amendments."`
      
         - **[U] Update:**
           1. Halt the workflow immediately.
           2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
           3. Exit with status `halted-for-brief-refinement`. Change manifest is discarded — no partial writes.
      
      7. **Summary.** After all candidates are resolved (or none were found):
      
         - `"Authoritative files mirror: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments, {T} already tracked in provenance."`
         - If N = 0: `"Authoritative files mirror: no candidates."`
      
      **Record for evidence report:** the update-skill evidence report appends `authoritative_files_mirror: {candidates: N, promoted: P, skipped: S, pre_decided: A, already_tracked: T, decisions: [{path, action, heuristic, reason}]}`.
      
      **Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, not in the change manifest. §2.0's `change_detection_excludes` set (built below) is what stops §2 Category A from re-classifying them as ADDED — it is the only coordination that survives across the parallel Category A subprocesses.
      
      ### 1c. Major-Version Scope Reconciliation (Pre-Detection)
      
      **Purpose:** §1b handles new authoritative-doc files; §1c handles new **code globs** that fall outside the original scope when upstream restructures (rebrand, package restructure, major-version rewrite) so the brief's `scope.include` no longer reflects the real public API. Without it, update-skill silently misses the new public surface and pays the gap cost on every future update.
      
      **Skip this section entirely if:**
      
      - `update_mode == "gap-driven"` (test-report mode — source hasn't drifted), OR
      - `metadata.json.source_type == "docs-only"` (no source tree to scope), OR
      - No audit drift report is available at the path computed in step 1 below.
      
      **Procedure:**
      
      1. **Discover, parse, and reconcile in one call.** The helper handles drift-report discovery (glob, timestamp-DESC sort, latest wins), Out-of-Scope section extraction (both `## Out-of-Scope Observations` and `### Out-of-Scope New Public API` under `## Remediation Suggestions` heading shapes), candidate parsing (bullet and table markdown formats), and amendment reconciliation against `brief.scope.amendments[]`:
      
         ```bash
         uv run {provenanceGapDispatchHelper} dispatch \
             --skill-name {skill_name} \
             --baseline-version {baseline_version} \
             --forge-data-folder {forge_data_folder} \
             --brief {forge_data_folder}/{skill_name}/skill-brief.yaml
         ```
      
         Output envelope:
      
         ```json
         {
           "status": "no-report" | "no-candidates" | "candidates-found",
           "report_path": "<abs path>" | null,
           "candidates_total": N,
           "classified": [
             {
               "path": "<glob or file path>",
               "evidence": "<one-liner>",
               "status": "already-in-scope" | "pre-decided-skipped"
                       | "pre-decided-demoted" | "unresolved",
               "prior_action": "promoted" | "skipped"
                             | "demoted-include" | "demoted-exclude"
                             | null
             }, ...
           ],
           "summary": {"pre_decided_count": N, "unresolved_count": N}
         }
         ```
      
      2. **Dispatch on `status`:**
      
         - **`no-report`** — no drift report under `{forge_data_folder}/{skill_name}/{baseline_version}/`. **Skip §1c entirely** — proceed to step 6's summary line (omit). §2.2's post-detection deletion-ratio trigger still catches major restructures.
         - **`no-candidates`** — report exists but the Out-of-Scope section is absent or empty. Proceed to step 6's summary line with `"no out-of-scope observations in drift report."`
         - **`candidates-found`** — iterate `classified[]`:
           - `status: "already-in-scope"` → skip silently (`prior_action: "promoted"`).
           - `status: "pre-decided-skipped"` → honor silently (`prior_action: "skipped"`).
           - `status: "pre-decided-demoted"` → record as `pre_decided`; do not re-prompt (`prior_action` ∈ `demoted-include`, `demoted-exclude`).
           - `status: "unresolved"` → continue to step 4 (user prompt).
      
         **Note:** the Out-of-Scope section is an optional audit-skill output and is often absent or empty — new-file detection is update-skill's job (§1b/§2.2), not audit-skill's — so the `no-report` / `no-candidates` paths above are the common case.
      
      3. **Prompt for each unresolved candidate.** Present the same menu shape as §1b:
      
         ```
         **Out-of-scope new public API discovered**
      
         Path:          {candidate.path}
         Evidence:      {evidence from drift report}
         Drift report:  {report relative path}
      
         This path was not in the brief's `scope.include` when the skill was created. How should update-skill handle it?
      
         [P] Promote — add to scope.include AND extract in this run
         [S] Skip    — leave out of scope 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 `action: "skipped"`, `category: "scope-expansion"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently expand scope. **Also append one entry per candidate to in-context `headless_decisions[]`** (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "detect-changes.scope-expansion", default_action: "S", taken_action: "S", reason: "headless: no user to prompt", evidence: {path: "<candidate.path>"}}`.
      
      5. **Apply decision:**
      
         - **[P] Promote:**
           1. Append `candidate.path` to `brief.scope.include` as a literal glob (preserve any wildcards from the drift report).
           2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "out-of-scope new public API — drift report {report basename}"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
           3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
           4. Display: `"Promoted {path} — brief amended; §2 Category A will pick up matching files as ADDED."`
           5. **No `promoted_docs_new[]` entry and no `change_detection_excludes` write** — promoted code globs flow through the standard §2 Category A → §3 extraction path, unlike §1b's promoted docs which bypass extraction.
      
         - **[S] Skip:**
           1. Do NOT modify `scope.include` or `scope.exclude`.
           2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1c"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
           3. **Write the amended brief back to disk** so neither §1c nor a future run will re-prompt.
           4. Display: `"Skipped {path} — decision recorded in amendments."`
      
         - **[U] Update:**
           1. Halt the workflow immediately.
           2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
           3. Exit with status `halted-for-brief-refinement`. Change manifest is not yet built — no partial writes to provenance.
      
      6. **Summary:** After all candidates are resolved (or none were found):
      
         - `"Scope reconciliation: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments."`
         - If N = 0 (section absent or empty): `"Scope reconciliation: no out-of-scope observations in drift report."`
         - If §1c was skipped entirely (no drift report): omit this line; §2.2 will still run.
      
      **Record for evidence report:** the update-skill evidence report appends `scope_reconciliation_pre: {drift_report: path, candidates: N, promoted: P, skipped: S, pre_decided: A, decisions: [{path, action, evidence}]}` (omit when §1c was skipped).
      
      ### 2. Compare Against Provenance Map
      
      **If normal mode (provenance map available):**
      
      #### 2.0 — Build Pre-filter Exclusion Set
      
      Before launching parallel subprocesses, build a `change_detection_excludes` set in context that Category A subprocess workers must honor. Parallel subprocesses cannot see each other's in-memory state, so any coordination between §1b's decisions and §2's scan results must be pre-materialized into an explicit input the subprocesses receive.
      
      The exclusion set includes:
      
      - Every path in `promoted_docs_new[]` (populated by §1b). These files are tracked as `file_entries[]` via step 4 Priority 7, not through Category A code extraction. Without this exclusion, Category A would classify them as ADDED (because they're in source but not yet in the provenance map) and §3 re-extract would send them to AST extraction, producing ghost entries.
      - Every source path in `file_entries[].source_file` where `file_type == "doc"` in the existing provenance map. These are already-tracked authoritative docs; any drift in them is handled by Category D (script/asset file changes), not Category A.
      
      Record the set size: "**Change-detection excludes:** {count} paths ({promoted_docs_new count} new promotions + {existing doc file_entries count} already tracked)."
      
      #### 2.1 — Launch Category Subprocesses
      
      Launch subprocesses in parallel that compare source state against provenance map across these categories, returning change findings per category. **Every subprocess receives `change_detection_excludes` as an explicit input** and applies it to its file-path iteration loop.
      
      **Category A — File-level changes:**
      - Files in provenance map but missing from source → DELETED
      - Files in source but not in provenance map AND not in `change_detection_excludes` → ADDED
      - Files in `change_detection_excludes`: skip entirely (routed to file_entries via §1b → step 4 Priority 7, never through Category A)
      - Files in both but with different timestamps/sizes → MODIFIED
      - Files with same content at different paths → MOVED
      
      **Category B — Export-level changes (for MODIFIED files only):**
      - For each modified file, compare export list against provenance map exports
      - Exports in provenance but not in source → DELETED_EXPORT
      - Exports in source but not in provenance → NEW_EXPORT
      - Exports with changed signatures/types → MODIFIED_EXPORT
      - Exports at different line numbers but same content → MOVED_EXPORT
      
      **Category C — Rename detection:**
      - Cross-reference deleted files/exports with added files/exports
      - If content similarity > 80% (fixed bundled threshold — not configurable): classify as RENAMED instead of deleted+added. **Similarity mechanism by tier:** Quick: compare file size ratio (within 20%) and export name overlap (>70% of exports match by name). Forge and above: use ast-grep to compare export signatures between the deleted and added files. Forge+/Deep: use CCC semantic similarity when available
      
      **Subprocess return contract.** Hand each Category worker its exact output slice so it returns parse-ready JSON, not prose the parent must re-read. Each worker returns ONLY its own object — no prose, no commentary, no markdown fences (parent strips wrapping fences before parsing). The slice each worker fills is exactly the key §3's `build` helper consumes (the `category_a/b/c` shape below; Category D is helper-driven, not a worker):
      
      ```json
      // Category A worker returns ONLY:
      {"category_a": {"modified": [...], "added": [...], "deleted": [...]}}
      // Category B worker returns ONLY:
      {"category_b": {"modified_exports": [...], "new_exports": [...], "deleted_exports": [...], "moved_exports": [...]}}
      // Category C worker returns ONLY:
      {"category_c": {"renamed_files": [...], "renamed_exports": [...]}}
      ```
      
      The parent merges the three slices (plus Category D and the `degraded_mode`/`update_mode` flags) into the single category-JSON object §3 pipes to the `build` helper.
      
      **Category D — Script/asset file changes:**
      
      Run the bulk comparison once via:
      
      ```bash
      uv run {hashContentHelper} compare <source-root> \
          --provenance-map <provenance-map-path>
      ```
      
      The helper emits:
      
      ```json
      {
        "comparisons": [
          {"source_file": "...", "classification": "UNCHANGED|MODIFIED_FILE|DELETED_FILE",
           "stored_hash": "sha256:...", "current_hash": "sha256:..."|null,
           "current_size_bytes": N|null}, ...
        ],
        "stats": {"total": N, "unchanged": U, "modified": M, "deleted": D}
      }
      ```
      
      Translate the helper's output into the change manifest:
      - `MODIFIED_FILE` rows → add to manifest as MODIFIED_FILE
      - `DELETED_FILE` rows → add to manifest as DELETED_FILE
      - `UNCHANGED` rows → omit from the manifest (no action needed)
      
      The compare helper reports only tracked files; NEW_FILE detection (a file present in source but absent from the provenance map) is a set-difference, so it runs through a script rather than the prompt. Pipe the same deterministic detector create-skill step 3 §4c uses (resolved via `detectScriptsAssetsProbeOrder`) into `{newFileDiffHelper}`, which subtracts the provenance map's `file_entries[].source_file` and sets aside user-authored `[MANUAL]` paths:
      
      ```bash
      uv run {detectScriptsAssetsHelper} detect <source-root> \
          | uv run {newFileDiffHelper} {forge_version}/provenance-map.json
      ```
      
      It emits `{"new_files":[{source_file, kind}], "skipped_manual":[...], "already_tracked":[...], "stats":{...}}`. Add each `new_files[]` entry to the manifest as NEW_FILE — `kind` (`script`/`asset`) selects the target array. `skipped_manual[]` are user-authored files under `scripts/[MANUAL]/` or `assets/[MANUAL]/`, preserved and not touched; `already_tracked[]` were handled by the compare above.
      
      Aggregate all subprocess results into a unified change manifest.
      
      **If degraded mode (no provenance map):**
      - All source files are treated as MODIFIED
      - All exports will be fully re-extracted in step 03
      - Skip export-level comparison
      
      #### 2.2 — Major-Version Scope Reconciliation (Post-Detection)
      
      **Purpose:** §1c catches the major-version case when an audit drift report supplies explicit candidates. §2.2 is the safety net that fires when no audit was run (or audit emitted no out-of-scope section): it inspects the just-built Category A/B results for the deletion-ratio signature of a major-version restructure and gives the user an off-ramp before §3 commits the change manifest.
      
      **Trigger computation:** invoke the helper with the same Category A/B/C/D JSON used for §3, plus the provenance map:
      
      ```bash
      echo "{category JSON}" | uv run {buildChangeManifestHelper} deletion-ratio \
          --provenance-map {forge_version}/provenance-map.json
      ```
      
      The helper handles the three skip conditions internally — when the input has `update_mode: "gap-driven"`, `degraded_mode: true`, or the provenance has zero entries, it returns `skip_reason` set and `should_trigger: false`. `should_trigger` fires when the deletion ratio reaches the fixed bundled threshold of 50% (`ratio >= 0.50`, baked into the helper — not configurable). The output envelope:
      
      ```json
      {
        "skip_reason": "gap-driven" | "degraded-mode" | "zero-provenance-exports" | null,
        "deleted_export_count": N,
        "total_provenance_exports": N,
        "deletion_ratio": 0.X,
        "deleted_file_count": N,
        "added_in_scope_count": N,
        "renamed_or_moved_count": N,
        "should_trigger": <bool>
      }
      ```
      
      If `skip_reason` is non-null OR `should_trigger` is false, skip the prompt and continue to §3. If `should_trigger` is true, present the prompt below.
      
      **Prompt:**
      
      ```
      **Major-version scope shift detected**
      
      Deleted exports:        {deleted_export_count} of {total_provenance_exports} ({percent}%)
      Deleted files:          {deleted_file_count}
      Added files (in scope): {added_in_scope_count}
      Renamed/moved exports:  {renamed_or_moved_count}
      
      The upstream surface appears to have been substantially replaced. The brief's
      `scope.include` patterns may no longer reflect the real public API.
      
      [C] Continue — proceed with re-extraction; the deletion is intentional
      [B] Brief    — halt and re-run skf-brief-skill to refine scope first
      [A] Audit    — halt and run skf-audit-skill to map the new surface, then re-run update-skill
      ```
      
      **Headless mode (`{headless_mode}` is true):** auto-select `[C] Continue`, log a WARN-level entry to the evidence report (`scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "headless-continue"}`), and surface the warning in step 7's report. A non-interactive run must not silently halt, but the user must be able to see the signal post-hoc. **Also append to in-context `headless_decisions[]`** (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "detect-changes.deletion-ratio", default_action: "C", taken_action: "C", reason: "headless: deletion-ratio threshold exceeded but no user to halt", evidence: {deletion_ratio: <ratio>, deleted_export_count: <N>, total_provenance_exports: <T>}}`.
      
      **Apply decision:**
      
      - **[C] Continue:** record `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "continue"}` and proceed to §3.
      - **[B] Brief:** halt with status `halted-for-brief-refinement`. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` Change manifest discarded — no partial writes.
      - **[A] Audit:** halt with status `halted-for-audit`. Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface — its drift report will feed §1c on the next update-skill run."` Change manifest discarded.
      
      ### 3. Build Change Manifest
      
      Hand the assembled Category A/B/C/D JSON to the helper:
      
      ```bash
      echo "{category JSON}" | uv run {buildChangeManifestHelper} build
      ```
      
      The category JSON shape is:
      
      ```json
      {
        "category_a": {"modified": [...], "added": [...], "deleted": [...]},
        "category_b": {"modified_exports": [...], "new_exports": [...],
                       "deleted_exports": [...], "moved_exports": [...]},
        "category_c": {"renamed_files": [...], "renamed_exports": [...]},
        "category_d": {"scripts_modified": [...], "scripts_added": [...],
                       "scripts_deleted": [...], "assets_modified": [...],
                       "assets_added": [...], "assets_deleted": [...]},
        "degraded_mode": <bool>,
        "update_mode": "normal" | "gap-driven"
      }
      ```
      
      The helper emits the unified manifest envelope:
      
      ```json
      {
        "no_changes": <bool>,
        "degraded_mode": <bool>,
        "counts": {
          "files_changed": N, "files_added": N, "files_deleted": N, "files_moved": N,
          "exports_modified": N, "exports_new": N, "exports_deleted": N,
          "exports_renamed": N, "exports_moved": N,
          "scripts_modified": N, "scripts_added": N, "scripts_deleted": N,
          "assets_modified": N, "assets_added": N, "assets_deleted": N
        },
        "total_export_changes": N,
        "per_file": [
          {"file_path": "...", "status": "MODIFIED|ADDED|DELETED|MOVED",
           "exports_affected": [{name, change_type, old_line, new_line}, ...]}
        ]
      }
      ```
      
      `per_file` entries are sorted MODIFIED → ADDED → DELETED → MOVED, then alphabetically within each status group, so downstream stages can rely on stable ordering. MOVED entries include an extra `old_path` field. Stash the envelope as the change manifest in workflow context.
      
      ### 4. Check for No-Change Shortcut
      
      **If zero changes detected across all categories:**
      
      "**No changes detected.** Source code matches provenance map exactly.
      
      The skill `{skill_name}` is current — no update needed.
      
      **Skipping to report step...**"
      
      → Skip steps 03-06, immediately load {noChangeReportFile} with "no changes" status.
      
      ### 5. Display Change Summary and Route
      
      "**Change Detection Complete:**
      
      | Category | Count |
      |----------|-------|
      | Files modified | {count} |
      | Files added | {count} |
      | Files deleted | {count} |
      | Files moved/renamed | {count} |
      | Exports affected | {total_export_changes} |"
      
      This step auto-proceeds — no user choices. Once the change manifest is fully built, load and fully read the next file, then execute it, per the branch that applies:
      
      - **`detect_only_mode == true`** → display "**Detect-only mode — skipping re-extract/merge/validate/write.** Loading report..." and load `{noChangeReportFile}` (report.md), which emits status `detect-only`. Do not load `{nextStepFile}`.
      - **No changes detected** (section 4) → load `{noChangeReportFile}` (report.md), which emits status `no-changes`.
      - **Otherwise** → display "**Proceeding to re-extraction of {affected_file_count if normal mode, or gap_count if gap-driven mode} changes...**" and load `{nextStepFile}` (re-extract.md) to begin re-extraction.
      
      
    • health-check.md 1.3 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 8: Workflow Health Check
      
      ## STEP GOAL:
      
      Chain to the shared workflow self-improvement health check at `{nextStepFile}`. This is the terminal step of update-skill — after the shared health check completes, the workflow is fully done. This step only releases the concurrency lock and delegates: no user-facing reports, file writes, or result contracts here (those belong in step 7).
      
      ## Steps
      
      1. **Release the concurrency lock** acquired by init.md §1b (skip when `detect_only_mode` or `dry_run_mode` is true — those modes never acquired one):
      
         ```bash
         rm -f "{forge_data_folder}/{skill_name}/.skf-update.lock"
         ```
      
         Release the lock before delegating to the shared health-check: the health-check is the terminal step, so once it returns the workflow is done and any still-held lock is orphaned until the next run clears it. Releasing here keeps the lock lifecycle tight against the workflow's actual span.
      
      2. Load `{nextStepFile}`, read it fully, then execute it.
      
    • init.md 18 KB
      ---
      nextStepFile: 'detect-changes.md'
      manualSectionRulesFile: 'references/manual-section-rules.md'
      # Resolve `{hashContentHelper}` to the first existing path; HALT if neither
      # candidate exists. §5 uses its `manual-inventory` subcommand to capture the
      # exact pre-write [MANUAL] inventory (per-block byte-exact interior hashes),
      # which write.md §1 (HALT gate) and validate.md Check B later verify against.
      # An LLM marker-count would miss an interior truncation that leaves the marker
      # count unchanged.
      hashContentProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
        - '{project-root}/src/shared/scripts/skf-hash-content.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 1: Initialize Update
      
      ## STEP GOAL:
      
      Load the existing skill and all its provenance data, detect whether this is an individual or stack skill, load the forge tier configuration, and present a baseline summary so the user can confirm the update scope before proceeding.
      
      ## Rules
      
      - Focus only on loading existing artifacts and establishing the baseline — read-only operations
      - Do not begin change detection (Step 02)
      
      ## Steps
      
      ### 1. Request Skill Path
      
      "**Which skill would you like to update?**
      
      Provide either:
      - A skill name (resolves via version-aware path resolution — see `knowledge/version-paths.md`)
      - A full path to the skill folder
      - A skill name with `--from-test-report` to use the test report's gap findings instead of source drift detection
      - `--allow-workspace-drift` (gap-driven mode only) to intentionally bypass the step 3 §0.a guard that halts when the local workspace HEAD does not match `metadata.source_commit`. Only use this if you know the spot-checks should read the current workspace instead of the pinned tree — step 6 will NOT automatically re-pin
      - `--allow-degraded` (headless mode only) to pre-authorize the lossy degraded full re-extraction if §4 finds no provenance map — without it, a headless run halts `blocked` there rather than silently rebuilding
      - `--detect-only` to run detect-changes only and exit; emits the change manifest with no further work and no writes
      - `--dry-run` to run detect-changes + re-extract and exit before merge/write; emits what WOULD change without modifying any artifact
      
      **Skill:** {user provides path or name}"
      
      **Version-Aware Path Resolution:**
      1. Read `{skills_output_folder}/.export-manifest.json` and look up the skill name in `exports` to get `active_version`
      2. If found: resolve to `{skill_package}` = `{skills_output_folder}/{skill-name}/{active_version}/{skill-name}/`
      3. If not in manifest: check for `active` symlink at `{skills_output_folder}/{skill-name}/active` — resolve to `{skill_group}/active/{skill-name}/`
      4. If neither: fall back to flat path `{skills_output_folder}/{skill-name}/`. If SKILL.md exists at the flat path, auto-migrate per `knowledge/version-paths.md` migration rules
      5. Store the resolved path as `{resolved_skill_package}` for all subsequent artifact loading
      6. Bind `{baseline_version}` to the pre-update version — for an update this is the version being updated, i.e. the `{active_version}` resolved in step 1 above (the flat-path fallback in step 4 has no version, so use the package version read from metadata.json in §2). Step 2 §1c passes `{baseline_version}` to `skf-provenance-gap-dispatch.py` as a required argument; leaving it unbound makes the helper search a wrong/empty directory and silently return `no-report`, dropping the major-version off-ramp.
      
      Resolve the path to an absolute skill folder location.
      
      **If `--from-test-report` was provided (or user references a test report):**
      
      `skf-test-skill` writes timestamped test-report filenames (`test-report-{skill_name}-{ISO-TIMESTAMP}-{HASH}.md`) — there is no exact-name `test-report-{skill_name}.md` on disk. Locate the most recent report by glob, mirroring `skf-export-skill/references/load-skill.md §4b`:
      
      1. Glob `{forge_data_folder}/{skill_name}/{active_version}/test-report-{skill_name}-*.md` (i.e. `{forge_version}/test-report-{skill_name}-*.md`). Sort matches descending by the parsed ISO-timestamp segment in the filename (`YYYYMMDDTHHMMSSZ` between the skill name and the hash — `sort -r` on the filename works because the timestamp is the first variable component). Take the first match.
      2. If the versioned glob returns nothing, fall back to the same glob at the flat path `{forge_data_folder}/{skill_name}/test-report-{skill_name}-*.md`. Pick the newest by parsed timestamp.
      3. If neither glob returns anything, look for the stable companion `skf-test-skill-result-latest.json` in the same two directories (versioned first, then flat). Read the report path from `outputs[]` per the canonical contract documented at `shared/references/output-contract-schema.md` (resolved by skf-test-skill step 6 §4c) and load that file.
      
      If a report is located, set `test_report_path` in context to the resolved absolute path and set `update_mode: gap-driven`. Surface the actual file picked in the message (e.g. `test-report-{skill_name}-20260507T050917Z-487606-9b2f.md`) so an operator can navigate to the report from the log. If all three lookups fail, warn and continue with normal source drift mode.
      
      **If `--allow-workspace-drift` was provided:** set `allow_workspace_drift: true` in workflow context. This flag is consumed by step 3 §0.a's pre-flight drift guard (gap-driven mode only) and has no effect in normal source-drift mode.
      
      **If `--allow-degraded` was provided:** set `allow_degraded: true` in workflow context. This flag is consumed by §4 below when no provenance map is found under `{headless_mode}`; it has no effect interactively (the [D]/[X] prompt is shown) or when a provenance map is present.
      
      **If `--detect-only` was provided:** set `detect_only_mode: true` in workflow context. After step 2 (detect-changes) completes, jump directly to step 7 (report) — skip re-extract, merge, validate, and write. The report emits the change manifest and a `SKF_UPDATE_RESULT_JSON` envelope with `status: "detect-only"`. **Compatibility:** `--detect-only` short-circuits before §0.a runs, so `--allow-workspace-drift` is silently ignored in detect-only mode (warn the user once at flag-parse time: "`--allow-workspace-drift` has no effect with `--detect-only` — workspace drift guard runs in step 3 §0.a, which is skipped").
      
      **If `--dry-run` was provided:** set `dry_run_mode: true` in workflow context. After step 3 (re-extract) completes, jump directly to step 7 (report) — skip merge, validate, and write. The report emits what would change with `status: "dry-run"` in the envelope. No artifact on disk is modified — `--dry-run` is the "show me what an update would do without committing" mode.
      
      **If BOTH `--detect-only` AND `--dry-run` were provided:** `--detect-only` wins (it's the more restrictive). Warn the user once: "`--detect-only` supersedes `--dry-run`; re-extract is skipped." Set `detect_only_mode: true`, ignore `dry_run_mode`.
      
      ### 1b. Concurrency Guard
      
      **Skip this section entirely if `detect_only_mode` OR `dry_run_mode` is true.** Both inspection modes are read-only — they do not modify any artifact and are safe to run alongside a concurrent real update.
      
      Two concurrent `skf-update-skill` runs against the same `{forge_data_folder}/{skill_name}/` can corrupt provenance: one would write metadata.json mid-way through the other's extraction. The lock below catches the common accidental-double-invoke case (user re-runs in another shell before the first finishes). It is a **best-effort PID-file guard**, not a held flock — the LLM-driven workflow spans many turn boundaries and no single bash invocation can hold flock across them. Use the pattern from `skf-create-skill/references/source-resolution-protocols.md:87` (workspace concurrency guard) as the conceptual model.
      
      **Mirror this exactly so the guard works the same way every run:**
      
      ```bash
      # Lock file path — one per skill, lives next to skill-brief.yaml
      LOCK={forge_data_folder}/{skill_name}/.skf-update.lock
      mkdir -p "$(dirname "$LOCK")"
      
      if [ -f "$LOCK" ]; then
        HELD_PID=$(head -n1 "$LOCK" 2>/dev/null | awk '{print $1}')
        if [ -n "$HELD_PID" ] && kill -0 "$HELD_PID" 2>/dev/null; then
          # Live PID — another update is running. HALT.
          echo "skf-update-skill: another update is in progress (pid=$HELD_PID, started $(awk 'NR==2' "$LOCK" 2>/dev/null))"
          # (LLM emits SKF_UPDATE_RESULT_JSON status=halted-for-concurrent-run, see below)
          exit 1
        fi
        # Dead PID — lock left by a prior halted or crashed run; clear + overwrite
        echo "skf-update-skill: clearing lock from a prior halted/crashed run (pid=$HELD_PID)"
      fi
      
      # Acquire: write our PID + start timestamp (one per line)
      printf '%s\n%s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$LOCK"
      ```
      
      **Halt protocol on live-PID collision:**
      
      - Display: `"**Another update is in progress.** The skill {skill_name} is locked by pid={HELD_PID} (started {timestamp from line 2 of the lock file}). Wait for that run to finish, or — if you know that pid is no longer running — delete {LOCK} manually and re-run."`
      - In `{headless_mode}`, emit `SKF_UPDATE_RESULT_JSON` with `status: "halted-for-concurrent-run"`, `error: {phase: "init:concurrency-guard", path: "{LOCK}", reason: "another update in progress (pid={HELD_PID})"}`, and exit immediately. **No `headless_decisions[]` entry** — this is a hard halt before any gate fires.
      
      **Release contract:**
      
      - The terminal health-check step (step 8) deletes the lock as its final action — the normal end of every non-inspection run. The two init-stage headless halts below (§4 no-provenance-map, §6 invalid-source-path) also delete it explicitly, since they fire right after acquisition, before the terminal step runs.
      - Mid-workflow halts (detect-changes, re-extract, merge, write) do **not** delete the lock themselves — they rely on the self-heal below. This is deliberate: several of those halt sites are also reachable under `--detect-only`/`--dry-run`, which never acquired this lock, so a blind `rm -f` there could clobber a concurrent real update's lock.
      - The lock is best-effort and self-healing: whatever a halt or crash (process kill, host reboot) leaves behind is cleared by the next run's live-PID check above, since the stored PID is a short-lived bash PID that is already dead. No manual cleanup needed in the common case.
      
      ### 2. Validate Required Artifacts
      
      **Check SKILL.md exists:**
      - Load `{resolved_skill_package}/SKILL.md`
      - If missing: **ABORT** — "No SKILL.md found at `{resolved_skill_package}`. Run create-skill first."
      
      **Check metadata.json exists:**
      - Load `{resolved_skill_package}/metadata.json`
      - Extract: `name`, `skill_type` (single or stack), `version`, `generation_date`, `confidence_tier`, `source_root`
      - If missing: **ABORT** — "No metadata.json found. This skill may have been created manually. Run create-skill to generate provenance data."
      
      **Detect skill type from metadata:**
      - If `skill_type == "single"` or absent: flag as single skill
      - If `skill_type == "stack"`: flag as stack skill — the guard below redirects it
      
      ### Stack Skill Guard
      
      After loading metadata.json, check `skill_type`:
      - If `skill_type` is `"stack"`: display message:
        "**Stack skills cannot be surgically updated.** Stack skills compose exports from multiple sources — surgical re-extraction requires re-running the full composition pipeline.
        
        **To update this stack skill**, run `skf-create-stack-skill` with the same project path. It will re-analyze manifests (code-mode) or re-read constituent skills (compose-mode) and produce an updated stack.
        
        If you came here from an audit report, the drift report identifies which constituent libraries changed — use that to decide whether re-composition is needed."
      - Exit the workflow (do not proceed to step 2)
      
      **This guard is the single gate for stack skills** — every stack is redirected to `skf-create-stack-skill` here, before step 2, and no flag (`--detect-only`, `--dry-run`, `--from-test-report`, `--allow-workspace-drift`) bypasses it. Every later stage therefore runs against a single skill only and carries no stack-merge branch.
      
      ### 3. Load Forge Tier Configuration
      
      **Load `{sidecar_path}/forge-tier.yaml`:**
      - Extract: `tier` (Quick, Forge, Forge+, or Deep), available tools
      - If missing: **ABORT** — "No forge-tier.yaml found. Run setup first to detect available tools."
      
      **Apply tier override:** Read `{sidecar_path}/preferences.yaml`. If `tier_override` is set and is a valid tier value (Quick, Forge, Forge+, or Deep), use it instead of the detected tier.
      
      **Determine analysis capabilities:**
      - **Quick:** text pattern matching only → T1-low confidence
      - **Forge:** AST structural extraction → T1 confidence
      - **Forge+:** AST structural extraction + CCC semantic ranking → T1 confidence (with ccc signals)
      - **Deep:** AST + QMD semantic enrichment → T1 + T2 confidence
      
      ### 4. Load Provenance Map
      
      **Load `{forge_data_folder}/{skill_name}/{active_version}/provenance-map.json`** (i.e., `{forge_version}/provenance-map.json`). If not found at the versioned path, fall back to `{forge_data_folder}/{skill_name}/provenance-map.json`:
      - Extract: export list, file mappings, extraction timestamps, confidence tiers
      - Calculate provenance age (days since last extraction)
      
      **If provenance map missing at both paths:**
      
      "**WARNING:** No provenance map found at `{forge_version}/provenance-map.json` or flat fallback.
      
      Without a provenance map, update-skill cannot perform targeted change detection. Options:
      
      **[D]egraded mode** — Perform full re-extraction with T1-low confidence (equivalent to re-running create-skill but preserving [MANUAL] sections)
      **[X]** — Abort and run create-skill first to generate provenance data
      
      Select: [D] Degraded / [X] Abort"
      
      - If D: set `degraded_mode = true`, proceed with full extraction scope
      - If X: **ABORT**
      
      **In `{headless_mode}` without `--allow-degraded` (default):** do not auto-select [D]. Degraded mode is a full, lossy T1-low re-extraction — choosing it unattended would silently swap surgical update for a create-skill-equivalent rebuild, a policy call that belongs to an operator. Halt instead: release the lock (`rm -f "$LOCK"`), emit `SKF_UPDATE_RESULT_JSON` with `status: "blocked"`, `error: {phase: "init:load-provenance-map", path: "{forge_version}/provenance-map.json", reason: "no provenance map at versioned or flat path; degraded full re-extraction needs a human decision"}`, and exit. No `headless_decisions[]` entry — this is a hard halt, not an auto-resolved gate.
      
      **In `{headless_mode}` with `--allow-degraded` (`allow_degraded: true`):** the operator pre-authorized the lossy rebuild for this run, so treat it as an auto-resolved [D] rather than a halt. Set `degraded_mode = true`, proceed with full extraction scope, and append to in-context `headless_decisions[]`: `{gate: "init.degraded-rebuild", default_action: "X", taken_action: "D", reason: "headless: --allow-degraded pre-authorized degraded full re-extraction", evidence: "no provenance map at {forge_version}/provenance-map.json or flat fallback"}`. Continue to step 2.
      
      ### 5. Load [MANUAL] Section Inventory
      
      Load {manualSectionRulesFile} to understand [MANUAL] detection patterns (the human-readable rules for markers, parent-section mapping, and orphan/nesting handling).
      
      **Capture the [MANUAL] inventory deterministically.** The workflow's headline rule is "[MANUAL] sections survive regeneration with zero content loss" — the pre-write inventory captured here is the exact baseline that write.md §1 and validate.md Check B verify against, so it must be a per-block byte-exact hash, not an eyeballed marker count. Run the `manual-inventory` subcommand of `{hashContentHelper}` and persist its JSON:
      
      ```bash
      uv run {hashContentHelper} manual-inventory {resolved_skill_package}/SKILL.md \
          > {forge_version}/.manual-inventory.json
      ```
      
      The emitted JSON is `{"blocks":[{name, content_hash, byte_offset, parent_heading}...], "count":N}` — each `content_hash` covers the block's byte-exact interior, so a later interior truncation that leaves the marker count unchanged is still caught. Bind the persisted path as `{manual_inventory}` in context; write.md §1 and validate.md Check B pass it to `manual-verify`. Surface the block `count` in the baseline summary (§7 `{manual_count}`).
      
      ### 6. Resolve Source Code Path
      
      **From provenance map (if available):**
      - Extract `source_root` path
      - Validate source path exists and is accessible
      
      **If source path invalid or missing:**
      
      "**Source path from provenance map is invalid:** `{source_root}`
      
      Provide the current source code path:
      **Path:** {user provides path}"
      
      **In `{headless_mode}`:** there is no operator to supply a path. Halt: release the lock (`rm -f "$LOCK"`), emit `SKF_UPDATE_RESULT_JSON` with `status: "blocked"`, `error: {phase: "init:resolve-source-path", path: "{source_root}", reason: "source_root from provenance map is invalid or inaccessible and no interactive path can be supplied"}`, and exit. No `headless_decisions[]` entry — this is a hard halt, not an auto-resolved gate.
      
      ### 7. Present Baseline Summary
      
      "**Update Skill Baseline:**
      
      | Property | Value |
      |----------|-------|
      | **Skill** | {skill_name} |
      | **Type** | single |
      | **Version** | {version} |
      | **Created** | {created date} |
      | **Source** | {source_root} |
      | **Forge Tier** | {forge_tier} (current) vs {original_tier} (at creation) |
      | **Provenance Age** | {days} days since last extraction |
      | **Exports** | {export_count} tracked exports |
      | **[MANUAL] Sections** | {manual_count} preserved sections |
      | **Mode** | {normal/degraded/gap-driven} |
      
      **Analysis plan:** {tier_description}
      - {Quick: text pattern diff → T1-low findings}
      - {Forge: AST structural diff → T1 findings}
      - {Deep: AST structural + QMD semantic diff → T1 + T2 findings}
      
      **Ready to detect changes and update this skill?**"
      
      ### 8. Confirmation Gate
      
      Present "**Select:** [C] Continue to Change Detection" and wait for the user to confirm; on [C], load, read the full file, then execute {nextStepFile}.
      
      **Headless (`{headless_mode}` true):** auto-continue and append to in-context `headless_decisions[]` (step 7 surfaces it in `SKF_UPDATE_RESULT_JSON`): `{gate: "init.update-confirmation", default_action: "C", taken_action: "C", reason: "headless: no user to prompt"}`. Entry shape: `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`.
      
      
    • manual-section-rules.md 2.1 KB
      ---
      type: static-reference
      ---
      
      # [MANUAL] Section Rules
      
      ## Detection Pattern
      
      [MANUAL] sections are developer-authored content blocks within generated SKILL.md files that must survive regeneration.
      
      ### Identification
      
      A [MANUAL] section is delimited by markers in the SKILL.md:
      
      ```markdown
      <!-- [MANUAL:section-name] -->
      Developer-authored content here.
      This content was added by the developer after skill generation.
      It must be preserved during any update operation.
      <!-- [/MANUAL:section-name] -->
      ```
      
      ### Rules
      
      1. **Never modify content between [MANUAL] markers** — treat as immutable
      2. **Preserve marker positions** — if the surrounding generated content moves, the [MANUAL] block moves with its logical parent section
      3. **Orphan detection** — if the parent section is deleted (export removed), flag as WARNING and present to user
      4. **Multiple [MANUAL] blocks** — a single SKILL.md may have multiple [MANUAL] sections; preserve all
      5. **Nested [MANUAL] forbidden** — [MANUAL] blocks cannot be nested; if detected, flag as ERROR
      
      ### Conflict Types
      
      | Conflict                                       | Severity | Resolution                                            |
      |------------------------------------------------|----------|-------------------------------------------------------|
      | Regenerated content overlaps [MANUAL] position | HIGH     | Present both versions, user chooses                   |
      | Parent section deleted                         | WARNING  | Flag orphaned [MANUAL], user decides keep/remove      |
      | [MANUAL] references deleted export             | MEDIUM   | Flag stale reference, suggest update                  |
      | New export inserted adjacent to [MANUAL]       | LOW      | Auto-resolve: place new content before [MANUAL] block |
      
      ### Preservation Algorithm
      
      1. Extract all [MANUAL] blocks with their section-name identifiers
      2. Map each block to its parent section (by heading hierarchy)
      3. Perform merge on generated content only
      4. Re-insert [MANUAL] blocks at their mapped positions
      5. If position conflict: halt and present to user
      6. If clean insert: auto-place and continue
      
    • merge-conflict-rules.md 878 B
      ---
      type: static-reference
      ---
      
      # Merge Conflict Rules
      
      > Change-category actions and the merge priority order (deleted → moved → renamed → modified → new, plus the gap-driven priorities) are specified authoritatively in `merge.md` §3. This reference carries only the one thing §3 does not: the conflict-resolution strategy table below.
      
      ## Conflict Resolution Strategies
      
      | Strategy     | When                                | Action                                    |
      |--------------|-------------------------------------|-------------------------------------------|
      | Auto-resolve | No [MANUAL] conflicts, clean merge  | Proceed without user input                |
      | User-resolve | [MANUAL] conflicts detected         | Halt, present conflicts, require decision |
      | Abort        | Critical structural incompatibility | Stop workflow, recommend full re-creation |
      
    • merge.md 10.3 KB
      ---
      nextStepFile: 'validate.md'
      manualSectionRulesFile: 'references/manual-section-rules.md'
      mergeConflictRulesFile: 'references/merge-conflict-rules.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 4: Merge
      
      ## STEP GOAL:
      
      Merge freshly extracted export data into the existing SKILL.md content while preserving all [MANUAL] sections. Detect and resolve conflicts where regenerated content overlaps developer-authored content.
      
      ## Rules
      
      - Focus only on merging extractions into existing skill content
      - Never delete or modify [MANUAL] section content
      - Write merged SKILL.md directly to disk at section 6b — Claude Code's Edit/Write tools commit on call, so there is no held-in-memory "edit plan" primitive; subsequent steps validate and verify against the on-disk files
      - If [MANUAL] conflicts detected: halt and present to user. If clean merge: auto-proceed
      
      ## Steps
      
      ### 1. Load Merge Rules
      
      Load {manualSectionRulesFile} for [MANUAL] detection and preservation patterns.
      Load {mergeConflictRulesFile} for the conflict-resolution strategy table (the change-category actions and priority order live in §3 below).
      
      ### 2. Extract [MANUAL] Blocks
      
      From the [MANUAL] inventory captured in step 01:
      - Extract every `<!-- [MANUAL:section-name] -->` ... `<!-- [/MANUAL:section-name] -->` block
      - Map each block to its parent section heading
      - Store blocks in a preservation map keyed by section-name
      
      ### 3. Apply Merge by Priority Order
      
      Apply merge in the following priority order:
      
      **Priority 1 — Process DELETED exports:**
      - Remove generated content for deleted exports
      - Check if deleted export has attached [MANUAL] blocks
      - If [MANUAL] attached: flag as ORPHAN conflict (do not remove)
      - If no [MANUAL]: remove generated content cleanly
      - **Gap-driven rescopes** (`DELETED_EXPORT` from detect-changes §0 rule R1, verification `rescoped`) are processed here with the same removal. Step 6 also removes the provenance `entries[]` row and recomputes `stats` from the amended `brief.scope` (write.md §2/§3). The brief's `scope.amendments[]` (`action: "excluded"`) + `scope.exclude` entry must already exist — step 3 §0 HALTs otherwise, so no unscoped removal reaches here.
      
      **Priority 2 — Process MOVED exports:**
      - Update file:line citations in generated content
      - Update provenance map file references
      - [MANUAL] blocks unaffected (content unchanged)
      
      **Priority 3 — Process RENAMED exports:**
      - Replace old identifier with new identifier in generated content
      - Check if [MANUAL] blocks reference old identifier name
      - If referenced: flag as STALE_REFERENCE conflict
      
      **Priority 4 — Process MODIFIED exports:**
      - Replace generated content for the export with fresh extraction
      - Preserve [MANUAL] blocks adjacent to the export
      - Check for position conflicts (new content shifts [MANUAL] block)
      - If position conflict: flag as POSITION conflict
      
      **Priority 5 — Process NEW exports:**
      - Append new export content to appropriate section
      - Place before any [MANUAL] blocks at section boundary
      - No conflicts expected (new content, no existing [MANUAL])
      
      **Priority 6 — Process script/asset file changes (from Category D in change manifest):**
      
      Category D operates on every `file_entries[]` row regardless of `file_type`. Handle each entry by its type:
      
      - **`file_type: "script"` or `file_type: "asset"`:**
        - MODIFIED_FILE: queue file for re-copy from source, update `file_entries` content_hash
        - DELETED_FILE: queue file for removal from `scripts/` or `assets/`, remove from `file_entries`
        - NEW_FILE: queue file for copy from source, add to `file_entries`
        - Files in `scripts/[MANUAL]/` or `assets/[MANUAL]/` are never modified (user-authored)
        - Update Section 7b manifest table to reflect changes
        - Update `metadata.json` `scripts[]`/`assets[]` arrays and `stats.scripts_count`/`stats.assets_count`
      
      - **`file_type: "doc"`** (authoritative docs promoted by §2a/§1b):
        - MODIFIED_FILE: update `file_entries` content_hash only. **Do NOT copy the file** — doc-type entries are source-tracked but not bundled. Record the drift in the update report.
        - DELETED_FILE: remove from `file_entries`. **Do NOT remove any file from the skill package** (there was nothing copied). Record the removal in the update report — a deleted authoritative doc is a meaningful upstream signal.
        - NEW_FILE: this path is not used for doc type — new doc entries come from Priority 7 below, not from Category D. If Category D reports NEW_FILE with `file_type: "doc"`, log a warning and route to Priority 7.
      
      **Priority 7 — Process new authoritative docs (from `promoted_docs_new[]` populated by §1b):**
      
      For each entry in the in-context `promoted_docs_new[]` list:
      
      - Add a new row to `file_entries[]` in the merged provenance map with:
        - `file_name`: `"docs/authoritative/{source_path}"` (synthetic namespace — see skill-sections.md for convention)
        - `file_type`: `"doc"`
        - `source_file`: the path from `promoted_docs_new[].path`
        - `content_hash`: the hash pre-computed by §1b
        - `confidence`: `"T1-low"`
        - `extraction_method`: `"promoted-authoritative"`
      - Do NOT copy the file into the skill package (doc type is source-tracked, not bundled).
      - Record in the update report: `"Added authoritative doc: {path} (heuristic: {basename})"`.
      
      **If `promoted_docs_new[]` is empty:** skip Priority 7 silently. No report entry.
      
      **Priority 8 — Process STRUCTURAL_FIX entries (gap-driven, from detect-changes §0 rule R2):**
      
      For each `STRUCTURAL_FIX` entry forwarded by step 3 §0/1a:
      
      - Apply the surgical edit described in the entry's `remediation` text to the **generated output file only** (e.g., escape an unescaped `|` inside a code span, balance a fence, repair a broken intra-skill anchor in SKILL.md or a `references/*.md`).
      - Do **not** add, modify, or remove any provenance `entries[]` row — STRUCTURAL_FIX never touches the provenance map.
      - Preserve any [MANUAL] blocks; if the fix location overlaps a [MANUAL] block, flag as a POSITION conflict instead of editing.
      - Record in the update report: `"Structural fix: {remediation summary} at {file}:{line}"`.
      
      **If no STRUCTURAL_FIX entries:** skip Priority 8 silently.
      
      **Priority 8b — Process metadata-update entries (gap-driven, from detect-changes §0 rule R4):**
      
      For each `metadata update` entry forwarded by step 3 §0/1a:
      
      - Queue the surgical metadata patch described in the entry's `remediation` (e.g., reconcile a divergent `stats` count, add an explanatory stat) in workflow context as `metadata_patches[]` for write.md §2 to apply **before** its automatic stat recount.
      - Touch no provenance `entries[]` row and no generated markdown — this priority only stages the patch; write.md §2 applies it.
      - Record in the update report: `"Metadata patch queued: {remediation summary}"`.
      
      **If no metadata-update entries:** skip Priority 8b silently.
      
      ### 4. Check for Conflicts
      
      Scan all merge operations for flagged conflicts:
      
      **If ZERO conflicts:**
      - Report clean merge
      - Auto-proceed to step 05
      
      **If conflicts detected:**
      
      Present each conflict to user:
      
      "**[MANUAL] Conflict Resolution Required:**
      
      **Conflict {N} of {total}:** {conflict_type}
      
      {Detailed description of the conflict with before/after context}
      
      **Options:**
      - **[K]eep** — Preserve [MANUAL] content as-is, adjust generated content around it
      - **[R]emove** — Remove the [MANUAL] block (content will be lost)
      - **[E]dit** — Show me both versions, I'll provide the resolution
      
      Select: [K] Keep / [R] Remove / [E] Edit"
      
      Process each conflict with user's decision.
      
      ### 6. Compile Merge Results
      
      Build merge result summary:
      
      ```
      Merge Results:
        exports_updated: [count]
        exports_added: [count]
        exports_removed: [count]
        exports_moved: [count]
        exports_renamed: [count]
      
        manual_sections_preserved: [count]
        manual_conflicts_resolved: [count]
        manual_orphans_kept: [count]
        manual_orphans_removed: [count]
      ```
      
      ### 6b. Write Merged Files to Disk
      
      Write the merged content produced by sections 3–4 directly to disk now. Later steps read from these files for validation and verification. The write must happen exactly once, here.
      
      **Write SKILL.md:**
      - Use the `Edit` or `Write` tool to write merged SKILL.md content to `{skill_package}/SKILL.md`
      - Preserve UTF-8 encoding
      - If the source version detected during step 3 differs from the previous metadata version, create the new `{skill_package}` directory (`{skill_group}/{new_version}/`) first and write there — the previous version's directory is preserved on disk. Update `{skill_package}` in context to point at the new path.
      
      **Do NOT write here:**
      - `metadata.json`, `provenance-map.json`, `evidence-report.md` — derived from merge + validation output, written by step 6 sections 2–4
      - `context-snippet.md` — regenerated from the on-disk SKILL.md + metadata.json by step 6 section 5
      
      **Halt-on-tool-failure:** If any `Edit`/`Write` call errors (permission denied, disk full, path invalid, etc.), halt with status `halted-for-write-failure` and report the failure — do not proceed to step 5 validation. The skill package may be in a partial state and will need manual recovery before re-running update-skill. In `{headless_mode}`, emit the halt envelope per SKILL.md §Headless (`error: {phase: "merge:write-skill-md", path: "{skill_package}/SKILL.md", reason: "..."}`).
      
      ### 7. Display Merge Summary
      
      "**Merge Complete:**
      
      | Metric | Count |
      |--------|-------|
      | Exports updated | {count} |
      | Exports added | {count} |
      | Exports removed | {count} |
      | [MANUAL] sections preserved | {count} |
      | Conflicts resolved | {count} |"
      
      ### 8. Gate to Validation
      
      **Clean merge (no conflicts):** display "**Clean merge — proceeding to validation...**", then load, read the full file, and execute {nextStepFile} (auto-proceed).
      
      **Conflicts were resolved (user interaction occurred):** present "**Merge complete with conflict resolution. Select:** [C] Continue to Validation" and wait for the user to confirm before loading {nextStepFile}.
      
      **Headless (`{headless_mode}` true):**
      
      - Clean merge → auto-continue and append to in-context `headless_decisions[]` (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "merge.clean-merge-gate", default_action: "C", taken_action: "C", reason: "headless: clean merge, no conflicts to resolve"}`.
      - Conflicts present → halt even in headless mode (conflicts require human judgment): status `halted-for-manual-mismatch`, emitting the halt envelope per SKILL.md §Headless (`error: {phase: "merge:conflict-resolution", reason: "..."}`); no `headless_decisions[]` entry is added.
      
      
    • re-extract.md 29.4 KB
      ---
      nextStepFile: 'merge.md'
      extractionPatternsData: 'skf-create-skill/references/extraction-patterns.md'
      extractionPatternsTracingData: 'skf-create-skill/references/extraction-patterns-tracing.md'
      remoteSourceResolutionData: 'references/remote-source-resolution.md'
      tierDegradationRulesData: 'skf-create-skill/references/tier-degradation-rules.md'
      # Resolve `{checkWorkspaceDriftHelper}` to the first existing path; HALT if
      # neither candidate exists. §0.a relies on the helper for the deterministic
      # workspace-pinning guard (git rev-parse + short-SHA prefix match + halt
      # message rendering). Falling back to prose-driven git invocation would
      # lose the four-state dispatch (ok / skipped / mismatch / overridden).
      checkWorkspaceDriftProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-check-workspace-drift.py'
        - '{project-root}/src/shared/scripts/skf-check-workspace-drift.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 3: Re-Extract Changed Exports
      
      ## STEP GOAL:
      
      Perform tier-aware extraction on only the changed files identified in step 02, producing fresh export data with confidence tier labels (T1/T1-low/T2) that will be merged into the existing skill in step 04.
      
      ## Rules
      
      - Focus only on extracting changed exports — do not merge or modify existing skill
      - Only extract files in the change manifest — do not touch unchanged files. **Exception (gap-driven mode):** §0a's Targeted Re-Extraction Branch also scans files listed in each manifest entry's `remediation_paths[]` to resolve citation-less Critical/High gaps.
      - For each changed file, launch a subprocess for deep AST analysis (Pattern 2); if unavailable, extract sequentially
      
      ## Steps
      
      ### 0. Check for Gap-Driven Mode
      
      **If `update_mode == "gap-driven"` (set in step 1 via `--from-test-report`, confirmed in step 2 section 0):**
      
      Source code has not drifted — the gap-derived manifest from step 2 contains export-level findings translated from the test report, not file-level changes. Perform citation spot-checks instead of full re-extraction to verify each gap-affected export is still at its recorded location.
      
      **0.a Pre-flight: verify workspace HEAD matches pinned commit.** Gap-driven spot-checks read source at recorded `source_line` positions and must see the exact bytes the skill was pinned against. A drifted workspace silently verifies against the wrong tree — moved/renamed symbols appear "verified" because the recorded line now points at different code. Before reading any source, run the guard via `{checkWorkspaceDriftHelper}`:
      
      ```bash
      uv run {checkWorkspaceDriftHelper} <source-root> \
          --pinned-commit "<metadata.source_commit>" \
          [--source-ref "<metadata.source_ref>"] \
          [--allow-drift]
      ```
      
      Pass `--allow-drift` only when the user provided `--allow-workspace-drift` to update-skill. The helper accepts `""` and `"local"` as the "no pinned commit" sentinels; it also auto-skips when `source_root` is not a git working tree (bare checkout, tarball extract, etc.).
      
      The helper emits a result envelope:
      
      ```json
      {
        "status": "ok" | "skipped" | "mismatch" | "overridden",
        "skip_reason": "no-pinned-commit" | "not-a-git-tree" | null,
        "head_sha": "...", "head_short_sha": "...", "match_kind": "full" | "short-prefix" | null,
        "log_message": "workspace_drift_check: ...",
        "halt_message": "<multi-line user-facing message>" | null
      }
      ```
      
      **Dispatch on `status`:**
      
      - **`ok` or `skipped`** (helper exit 0): log `log_message` and continue to bullet 1.
      - **`overridden`** (helper exit 0): log `log_message`, surface a visible warning in the final report ("**Workspace drift accepted via --allow-workspace-drift** — spot-checks read HEAD {head_short_sha}, not pinned {pinned_commit}"), and continue to bullet 1. The override does not automatically re-pin `metadata.source_commit`; re-pinning is explicit user work (run the normal-mode update-skill flow against the same HEAD, or re-create the skill).
      - **`mismatch`** (helper exit 2): HALT immediately with status `halted-for-workspace-drift`. Display the helper's `halt_message` verbatim — it already substitutes `{pinned_commit}`, `{source_ref or "unset"}`, `{source_root}`, `{head_sha}`, and the suggested `git checkout` command. Do not proceed to bullet 1. Step-04 merge has not run; no partial writes. In `{headless_mode}`, emit the halt envelope per SKILL.md §Headless (`error: {phase: "re-extract:workspace-drift", path: "{source_root}", reason: "..."}`).
      
      1. Use the provenance map already loaded in step 1 (at `{forge_version}/provenance-map.json`) — do not re-read
      2. **Partition by change category, then iterate the export-bearing entries.** Entries that do not name an export skip the per-export verification below — they have no symbol to resolve against source:
         - **`STRUCTURAL_FIX`** (detect-changes §0 rule R2): forward verbatim to the merge step (its `remediation` text describes a generated-markdown edit). No spot-check, no provenance lookup, no `entries[]` change.
         - **`metadata update`** (rule R4): forward the metadata-patch payload to the merge step. No spot-check, no provenance lookup.
      
         Carry both through workflow context to merge.md unchanged. Then, for each export-bearing entry (`NEW_EXPORT`, `MODIFIED_EXPORT`, `DELETED_EXPORT`):
         - **If the entry is `DELETED_EXPORT` (rescope, rule R1):** do not resolve against source — the export is being removed from the public surface. Record `verification: rescoped` and flag for merge Priority 1 (removal). Confirm the brief carries the matching `scope.amendments[]` (`action: "excluded"`) + `scope.exclude` entry that step 2 R1 wrote; if absent, HALT — a rescope without a brief scope amendment is denominator deflation and must not be written.
         - Look up the export by `name` in `provenance_map.exports` — read `source_file` and `source_line`
         - **If export not found in provenance map:**
           - **If the manifest entry has a `source_citation` (propagated from the test report by step 2 §0 bullet 4):** read the file at that citation's `file:line ± 5` lines and verify the symbol name still appears within that window. Record a full `verified` / `moved` / `missing` entry using the citation as the starting location — same spot-check logic as the "export found" branch below, keyed on the manifest-supplied citation instead of the provenance map. The export is still flagged `NEW_EXPORT` for the merge step; this branch only upgrades the provenance entry from `unknown` to a live spot-check result so step 6 writes `source_file` / `source_line` instead of `null`.
           - **If the entry is a provenance-completeness gap (rule R3 — documented in SKILL.md/`references/` but missing from the provenance-map) AND `source_root` is pinned and readable:** route this entry to §0a (Targeted Re-Extraction Branch) **regardless of severity**. These exports are documented-and-known; `unknown` is never the correct outcome for them. §0a resolves them against pinned source and records a full `re-extracted` provenance entry. This route takes precedence over the severity-gated branches below.
           - **If the manifest entry has no `source_citation` but has a non-empty `remediation_paths[]` AND `severity` is `Critical` or `High`:** route this entry to §0a (Targeted Re-Extraction Branch). §0a scans the remediation paths with the tier-appropriate extractor and, on success, records a full verification record with `verification: re-extracted`, a live `provenance_citation`, and full signature/params/return-type fields for the merge step to consume as a NEW_EXPORT. On failure, §0a halts the workflow — Critical/High gaps are not allowed to degrade to `unknown`. See §0a for the procedure, the consolidated halt protocol, and the output record shape.
           - **If the manifest entry has no `source_citation` AND (`remediation_paths[]` is empty OR `severity` is `Medium`, `Low`, or `Info`) AND it is not a provenance-completeness gap:** record as new (`provenance_citation: unknown`) — no spot-check possible; flag for merge step to handle as `NEW_EXPORT`. Step-06 §3 only accepts null `source_file` / `source_line` for these lower-severity unknowns; a Critical/High unknown reaching step 6 indicates §0a was skipped or bypassed and is a workflow bug.
         - **If export found:** read the source file at `source_line ± 5` lines and verify the symbol name still appears within that window
         - Record verification outcome: `verified` (symbol at recorded line), `moved` (symbol found elsewhere in same file — record new line), `missing` (symbol not found in file), `re-extracted` (resolved via §0a from `remediation_paths[]` or rule R3), `rescoped` (DELETED_EXPORT — flagged for removal), or `unknown` (no usable provenance data)
         - **Public-reachability gate (`NEW_EXPORT` only):** before an entry is flagged `NEW_EXPORT` for the merge step, confirm the symbol is reachable as **public API** — not merely that a `pub` / `export` / top-level definition exists at the citation. The spot-check already read `source_file` at `source_line ± 5`; extend that read to resolve the symbol's module path and confirm at least one of: (a) it is re-exported from the package entry-point barrel (`lib.rs` `pub use`, `index.ts` / `index.js` export, `__init__.py` import or `__all__`), or (b) every ancestor module on the path from that barrel is public (Rust `pub mod`, an exported TS namespace, a non-underscore Python package). A `pub(crate)` / `pub(super)` item, or one under a private module (e.g. `mod authority;` declared without `pub`), is **not** reachable. **On failure:** do not document it as public — drop it from the export-bearing set so it is never written to the documented `exports[]` array or SKILL.md, and re-queue it as a `metadata update` (rule R4) recording `reclassified: internal-unreachable` for the evidence report. Do **not** hand-set any `stats` count: write.md §2's automatic recount derives `exports_internal` / `exports_total` from the merged surface and owns those fields. This honors the workflow rule that documented API must be importable by users — a `pub`-but-internal symbol would otherwise inflate the documented public surface with a type users cannot import.
      3. Build a minimal extraction results block matching section 4's shape, with `mode: gap-driven` and per-export verification records:
      
         ```
         Extraction Results:
           mode: gap-driven
           files_extracted: {count}  # non-zero only when §0a scanned remediation_paths[]
           exports_extracted: {gap_count}
           confidence_breakdown:
             T1: {verified_count + moved_count + re_extracted_t1_count}
             T1-low: {re_extracted_t1_low_count}
             T2: 0
      
           Per-export verification:
             {export_name}:
               provenance_citation: {source_file}:{source_line}
               verification: verified|moved|missing|unknown|re-extracted|rescoped
               new_location: {source_file}:{new_line}  # set when moved OR re-extracted
               resolution_source: remediation-paths    # set only when verification == re-extracted
               gap_category: NEW_EXPORT|MODIFIED_EXPORT|DELETED_EXPORT|metadata_update
      
           Per-file extractions:   # populated only when §0a produced re-extracted records
             {file_path}:
               exports:
                 - name: {export_name}
                   type: function|class|type|constant
                   signature: {full signature}
                   location: {file}:{start_line}-{end_line}
                   confidence: T1|T1-low
                   params: [{name, type}]
                   return_type: {type}
                   docstring: {summary}
         ```
      
      4. Set `no_reextraction: true` in workflow context — step 6 will use this flag to skip stale `source_file`/`source_line`/`confidence` field updates for `verified` exports. `moved` exports get updated citations; `re-extracted` exports get full fresh provenance from §0a's extraction records (see step 6 §3). The flag is a global gap-driven marker, not a per-entry one — step 6 dispatches on each verification outcome independently.
      5. **Skip all remaining sections of step 3** — sections 1–5 are source-drift extraction paths that do not apply. Display the summary below and load `{nextStepFile}` to proceed directly to the merge step.
      
      "**Gap-driven re-extraction.** Verified {verified_count}/{gap_count} citations against live source. Moved: {moved_count}. Missing: {missing_count}. Re-extracted (via remediation paths or provenance-completeness, §0a): {re_extracted_count}. Rescoped (removed from surface): {rescoped_count}. Unknown (not in provenance map): {unknown_count}. Proceeding to merge."
      
      **If normal mode (`update_mode` unset or not `gap-driven`):** Continue with docs-only check and source extraction below.
      
      ### 0a. Targeted Re-Extraction Branch (Helper — Called from §0 bullet 2)
      
      **Do not execute this section sequentially.** It is a helper procedure invoked by §0 bullet 2 when specific conditions are met (see below). Normal-mode runs, and gap-driven runs where every entry has a `source_citation` or qualifies as `Medium`/`Low`/`Info` unknown, skip this section entirely. §0's "skip sections 1–5" instruction does not apply here — §0a is addressed by name from §0, not by sequential fall-through.
      
      **Used by:** §0 bullet 2, in either of two cases:
      
      - a manifest entry has no `source_citation`, has non-empty `remediation_paths[]`, and `severity` is `Critical` or `High`; **or**
      - a provenance-completeness gap (rule R3 — documented but missing from the provenance-map), routed here **regardless of severity** as long as `source_root` is pinned and readable. For this case, treat the export's documented `source` reference (or `remediation_paths[]` when present) as the path set to scan.
      
      **Purpose:** produce AST-backed provenance for citation-less Critical/High gaps so step 6 §3 never writes `source_file: null` for blocking findings. Honors the workflow-level rule **Never hallucinate — every statement must have AST provenance** against the most common gap-driven trigger — a failing test report whose Gap Report `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) rather than a `file:line` pair. Gap-driven mode skips §1 through §5, so §0a is also the only place §1b's source-access and extraction machinery is invoked during gap-driven runs.
      
      **Procedure:**
      
      1. **Resolve source access** — invoke §1b's MCP-fallback chain (gh API → zread → deepwiki → workspace / ephemeral clone) once per workflow run to ensure files under `{source_root}` are readable. Cache the chosen access path; do not re-resolve per entry.
      2. **Expand `remediation_paths[]`** — for each path across all qualifying entries:
         - Literal source file (ends in a recognized source extension): use as-is.
         - Directory or glob: expand under `{source_root}` using the provenance map's file patterns.
         - **Security boundary:** reject and skip any path that resolves outside `{source_root}`. Remediation text is user-editable and must never be allowed to escape the source tree.
         - Deduplicate the resolved file set across all entries routed to §0a — each physical file is scanned at most once.
      3. **Extract** — run the tier-appropriate extractor from §1b (Quick pattern-match → T1-low; Forge/Forge+/Deep AST via ast-grep → T1) over the resolved file set. Launch subprocesses in parallel (Pattern 4) when available; sequential fallback otherwise. Follow the AST Extraction Protocol in `{extractionPatternsData}` for Forge/Deep tiers, and the tier-degradation rules in `{tierDegradationRulesData}` when AST tools fail on individual files.
      4. **Match by name** — for each manifest entry routed here, search the aggregated extraction results for an export whose `name` matches the manifest entry's `name`. Record the first hit as:
         - `verification: re-extracted`
         - `provenance_citation: {file}:{start_line}` from the AST result
         - `new_location: {file}:{start_line}` (same value — satisfies the existing consumer contract)
         - `resolution_source: remediation-paths`
         - `confidence: T1` (AST-extracted) or `T1-low` (pattern-matched fallback)
         - the full extraction signature (type, params, return_type, docstring) — mirror the shape of §4's per-file extraction record so step 4 Priority 5 can merge it with the same code path used in normal mode.
      
         Then apply the §0 bullet 2 **public-reachability gate** to each matched symbol before recording it as a NEW_EXPORT: §0a has full source access, so resolve the symbol's module path and confirm barrel re-export or a fully-`pub` module chain. If it is unreachable (`pub(crate)` / private module), do **not** record `re-extracted` — drop it from the export-bearing set and re-queue the entry as a `metadata update` (rule R4, `reclassified: internal-unreachable`), exactly as the gate specifies. A re-extracted symbol that is not public API is not a documentable NEW_EXPORT. This is a resolution, not an `unresolved[]` failure (step 5) — the symbol was found, just not public — so it does not trigger the Critical/High HALT.
      5. **Track failures across all qualifying entries.** Collect every entry whose symbol was not found in any scanned remediation path into an `unresolved[]` list. After processing every qualifying entry, if `unresolved[]` is non-empty: HALT with a consolidated report listing every unresolved entry (`name`, `severity`, `remediation_paths`, `files_scanned`, `exports_found_in_scan`). Template:
      
         ```
         Targeted re-extraction failed for {N} Critical/High gap(s).
      
         Critical and High gaps must resolve to AST provenance. The Remediation text for
         the entries below does not name a file that contains the expected export, so the
         workflow cannot produce a non-null `source_file` / `source_line` without
         hallucinating.
      
         Unresolved entries:
           {for each entry in unresolved[]:}
             - {name} ({severity})
               remediation_paths: {paths}
               files_scanned:     {count}
               exports_matched:   0
      
         Fix one of the following, then re-run update-skill:
           a) Add a `file:line` citation to the Gap Report `Source:` field.
           b) Edit the Remediation text to name the file(s) that actually contain the export(s).
           c) Downgrade the gap(s) to Medium/Low/Info (accepts the degraded documentation outcome).
         ```
      
         Exit with status `halted-for-remediation-path`. Step-04 merge has not run; no partial writes. In `{headless_mode}`, emit the halt envelope per SKILL.md §Headless (`error: {phase: "re-extract:targeted-reextraction", reason: "..."}`).
      
      6. **Success summary** — record `targeted_reextraction: {resolved_count, files_scanned, exports_matched, tier}` in workflow context. The evidence report (step 6 §4) surfaces this alongside the verified / moved / missing tally.
      
      **Why halt instead of degrading to `unknown`:** a Critical or High gap by definition blocks skill usefulness — it is either missing documentation for a public API or a wrong signature. Silently writing `source_file: null` for a blocking gap produces a skill that passes re-test but still hides the broken behavior behind a placeholder. The halt forces the test report to carry usable remediation information — a one-time fix-up that is far cheaper than a downstream audit trying to track why the "repaired" skill still fails.
      
      ### 1. Check for Docs-Only Mode
      
      **If `source_type: "docs-only"` in the original brief or metadata:**
      
      "**Docs-only skill detected.** This skill was generated from external documentation, not source code. Re-extraction will re-fetch the original `doc_urls` to check for updated content."
      
      - Re-fetch each URL from `doc_urls` (from the brief or metadata) using whatever web fetching capability is available
      - Extract updated API information with T3 `[EXT:{url}]` citations
      - Build the updated extraction inventory from fetched content
      - Skip all source code extraction below — proceed directly to the merge step (section 5 or equivalent)
      
      **If `source_type: "source"` (default):** Continue with source extraction below.
      
      ### 1b. Determine Extraction Strategy by Tier
      
      **Remote Source Resolution (Forge/Deep only):**
      
      **MCP source access (ordered fallback):** When `source_repo` is set in metadata.json, try each MCP tool in order to fetch only the changed files from the change manifest. This avoids clone overhead entirely. Tools are ordered by data freshness — gh API returns live GitHub content and is preferred for update-skill where current file versions are required. zread and deepwiki depend on manual indexing and may return stale data if indexes haven't been refreshed since the changes being extracted.
      
      1. **gh API** — `gh api repos/{owner}/{repo}/contents/{path}` for raw file content
         - If accessible: fetch file content (base64-decoded), always current
         - If rate-limited, 404, or inaccessible: log tool and reason, continue to next tool
      2. **zread** — `get_repo_structure` + `read_file` for targeted file access
         - If repo found: fetch changed files, proceed with extraction
         - If "repo not found" or error: log tool and reason, continue to next tool
         - Caveat: indexed data — may be stale if index wasn't refreshed after the target changes
      3. **deepwiki** — `ask_question` for targeted export/signature queries
         - If repo indexed and returns usable source data: extract from response
         - If no results or repo not indexed: log tool and reason, continue to next tool
         - Caveat: returns synthesized content, not raw source — extraction quality varies; index may be stale
      
      **Confidence labeling:** MCP-fetched content written to a temp file and analyzed with ast-grep → T1. MCP-fetched content analyzed with pattern matching (AST unavailable) → T1-low.
      
      **If all MCP tools fail for this repo:** Fall back to workspace or ephemeral clone — load and follow `{remoteSourceResolutionData}` for clone setup, version reconciliation, and AST tool unavailability handling.
      
      **If all approaches fail (MCP + workspace/ephemeral clone):** Degrade to provenance-map-only analysis (State 2, T1 confidence from compilation-time data). Warn user: "Source access failed for {source_repo}. Analysis limited to provenance-map baseline."
      
      **Quick tier (text pattern matching):**
      - Extract function/class/type names via regex patterns
      - Extract export statements via text matching
      - Confidence: T1-low (pattern-matched, not AST-verified)
      
      **Forge tier (AST structural extraction):**
      
      Load and follow the **AST Extraction Protocol** from `{extractionPatternsData}`. Use the decision tree based on the number of changed files: prefer MCP `find_code()` for small sets, `find_code_by_rule()` with scoped YAML rules for medium sets, and CLI `--json=stream` with line-by-line streaming for large sets. Never use `ast-grep --json` (without `=stream`) — it loads the entire result set into memory and will fail on large codebases.
      
      - Extract: function signatures, type definitions, class members, exported constants
      - Extract: parameter types, return types, JSDoc/docstring comments
      - Confidence: T1 (AST-verified structural truth)
      
      **Tier degradation handling (Forge/Forge+/Deep):** If ast-grep is unavailable or fails on individual files, follow `{tierDegradationRulesData}` for fallback strategy and user notification requirements. Silent degradation is forbidden — the user must always know when AST extraction was skipped.
      
      **Deep tier (AST + QMD semantic enrichment):**
      - Perform all Forge tier extractions (T1)
      - Additionally: launch a subprocess that queries qmd_bridge for temporal context on changed exports, returning T2 evidence per export
      - QMD provides: usage patterns, historical context, related documentation
      - Confidence: T1 for structural, T2 for semantic enrichment
      
      **Tool resolution:** `ast_bridge` → ast-grep MCP tools (`find_code`, `find_code_by_rule`) or `ast-grep` CLI. `qmd_bridge` → QMD MCP tools (`mcp__plugin_qmd-plugin_qmd__search`, `vector_search`) or `qmd` CLI. See `knowledge/tool-resolution.md`.
      
      ### 2. Extract Changed Files
      
      **Skip authoritative doc paths.** Before iterating the change manifest, build a skip set from `promoted_docs_new[]` (populated by step 2 §1b) and any existing `file_entries[]` entries with `file_type: "doc"` from the provenance map. These are documentation files tracked for drift detection only — they must not reach AST extraction, which would produce ghost entries on non-code content. If a change manifest entry matches the skip set, skip it silently and continue; doc-type drift is handled by step 2 Category D and step 4 Priority 6/7.
      
      For each remaining file in the change manifest with status MODIFIED, ADDED, or RENAMED, launch a subprocess that:
      
      1. Loads the source file
      2. Performs tier-appropriate extraction (Quick/Forge/Forge+/Deep)
      3. Extract each export into the per-file return contract shown in bullet 4.
      4. **Return contract.** Each extraction worker returns ONLY this per-file block — no prose, no commentary, no markdown fences (the parent strips wrapping fences before parsing). The shape is exactly the per-file record §4 aggregates (the `Per-file extractions` block, lines below), so the parent appends it verbatim rather than re-parsing free text:
      
         ```json
         {
           "file_path": "...",
           "exports": [
             {"name": "...", "type": "function|class|type|constant",
              "signature": "...", "location": "{file}:{start_line}-{end_line}",
              "confidence": "T1|T1-low|T2",
              "parameters": [{"name": "...", "type": "..."}],
              "return_type": "...", "docstring": "...",
              "qmd_evidence": "<if Deep tier, else omit>"}
           ]
         }
         ```
      
      **For DELETED files:** No extraction needed — deletions handled in merge step.
      
      **For MOVED files:** Re-extract at new location to update file:line references.
      
      **Re-export tracing (Forge/Deep only):** After extracting changed files, check if any public exports from the package entry point (`__init__.py`, `index.ts`, `lib.rs`) are unresolved — particularly when a changed file is part of a module re-export chain. Follow the **Re-Export Tracing** protocol in `{extractionPatternsTracingData}` to trace unresolved symbols to their actual definition files.
      
      ### 2b. CCC Semantic Ranking (Forge+ and Deep with ccc)
      
      **IF `tools.ccc` is true in forge-tier.yaml:**
      
      Before aggregating extraction results, use CCC to assess semantic significance of changes:
      
      1. Run `ccc_bridge.search("{skill_name}", source_root, top_k=15)` — **Tool resolution:** `/ccc` skill search (Claude Code), ccc MCP (Cursor), `ccc search` (CLI) — to get the skill's most semantically central files
      2. Cross-reference the change manifest files with CCC results
      3. Files appearing in BOTH the change manifest AND CCC's top results are **semantically significant changes** — flag them for priority in the merge step
      4. Store `{ccc_significant_changes: [{file, score}]}` in context
      
      This helps the merge step (section 4) prioritize which changes are most likely to affect the skill's core content vs. peripheral modifications.
      
      CCC failures: skip ranking silently, all changes treated equally.
      
      **Note on remote sources:** If `source_root` is a workspace clone, the CCC index may already exist from a prior forge and can be reused via `ccc search --refresh`. If the source is an ephemeral fallback clone, the clone path is not indexed by CCC — the search returns empty results, so semantic ranking is skipped and all changes are treated equally.
      
      **IF `tools.ccc` is false:** Skip this section silently.
      
      ### 3. Deep Tier QMD Enrichment (Conditional)
      
      **ONLY if forge_tier == Deep:**
      
      Read the `qmd_collections` registry from `{sidecar_path}/forge-tier.yaml`.
      
      Find the collection entry matching the current skill: look for an entry where `skill_name` matches the skill being updated AND `type` is `"extraction"`.
      
      **If a matching extraction collection is found:**
      Launch a subprocess that loads qmd_bridge and for each changed export:
      1. Queries the `{skill_name}-extraction` collection for semantic context related to the export
      2. Searches for usage patterns, documentation references, temporal history
      3. Returns T2 evidence per export (usage frequency, context snippets, related concepts)
      
      **If no matching collection found in registry:**
      Log: "No QMD extraction collection found for {skill_name}. T2 enrichment skipped. Re-run [CS] Create Skill to generate the collection."
      Continue without T2 enrichment — extraction still produces T1 results.
      
      **If forge_tier != Deep:** Skip this section with notice: "QMD enrichment skipped (tier: {forge_tier})"
      
      ### 4. Compile Extraction Results
      
      Aggregate all subprocess results into structured extraction data:
      
      ```
      Extraction Results:
        files_extracted: [count]
        exports_extracted: [count]
        confidence_breakdown:
          T1: [count]
          T1-low: [count]
          T2: [count]
      
        Per-file extractions:
          {file_path}:
            exports:
              - name: {export_name}
                type: function|class|type|constant
                signature: {full signature}
                location: {file}:{start_line}-{end_line}
                confidence: T1|T1-low|T2
                parameters: [{name, type}]
                return_type: {type}
                docstring: {summary}
                qmd_evidence: {if Deep tier}
      ```
      
      ### 5. Display Extraction Summary and Auto-Proceed
      
      "**Re-Extraction Complete:**
      
      | Metric | Count |
      |--------|-------|
      | Files extracted | {count} |
      | Exports extracted | {count} |
      | T1 (AST-verified) | {count} |
      | T1-low (pattern-matched) | {count} |
      | T2 (QMD-enriched) | {count} |
      
      **Proceeding to merge with existing skill...**"
      
      ### 6. Route to Next Step
      
      This step auto-proceeds — no user choices. Once all changed files are extracted and results compiled, load and fully read the next file, then execute it, per the branch that applies:
      
      - **`dry_run_mode == true`** → display "**Dry-run mode — skipping merge/validate/write.** Loading report..." and load `report.md` (NOT `{nextStepFile}`); it emits status `dry-run` describing what merge+write would have done. No artifact is modified on disk by this run.
      - **Otherwise** → display "**Proceeding to merge...**" and load `{nextStepFile}` (merge.md) to begin the merge operation.
      
      
    • remote-source-resolution.md 5.9 KB
      ---
      type: static-reference
      ---
      
      # Remote Source Resolution (Forge/Deep Tier)
      
      If `source_root` is a local path: proceed with the tier-appropriate strategy as normal.
      
      If `source_root` (from metadata.json) is a remote URL (GitHub URL or owner/repo format) AND tier is 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. **Resolve source ref:** Read `source_ref` from the existing `metadata.json`. If the user provided a new `target_version`, resolve its tag first (using the Tag Resolution algorithm in `create-skill/references/source-resolution-protocols.md`).
      
         **Note — implicit tag resolution is NOT re-run on update:** `create-skill`'s Implicit Tag Resolution path (which treats `brief.version` as an implicit `target_version` for remote sources) runs only at create time. On update, `skf-update-skill` faithfully re-uses the `source_ref` that was stored in `metadata.json` by the original create — even if that ref is `HEAD` and the brief now has a `version` that would resolve to a tag. This preserves the invariant that an update reflects source drift on the same ref the skill was originally built against, not a re-pinning to a different commit. If the intent is to re-pin an older HEAD-based skill to a tag derived from `brief.version`, re-run `skf-create-skill` (which applies implicit resolution) rather than `skf-update-skill`. Explicit re-pinning via a new `target_version` on update remains supported and takes priority over the stored `source_ref`.
      
      3. **Workspace check:** Compute the workspace path using the same algorithm as `create-skill/references/source-resolution-protocols.md` (parse URL → `{workspace_root}/repos/{host}/{owner}/{repo}/`).
      
         **If workspace repo exists (`{workspace_repo_path}/.git/` present):**
      
         Fetch and checkout the requested ref. For update-skill, `changed_files_from_manifest` scoping happens at extraction time via file-level filtering — the workspace has a full checkout.
      
         ```
         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
         ```
      
         Set `remote_clone_path = {workspace_repo_path}`, `remote_clone_type = "workspace"`.
      
         **If workspace repo does NOT exist:**
      
         Clone into workspace. Create the parent directory first:
      
         ```
         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`:
      
         ```
         # If source_ref is a real branch or tag (not HEAD/null/"local"):
         git clone --depth 1 --branch {source_ref} --single-branch "{source_repo}" "{workspace_repo_path}"
      
         # If source_ref is HEAD or not set (default branch):
         git clone --depth 1 --single-branch "{source_repo}" "{workspace_repo_path}"
         ```
      
         Set `remote_clone_path = {workspace_repo_path}`, `remote_clone_type = "workspace"`.
      
         **On any workspace failure:** Fall back to ephemeral clone:
         ```
         temp_path = {system_temp}/skf-ephemeral-{skill-name}-{timestamp}/
      
         # If source_ref is a real branch or tag (not HEAD/null/"local"):
         git clone --depth 1 --branch {source_ref} --single-branch --filter=blob:none "{source_root}" "{temp_path}"
      
         # If source_ref is HEAD or not set (default branch):
         git clone --depth 1 --single-branch --filter=blob:none "{source_root}" "{temp_path}"
         ```
         Set `remote_clone_path = {temp_path}`, `remote_clone_type = "ephemeral"`.
      
      4. **If clone/fetch succeeds:** Set `source_root = {remote_clone_path}` — this updates the working source path for all subsequent operations. Apply `changed_files_from_manifest` as file-level filters at extraction time. Proceed with the **Forge tier** extraction strategy below.
      
      5. **If all cloning fails (workspace AND ephemeral):**
      
         Warning message: "Clone of `{source_root}` failed: {error}. Degrading to source reading (T1-low) for this run. For T1 (AST-verified) confidence, clone the repository locally and re-run [CS] Create Skill with the local path, then re-run this update."
      
         Override the extraction strategy to Quick tier for this run. 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 extraction summary:
      
      - **If `remote_clone_type == "ephemeral"`:** Reset the working directory first (`cd "{project-root}"` using the absolute path captured at workflow start), then delete the `{temp_path}` directory (`rm -rf "{temp_path}"`). Log: "Ephemeral source clone cleaned up." This ensures cleanup runs even if some extractions failed.
      - **If `remote_clone_type == "workspace"`:** No cleanup. The workspace checkout persists for future forges and updates.
      
      ## Version Reconciliation
      
      After the source path is accessible, check whether the source version has changed since the original skill was created. Look for the version file matching the detected language (e.g., `pyproject.toml`, `package.json`, `Cargo.toml`). If the source version differs from the current `metadata.json` version, record `source_version_detected` in context for step 6 to use when updating `metadata.json`. No warning needed here — step 6 handles the version update.
      
      ## AST Tool Unavailability (Local Source)
      
      If AST tool is unavailable at Forge/Deep tier with local source:
      
      Warning message: "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.
      
    • report.md 10.6 KB
      ---
      nextStepFile: 'health-check.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 7: Report
      
      ## STEP GOAL:
      
      Present a comprehensive change summary showing what was updated, [MANUAL] sections preserved, confidence tier breakdown, and recommend next workflow actions in the SKF chain.
      
      ## Rules
      
      - Focus only on reporting — all operations are complete; do not modify any files
      - Present clear, actionable summary with next step recommendations
      - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing summary is NOT the terminal step
      
      ## Steps
      
      ### 1. Handle No-Change Shortcut
      
      **If routed here from step 02 with no changes detected:**
      
      "**Update Skill Report: {skill_name}**
      
      **Status:** No changes detected
      
      Source code matches provenance map exactly. The skill `{skill_name}` is current — no update was needed.
      
      **Provenance age:** {days} days since last extraction
      **Forge tier:** {tier}
      
      **Recommendation:** No action required. Run audit-skill periodically to monitor for drift."
      
      → Load, read the full file, and execute `{nextStepFile}` — the health-check step is the true terminal step of this workflow.
      
      ### 1a. Handle Detect-Only Mode
      
      **If `detect_only_mode` is true (routed here from detect-changes.md §6):**
      
      "**Update Skill Report: {skill_name} — Detect-Only Mode**
      
      **Status:** Detect-only (no writes)
      
      The change manifest below describes what would be updated. No artifact was modified — re-run without `--detect-only` to apply.
      
      {render the change manifest summary table from detect-changes.md §5, plus the per-file detail section}
      
      **Recommendation:** Review the manifest; if it matches expectations, re-run `skf-update-skill` without `--detect-only` to perform the actual update."
      
      The headless envelope (`SKF_UPDATE_RESULT_JSON`) carries `status: "detect-only"`, `files_written: []`, and any `headless_decisions[]` recorded by detect-changes' §1b / §1c / §2.2 gates. `version` and `previous_version` are both equal to the on-disk version (detect-only does not bump). `update_mode` reflects the run's mode (`normal` or `gap-driven` or `degraded`) so consumers know which detection path produced the manifest.
      
      → Load, read the full file, and execute `{nextStepFile}` (health-check) — even detect-only runs through the terminal health-check step.
      
      ### 1b. Handle Dry-Run Mode
      
      **If `dry_run_mode` is true (routed here from re-extract.md §6):**
      
      "**Update Skill Report: {skill_name} — Dry-Run Mode**
      
      **Status:** Dry-run (no writes)
      
      The change manifest below shows what was detected; re-extraction ran to compute the planned merge but neither merge nor write executed. No artifact was modified.
      
      {render the change manifest summary AND the re-extraction summary — what merge+validate+write WOULD have done}
      
      **Planned writes (skipped):**
      - SKILL.md re-merge with re-extracted exports
      - metadata.json version bump (or hold for gap-driven)
      - provenance-map.json update with re-extraction results
      - evidence-report.md
      - context-snippet.md (only if a staleness trigger fired)
      - active-symlink flip (only if version changed)
      
      **Recommendation:** Review the manifest and re-extraction summary; if both match expectations, re-run `skf-update-skill` without `--dry-run` to perform the actual update."
      
      The headless envelope carries `status: "dry-run"`, `files_written: []`, the `headless_decisions[]` recorded so far (everything before merge), and `update_mode` from the run.
      
      → Load, read the full file, and execute `{nextStepFile}` (health-check).
      
      ### 2. Present Change Summary
      
      "**Update Skill Report: {skill_name}**
      
      ---
      
      ### Operation Summary
      
      | Metric | Value |
      |--------|-------|
      | **Skill** | {skill_name} |
      | **Forge Tier** | {tier} |
      | **Mode** | {update_mode}{mode_fallback_note} |
      | **Duration** | {step count} steps |
      
      **`{update_mode}`** is one of `normal`, `gap-driven`, or `degraded` (mirrors the `update_mode` field of `SKF_UPDATE_RESULT_JSON`).
      
      **`{mode_fallback_note}`** surfaces weak-signal fallbacks the workflow took silently and would otherwise be buried in the evidence report. Render it inline after the mode value when any of these conditions fire; render the empty string when none did:
      
      - `--from-test-report` was passed but the test report was missing at the expected path, so step 1 fell back to `normal` mode → ` (gap-driven requested; test report missing — fell back to normal)`
      - `re-extract.md §0.a` skipped the workspace-drift guard because `source_root` is not a git working tree (or HEAD was unreadable) → ` (workspace-drift check skipped: {skip_reason})` where `{skip_reason}` is the helper's `skip_reason` field (`not-a-git-tree` or `HEAD unreadable`)
      - Both fallbacks fired: concatenate both parenthetical notes with `; ` between them
      
      These signals also appear in `warnings[]` on the headless envelope; the Mode row makes them visible to interactive users who scan the report without parsing the envelope.
      
      ### Changes Applied
      
      | Category | Count |
      |----------|-------|
      | Files modified | {count} |
      | Files added | {count} |
      | Files deleted | {count} |
      | Files moved/renamed | {count} |
      | **Total exports affected** | {count} |
      
      ### Export Changes
      
      | Change Type | Count |
      |-------------|-------|
      | Updated (signature/type change) | {count} |
      | Added (new exports) | {count} |
      | Removed (deleted exports) | {count} |
      | Moved (file relocated) | {count} |
      | Renamed (identifier changed) | {count} |
      
      ### Confidence Tier Breakdown
      
      | Tier | Count | Description |
      |------|-------|-------------|
      | T1 | {count} | AST-verified structural extraction |
      | T1-low | {count} | Pattern-matched (Quick tier or degraded) |
      | T2 | {count} | QMD-enriched semantic context |
      
      ### [MANUAL] Section Preservation
      
      | Metric | Count |
      |--------|-------|
      | Sections preserved | {count} |
      | Conflicts resolved | {count} |
      | Orphans kept | {count} |
      | Orphans removed | {count} |
      | **Integrity** | {VERIFIED / count issues} |"
      
      ### 3. Present Validation Findings (If Any)
      
      **If validation findings exist from step 05:**
      
      "### Validation Findings
      
      | Check | Status | Issues |
      |-------|--------|--------|
      | Spec compliance | {PASS/WARN/FAIL} | {count} |
      | [MANUAL] integrity | {PASS/WARN/FAIL} | {count} |
      | Confidence tiers | {PASS/WARN/FAIL} | {count} |
      | Provenance | {PASS/WARN/FAIL} | {count} |
      
      {List specific findings if WARN or FAIL}"
      
      **If all validations passed:** "### Validation: All checks passed."
      
      ### 4. Show Files Updated
      
      "### Files Written
      
      | File | Status |
      |------|--------|
      | `{resolved_skill_package}/SKILL.md` | Updated |
      | `{resolved_skill_package}/metadata.json` | Updated |
      | `{forge_version}/provenance-map.json` | Updated |
      | `{forge_version}/evidence-report.md` | Appended |
      
      Where `{resolved_skill_package}` = `{skills_output_folder}/{skill_name}/{version}/{skill_name}/` and `{forge_version}` = `{forge_data_folder}/{skill_name}/{version}/` — see `knowledge/version-paths.md`."
      
      ### 5. Workflow Chaining Recommendations
      
      "### Next Steps
      
      Based on the update results:"
      
      **If all validations passed:**
      "- **audit-skill** — Run to verify the update resolved known drift
      - **export-skill** — Package the updated skill for distribution
      - **test-skill** — Run test suite against the updated skill"
      
      **If validation warnings/failures exist:**
      "- **audit-skill** — Run to identify remaining issues
      - Review validation findings above before exporting"
      
      **If triggered by audit-skill chain:**
      "- **audit-skill** — Re-run to verify CRITICAL/HIGH drift resolved
      - **export-skill** — Package once audit confirms clean state"
      
      ### 5b. Result Contract
      
      Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{forge_version}/update-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_version}/update-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include all modified file paths in `outputs`; include `exports_affected`, `files_modified`, and `validation_status` (passed/warnings/failures) in `summary`.
      
      **Headless envelope (`SKF_UPDATE_RESULT_JSON`):** when `{headless_mode}` is true, ALSO emit a single-line JSON envelope to stdout prefixed with the literal `SKF_UPDATE_RESULT_JSON: `. Schema: `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`. Construct the envelope from in-context state:
      
      ```json
      SKF_UPDATE_RESULT_JSON: {"skf_update":{"status":"success|no-changes|detect-only|dry-run|halted-for-*|blocked","skill_name":"<name>","version":"<v>","previous_version":"<v>","update_mode":"normal|gap-driven|degraded","files_written":[...],"headless_decisions":[...],"warnings":[...],"error":null|{...}}}
      ```
      
      - `headless_decisions[]` — verbatim from the in-context array populated by gates (init.md §confirmation and §4 degraded-rebuild, detect-changes.md §1b/§1c/§2.2, merge.md §gate). Each entry `{gate, default_action, taken_action, reason, evidence?}`. Empty when no gates auto-resolved (e.g. no-changes path skipped detect-changes' gates).
      - `status` — single-field outcome for pipeline branching. `"success"` when the run wrote artifacts and produced no halts; `"no-changes"` when §1 short-circuited; `"detect-only"` / `"dry-run"` for the §1a/§1b read-only exits; one of the documented `halted-for-*` codes when a halt fired; `"blocked"` as the catch-all. The full enum lives in the schema (this step emits the value already resolved in context).
      - `error` — null on success or no-changes. Object `{phase, path?, reason}` describing the failure when a halt or write error fired. Pipelines branch on `error !== null` for non-zero exit semantics.
      
      The headless envelope is the structured channel; the per-run JSON written above is the audit trail. Both coexist — the envelope is one line on stdout for grep-friendly consumption, the per-run JSON is the full record on disk.
      
      **Post-finalization hook.** If `{onCompleteCommand}` (resolved in SKILL.md On Activation §3 from `workflow.on_complete`) is non-empty, invoke it after both result-JSON writes complete:
      
      ```bash
      {onCompleteCommand} --result-path={forge_version}/update-skill-result-latest.json
      ```
      
      Run it with a bounded timeout (default 60s). On success, log an Info note and continue; on non-zero exit, timeout, or any failure, append the reason to `warnings[]` (surfaced on the headless envelope) and continue. The hook must never fail the workflow — it is integration glue (notify a CI router, chain audit/export/test) orthogonal to the update outcome. Empty `{onCompleteCommand}` = no-op, no log entry.
      
      ### 6. Chain to Health Check
      
      Once the change summary has been presented, the files-written list displayed, and the result contract saved, load, read the full file, and execute `{nextStepFile}`. The health-check step is the true terminal step — do not stop at the report even though it reads as final.
      
      
    • validate.md 7.2 KB
      ---
      nextStepFile: 'write.md'
      # Resolve `{hashContentHelper}` to the first existing path; HALT if neither
      # candidate exists. Check B uses its `manual-verify` subcommand to verify the
      # merged (on-disk) SKILL.md against the byte-exact [MANUAL] inventory captured
      # in step 1 §5 — the deterministic replacement for the LLM byte-identity
      # eyeball, which could silently pass a subtly truncated block.
      hashContentProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
        - '{project-root}/src/shared/scripts/skf-hash-content.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5: Validate
      
      ## STEP GOAL:
      
      Validate the merged skill content against the agentskills.io specification, verify all [MANUAL] sections survived the merge intact, and check confidence tier consistency across all re-extracted content. This is an advisory validation — findings are warnings, not blockers.
      
      ## Rules
      
      - Focus only on validation — do not fix issues (that's the user's choice)
      - Validation is read-only — do not modify merged content
      - Run the two live structural checks (B, then C) in order; Checks A, D, E, F are deferred to step 6
      
      ## Steps
      
      ### 1. Check Tool Availability and Validation Timing
      
      Run: `npx skill-check -h`
      
      - If succeeds: skill-check is available for Checks A, E, F below
      - If fails: Use manual fallback paths in those checks
      
      **Important:** Do not assume availability — empirical check required.
      
      **Validation timing note:** Step-04 section 6b has already written SKILL.md to disk. External-tool checks against written files (skill-check Checks A, E, F) still run in **step 6 section 7** to co-locate external-tool validation with post-write verification. Check D (Provenance Completeness) is deterministic but needs both `metadata.json` and `provenance-map.json` on disk — neither exists yet at this step — so it is deferred to **step 6 section 6a**, which runs it via the completeness helper against the freshly-written artifacts. Structural checks (B, C) run here against the merged content — content on disk is byte-identical to the in-context copy.
      
      ### 2. Run Validation Checks
      
      Per the §1 timing note, only Check B (a single deterministic `manual-verify` script) and Check C (an in-prompt tier-label consistency check) run live at this step; Checks A, D, E, F are deferred to step 6. Run B then C in order — one script plus one in-prompt comparison have nothing to fan out.
      
      **Check A — Spec Compliance (deferred to post-write):**
      
      Skill-check requires written files on disk. This check is deferred to step 6 section 7. Perform manual structural check only: verify merged SKILL.md has required sections (exports, usage patterns, conventions), verify export entries have name/type/signature/file:line reference, flag missing sections.
      
      **Check B — [MANUAL] Section Integrity:**
      
      Run the deterministic [MANUAL]-integrity verifier against the step-1 §5 inventory — do not eyeball byte-identity (a subtly truncated block reads as intact to the eye):
      
      ```bash
      uv run {hashContentHelper} manual-verify {skill_package}/SKILL.md \
          --inventory {manual_inventory}
      ```
      
      Read the verdict JSON `{"preserved":[...], "modified":[...], "missing":[...], "moved":[...], "ok":bool}` (the on-disk SKILL.md is byte-identical to the merged in-context copy per the timing note above):
      - `ok == true` → status PASS. `preserved` blocks are byte-identical in place; any `moved` blocks are byte-identical but relocated with their logical parent section (report as informational, not a finding).
      - `modified` non-empty → FAIL: these blocks' byte-exact interiors changed (the truncation a marker-count/eyeball check would miss). List each by name.
      - `missing` non-empty → FAIL: these blocks lost their markers entirely. List each by name.
      
      Populate the §3 `manual_integrity` record from this verdict (`sections_verified` = inventory count, `sections_intact` = `len(preserved) + len(moved)`). This check is advisory here (findings inform, do not block); write.md §1 enforces the same `ok` verdict as a HALT gate before any derived artifact is written.
      
      **Check C — Confidence Tier Consistency:**
      - Verify all re-extracted exports have confidence labels (T1/T1-low/T2)
      - Verify tier labels match forge tier: Quick=T1-low only, Forge=T1 (T1-low for degraded), Forge+=T1 (same as Forge, CCC improves coverage not confidence), Deep=T1+T2
      - Flag mismatched or missing tier labels
      
      **Check D — Provenance Completeness (deferred to post-write):**
      
      The provenance map does not exist on disk until step 6 §3 writes it, and completeness is a deterministic set-diff (metadata `exports[]` vs provenance `entries[].export_name`) plus file:line citation resolution — not something to eyeball. This check is deferred to **step 6 section 6a**, which runs `skf-verify-provenance-completeness.py` against the just-written `metadata.json` and `provenance-map.json` and reads back `missing[]` / `orphaned[]` / `stale[]` findings. Do not attempt the set comparison here — there is no provenance map to compare against yet. The `Provenance` row in §5's summary is populated from §6a's result.
      
      **Check E — Diff Comparison (via skill-check):**
      
      **If available** and previous skill version exists: `npx skill-check diff <original-skill-dir> <updated-skill-dir>`
      
      Shows diagnostic changes between original and updated skill. Record diff results as informational context.
      
      **If unavailable or no previous version:** Skip with note.
      
      **Check F — Security Scan:**
      
      **If available**, run: `npx skill-check check <skill-dir> --format json` (security scan enabled by default).
      
      Record security findings as advisory warnings — they do not block the update.
      
      **If unavailable:** Skip with note: "Security scan skipped — skill-check tool unavailable"
      
      ### 3. Aggregate Validation Results
      
      Compile results from all checks:
      
      ```
      Validation Results:
        spec_compliance: {status: PASS|WARN|FAIL, findings: [{severity, description, location}]}
        manual_integrity: {status, sections_verified, sections_intact, findings}
        confidence_consistency: {status, exports_checked, findings}
        provenance_completeness: {status, entries_checked, findings}
        diff_comparison: {status: PASS|SKIP, new_issues, fixed_issues, unchanged}
        security_scan: {status: PASS|WARN|SKIP, findings}
        quality_score: [0-100]  # from skill-check, if available
      ```
      
      ### 5. Display Validation Summary
      
      "**Validation Results:**
      
      | Check | Status | Findings |
      |-------|--------|----------|
      | Spec Compliance | {PASS/WARN/FAIL} | {count} findings (quality score: {score}/100) |
      | [MANUAL] Integrity | {PASS/WARN/FAIL} | {count} findings |
      | Confidence Tiers | {PASS/WARN/FAIL} | {count} findings |
      | Provenance | {PASS/WARN/FAIL} | {count} findings |
      | Diff Comparison | {PASS/SKIP} | {new} new, {fixed} fixed |
      | Security Scan | {PASS/WARN/SKIP} | {count} findings |
      
      **Overall: {ALL_PASS / WARNINGS_FOUND / FAILURES_FOUND}**"
      
      **If findings exist:** List each with severity, description, and location. Add: "Validation is advisory. Findings do not block the update."
      
      ### 6. Route to Next Step
      
      This step auto-proceeds — no user choices; validation is advisory and does not block, it only informs. Once all validation checks have completed and findings are displayed, display "**Proceeding to write updated files...**", then load, fully read, and execute `{nextStepFile}` to write the updated files.
      
      
    • write.md 29.1 KB
      ---
      nextStepFile: 'report.md'
      descriptionGuardProtocol: '{project-root}/src/shared/references/description-guard-protocol.md'
      # 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 merged description field stand would silently
      # regress discovery quality and re-introduce angle-bracket tokens.
      descriptionGuardProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-description-guard.py'
        - '{project-root}/src/shared/scripts/skf-description-guard.py'
      # Resolve `{updateActiveSymlinkHelper}` to the first existing path; HALT if
      # neither candidate exists. §5b uses it to atomically flip the active
      # symlink; §6 uses it (verify mode) to confirm the post-state. Without
      # the helper, §5b's "rm and recreate" pattern leaves a brief window where
      # concurrent readers see a missing symlink.
      updateActiveSymlinkProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-update-active-symlink.py'
        - '{project-root}/src/shared/scripts/skf-update-active-symlink.py'
      # Resolve `{verifyProvenanceCompletenessHelper}` to the first existing path.
      # §6a runs Check D (Provenance Completeness), deferred from step 5 because it
      # needs both metadata.json + provenance-map.json on disk: the export/provenance
      # set-diff (missing + orphaned entries) and file:line citation resolution.
      # Advisory — do not HALT if neither path resolves; fall back to the in-prose
      # set comparison §6a documents (graceful degradation).
      verifyProvenanceCompletenessProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-verify-provenance-completeness.py'
        - '{project-root}/src/shared/scripts/skf-verify-provenance-completeness.py'
      # Resolve `{hashContentHelper}` to the first existing path; HALT if neither
      # candidate exists. §1 uses its `manual-verify` subcommand to verify the
      # post-merge file against the byte-exact [MANUAL] inventory captured in step 1
      # §5; a marker-count comparison would pass a block whose interior was truncated
      # without changing the marker count.
      hashContentProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
        - '{project-root}/src/shared/scripts/skf-hash-content.py'
      # Resolve `{renderMetadataStatsHelper}` by probing `{renderMetadataStatsProbeOrder}`
      # in order (installed SKF module path first, src/ dev-checkout fallback); first
      # existing path wins. HALT if neither resolves — §2's `stats` block and
      # `confidence_distribution` are computed values that must not be hand-binned,
      # and this is the same helper sibling create-skill compiles them with.
      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: Write Updated Files
      
      ## STEP GOAL:
      
      Verify the merged SKILL.md that step 4 section 6b wrote to disk, then write the derived artifacts (metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, and the active symlink).
      
      ## Rules
      
      - Focus only on verifying merged files and writing derived artifacts — merge content was already written in step 4
      - Do not modify merged SKILL.md content — any mismatch detected during verification triggers HALT, not repair
      - Do not skip provenance map update — critical for future audits
      - HALT immediately on verification failure before writing any derived artifact — a partial-write skill package is worse than an unchanged one
      
      ## Steps
      
      ### 0. Description Guard Protocol
      
      **Used by:** §7 (`skill-check check --fix` and `skill-check split-body --write`), and any future tool invocation that may modify SKILL.md's frontmatter on disk.
      
      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}` — §7 invokes the helper at the capture and verify-restore points around every `skill-check` call. `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.
      
      Update-skill does not run the optional post-restore frontmatter re-validation today — the post-write checks in §1 catch downstream issues, and a `restored: true` outcome is already surfaced through the evidence report (§4).
      
      ### 1. Verify SKILL.md Write
      
      SKILL.md was written in step 4 section 6b. Verify the write landed intact before proceeding to any derived-artifact writes.
      
      - Verify the resolved `{skill_package}` path matches the version directory step 4 wrote to (if the version changed, step 4 updated `{skill_package}` in context to point at the new path)
      - Run the deterministic [MANUAL]-integrity verifier against the byte-exact inventory captured in step 1 §5:
      
        ```bash
        uv run {hashContentHelper} manual-verify {skill_package}/SKILL.md \
            --inventory {manual_inventory}
        ```
      
        The verdict JSON is `{"preserved":[...], "modified":[...], "missing":[...], "moved":[...], "ok":bool}`. A `modified` block is one whose byte-exact interior changed (an interior truncation); a `missing` block lost its markers entirely; a `moved` block is byte-identical but relocated with its logical parent section (clean — does not fail the gate). `ok == (modified empty AND missing empty)`.
      - If `ok == true` and the path resolves: proceed to section 2
      - **If `ok == false`: HALT immediately** with status `halted-for-manual-mismatch`. Do not write `metadata.json`, `provenance-map.json`, or any other artifact — further writes would compound the inconsistency. In `{headless_mode}`, emit the halt envelope per SKILL.md §Headless (`error: {phase: "write:verify-manual-integrity", path: "{skill_package}/SKILL.md", reason: "..."}`). Alert the user:
      
        "**[MANUAL] section integrity failure after write.** Blocks modified (interior changed): {modified}. Blocks missing (markers lost): {missing}. Relocated-but-intact (advisory only): {moved}. Verified against the step-1 inventory `{manual_inventory}`, on disk at `{skill_package}/SKILL.md`. The skill package is in an inconsistent state. Manual recovery required — restore the previous version from `{skill_group}/{previous_version}/` or fix the file in place, then re-run update-skill."
      
      ### 2. Write Updated metadata.json
      
      Update `{skill_package}/metadata.json`:
      - **First, apply any queued `metadata_patches[]`** (staged by merge Priority 8b from gap-driven `metadata update` entries): apply each surgical patch described in the gap's remediation (reconcile a divergent count, add an explanatory stat, etc.) *before* the automatic recount below, so the recount overrides only the fields it owns and the patch survives for any field it does not. If a patch and the recount disagree on a field the recount owns (e.g., `exports_documented`), the recount wins — log the divergence so a still-stale stat surfaces in the report rather than being silently overwritten.
      - **For gap-driven rescopes** (`DELETED_EXPORT` / verification `rescoped`): the removed exports are already dropped from the `exports` array below, and `stats` recompute from that reduced surface — never set a `stats` count by hand to match the documented total. The reduction is justified by the `brief.scope.exclude` + `scope.amendments[]` (`action: "excluded"`) written in step 2; the recount simply reflects the smaller surface.
      - Update `version`: **if `update_mode == "gap-driven"`, do not bump — the skill is being repaired against the same source commit, so leave `version` unchanged and update only `generation_date` / `last_update` below.** This keeps metadata `version` consistent with the on-disk `{skill_package}` path, which step 4 §6b also leaves unchanged in gap-driven mode (see step 4 §6b's "If the source version detected during step 3 differs..." carve-out — in gap-driven mode no source version is detected, so step 4 writes into the existing version directory). Otherwise, if a source version was detected during re-extraction and differs from the current metadata version, use the source version; otherwise increment patch version
      - Update `generation_date` timestamp to current ISO-8601 date
      - Update `exports` array to reflect current export list
      - **Compute the `stats` block and `confidence_distribution` deterministically** with `{renderMetadataStatsHelper}` (resolve from `{renderMetadataStatsProbeOrder}`; first existing path wins) — the same helper sibling create-skill compiles them with, so create and update emit byte-identical stats for identical inputs. The helper owns all the arithmetic: it bins each provenance `entries[]` row once by its `signature_source` tier into `confidence_distribution.{t1,t1_low,t2,t3}`, sets `exports_documented` = the entry count, and derives `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), plus `scripts_count` / `assets_count` from the inventory arrays. Run `uv run {renderMetadataStatsHelper} --help` for the full contract. Do not hand-bin the distribution — binning T2 annotations + T3 doc items on top of the per-export tiers double-counts, which per-entry binning by `signature_source` makes structurally impossible. You supply only the judgment payload:
      
        **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)
        - `exports_internal`: count of all other non-underscore-prefixed exports
        - `scripts` / `assets`: the scripts / assets inventory arrays (or `[]` when empty) — the helper sets `scripts_count` / `assets_count` from their lengths
      
        **Invoke** — since the helper reads `entries[]`, stage §3's `provenance-map.json` write first (§3 does not depend on these stats):
      
        ```bash
        echo '{"exports_public_api": {N}, "exports_internal": {M}, "scripts": {scripts-inventory-or-[]}, "assets": {assets-inventory-or-[]}}' \
          | uv run {renderMetadataStatsHelper} {forge_version}/provenance-map.json
        ```
      
        Write the returned `stats` and `confidence_distribution` objects into `metadata.json` **verbatim**. If the helper reports `coherence.ok: false`, some provenance entries carry a missing/unrecognized `signature_source` (§3 must write it on every entry) — fix the provenance map, do not hand-edit the stats.
      
      ### 3. Write Updated provenance-map.json
      
      Write to `{forge_version}/provenance-map.json`:
      
      **Every entry this step writes or rewrites carries a `signature_source` (`T1` / `T1-low` / `T2` / `T3`)** — the tier that contributed the structural signature, matching create-skill's entry contract. §2's stats helper bins each entry on this field, so a missing value trips its `coherence.ok: false` check. Preserve it byte-identical on untouched entries; set it from the contributing extraction tier on every re-extracted or new entry.
      
      **If `no_reextraction == true` (gap-driven mode from step 3 section 0):**
      Dispatch per-entry on the verification outcome recorded by step 3 — gap-driven runs produce a mix of `verified`, `moved`, `re-extracted`, and `unknown` outcomes, and each requires a different provenance-map write strategy:
      
      - **`verified` exports**: no fresh extraction data exists — do NOT overwrite `confidence`, `extraction_method`, `ast_node_type`, `params[]`, `return_type`, `source_file`, or `source_line`. The provenance entry stays byte-identical.
      - **`moved` exports**: update `source_line` (and `source_file` if different) to the new location recorded by the spot-check. Do not touch other fields.
      - **`re-extracted` exports** (resolved via step 3 §0a's Targeted Re-Extraction Branch from `remediation_paths[]`): write a full entry — `source_file`, `source_line`, `confidence`, `extraction_method`, `ast_node_type`, `params[]`, `return_type` — from §0a's fresh AST extraction record. This is the only gap-driven outcome that produces normal-mode-quality provenance; do NOT fall through to the byte-identical preservation above.
      - **`unknown` exports** (not in provenance map; no `source_citation`; `severity` is `Medium`, `Low`, or `Info`, OR `remediation_paths[]` was empty and §0a did not halt): add new entries with fields populated from step 4 merge output. `source_file`/`source_line` may be `null` here — leave these fields unset rather than writing stale values. **This path is only acceptable for `severity` in `Medium`, `Low`, or `Info`.** A Critical/High `unknown` reaching this branch indicates step 3 §0a was skipped or bypassed and is a workflow bug — step 3 §0a should have halted with status `halted-for-remediation-path` before step 6 ran. If you encounter one, halt with a pointer to §0a rather than writing null citations for a blocking gap.
      - **`rescoped` exports** (`DELETED_EXPORT`, removed from the public surface per detect-changes §0 rule R1): remove the entry from the provenance map — identical to the normal-mode "For deleted exports" path below. The reduction's audit trail is the `brief.scope.exclude` + `scope.amendments[]` (`action: "excluded"`) entry written in step 2; do not record the removal by editing `stats` directly — §2 recomputes `stats` from the reduced `exports` array.
      - Skip the "For each export in the updated skill" bullets below — they apply only to normal re-extraction mode.
      
      **For each export in the updated skill (normal mode only):**
      - Update `export_name` if renamed
      - Update `params[]` array if parameters changed (add, remove, or modify individual entries)
      - Update `return_type` if changed
      - Update `source_file` if moved
      - Update `source_line` from fresh extraction
      - Update `confidence` from extraction results
      - Update `extraction_method` and `ast_node_type` if re-extracted with different tools
      
      **For deleted exports:**
      - Remove entry from provenance map
      
      **For new exports:**
      - Add new entry with full structured fields: `export_name`, `export_type`, `params[]`, `return_type`, `source_file`, `source_line`, `confidence`, `extraction_method`, `ast_node_type`
      
      **For script/asset file changes (if `file_entries` exists):**
      - MODIFIED_FILE: copy updated file to `scripts/` or `assets/`, update `content_hash` in `file_entries`
      - DELETED_FILE: remove file from `scripts/` or `assets/`, remove entry from `file_entries`
      - NEW_FILE: copy file to `scripts/` or `assets/`, add entry to `file_entries` with `file_name`, `file_type`, `source_file`, `confidence: "T1-low"`, `extraction_method: "file-copy"`, `content_hash`
      
      **Add update operation metadata:**
      ```json
      {
        "last_update": "{current_date}",
        "update_type": "{incremental if normal mode | full if degraded_mode}",
        "files_changed": {count},
        "exports_affected": {count},
        "confidence_tier": "{tier}",
        "manual_sections_preserved": {count}
      }
      ```
      
      `manual_sections_preserved` = `len(preserved) + len(moved)` from the §1 `manual-verify` verdict (blocks that survived byte-identical, whether in place or relocated). Do not re-count markers by hand — the §1 verdict is the deterministic source.
      
      ### 4. Write Updated evidence-report.md
      
      Append update operation section to `{forge_version}/evidence-report.md` (create the file with a standard header if it does not yet exist):
      
      ```markdown
      ## Update Operation — {current_date}
      
      **Trigger:** {manual / audit-skill chain}
      **Forge Tier:** {tier}
      **Mode:** {normal / degraded}
      
      ### Changes Detected
      - Files modified: {count}
      - Files added: {count}
      - Files deleted: {count}
      - Exports affected: {total}
      
      ### Merge Results
      - Exports updated: {count}
      - Exports added: {count}
      - Exports removed: {count}
      - [MANUAL] sections preserved: {count}
      - Conflicts resolved: {count}
      
      ### Validation Summary
      - Spec compliance: {PASS/WARN/FAIL}
      - [MANUAL] integrity: {PASS/WARN/FAIL}
      - Confidence tiers: {PASS/WARN/FAIL}
      - Provenance: {PASS/WARN/FAIL}
      
      ### Description Guard
      - Restored: {true/false}
      - Triggering tool: {tool_name or —}
      - Original description preserved: {true/false}
      - Notes: {one-sentence detail or —}
      
      ### Context Snippet
      - Regenerated: {true/false}
      - Triggers fired: {list or —}
      - Notes: {one-sentence detail or —}
      ```
      
      **Description Guard population** (used by §7 Post-Write Validation when the §0 protocol fires): fill all four fields from context when `description_guard_restored == true` (triggering tool, whether restore succeeded, what changed). When `Restored: false`, the other three fields are `—` — this is the clean-run expected state — except when `description_guard_refused == "empty-capture"` (the §0 protocol's empty-snapshot rule): then set `Triggering tool` to the recorded tool name, `Original description preserved: false`, and `Notes: guard refused — empty captured snapshot (empty-capture)`, so a refused restore is distinguishable from a run where the guard never fired. Same field semantics and populator logic as create-skill step 6 §8.
      
      **Context Snippet population** (used by §5 after the staleness check runs): §4 writes the sub-block with placeholders; §5 updates the on-disk evidence report in place after deciding whether to regenerate. Set `Regenerated: true` and populate `Triggers fired` with any combination of `headline-exports`, `version`, `gotchas` when at least one trigger fired. Set `Regenerated: false` and `Triggers fired: —` when none fired (the gap-driven / internals-only outcome). Always fill `Notes` with a one-sentence reason (e.g., `"Gap-driven repair — no snippet surface changed"`, `"Version bumped 0.1.0 → 0.2.0; headline exports re-ranked"`).
      
      ### 5. Regenerate context-snippet.md
      
      **Regenerate `context-snippet.md` if stale:**
      
      `context-snippet.md` is a `{skill_package}` deliverable that goes stale whenever **headline exports**, **version**, or **gotchas** change in this run. Regenerate it only when at least one of these triggers fired; otherwise skip — a skip is the correct outcome for gap-driven repairs and other runs that touch internals below the snippet's surface, where regenerating would produce byte-identical content.
      
      **Staleness triggers:**
      
      - **Headline exports changed** — the top-K exports surfaced in the snippet differ from the prior snippet (a `NEW_EXPORT` was promoted into a headline slot, or a `MODIFIED_EXPORT` changed the signature/shape of a surfaced export).
      - **Version changed** — §2 bumped `version` (normal mode with detected source drift; never fires in gap-driven mode per §2's carve-out).
      - **Gotchas changed** — new gotchas surfaced from this run's evidence that were not in the prior snippet, or a prior gotcha was invalidated and removed.
      
      **Record the decision on the on-disk evidence report:** open `{forge_version}/evidence-report.md` (written by §4 with placeholder values in the `### Context Snippet` sub-block) and update that sub-block under the Update Operation section just written. Set `Regenerated: true|false`, fill `Triggers fired:` with the list of triggers that fired (or `—` when none), and write a one-sentence `Notes:` entry. See §4's "Context Snippet population" note for field semantics.
      
      **If no trigger fired:** skip regeneration — do not touch `context-snippet.md` on disk. The snippet remains valid against the prior run's surface. Continue to §5b.
      
      **If at least one trigger fired:** regenerate the snippet using the format from `skf-create-skill/assets/skill-sections.md` (pipe-delimited indexed format).
      
      Use the **flat draft form** for the `root:` path in the draft snippet: `root: skills/{skill-name}/`. The per-IDE skill root (e.g., `.claude/skills/`, `.windsurf/skills/`, `.github/skills/` — see `skf-export-skill/assets/managed-section-format.md`) is applied later by `export-skill` step 3 when the skill is exported. Do not choose an IDE-specific prefix in update-skill — that is an export-time decision that depends on config.yaml.
      
      Pull values for the regenerated snippet from the updated metadata.json (version, top exports), the merged SKILL.md (section anchors, inline summaries), and the evidence report (new gotchas). If gotchas cannot be derived from the updated evidence but the prior snippet has a `|gotchas:` line, carry forward the prior line with the `[CARRIED]` marker — see `skf-export-skill/references/generate-snippet.md` for the carry-forward protocol (one-cycle limit).
      
      Write the regenerated snippet to `{skill_package}/context-snippet.md`, preserving file permissions.
      
      ### 5b. Update Active Symlink
      
      Flip `{skill_group}/active` to point at the current `{version}` via the helper. The call is **always** run — atomic, idempotent, and verified in one shot. The helper's no-op path handles the "version did not change" case (gap-driven mode, or no source drift) without writing to disk:
      
      ```bash
      uv run {updateActiveSymlinkHelper} update \
          --skill-group {skill_group} \
          --version {version}
      ```
      
      The helper emits a result envelope with `status` ∈ `{ok, flipped, mismatch, missing-target}` and a pre-formatted `log_message`. Log the message to the evidence report.
      
      **Dispatch on `status`:**
      
      - **`ok`** (exit 0): symlink already points at `{version}` — no disk write. Continue to §6.
      - **`flipped`** (exit 0): symlink was atomically updated (temp-and-replace). Continue to §6.
      - **`missing-target`** (exit 2): `{skill_group}/{version}/` directory does not exist on disk. HALT — display `halt_message` verbatim. This indicates §4 §6b did not write the version directory before §5b ran (a workflow bug, not a user error).
      - **`mismatch`** (exit 2): re-read after flip showed the symlink still points elsewhere. HALT — display `halt_message`. Should be impossible because the helper uses `os.replace` (atomic rename); a mismatch here indicates filesystem-level interference (concurrent writer, broken FUSE mount).
      
      Both exit-2 halts carry status `halted-for-write-failure`; in `{headless_mode}`, emit the halt envelope per SKILL.md §Headless (`error: {phase: "write:active-symlink", reason: "..."}`).
      
      ### 6. Verify Derived Artifact Writes
      
      SKILL.md was verified in section 1 (written by step 4 section 6b). This section verifies the artifacts this step wrote: `metadata.json`, `provenance-map.json`, `evidence-report.md`, `context-snippet.md`, and the `active` symlink from §5b.
      
      For each derived artifact:
      - Read back the file
      - Confirm content matches expected output
      - Report verification status
      
      **Active symlink verification:** run `{updateActiveSymlinkHelper} verify --skill-group {skill_group} --version {version}` — read-only check that the symlink resolves to the version just written to `metadata.json` in §2. This closes the §5b gap where a silent skip would otherwise leave the manifest and symlink divergent — the symlink is the fallback resolver for consumers that don't read the manifest (see `knowledge/version-paths.md` §Reading Workflows step 5), so a `mismatch` must fail the step, not warn. Applies in every mode — gap-driven runs do not bump `version`, but the symlink must still point to the current `version`, otherwise a prior partial run left it pointing elsewhere.
      
      "**Write Verification:**
      
      | File | Status |
      |------|--------|
      | SKILL.md | {VERIFIED in section 1} |
      | metadata.json | {VERIFIED/FAILED} |
      | provenance-map.json | {VERIFIED/FAILED} |
      | evidence-report.md | {VERIFIED/FAILED} |
      | context-snippet.md | {VERIFIED/FAILED} |
      | {skill_group}/active symlink | {VERIFIED/FAILED} (readlink → {resolved_version}, expected {version}) |
      
      **On symlink `mismatch` (helper exit 2):** HALT with status `halted-for-write-failure`. Do not proceed to §7 post-write validation or §8 menu. Display the helper's `halt_message` verbatim — it already includes the diverged target, the expected version, and the recovery command; in `{headless_mode}`, emit the halt envelope per SKILL.md §Headless (`error: {phase: "write:verify-active-symlink", reason: "..."}`). This matches the severity of the other four artifact checks — silent divergence here mis-routes any downstream consumer that uses the symlink fallback.
      
      **All files written and verified.**"
      
      ### 6a. Provenance Completeness (Check D — Deferred from Step 05)
      
      `validate.md` Check D (Provenance Completeness) is a deterministic set-diff plus citation resolution, and it needs both `metadata.json` (written in §2) and `provenance-map.json` (written in §3) on disk — neither exists at validate time, so the check is deferred here. Run it against the just-written artifacts via `{verifyProvenanceCompletenessHelper}`:
      
      ```bash
      uv run {verifyProvenanceCompletenessHelper} verify \
          --metadata {skill_package}/metadata.json \
          --provenance {forge_version}/provenance-map.json \
          --source-root {source_root}
      ```
      
      The helper reads its `--source-root` (falling back to the provenance map's own `source_root` field when the flag is omitted); pass the resolved `{source_root}` so citation resolution runs against the same tree re-extraction read. **Read the emitted JSON — do NOT recompute the set operations by eye; an LLM set-diff can silently pass a dropped or orphaned entry:**
      
      - `missing[]` — documented exports (metadata `exports[]`) with no provenance entry: a coverage gap.
      - `orphaned[]` — provenance `entries[].export_name` whose export was removed but the entry remains.
      - `stale[]` — entries whose `source_file:source_line` no longer resolves; each carries a `reason` of `file-missing`, `line-out-of-bounds`, or `line-invalid`. Internal names are canonicalized through `reexport_map` before the diff, so a barrel-renamed export does not read as missing or orphaned.
      - `summary.stale_check` — `"checked"` when citations were resolved against the source tree, or `"skipped-no-source-root"` when no source root resolved on disk (the completeness + orphan diffs still ran; `stale` is empty by construction, not clean-by-verification).
      
      Map `status` to the `Provenance:` line of the §4 evidence report's Validation Summary: `PASS` when `status == "pass"`, `WARN` when `status == "findings"` (this is the persistent record — step 5 §5's table was rendered before the provenance map existed, so it necessarily showed this row as deferred). Provenance findings are **advisory** — they do not block the update. Surface each `missing` / `orphaned` / `stale` entry in the evidence report's Validation Summary so the user can decide, and note when `stale_check` was `skipped-no-source-root`.
      
      **Graceful degradation:** if neither probe path resolves (no `uv` / script available), fall back to the manual set comparison the script encapsulates — enumerate metadata `exports[]` and provenance `entries[].export_name` (canonicalizing internal names through `reexport_map`), diff the two sets for missing/orphaned entries, and spot-check that each `source_file:source_line` still points at a real line in the source tree. Prefer the script — it does this deterministically.
      
      ### 7. Run Post-Write Validation (Deferred from Step 05)
      
      External tool checks deferred from step 5 now run against the written files.
      
      **Description Guard Protocol:** every invocation below that may modify SKILL.md (`skill-check check --fix` and any `split-body` write) must run inside the four-phase guard defined in §0. Invoke `{descriptionGuardHelper}` at the capture and verify-restore points around each call:
      
      ```bash
      # Phase 1 — capture before any frontmatter-touching tool call
      uv run {descriptionGuardHelper} capture {skill_package}/SKILL.md
      # stash returned `description` as `guarded_description`
      
      # Phase 2 — run the tool (skill-check --fix, split-body --write, etc.)
      
      # Phases 3+4 — verify and restore after the tool call
      uv run {descriptionGuardHelper} verify-restore {skill_package}/SKILL.md \
          --captured-description "{guarded_description}"
      ```
      
      Do not rely on per-call ad-hoc preservation logic — use the helper.
      
      **If skill-check available:**
      
      - Run: `npx skill-check check {skill_package} --fix --format json --no-security-scan` **inside the §0 guard**.
      - **Context sync after --fix:** If `fixed[]` is non-empty (i.e., `--fix` modified files on disk), re-read the modified SKILL.md to update the in-context copy. This prevents silent divergence between the in-context SKILL.md and the on-disk version that report will reference. The §0 guard has already restored `description` if divergent; the re-read picks up any other fix-corrected content.
      - If `body.max_lines` reported, prefer selective split — extract only the largest Tier 2 section(s) to `references/`, keeping Tier 1 inline (inline passive context achieves 100% task accuracy vs 79% for on-demand retrieval). **If falling back to `npx skill-check split-body {skill_package} --write`, run it inside the §0 guard** — split-body can also touch frontmatter. Verify anchors resolve after split.
      - Run: `npx skill-check diff` if original version was preserved.
      - Run: `npx skill-check check {skill_package} --format json` for security scan. (Read-only; guard not required.)
      
      Record findings in the evidence report (section 4), including any `description_guard_restored` events recorded by the §0 protocol. These are advisory — do not block on warnings.
      
      **If skill-check unavailable:** Skip with note — structural checks from step 5 are sufficient.
      
      ### 8. Route to Next Step
      
      This step auto-proceeds — no user choices. Once all files have been written and verified and post-write validation is complete, display "**Proceeding to report...**", then load, fully read, and execute `{nextStepFile}` to display the change report.
      
      
  • scripts
    • skf-new-file-diff.py 5.8 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.10"
      # ///
      """Deterministic NEW_FILE detection for skf-update-skill (detect-changes Category D).
      
      `skf-hash-content compare` classifies files already tracked in the provenance
      map (UNCHANGED / MODIFIED_FILE / DELETED_FILE) but by design cannot report a
      file that is present in source yet absent from the provenance map — a NEW_FILE.
      Deriving that set is a set-difference plus a filter with exactly one correct
      answer per input: take every `source_file` in the scripts/assets inventory
      (emitted by `skf-detect-scripts-assets detect`), subtract the paths already in
      `file_entries[].source_file`, and set aside any user-authored `[MANUAL]` path.
      Doing that subtraction in the prompt drifts across runs; doing it here is
      byte-stable.
      
      Usage:
        skf-detect-scripts-assets.py detect <source-root> \
          | skf-new-file-diff.py <provenance-map-path>
      
      Reads the detect JSON on stdin (needs `scripts_inventory[]` and
      `assets_inventory[]`, each row carrying `source_file`). Reads the provenance
      map at the path argument (canonical `{file_entries: [...]}` object, or a bare
      array of entries).
      
      Output JSON (stdout):
        {
          "new_files":       [ {"source_file": "...", "kind": "script|asset"}, ... ],
          "skipped_manual":  [ "...", ... ],   # under scripts/[MANUAL]/ or assets/[MANUAL]/
          "already_tracked": [ "...", ... ],   # present in file_entries
          "stats": {"inventory_total": N, "new": N,
                    "skipped_manual": N, "already_tracked": N}
        }
      
      All three arrays are sorted by source_file; new_files carries the kind of the
      inventory it came from (scripts -> "script", assets -> "asset"). A path found
      in both inventories is counted once, resolved as "script".
      
      Exit codes:
        0  success
        2  bad input (unreadable/invalid stdin JSON, missing/invalid provenance map)
      """
      
      import json
      import re
      import sys
      from pathlib import Path
      
      # scripts/[MANUAL]/... or assets/[MANUAL]/... anywhere in the (posix) path
      _MANUAL_RE = re.compile(r"(?:^|/)(?:scripts|assets)/\[MANUAL\]/")
      
      
      def _fail(msg: str) -> "NoReturn":  # type: ignore[valid-type]
          print(json.dumps({"error": msg}), file=sys.stderr)
          sys.exit(2)
      
      
      def _posix(p: str) -> str:
          return p.replace("\\", "/")
      
      
      def load_tracked_source_files(provenance_path: Path) -> set[str]:
          """Return the set of file_entries[].source_file already in the provenance map."""
          try:
              data = json.loads(provenance_path.read_text(encoding="utf-8"))
          except (json.JSONDecodeError, OSError) as exc:
              _fail(f"failed to read provenance map {provenance_path}: {exc}")
      
          if isinstance(data, list):
              entries = data
          elif isinstance(data, dict):
              entries = data.get("file_entries")
              if entries is None:
                  _fail(f"provenance map {provenance_path} has no `file_entries` field")
              if not isinstance(entries, list):
                  _fail(f"`file_entries` in {provenance_path} is not an array")
          else:
              _fail(
                  f"provenance map {provenance_path} must be an object or array; "
                  f"got {type(data).__name__}"
              )
      
          tracked: set[str] = set()
          for entry in entries:
              if isinstance(entry, dict):
                  sf = entry.get("source_file")
                  if isinstance(sf, str) and sf:
                      tracked.add(_posix(sf))
          return tracked
      
      
      def collect_inventory(detect: dict) -> list[tuple[str, str]]:
          """Return [(source_file, kind), ...] from the detect JSON, de-duplicated.
      
          Scripts win over assets when a path appears in both, so kind is stable.
          """
          seen: dict[str, str] = {}
          for key, kind in (("scripts_inventory", "script"), ("assets_inventory", "asset")):
              rows = detect.get(key)
              if rows is None:
                  continue
              if not isinstance(rows, list):
                  _fail(f"`{key}` in detect JSON is not an array")
              for row in rows:
                  if not isinstance(row, dict):
                      _fail(f"`{key}` entry is not an object: {row!r}")
                  sf = row.get("source_file")
                  if not isinstance(sf, str) or not sf:
                      _fail(f"`{key}` entry missing string `source_file`: {row!r}")
                  sf = _posix(sf)
                  seen.setdefault(sf, kind)  # first inventory wins the kind
          return sorted(seen.items())
      
      
      def diff(detect: dict, tracked: set[str]) -> dict:
          new_files: list[dict] = []
          skipped_manual: list[str] = []
          already_tracked: list[str] = []
      
          inventory = collect_inventory(detect)
          for sf, kind in inventory:
              if sf in tracked:
                  already_tracked.append(sf)
              elif _MANUAL_RE.search(sf):
                  skipped_manual.append(sf)
              else:
                  new_files.append({"source_file": sf, "kind": kind})
      
          return {
              "new_files": new_files,
              "skipped_manual": sorted(skipped_manual),
              "already_tracked": sorted(already_tracked),
              "stats": {
                  "inventory_total": len(inventory),
                  "new": len(new_files),
                  "skipped_manual": len(skipped_manual),
                  "already_tracked": len(already_tracked),
              },
          }
      
      
      def main(argv: list[str]) -> int:
          if len(argv) != 2:
              _fail("usage: skf-new-file-diff.py <provenance-map-path>  (detect JSON on stdin)")
      
          provenance_path = Path(argv[1])
          if not provenance_path.exists():
              _fail(f"provenance map not found: {provenance_path}")
      
          raw = sys.stdin.read()
          if not raw.strip():
              _fail("no detect JSON on stdin")
          try:
              detect = json.loads(raw)
          except json.JSONDecodeError as exc:
              _fail(f"invalid detect JSON on stdin: {exc}")
          if not isinstance(detect, dict):
              _fail(f"detect JSON must be an object; got {type(detect).__name__}")
      
          tracked = load_tracked_source_files(provenance_path)
          result = diff(detect, tracked)
          print(json.dumps(result, indent=2))
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main(sys.argv))
      
  • customize.toml 1.9 KB
    # DO NOT EDIT -- overwritten on every update.
    #
    # Workflow customization surface for skf-update-skill.
    # Team overrides:     _bmad/custom/skf-update-skill.toml (under {project-root})
    # Personal overrides: _bmad/custom/skf-update-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 update 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
    # (drift policies, manual-section preservation rules, compliance
    # guardrails). Overrides append.
    #
    # Each entry is either:
    #   - a literal sentence, e.g. "Updates must preserve [MANUAL] sections verbatim."
    #   - a file reference prefixed with `file:`, e.g.
    #     "file:{project-root}/docs/update-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 report.md §5b writes the result
    # contract (the per-run record plus update-skill-result-latest.json). The
    # command is called as:
    #   <on_complete> --result-path=<{forge_version}/update-skill-result-latest.json>
    # Useful for Slack notifications, dashboard ingest, CI hooks, or chaining a
    # downstream skill (audit, export, test). Failures are logged to the run's
    # warnings[] but never fail the workflow.
    #
    # Empty string = no-op (default).
    
    on_complete = ""
    
  • SKILL.md 7.8 KB
    ---
    name: skf-update-skill
    description: Smart regeneration preserving [MANUAL] sections after source changes. Use when the user requests to "update a skill" or "regenerate a skill."
    ---
    
    # Update Skill
    
    ## Overview
    
    Surgically updates existing skills when source code changes, preserving all [MANUAL] developer content while re-extracting only affected exports with full provenance tracking. Only changed exports are re-extracted — unchanged content is never touched. Every regenerated instruction must trace to code with file:line citations. Stack skills (`skill_type: "stack"` in metadata.json) are not supported by surgical update — use `skf-create-stack-skill` to re-compose from updated constituents. If a stack skill is provided, this workflow exits with a redirect message.
    
    ## Conventions
    
    - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
    - **Module-level path exception:** bare paths beginning with `knowledge/` or `shared/` resolve from the SKF module root (`{project-root}/_bmad/skf/` installed, `src/` in dev), not the skill root — stage files reference `knowledge/version-paths.md` and `knowledge/tool-resolution.md`, and the terminal step chains to `shared/health-check.md`.
    - `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.
    - **Cross-skill data coupling:** stages in this workflow load four shared assets from `skf-create-skill` to keep extraction semantics aligned between create and update — `re-extract.md` pulls `extraction-patterns.md`, `extraction-patterns-tracing.md`, and `tier-degradation-rules.md` from `skf-create-skill/references/`; `remote-source-resolution.md` references `source-resolution-protocols.md`; `write.md` reads `skill-sections.md` from `skf-create-skill/assets/`. Update-skill assumes these files are present at install time and that their semantics are stable across the two skills' versions.
    
    ## Role
    
    You are a precision code analyst operating in Ferris Surgeon mode. This is a surgical operation, not an exploratory session. You bring AST-backed structural analysis and provenance-driven change detection expertise, while the source code provides the ground truth.
    
    ## Workflow Rules
    
    These rules apply to every step in this workflow:
    
    - Never hallucinate — every statement must have AST provenance
    - [MANUAL] sections survive regeneration with zero content loss
    - 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 and log each auto-decision
    
    ## Stages
    
    | # | Step | File | Auto-proceed |
    |---|------|------|--------------|
    | 1 | Initialize & Load | references/init.md | No (confirm) |
    | 2 | Detect Changes | references/detect-changes.md | Yes |
    | 3 | Re-Extract | references/re-extract.md | Yes |
    | 4 | Merge | references/merge.md | Yes |
    | 5 | Validate | references/validate.md | Yes |
    | 6 | Write | references/write.md | Yes |
    | 7 | Report | references/report.md | Yes |
    | 8 | Workflow Health Check | references/health-check.md | Yes |
    
    ## Invocation Contract
    
    | Aspect | Detail |
    |--------|--------|
    | **Inputs** | skill_name [required] |
    | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--from-test-report` (gap-driven mode); `--allow-workspace-drift` (gap-driven only — bypass §0.a pinning guard); `--allow-degraded` (headless only — pre-authorize the lossy degraded full re-extraction when the provenance map is missing, instead of halting `blocked`; init.md §4); `--detect-only` (run detect-changes only, exit before re-extract; envelope `status="detect-only"`); `--dry-run` (run detect-changes + re-extract, exit before merge/write; envelope `status="dry-run"` describes what would change). If both `--detect-only` and `--dry-run` are passed, `--detect-only` wins. |
    | **Gates** | step 1: Confirm Gate [C] | step 4: Confirm Gate [C if clean merge, HALT if conflicts] |
    | **Outputs** | Updated SKILL.md, metadata.json, provenance-map.json, evidence-report.md (none when `--detect-only` or `--dry-run` is set — those modes are read-only inspection paths) |
    | **Concurrency** | Two simultaneous real-update runs against the same skill would corrupt provenance. init.md §1b acquires a PID-file lock at `{forge_data_folder}/{skill_name}/.skf-update.lock` before any artifact read; live-PID collisions halt with `status: "halted-for-concurrent-run"`. Stale locks (dead PID) are cleared silently with a warning. The lock is released by the terminal health-check step (step 8) on the normal path and by the two init-stage headless halts (§4/§6); mid-workflow halts leave it for the next run's stale-lock self-heal (see init.md §1b Release contract). Read-only modes (`--detect-only`, `--dry-run`) skip the lock entirely — they're safe alongside a concurrent real update. |
    | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Each auto-resolved gate appends a `{gate, default_action, taken_action, reason, evidence?}` entry to `headless_decisions[]`, surfaced in step 7's `SKF_UPDATE_RESULT_JSON` envelope so non-interactive runs can be audited post-hoc. A HALT reached in headless mode emits its own `SKF_UPDATE_RESULT_JSON` at the halt site (the site's `status` code plus an `error: {phase, path?, reason}` object) and exits — halts do not fall through to step 7. Pipeline branches on the envelope's top-level `status` field (`success`, `no-changes`, `detect-only`, `dry-run`, or one of the documented `halted-for-*`/`blocked` codes). The first four are successful exits — pipelines treating non-success as failure must include them in the success set. The status enum is defined once in `src/shared/scripts/schemas/skf-update-result-envelope.v1.json`. |
    
    ## On Activation
    
    1. Load config from `{project-root}/_bmad/skf/config.yaml` and resolve:
       - `project_name`, `output_folder`, `user_name`, `communication_language`, `document_output_language`
       - `skills_output_folder`, `forge_data_folder`, `sidecar_path`
    
    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
       ```
    
       This merges the three layers per `bmad-customize` rules (scalars override, arrays append): `{skill-root}/customize.toml` (bundled defaults), `_bmad/custom/<skill-name>.toml` under `{project-root}` (team overrides), and `_bmad/custom/<skill-name>.user.toml` under `{project-root}` (personal overrides). If the script is missing or fails, read `{skill-root}/customize.toml` directly.
    
       Apply the resolved values so no surface is 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 — the bundled default loads any `project-context.md` under `{project-root}`); resolve `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty string, and stash it in workflow context (`references/report.md` §5b invokes it after the result contract is written; empty string = the hook is a no-op). After activation completes, execute each entry in `workflow.activation_steps_append` in order before `init.md` runs.
    
    4. Load, read the full file, and then execute `references/init.md` to begin the workflow.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related