Claude Skill

skf-audit-skill

Drift detection between skill and current source code. Use when the user requests to "audit a skill" or "audit skill" for drift.

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-audit-skill-492e73e.zip · 38 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-audit-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

Audit Skill

Overview

Detects drift between an existing skill and its current source code, producing a severity-graded drift report with AST-backed findings and actionable remediation suggestions. Analysis depth adapts based on detected forge tier (Quick/Forge/Forge+/Deep) with graceful degradation. Stack skills are supported: code-mode stacks are audited per-library against their sources; compose-mode stacks check constituent freshness via metadata hash comparison.

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.

Role

You are a skill auditor in Ferris Audit mode: a deterministic drift-detection workflow where the source code is the ground truth and every finding traces back to it.

Workflow Rules

These rules apply to every step in this workflow:

  • Never fabricate findings — all data must trace to source code with file:line citations
  • Only load one step file at a time — never preload future steps
  • Update stepsCompleted in output file frontmatter before loading next step
  • 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 & Baseline references/init.md No (confirm)
2 Re-Index Source references/re-index.md Yes
3 Structural Diff references/structural-diff.md Yes
4 Semantic Diff references/semantic-diff.md Yes (skip at non-Deep)
5 Severity Classification references/severity-classify.md Yes
5a Doc Drift references/step-doc-drift.md Yes
6 Report references/report.md Yes
7 Workflow Health Check references/health-check.md Yes

Invocation Contract

Aspect Detail
Inputs skill_name [required], skill_path [optional override — full path to skill directory; bypasses manifest/symlink resolution], tier_override [optional: Quick / Forge / Forge+ / Deep — overrides detected tier], degraded [optional bool — pre-confirm degraded-mode opt-in when no provenance map exists], upstream_drift_choice [optional: C / S / X — pre-supplied answer for the upstream-drift gate at init.md §5b], dirty_worktree_choice [optional: T / A / F — pre-supplied answer for the dirty-worktree sub-gate at init.md §5b], force [optional bool — when paired with dirty_worktree_choice=F or used for any future destructive-action gate, signals consent to skip the confirmation]
Gates step 1: Manifest-vs-Symlink Gate [N] · Upstream-Drift Gate [C/S/X] · Dirty-Worktree Sub-Gate [T/A/F] · Degraded-Mode Gate [D/X] · Baseline Confirm Gate [C]
Outputs drift-report-{timestamp}.md at {forge_version}/ with drift_score and nextWorkflow frontmatter; per-run result contract at {forge_version}/audit-skill-result-{timestamp}.json plus -latest.json copy; final SKF_AUDIT_RESULT_JSON line on stdout when {headless_mode} is true
Headless All gates auto-resolve with default action when {headless_mode} is true; pre-supplied inputs (upstream_drift_choice, dirty_worktree_choice, degraded, tier_override) consumed at the gates that would otherwise prompt
Exit codes See "Exit Codes" below

Exit Codes

Every hard halt in this workflow exits with a stable code so headless automators can branch on the failure class without grepping message text:

Code Meaning Raised by
0 success step 7 (terminal health-check)
2 input-missing step 1 §1 — no skill_name supplied in headless mode (interactive prompt cannot resolve)
3 resolution-failure step 1 §1 (skill not found at resolved path: missing SKILL.md); step 1 §2 (forge-tier.yaml missing — setup-forge not run); step 1 §5 (source directory from provenance map no longer exists / inaccessible)
4 write-failure step 1 §6 / step 6 §3 (drift report write failed: read-only mount, disk full, permissions denied)
6 user-cancelled step 1 §1 manifest-vs-symlink gate [X] · step 1 §4 degraded-mode gate [X] · step 1 §5b upstream-drift gate [X] · step 1 §5b dirty-worktree sub-gate [A] (and [A] headless default)

Result Contract (Headless)

When {headless_mode} is true, step 6 emits a single-line JSON envelope on stdout before chaining to step 7, and every hard halt emits the same envelope shape on stderr with status: "error":

SKF_AUDIT_RESULT_JSON: {"status":"success|error","skill_name":"…","drift_score":"CLEAN|MINOR|SIGNIFICANT|CRITICAL|null","report_path":"…|null","next_workflow":"update-skill|null","audit_ref":"…|null","exit_code":0,"halt_reason":null}

status is "success" on the terminal happy path, "error" on any halt. drift_score is null when the workflow halted before severity classification ran. next_workflow is "update-skill" when CRITICAL or HIGH findings exist, otherwise null. halt_reason is one of: null (success), "input-missing", "skill-not-found", "forge-tier-missing", "source-dir-missing", "write-failed", "user-cancelled". exit_code matches the table above.

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
    • Generate and store timestamp as YYYYMMDD-HHmmss format. This value is fixed for the entire workflow run.
  2. Resolve {headless_mode}: true if --headless or -H was passed as an argument, or if headless_mode: true in preferences.yaml. Default: false.

  3. Resolve workflow customization. Run:

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

    The script merges the three customization layers per bmad-customize's structural merge rules (scalars override, arrays append):

    • {skill-root}/customize.toml — bundled defaults
    • _bmad/custom/<skill-name>.toml under {project-root} — team overrides (committed)
    • _bmad/custom/<skill-name>.user.toml under {project-root} — personal overrides (gitignored)

    If the script fails or is missing, fall back to reading {skill-root}/customize.toml directly — the bundled defaults are an empty string for each path scalar.

    Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each of the scalars, if the merged value is empty or absent, use the bundled default:

    • {driftReportTemplatePath} ← workflow.drift_report_template_path if non-empty, else assets/drift-report-template.md
    • {severityRulesPath} ← workflow.severity_rules_path if non-empty, else references/severity-rules.md
    • {onCompleteCommand} ← workflow.on_complete if non-empty, else empty (no-op — report.md skips the hook invocation entirely)

    Stash all three as workflow-context variables. Stage files reference {driftReportTemplatePath} / {severityRulesPath} / {onCompleteCommand} directly.

    Also apply the array surfaces (not silent no-ops): run workflow.activation_steps_prepend now, treat workflow.persistent_facts as standing context for the run (file:-prefixed entries load their file/glob contents as facts — the bundled default globs any project-context.md), then run workflow.activation_steps_append after activation.

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

Files (bmad-module-skill-forge)
  • assets
    • drift-report-template.md 981 B
      ---
      workflowType: 'audit-skill'
      stepsCompleted: []
      lastStep: ''
      date: ''
      user_name: ''
      skill_name: ''
      skill_path: ''
      source_path: ''
      forge_tier: ''
      drift_score: ''
      nextWorkflow: ''
      previousWorkflow: 'create-skill'
      ---
      
      # Drift Report: {skill_name}
      
      ## Audit Summary
      
      **Confidence Mode:** {confidence_mode}
      **Skill:** {skill_name}
      **Source:** {source_path}
      **Tier:** {forge_tier}
      **Date:** {date}
      **Overall Drift Score:** {drift_score}
      
      | Category  | Count |
      |-----------|-------|
      | CRITICAL  |       |
      | HIGH      |       |
      | MEDIUM    |       |
      | LOW       |       |
      | **Total** |       |
      
      ---
      
      ## Structural Drift
      
      <!-- Appended by structural-diff -->
      
      ---
      
      ## Semantic Drift
      
      <!-- Appended by semantic-diff (Deep tier only) -->
      
      ---
      
      ## Severity Classification
      
      <!-- Appended by severity-classify -->
      
      ---
      
      ## Documentation Drift
      
      <!-- Appended by step-doc-drift -->
      
      ---
      
      ## Remediation Suggestions
      
      <!-- Appended by report -->
      
      ---
      
      ## Provenance
      
      <!-- Appended by report -->
      
  • references
    • health-check.md 891 B
      ---
      # `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 7: Workflow Health Check
      
      ## STEP GOAL:
      
      Chain to the shared workflow self-improvement health check at `{nextStepFile}`. This is the terminal step of audit-skill — after the shared health check completes, the workflow is fully done.
      
      ## Rules
      
      - No user-facing reports, file writes, or result contracts in this step — those belong in step 6
      - Delegate directly to `{nextStepFile}` with no additional commentary
      - Do not attempt any other action between loading this step and executing `{nextStepFile}`
      
      ## MANDATORY SEQUENCE
      
      Load `{nextStepFile}`, read it fully, then execute it.
      
    • init.md 29.4 KB
      ---
      nextStepFile: 're-index.md'
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      templateFile: '{driftReportTemplatePath}'
      loadProvenanceProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-load-provenance.py'
        - '{project-root}/src/shared/scripts/skf-load-provenance.py'
      compareFileHashesProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-compare-file-hashes.py'
        - '{project-root}/src/shared/scripts/skf-compare-file-hashes.py'
      compareConstituentHashesProbeOrder:
        - '{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 Audit
      
      ## STEP GOAL:
      
      Load the existing skill artifacts, provenance map, and forge tier configuration to establish the baseline for drift detection. Create the drift report document and present a baseline summary for user confirmation before proceeding with analysis.
      
      ## Rules
      
      - Focus only on loading skill artifacts and establishing the baseline — do not perform any diff or analysis
      - Do not proceed if skill path is invalid or SKILL.md not found
      - Present baseline summary clearly so user can confirm before analysis begins
      - Docs-only limitation: If `metadata.json` indicates `source_type: "docs-only"` or `confidence_tier: "Quick"` with all T3 citations, inform user: "**This is a docs-only skill.** Drift detection compares against upstream documentation, not source code. Re-run `@Ferris US` to re-fetch documentation URLs and detect content changes." Recommend update-skill instead.
      
      ## MANDATORY SEQUENCE
      
      **Initialize workflow context defaults.** Before entering §1, set `confidence_mode = "normal"` as the default. §4 may upgrade this to `"degraded — all findings T1-low"` if the operator opts into degraded mode. Downstream steps (report.md, drift-report-template.md) consume this variable directly — no conditional at the usage site.
      
      ### 1. Get Skill Path
      
      "**Audit Skill — Drift Detection**
      
      Which skill would you like to audit? Please provide the skill name or path."
      
      **If user provides skill name (not full path) — version-aware path resolution (see `knowledge/version-paths.md`):**
      1. Read `{skills_output_folder}/.export-manifest.json` and look up the skill name in `exports` to get `active_version`
      2. If found: tentatively resolve `{skill_package}` = `{skills_output_folder}/{skill_name}/{active_version}/{skill_name}/`. **Manifest-vs-symlink drift gate:** before committing, also read the `active` symlink at `{skills_output_folder}/{skill_name}/active`. When the symlink target disagrees with `active_version`, the manifest lags behind on-disk state — typical sequence is `update-skill` flipped the symlink but `export-skill` has not yet rewritten the manifest. Auditing the older manifest version would re-audit a skill the user no longer cares about (or has already audited). Decide which version to audit:
         - Read `forge_data_folder/{skill_name}/{active_version}/provenance-map.json` (manifest version) and `forge_data_folder/{skill_name}/{symlink_target}/provenance-map.json` (symlink target). Compare their `generated_at` timestamps (or `mtime` if the field is absent).
         - If both versions exist and the symlink target's provenance is **fresher** than the manifest's `last_exported`, present a gate:
      
           "**Manifest lags behind active symlink.**
      
           | | Manifest | Symlink |
           |---|---|---|
           | Version | `{active_version}` | `{symlink_target}` |
           | Exported / forged | `{manifest.last_exported}` | `{symlink_provenance_generated_at}` |
      
           The manifest's `active_version` was set by an earlier export-skill run; the symlink was flipped later (typically by update-skill). Auditing the manifest version will re-audit a skill the user no longer treats as active. Options:
      
           - **[N] Audit symlink target ({symlink_target})** — recommended. The drift report describes the version the skill currently resolves to.
           - **[M] Audit manifest version ({active_version})** — only useful when investigating the older version specifically.
           - **[X] Abort** — halt without producing a report (exit 6, `halt_reason: "user-cancelled"`). Run `[EX] Export Skill` to reconcile the manifest before re-running audit-skill."
      
           Default is **[N]**. Headless mode auto-selects **[N]** with a loud log line: `"headless: manifest active_version ({active_version}) is older than symlink target ({symlink_target}); auditing symlink target. Run export-skill to reconcile."` When the manifest and on-disk state disagree, the working tree is the more honest signal under automation.
      
         - When the symlink target's provenance is **older** than (or equal to) the manifest's `last_exported`, the symlink predates the export — this is the normal post-export shape, no gate needed. Resolve to the manifest's `active_version`.
         - When only one of the two versions has a provenance map, resolve to the version that has one (the other is inert — auditing it would degrade to text-diff). Log the choice.
      
      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}`. Also store `audit_target_version` = the version that was actually selected (manifest, symlink, or flat) for step 6 Provenance to surface. When the gate above fired, also record the rejected version under `manifest_symlink_drift = {manifest: {active_version}, symlink: {symlink_target}, audited: {audit_target_version}, reason: {fresher-provenance|operator-choice|headless-default}}` so reviewers can audit the choice.
      
      **If user provides full path:**
      - Use as provided
      
      **Validate:** Check that `SKILL.md` exists at the resolved path.
      - If missing → "Skill not found at `{resolved_skill_package}`. Check the path and try again."
      - If found → Continue
      
      **Headless default** (when `{headless_mode}`): the interactive prompt and its "check the path and try again" re-prompt cannot be answered under automation, so §1 halts deterministically instead of looping. This is the origin site for the exit-2 / exit-3 rows the Exit Codes table attributes to step 1 §1 — emit the `SKF_AUDIT_RESULT_JSON` error envelope on **stderr** (shape per SKILL.md → Result Contract; `status: "error"`, `drift_score: null`, `report_path: null`, `next_workflow: null`, `audit_ref: null`) at each halt:
      - **No `skill_name` supplied** (neither name nor path given): HALT with **exit 2**, `halt_reason: "input-missing"`, `skill_name: null`. Log: `"headless: no skill_name supplied; cannot resolve interactively. Re-run with skill_name set."`
      - **`SKILL.md` missing at `{resolved_skill_package}`**: HALT with **exit 3**, `halt_reason: "skill-not-found"`, `skill_name: {skill_name}`. Log: `"headless: skill not found at {resolved_skill_package}; no interactive retry. Check the exported skill name/path."`
      
      ### 2. Load Forge Tier
      
      Load `{sidecar_path}/forge-tier.yaml` to detect available tools.
      
      **If file missing:**
      - "Setup-forge has not been run. Cannot determine tool availability. Run `[SF] Setup Forge` first."
      - HALT with **exit 3**, `halt_reason: "forge-tier-missing"`. When `{headless_mode}`, emit the error envelope on **stderr** (shape per SKILL.md → Result Contract) and log: `"headless: forge-tier.yaml missing at {sidecar_path}; run setup-forge. Aborting."`
      
      **If found:**
      - Extract tier level: Quick / Forge / Forge+ / Deep
      - Extract available tools: gh_bridge, ast_bridge, qmd_bridge — see `knowledge/tool-resolution.md` for concrete tool resolution per IDE
      
      **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.
      
      ### 3. Load Skill Artifacts
      
      Load the following from the skill directory:
      
      **Required:**
      - `SKILL.md` — The skill document to audit
      - `metadata.json` — Skill metadata (version, created date, export count)
      
      **Extract from metadata.json:**
      - `name`, `version`, `generation_date`, `confidence_tier` used during creation
      - `source_root` — Resolved source code path used during extraction
      
      **Detect split-body state:** If a `references/` directory exists and SKILL.md's `## Full` headings are absent or stubs, this is a split-body skill. Flag `split_body: true` in the baseline so downstream steps (especially semantic diff in step 4) know to also read `references/*.md` for complete content comparison.
      
      ### 4. Load Provenance Map
      
      Search for provenance map at `{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`:
      
      **Resolve `{loadProvenanceHelper}`** from `{loadProvenanceProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      **If found:**
      - Record provenance map age (days since last extraction) from file mtime
      - Normalize the map's deterministic projections in one subprocess call:
      
        ```bash
        uv run {loadProvenanceHelper} normalize {provenanceMap}
        ```
      
        Parse the emitted JSON and stash these fields in workflow context (downstream steps read them directly — no re-walk):
        - `{bounded_scan_files}` — sorted POSIX list, union of `entries[].source_file` and `file_entries[].source_file`. Step 2 `re-index.md` consumes this as the bounded scan list.
        - `{is_stack_skill}`, `{legacy_stack_provenance}` — stack-skill flags (see Stack Skill Detection below for downstream branching).
        - `{source_root}`, `{baseline_commit}`, `{baseline_ref}` — used by §5 Resolve Source Path and §5b Detect Upstream Drift.
        - `{reexport_map}` — `{<internal>: <public>}` mapping consumed by `structural-diff.md` §1 to collapse public-API renames before diffing.
      
        If the script exits non-zero, surface the stderr as a hard halt — the map is structurally invalid and downstream steps cannot proceed.
      
      **If missing at both paths:**
      - "No provenance map found for `{skill_name}`. This skill may not have been created by create-skill."
      - "**Degraded mode available:** I can perform text-based comparison without provenance data. Findings will have T1-low confidence."
      - "**[D]egraded mode** — proceed with text-diff only"
      - "**[X]** — abort audit"
      - Wait for user selection. If D, set `degraded_mode: true` and `confidence_mode = "degraded — all findings T1-low"`, then skip the normalize call above (no map to normalize). If X, halt workflow (exit 6, `halt_reason: "user-cancelled"`).
      
      **Headless default** (when `{headless_mode}`): consume the pre-supplied `degraded` input from the Invocation Contract. If `degraded=true`, auto-select **[D]** (set `degraded_mode: true`, `confidence_mode = "degraded — all findings T1-low"`, skip the normalize call) and log: `"headless: no provenance map for {skill_name}; proceeding in degraded mode (text-diff, T1-low) per pre-supplied degraded=true."` If `degraded` is unset or false, auto-select **[X] abort** (exit 6, `halt_reason: "user-cancelled"`) and log: `"headless: no provenance map for {skill_name} and degraded not pre-supplied; aborting. Re-run with degraded=true for text-diff."` Never silently emit a low-confidence report under automation without explicit opt-in — same stance as the [A]-abort default at §5b's dirty-worktree sub-gate.
      
      ### Stack Skill Detection
      
      `{is_stack_skill}` and `{legacy_stack_provenance}` are already resolved by the normalize call in §4 — no additional walk needed. Apply the post-detection logic:
      
      If `{is_stack_skill}` is true and `constituents` array is present (compose-mode stack):
      
      Re-hashing each constituent's live `metadata.json` and comparing against the compile-time snapshot is deterministic work — and the model cannot compute a `sha256:{hexdigest}` natively (it must shell out on every run). Delegate the whole read/hash/compare pass to the shared helper, which hashes byte-symmetrically with how `constituents[].metadata_hash` was written at compose time (the `sha256:`-prefixed digest of the raw `metadata.json` bytes). This mirrors the two sibling drift checks — `structural-diff.md` §4b (`skf-compare-file-hashes.py`) and `step-doc-drift.md` §2 (`skf-detect-docs.py compare-hashes`).
      
      **Resolve `{compareConstituentHashesHelper}`** from `{compareConstituentHashesProbeOrder}`; first existing path wins.
      
      Run one deterministic comparison subprocess over the provenance map's `constituents[]`. Relative constituent `skill_path` values (e.g. `skills/{skill-dir}/`) are project-root-relative, so resolve them against `{project-root}`:
      
      ```bash
      uv run {compareConstituentHashesHelper} compare-constituent-hashes {provenanceMap} --skills-root {project-root}
      ```
      
      Parse the emitted JSON:
      
      ```json
      {
        "drifted":           [{"skill_name": "...", "skill_path": "...", "stored_hash": "sha256:...", "current_hash": "sha256:..."}],
        "fresh":             [{"skill_name": "..."}],
        "missing":           [{"skill_name": "...", "skill_path": "...", "stored_hash": "sha256:...", "reason": "metadata-not-found|incomplete-record"}],
        "skipped_null_hash": [{"skill_name": "..."}],
        "stats": {"total": N, "drifted": N, "fresh": N, "missing": N, "skipped_null_hash": N}
      }
      ```
      
      - **Flag every entry in `drifted[]` as constituent drift with severity HIGH** — its live `metadata.json` differs from the compile-time snapshot recorded in the provenance map.
      - `fresh[]` are unchanged constituents. `missing[]` (each carrying a `reason`) are constituents whose `metadata.json` could not be located (`metadata-not-found`) or whose provenance record lacked `skill_name`/`skill_path` (`incomplete-record`) — surface these as a lower-severity note, not HIGH drift. `skipped_null_hash[]` had no compile-time baseline hash (recorded from a references/ cascade) — never reported as drift.
      - Record the constituent freshness results (the four buckets + `stats`) for the report — read the counts straight from `stats`, no manual recount.
      
      **If `uv`/the helper cannot execute** (e.g. claude.ai web): fall back to hashing by hand — for each constituent, read its `metadata.json` at `{constituent.skill_path}/active/{constituent.skill_name}/metadata.json` (resolve `skill_path` against `{project-root}` when relative, use as-is when absolute), SHA-256 the raw bytes, and compare against `constituent.metadata_hash` (a stored bare-hex form still matches after stripping any `sha256:` prefix from both sides). Flag mismatches as HIGH constituent drift.
      
      If `{legacy_stack_provenance}` is true: log a note that this stack uses v1 provenance format with reduced audit depth (library-level only, no per-export verification).
      
      ### 5. Resolve Source Path
      
      **If provenance map loaded:**
      - Use `{source_root}` (already extracted by §4 normalize) as source code path
      - Verify source path still exists and is accessible
      - `{baseline_commit}` and `{baseline_ref}` are already populated by §4. If `{baseline_commit}` is null in the projection, fall back to `metadata.source_commit`. `{baseline_ref}` may be a tag, branch, `HEAD`, or `"local"`.
      
      **If degraded mode:**
      - Ask user: "Please provide the path to the current source code."
      - `baseline_commit` and `baseline_ref` are unavailable — §5b will short-circuit
      
      **Validate:** Confirm the source directory exists and is accessible. If it is missing or unreadable → HALT with **exit 3**, `halt_reason: "source-dir-missing"`. When `{headless_mode}`, emit the error envelope on **stderr** (shape per SKILL.md → Result Contract) and log: `"headless: source directory {source_root} from the provenance map no longer exists; aborting."`
      
      ### 5b. Detect Upstream Drift
      
      Upstream drift detection is the primary use case of this workflow. If the local clone is still pinned to the baseline commit while upstream has shipped newer tags, auditing against the unchanged tree will misleadingly report CLEAN even after a major release.
      
      **Skip this section** if any of the following hold:
      - `baseline_ref` is `"local"`, `null`, or unset (non-git source)
      - `{source_root}` is not a git worktree (`git -C {source_root} rev-parse --git-dir` fails)
      - `baseline_commit` is unavailable
      - Degraded mode is active (no provenance map)
      
      When skipping, log the reason, then set the audit-ref context variables to baseline values so step 6 renders a coherent Provenance row: `audit_ref = baseline_ref or "(unknown)"`, `audit_ref_source = "baseline"` (or `"unavailable"` if both `baseline_ref` and `baseline_commit` are unset), `audit_commit = baseline_commit or "(unknown)"`, `latest_tag = null`, `remote_head = null`. Continue to §6.
      
      **Otherwise:**
      
      1. **Fetch upstream refs** (read-only, no working-tree mutation):
      
         ```bash
         git -C {source_root} fetch --tags --quiet origin
         ```
      
         If fetch fails (no network, no remote, detached clone), log the reason, record `upstream_fetch: "failed:{reason}"` in context, set `audit_ref = baseline_ref`, `audit_ref_source = "baseline"`, `audit_commit = baseline_commit`, `latest_tag = null`, `remote_head = null`, and continue to §6 without gating.
      
      2. **Find latest remote ref:**
         - Remote default-branch HEAD: `git -C {source_root} rev-parse origin/HEAD` (fall back to `origin/main` or `origin/master` if the symbolic ref is unavailable) — record as `remote_head`.
         - Newest semver tag: `git -C {source_root} for-each-ref --sort=-v:refname --format='%(refname:short)' 'refs/tags/v*' | head -1` — record as `latest_tag`.
      
      3. **Compare to baseline:**
         - If `baseline_commit` equals the commit that `remote_head` resolves to AND (`latest_tag` is empty OR semver-equals `baseline_ref` OR is older than `baseline_ref`), upstream has not moved. Set `audit_ref = baseline_ref`, `audit_ref_source = "baseline"`, `audit_commit = baseline_commit`. Continue to §6.
         - Otherwise upstream has moved — proceed to the gate.
      
      4. **User gate — Upstream drift detected:**
      
         "**Upstream has moved since this skill was created.**
      
         | | Baseline | Upstream |
         |---|---|---|
         | Ref | `{baseline_ref}` | `{latest_tag}` (newest tag) / `{remote_head}` (default HEAD) |
         | Commit | `{baseline_commit_short}` | `{latest_tag_commit_short}` / `{remote_head_short}` |
      
         Auditing against the baseline clone will report little-to-no structural drift even if the upstream API has changed. Options:
      
         - **[C] Checkout-and-audit-against-latest** — checkout `{latest_tag}` (or `{remote_head}` if no newer tag) in `{source_root}` and audit against that. Re-extraction will reflect the current upstream surface.
         - **[S] Stay-on-baseline** — keep `{source_root}` at `{baseline_ref}` and audit structural drift against the unchanged tree. The report will note `audit_ref = baseline`.
         - **[X] Abort** — halt the workflow without producing a report.
      
         **Select:** [C] / [S] / [X]"
      
         **Gate handling:**
         - **[C]:** Acquire an exclusive lock on `{source_root}/.skf-workspace.lock` (`flock -x` or `fcntl.flock(LOCK_EX)`) before mutating the working tree — matches the concurrency discipline in `src/skf-create-skill/references/source-resolution-protocols.md` and avoids racing with a concurrent create-skill / test-skill run against the same workspace clone. If `flock` is unavailable, emit a warning and proceed.
      
           **Dirty-worktree probe (mandatory before checkout).** Run `git -C {source_root} status --porcelain` after acquiring the lock and before the checkout. If the output is non-empty, the working tree has uncommitted changes — `git checkout {chosen_ref}` will abort with `error: Your local changes to the following files would be overwritten by checkout`, halting the workflow mid-step. The most common benign cause is a tooling-generated edit (e.g. the CCC daemon appending a `.cocoindex_code/` line to `.gitignore` after `setup-forge` pointed it at this clone), but the changes could also be the operator's in-progress work. Surface a sub-gate before mutating:
      
           "**Working tree has uncommitted changes.** `git status --porcelain` returned:
      
           ```
           {first 20 lines of porcelain output, ellipsis if more}
           ```
      
           A `git checkout` would abort. Options:
           - **[T] Transient stash** — `git stash push -m 'skf-audit: pre-checkout' --include-untracked`, perform the checkout, and pop the stash on the way out. Recommended when the changes look tooling-generated (e.g. a `.gitignore` line referencing `.cocoindex_code/`, lockfile churn from an indexer).
           - **[A] Abort** — halt the workflow and let the operator commit, stash, or discard manually before retrying.
           - **[F] Force checkout** — `git checkout --force` discards uncommitted changes irrecoverably. Only choose this after confirming the changes are safe to lose."
      
           **Gate handling:**
           - **[T]:** Run `git -C {source_root} stash push -m 'skf-audit-skill: pre-checkout {chosen_ref}' --include-untracked`. Capture the stash ref from the command output (e.g. `stash@{0}`) and store as `pre_checkout_stash_ref` in workflow context for step 6 Provenance to surface. Proceed to the checkout. (After audit completes, the operator restores the stash with `git stash pop` — step 6 puts the literal command in the report as a workflow-level convention rather than per-author ad-hoc prose.)
           - **[A]:** HALT the workflow. Do not write a drift report — the audit was never started.
           - **[F]:** Run `git -C {source_root} checkout --force {chosen_ref}` instead of the plain checkout. Record `pre_checkout_force_discard: true` in workflow context for step 6 to surface as a loud warning. Skip the stash path.
           - **Other input:** help user, redisplay the sub-gate.
      
           **Headless default** (when `{headless_mode}`): consume the pre-supplied `dirty_worktree_choice` from the Invocation Contract — the operator's explicit answer is the consent that a silent working-tree mutation would otherwise lack.
           - **`dirty_worktree_choice=T`**: run the `[T]` transient-stash path. Log: `"headless: dirty worktree at {source_root}; stashing before checkout per pre-supplied dirty_worktree_choice=T."`
           - **`dirty_worktree_choice=F` with `force=true`**: run the `[F]` force-checkout path — `force=true` is the required consent to discard uncommitted changes irrecoverably. Log: `"headless: dirty worktree at {source_root}; force-discarding uncommitted changes per pre-supplied dirty_worktree_choice=F force=true."`
           - **`dirty_worktree_choice=A`, unset, or `=F` without `force=true`**: auto-select **[A] Abort** (exit 6, `halt_reason: "user-cancelled"`). Abort is the safe default, and a force-discard without `force=true` consent is refused rather than executed. Log: `"headless: dirty worktree detected at {source_root}; refusing to checkout {chosen_ref} (dirty_worktree_choice={value or 'unset'}). Pass dirty_worktree_choice=T, or =F with force=true, to proceed non-interactively."` Stashing that is never popped could lose work; force-checkout without consent could destroy uncommitted work outright — so both require an explicit pre-supplied choice.
      
           If `git status --porcelain` is empty, skip the sub-gate and proceed directly to the checkout.
      
           Then `git -C {source_root} checkout {chosen_ref}` (prefer `latest_tag` when present, else `remote_head`). Set `audit_ref = {chosen_ref}`, `audit_ref_source = "checkout-latest"`, `audit_commit = git rev-parse HEAD`. Hold the lock through step 2 re-extraction and release only after the extraction snapshot is complete.
         - **[S]:** Keep baseline. Set `audit_ref = baseline_ref`, `audit_ref_source = "baseline"`, `audit_commit = baseline_commit`.
         - **[X]:** HALT workflow — do not create drift report.
         - **Other input:** help user, redisplay gate.
      
         **Headless default** (when `{headless_mode}`): consume the pre-supplied `upstream_drift_choice` from the Invocation Contract.
         - **`upstream_drift_choice=S`, or unset**: auto-select **[S] Stay-on-baseline** (default). Set `audit_ref = baseline_ref`, `audit_ref_source = "baseline"`, `audit_commit = baseline_commit`. Log: `"headless: upstream drift detected ({baseline_ref} → {latest_tag or remote_head}); staying on baseline per upstream_drift_choice={value or 'default S'}. Pass upstream_drift_choice=C to audit against latest."` Defaulting to a checkout would mutate the working tree without consent, so `[S]` remains the default when no choice is supplied.
         - **`upstream_drift_choice=C`**: run the `[C] Checkout-and-audit-against-latest` path above — the operator's pre-supplied choice is the explicit consent that a silent ref change would otherwise lack. The dirty-worktree sub-gate still applies and consults its own pre-supplied `dirty_worktree_choice`. Log: `"headless: upstream drift detected; checking out {latest_tag or remote_head} per pre-supplied upstream_drift_choice=C."`
         - **`upstream_drift_choice=X`**: HALT the workflow (exit 6, `halt_reason: "user-cancelled"`) — do not create a drift report. Log: `"headless: upstream drift detected; aborting per pre-supplied upstream_drift_choice=X."`
      
      5. **Record for report:** store `audit_ref`, `audit_ref_source`, `audit_commit`, `latest_tag`, `remote_head`, and `baseline_commit` in context. Step-06 surfaces them in the Provenance section so readers can tell which comparison actually ran.
      
      ### 6. Create Drift Report
      
      **Re-audit detection (before creating a fresh report).** Glob `{forge_version}/drift-report-*.md`. If one or more matches exist, identify the most recent one by file mtime (or by the `{timestamp}` segment in the filename when mtime is unreliable on the filesystem). If that report's age is `< 7 days` and its frontmatter `audit_ref` matches the current `{audit_ref}` (resolved in §5b), surface a soft gate before creating a fresh report:
      
        "**Recent audit found:** `{prior_report_path}` (created {prior_timestamp}, audit_ref=`{audit_ref}`).
        
        Options:
        - **[F] Fresh audit** (recommended; default in headless) — start a new drift report at `{outputFile}` and ignore the prior run.
        - **[D] Diff against prior report** — compute findings delta vs the prior report and emit a `## Diff Against Prior Report` subsection in step 6 (report.md).
        - **[R] Resume the prior report** — load the prior report's frontmatter (`stepsCompleted`, `drift_score`, intermediate findings) and jump to the next un-completed step instead of starting over.
        
        **Select:** [F] / [D] / [R]"
      
      **Gate handling:**
      - **[F]:** Default. Proceed with the fresh-report creation below — ignore the prior report.
      - **[D]:** Read the prior report's findings_list (parse the Structural/Semantic/Severity sections, or the appended findings tables) and stash as `prior_findings` in workflow context. Run the new audit normally. In step 6 (report.md), after the Remediation Suggestions section, emit a `## Diff Against Prior Report` subsection summarizing added / removed / changed findings vs `prior_findings`.
      - **[R]:** Load the prior report's frontmatter (`stepsCompleted`, `drift_score`, any intermediate state). Set `{outputFile}` to the prior report path (do NOT create a new one). Determine the next un-completed step from `stepsCompleted` and skip forward to it; downstream steps append to the existing report.
      
        > **Note (resumability):** the template frontmatter captures `stepsCompleted` and `drift_score` but does not persist an intermediate findings_list between stages. If [R] is selected and stepsCompleted indicates the prior run halted after structural-diff or semantic-diff, the appended report-body sections (`## Structural Drift`, `## Semantic Drift`, `## Severity Classification`) serve as the implicit intermediate state — re-parse them on resume rather than re-running completed stages.
      
      - **Other input:** help user, redisplay the gate.
      
      **Headless default** (when `{headless_mode}`): auto-select **[F]** and emit a loud log line: `"headless: recent audit found at {prior_report_path}; defaulting to fresh audit. Re-run interactively to choose [D]/[R]."`
      
      **Skip this gate** if no prior report matches the `< 7 days + same audit_ref` filter — proceed directly to creating the fresh report.
      
      **Create the fresh report** (when [F] is selected, headless default fires, or no prior report exists):
      
      Create `{outputFile}` from `{templateFile}`:
      
      - Populate frontmatter: skill_name, skill_path, source_path, forge_tier, date, user_name
      - Set `stepsCompleted: ['init']`
      - Fill Audit Summary skeleton with loaded baseline data
      
      If the write fails (read-only mount, disk full, permissions denied) → HALT with **exit 4**, `halt_reason: "write-failed"`. When `{headless_mode}`, emit the error envelope on **stderr** (shape per SKILL.md → Result Contract).
      
      ### 7. Present Baseline Summary and Confirm (User Gate)
      
      "**Audit Baseline Loaded**
      
      | Field | Value |
      |-------|-------|
      | **Skill** | {skill_name} v{version} |
      | **Created** | {generation_date} |
      | **Source** | {source_path} |
      | **Forge Tier** | {current_tier} (created at {original_tier}) |
      | **Provenance Age** | {days} days since last extraction |
      | **Export Count** | {count} exports in provenance map |
      | **Mode** | {normal / degraded} |
      
      **Analysis plan based on tier:**
      - {Quick: text-diff comparison (T1-low confidence)}
      - {Forge: AST structural comparison (T1 confidence)}
      - {Forge+: AST structural comparison + CCC-assisted rename detection (T1 confidence)}
      - {Deep: AST structural + QMD semantic comparison (T1 + T2 confidence)}
      
      **Ready to begin drift analysis?**"
      
      Halt and wait for the user's go-ahead. Only proceed once the drift report has been created with baseline data populated. On confirmation, save the baseline to `{outputFile}`, append `'init'` to the frontmatter `stepsCompleted`, then load, read the entire file, and execute `{nextStepFile}`. On any other input, help the user, then re-ask.
      
      **GATE [default: proceed]** — if `{headless_mode}`, auto-proceed and log: "headless: auto-continue past baseline confirmation".
      
      
    • re-index.md 9.8 KB
      ---
      nextStepFile: 'structural-diff.md'
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 2: Re-Index Source
      
      ## STEP GOAL:
      
      Re-scan the source code using the current forge tier tools to build a fresh extraction snapshot. This snapshot will be compared against the original provenance map in Step 03 to detect structural drift.
      
      ## Rules
      
      - Focus only on extracting current source state — do not compare yet (that's Step 03)
      - Extract every file in the bounded scan list — a file skipped here makes step 3 flag its exports as false "removed" drift
      - Use subprocess Pattern 2 (per-file deep analysis) when available for AST extraction; if unavailable, extract in main thread file by file
      
      ## MANDATORY SEQUENCE
      
      ### 1. Determine Extraction Strategy
      
      Based on forge tier detected in Step 01:
      
      **Quick tier (no AST tools):**
      - Read source files via gh_bridge or direct file I/O
      - Extract export names by text pattern matching (function/class/type declarations)
      - Confidence label: T1-low
      
      **Forge tier (ast-grep available):**
      - Use ast_bridge to perform AST extraction per source file
      - Extract: export name, type (function/class/type/const), full signature, file path, line number
      - Confidence label: T1
      
      **Forge+ tier (ast-grep + ccc available):**
      - Identical extraction to Forge tier: use ast_bridge for AST extraction per source file
      - Confidence label: T1
      - CCC rename detection available (see section 4b)
      
      **Deep tier (ast-grep + QMD available):**
      - Forge extraction (above) PLUS
      - Query qmd_bridge for temporal context: when exports were added, modification history, usage frequency
      - Confidence labels: T1 for structural, T2 for temporal context
      
      **Tool resolution:** `gh_bridge` → `gh api` commands or direct file I/O if local. `ast_bridge` → ast-grep MCP tools (`find_code`, `find_code_by_rule`) or `ast-grep` CLI. `qmd_bridge` → QMD MCP tools (`search`, `vector_search`) or `qmd` CLI. See `knowledge/tool-resolution.md`.
      
      ### 2. Build Bounded Scan List
      
      Audit-skill detects drift on files that were in scope during create-skill. The authoritative record of "what was in scope" is the provenance map loaded in step 1. Scan only those files — **audit-skill does NOT discover new files**. New-file detection is the responsibility of `skf-update-skill`, which maintains its own change manifest. To audit a project that has grown new files since creation, run update-skill first, then audit-skill.
      
      **Why bounded:** without this constraint, files that were deliberately excluded by the original brief's scope patterns (test fixtures, vendored code, generated artifacts, demo code, unrelated modules) get scanned on every audit and their exports are flagged by step 3 structural diff as "added" — false-positive drift that obscures real structural changes.
      
      **If a provenance map was loaded in step 1** (normal mode):
      
      1. The **bounded scan list** is `{bounded_scan_files}` — the union of `entries[].source_file` and `file_entries[].source_file`, deduplicated, sorted, and forward-slash normalized by init.md §4's `skf-load-provenance.py normalize` call. Consume it directly; do **not** re-walk the provenance map to rebuild it. The union/dedup/sort has one correct answer per map and is already scripted — re-deriving it in-prompt risks diverging from step 3, which diffs against the same normalized projection.
      2. Verify each path under `{source_root}`. Files that existed at creation time but are now missing are **not** errors at this stage — keep them in the list so step 3 can classify them as DELETED. Handling missing files is step 3's job, not step 2's.
      3. Record `bounded_scan: true` and `bounded_scan_source: "provenance-map"` in context for the evidence report.
      4. Report:
      
         "**Bounded scan:** {count} files from provenance map ({provenance_date})."
      
      **If degraded mode** (no provenance map was loaded — user confirmed `[D]egraded mode` at step 1 §4):
      
      1. Fall back to a source-tree scan: list all source files under `{source_root}` matching the project's primary language extensions (derive from `metadata.json.language` — e.g., `*.ts` / `*.tsx` for typescript, `*.py` for python, `*.rs` for rust, `*.go` for go).
      2. Apply generic exclusions: `**/tests/**`, `**/test/**`, `**/__tests__/**`, `*.test.*`, `*.spec.*`, `node_modules/**`, `dist/**`, `build/**`, `target/**`, `__pycache__/**`, `.venv/**`, `vendor/**`.
      3. Record `bounded_scan: false` and `bounded_scan_source: "source-tree-fallback"` in context.
      4. Report:
      
         "**Degraded mode scan:** {count} files from source tree (no provenance map — results may include files out of the original brief scope)."
      
      **Count files to process** and proceed to section 3 with the resolved scan list.
      
      ### 3. Extract Current Exports
      
      **For each file in the bounded scan list from §2, launch a subprocess that:**
      1. Loads the source file
      2. Extracts all public exports using tier-appropriate method
      3. Records: export name, type, signature, file path, line number, confidence tier
      4. Returns structured findings to parent
      
      **If a file from the bounded scan list is missing on disk:** record `{file, exports: [], status: "missing"}` and continue — step 3 structural diff will classify exports previously at this path as DELETED.
      
      **If subprocess unavailable:** Perform extraction in main thread, processing each file sequentially.
      
      **Build extraction snapshot and persist it to `{forge_version}/extraction-snapshot.json`** — step 3 (`structural-diff.md`) reads this file directly, so it must be written to disk, not merely held in context:
      ```
      {
        "extraction_date": "{timestamp}",
        "confidence_tier": "{tier}",
        "source_root": "{source_path}",
        "files_scanned": {count},
        "bounded_scan": true|false,
        "bounded_scan_source": "provenance-map|source-tree-fallback",
        "exports": [
          {
            "name": "{export_name}",
            "type": "function|class|type|const|interface",
            "signature": "{full signature}",
            "file": "{relative_path}",
            "line": {line_number},
            "confidence": "T1|T1-low|T2"
          }
        ]
      }
      ```
      
      Record the written path as `{extractionSnapshot}` in workflow context — step 3 passes it to the deterministic structural-diff helper.
      
      ### 4. Deep Tier Enhancement (Deep Only)
      
      **If forge tier is 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 current skill being audited AND `type` is `"extraction"`.
      
      Three collection states must be handled distinctly — semantic-diff.md §2 branches the same way:
      
      **If a matching extraction collection is found and populated** (pre-query probe via `qmd ls {collection_name}` or equivalent returns one or more files):
      Query qmd_bridge against the `{skill_name}-extraction` collection for temporal context on each extracted export:
      - When was this export first added?
      - Has it been modified recently?
      - What is its usage frequency across the codebase?
      - How does the current extraction compare to the previously compiled skill content?
      
      Append temporal metadata to each export in the snapshot.
      
      **If a matching extraction collection is found but empty** (pre-query probe reports `Files: 0 (updated never)` or an empty listing):
      Log: "QMD collection `{collection_name}` is registered but empty. Run `qmd update` to (re-)index `{collection.path}`, then re-audit. Temporal enrichment skipped for this run."
      Continue without T2 enrichment — the unpopulated collection is a setup gap, not an extraction failure. Step-04 will fall through to its direct-content fallback for semantic diff.
      
      **If no matching collection found in registry:**
      Log: "No QMD extraction collection found for {skill_name}. Temporal enrichment skipped. Re-run [CS] Create Skill to generate the collection."
      Continue without T2 enrichment — this is not an error.
      
      **If forge tier is Quick, Forge, or Forge+:**
      Skip this section. Temporal context requires Deep tier.
      
      ### 4b. CCC Rename Detection (Forge+ and Deep with ccc)
      
      **If `tools.ccc` is true in forge-tier.yaml:**
      
      For each export in the skill baseline that was NOT found at its recorded file path during re-extraction (potential "deleted" export):
      
      1. Run `ccc_bridge.search("{export_name}", source_root, top_k=5)` — **Tool resolution:** Use `/ccc` skill search (Claude Code), ccc MCP server (Cursor), or `cd {source_root} && ccc search --limit 5 "{export_name}"` (CLI) — to find candidate current locations. `ccc search` reads the index in the current working directory and has no project-selector flag (`--path` is a file-path glob filter *within* the index, and the result cap is `--limit`, not `--top`) — see `knowledge/ccc-bridge.md`.
      2. If CCC returns files containing the export name:
         - Run ast-grep verification on each candidate file
         - If verified at a new location: reclassify from "deleted" to "moved" with the new file:line reference
         - This reduces false-positive structural drift findings where exports were relocated, not removed
      3. If CCC returns no results or verification fails: keep the "deleted" classification
      
      CCC failures: skip rename detection silently, proceed with standard structural diff.
      
      **If `tools.ccc` is false:** Skip this section silently.
      
      ### 5. Validate Extraction Completeness
      
      "**Extraction complete.**
      
      | Metric | Value |
      |--------|-------|
      | Scan mode | {bounded (provenance-map) / degraded (source-tree)} |
      | Files scanned | {count} |
      | Exports found | {total_exports} |
      | Functions | {function_count} |
      | Classes | {class_count} |
      | Types/Interfaces | {type_count} |
      | Constants | {const_count} |
      | Confidence | {T1/T1-low/T2} |
      
      **Proceeding to structural comparison...**"
      
      ### 6. Update Report and Auto-Proceed
      
      Update {outputFile} frontmatter — append `'re-index'` to `stepsCompleted`. Once the extraction snapshot is complete with all source files processed, load, read fully, and execute `{nextStepFile}` (structural diff).
      
      
    • report.md 9.4 KB
      ---
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      nextStepFile: 'health-check.md'
      ---
      
      <!-- Config: communicate in {communication_language}. Drift report prose in {document_output_language}. -->
      
      # Step 6: Generate Report
      
      ## STEP GOAL:
      
      Finalize the drift report by completing the Audit Summary with calculated metrics, generating actionable remediation suggestions for each drift finding, and adding provenance metadata. Present the final report to the user with a next-workflow recommendation.
      
      ## Rules
      
      - Focus on completing the report — summary, remediation, provenance
      - Do not discover new drift items or reclassify severity
      - Remediation suggestions must be practical: what to change, where, and why
      - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing summary is NOT the terminal step
      
      ## MANDATORY SEQUENCE
      
      ### 1. Complete Audit Summary
      
      Update the ## Audit Summary section at the top of {outputFile} with final calculated values:
      
      - Fill in severity count table from Step 05 classification summary
      - Set overall drift score
      - Add total findings count
      - Include doc drift summary from `doc_drift_summary` context (set by step 5a):
        - If `changed > 0`: "**Doc Drift:** {changed} of {total_tracked} tracked doc(s) have changed since compile. Consider re-running CS to update doc_sources."
        - If `fetch_failed > 0`: "{fetch_failed} doc URL(s) could not be reached during audit."
        - If `skipped_entirely`: no mention in summary (already noted in the doc drift section)
      
      ### 2. Generate Remediation Suggestions
      
      For each classified drift finding, write one concrete remediation derived from the finding itself: **what** to change in the audited **SKILL.md** (or its `references/`) — not the source code — **where** (the section plus the source `{file}:{line}` the finding cites), and **why**. Set effort (`low`/`medium`/`high`) by how much of the skill doc the change touches. A reviewer should be able to act on each row without re-deriving the finding.
      
      Append to {outputFile}:
      
      ```markdown
      ## Remediation Suggestions
      
      ### Priority Actions (CRITICAL + HIGH)
      
      | # | Severity | Finding | Remediation | Effort |
      |---|----------|---------|-------------|--------|
      | 1 | {severity} | {finding} | {specific action} | {low/medium/high} |
      
      ### Recommended Updates (MEDIUM)
      
      | # | Finding | Remediation | Effort |
      |---|---------|-------------|--------|
      | 1 | {finding} | {specific action} | {low/medium} |
      
      ### Optional Improvements (LOW)
      
      | # | Finding | Remediation |
      |---|---------|-------------|
      | 1 | {finding} | {specific action} |
      
      ### Workflow Recommendation
      
      {IF any CRITICAL or HIGH findings:}
      **Recommended:** Run `[US] Update Skill` workflow to apply priority remediations automatically.
      
      {IF `audit_ref != baseline_ref` (source version bump detected in step 1 §5b):}
      **Version preservation (non-destructive).** `update-skill` preserves the prior version at `{skill_group}/{baseline_version}/` unchanged and writes the new skill to `{skill_group}/{audit_version}/` (see `skf-update-skill/references/merge.md` §6b, which creates the new version directory and leaves the previous one on disk). The `active` symlink at `{skill_group}/active` repoints to the new version (see `skf-update-skill/references/write.md` §5b). On the next export, the prior version's export-manifest entry transitions to `status: archived` — files retained for rollback (see `skf-export-skill/references/update-context.md`). Do **not** recommend `skf-drop-skill` + `skf-create-skill` for a version bump — that destroys the prior version's artifacts.
      
      **Surface new entry points for the brief gate.** If the audit observed new top-level modules, renamed package trees, or new public entry points (new `__init__.py`, `index.ts`, `lib.rs`, or equivalent) that were not in the brief's original scope, call them out here. `update-skill` step 2 §1b detects new candidate files via heuristic and prompts `[P]romote` / `[S]kip` / `[U]pdate-brief`; surfacing them in advance makes that gate faster to resolve, or lets the user refine scope via `skf-brief-skill` before running update-skill.
      
      {IF only MEDIUM or LOW findings:}
      **Optional:** Minor drift detected. Manual updates sufficient, or run `[US] Update Skill` for automated remediation.
      
      {IF CLEAN:}
      **No action needed.** Skill is current with source code.
      ```
      
      ### 3. Add Provenance Section
      
      Append to {outputFile}:
      
      ```markdown
      ## Provenance
      
      | Field | Value |
      |-------|-------|
      | **Audit Date** | {current_date} |
      | **Audited By** | Ferris (Audit mode) |
      | **Forge Tier** | {tier} |
      | **Tools Used** | {tool_list based on tier} |
      | **Source Path** | {source_path} |
      | **Skill Path** | {skill_path} |
      | **Provenance Map** | {provenance_map_path} |
      | **Provenance Age** | {days} days |
      | **Mode** | {normal / degraded} |
      | **Baseline Ref / Commit** | `{baseline_ref}` @ `{baseline_commit_short}` |
      | **Audit Ref / Commit** | `{audit_ref}` @ `{audit_commit_short}` ({audit_ref_source}) |
      | **Upstream Latest** | `{latest_tag or remote_head or "(not fetched)"}` |
      
      **Confidence Legend:**
      - **T1:** AST extraction — high reliability, structural truth
      - **T1-low:** Text pattern matching — moderate reliability
      - **T2:** QMD temporal context — evidence-backed semantic analysis
      ```
      
      ### 4. Update Report Frontmatter
      
      Update {outputFile} frontmatter:
      - Append `'report'` to `stepsCompleted`
      - Set `drift_score` to the score from step 5's classification helper
      - Set `nextWorkflow` to `'update-skill'` if CRITICAL or HIGH findings, otherwise leave empty
      
      If finalizing the report or writing the result JSON below fails (read-only mount, disk full, permissions denied) → HALT with **exit 4**, `halt_reason: "write-failed"`. When `{headless_mode}`, emit the error envelope on **stderr** (shape per SKILL.md → Result Contract) with `report_path: null`.
      
      ### 5. Present Final Report Summary
      
      Present a concise completion summary to the user conveying: the skill name, the **overall drift score** (CLEAN / MINOR / SIGNIFICANT / CRITICAL), the severity-count table (CRITICAL / HIGH / MEDIUM / LOW / Total), and the saved report path (`{outputFile}`). Close with the next-action recommendation matching the drift level:
      
      - **CRITICAL or HIGH findings** → action required: recommend running the `[US] Update Skill` workflow to apply the priority remediations; manual review at `{outputFile}` is the alternative.
      - **MEDIUM or LOW only** → minor drift: manual updates suffice, or run `[US] Update Skill` for automated remediation.
      - **CLEAN** → no action needed; the skill is current and ready for `[EX] Export Skill`.
      
      This summary reads as final but is **not** the terminal step — proceed to §6.
      
      ### Result Contract
      
      Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{forge_version}/audit-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_version}/audit-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include the drift report path in `outputs`; include `drift_count` and `severity` (CLEAN/MINOR/SIGNIFICANT/CRITICAL) in `summary`.
      
      **Stdout envelope (headless only).** When `{headless_mode}` is true, emit a single-line JSON envelope to **stdout** immediately after the on-disk result contract is written, so chaining workflows can consume `drift_score`, `report_path`, and `next_workflow` from a captured stdout line without polling the filesystem. The shape matches the "Result Contract (Headless)" section in SKILL.md verbatim:
      
      ```
      SKF_AUDIT_RESULT_JSON: {"status":"success","skill_name":"{skill_name}","drift_score":"{CLEAN|MINOR|SIGNIFICANT|CRITICAL}","report_path":"{outputFile}","next_workflow":"{update-skill|null}","audit_ref":"{audit_ref}","exit_code":0,"halt_reason":null}
      ```
      
      Field rules: `next_workflow` is `"update-skill"` when CRITICAL or HIGH findings exist (matches the frontmatter `nextWorkflow` set in §4), otherwise `null`. `audit_ref` carries the resolved value from step 1 §5b (`baseline_ref` when no upstream drift was detected, `latest_tag` or `remote_head` when the operator chose `[C] Checkout-and-audit-against-latest`).
      
      **Hard-halt envelope (headless only).** Every hard halt emits this same envelope shape on **stderr** with `status: "error"` and the `exit_code` / `halt_reason` for its failure class (per SKILL.md → Exit Codes and Result Contract), produced at the halting site before exit — it is the only failure signal a wrapping pipeline receives, so log it before exiting. `drift_score` carries its last known value (`null` if classification never ran); `report_path` is `null` when the report write failed.
      
      **Post-audit hook (optional).** If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 from `workflow.on_complete`), invoke it as:
      
      ```bash
      {onCompleteCommand} --result-path={result_json_path}
      ```
      
      where `{result_json_path}` is the per-run record path written above (`{forge_version}/audit-skill-result-{YYYYMMDD-HHmmss}.json`). Log success/failure to `workflow_warnings[]` — never fail the workflow on a hook error. The hook runs after the result contract is finalized so notifiers, ticket-tracker integrations, or downstream pipelines see a complete record. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
      
      ### 6. Chain to Health Check
      
      Only when the report has been written, presented, and the result contract saved do you then load, read the full file, and execute `{nextStepFile}`. The health-check step is the true terminal step — do not stop here even though the user-facing summary reads as final.
      
      
    • semantic-diff.md 6.2 KB
      ---
      nextStepFile: 'severity-classify.md'
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 4: Semantic Diff
      
      ## STEP GOAL:
      
      Compare QMD knowledge context between the original skill creation and current state to detect meaning-level changes that structural diff cannot catch. This step executes ONLY at Deep tier — at Quick, Forge, and Forge+ tiers, it appends a skip notice and auto-proceeds.
      
      ## Rules
      
      - At Quick/Forge/Forge+ tier, skip the entire analysis — append the skip notice only
      - Focus only on semantic/meaning-level changes via QMD context — do not repeat structural findings from Step 03
      - Do not classify severity (Step 05)
      - Use subprocess Pattern 3 when available for QMD queries; if unavailable, query in main thread
      
      ## MANDATORY SEQUENCE
      
      ### 1. Check Forge Tier
      
      **If forge tier is Quick, Forge, or Forge+:**
      
      Append to {outputFile}:
      
      ```markdown
      ## Semantic Drift
      
      **Status:** Skipped — Semantic diff requires Deep tier (current tier: {tier})
      
      Semantic analysis compares QMD knowledge context for meaning-level changes that structural diff cannot detect. To enable semantic diff, run setup with QMD available to unlock Deep tier.
      ```
      
      Update frontmatter: append `'semantic-diff'` to `stepsCompleted`
      
      "**Semantic diff skipped (requires Deep tier). Proceeding to severity classification...**"
      
      → Auto-proceed to {nextStepFile}
      
      **If forge tier is Deep:**
      
      Continue to section 2.
      
      ### 2. Query Original Knowledge Context
      
      Launch a subprocess (Pattern 3 — data operations) that:
      1. Read the `qmd_collections` registry from `{sidecar_path}/forge-tier.yaml`. Find the entry where `skill_name` matches `{skill_name}` AND `type` is `"extraction"`. Three cases must be handled distinctly — collapsing them into "found vs. not found" silently degrades semantic diff when a collection is registered but never indexed.
      
         - **Registry entry missing.** Log: "No QMD extraction collection found for `{skill_name}`. Semantic diff skipped." → Auto-proceed to {nextStepFile}.
         - **Registry entry present but collection empty.** Run a pre-query probe — `qmd ls {collection_name}` (CLI) or the equivalent MCP call. If it reports zero files (`Files: 0 (updated never)` or an empty listing), the collection is registered but has never been indexed. Do **not** proceed to querying — queries will return nothing and the step would silently degrade.
           - Log: "QMD collection `{collection_name}` is registered but empty. Run `qmd update` to (re-)index `{collection.path}`, then re-audit for full Deep-tier semantic coverage."
           - Fall through to the **direct-content fallback** below instead of skipping outright.
         - **Registry entry present and populated.** Use the `name` field from the registry entry as the collection to query. Proceed to bullet 2.
      
         **Direct-content fallback** (used when the collection is registered but empty): load `SKILL.md` and `references/*.md` from the audited skill, then spot-check each documented export against the current source tree under `{source_root}` using the Deep-tier AST tooling this step already requires (ast_bridge; see step 2 §1 "Deep tier"). This fallback is reachable only from Deep tier — §1 short-circuits Quick/Forge/Forge+ before §2 runs, so AST tooling is guaranteed available here. Record findings with confidence label `T1-low-fallback` rather than T2 — this is direct content inspection, not QMD-backed semantic analysis. The step's output schema is otherwise unchanged; set `qmd_collection = null` in the Semantic Drift header and annotate: "Semantic diff ran in direct-content fallback mode — QMD collection was registered but empty."
      
      2. Queries for knowledge context around each export documented in the skill
      3. Retrieves: usage patterns, conventions, architectural context, dependency relationships
      4. Returns structured findings to parent
      
      **If subprocess unavailable:** Query QMD in main thread.
      
      ### 3. Compare Knowledge Context
      
      For each export in the skill, compare original context (from skill creation) against current context (from QMD):
      
      **Detect:**
      - **New patterns:** Usage patterns that have emerged since skill was created
      - **Changed conventions:** Project conventions that have shifted (e.g., new error handling pattern)
      - **Dependency shifts:** Libraries or modules that exports now depend on differently
      - **Architectural changes:** Structural reorganization affecting how exports relate to each other
      - **Deprecated patterns:** Usage patterns documented in skill that are no longer followed
      
      For each finding, record:
      - What changed (description)
      - Evidence (QMD reference or source citation)
      - Affected exports
      - Confidence: T2
      
      ### 4. Compile Semantic Drift Section
      
      Append to {outputFile}:
      
      ```markdown
      ## Semantic Drift
      
      **Method:** QMD knowledge context comparison (Deep tier)
      **QMD Collection:** {collection_name}
      
      ### New Patterns Detected ({count})
      
      | Pattern | Description | Affected Exports | Evidence | Confidence |
      |---------|------------|-----------------|----------|------------|
      | {pattern} | {description} | {exports} | {evidence} | T2 |
      
      ### Changed Conventions ({count})
      
      | Convention | Before | After | Affected Exports | Evidence | Confidence |
      |-----------|--------|-------|-----------------|----------|------------|
      | {convention} | {old} | {new} | {exports} | {evidence} | T2 |
      
      ### Dependency Shifts ({count})
      
      | Export | Original Dependencies | Current Dependencies | Change | Confidence |
      |--------|---------------------|---------------------|--------|------------|
      | {export} | {old_deps} | {new_deps} | {description} | T2 |
      
      ### Deprecated Patterns ({count})
      
      | Pattern | Documented In Skill | Current Status | Evidence | Confidence |
      |---------|-------------------|----------------|----------|------------|
      | {pattern} | {skill_reference} | {status} | {evidence} | T2 |
      
      ### Summary
      
      | Category | Count |
      |----------|-------|
      | New patterns | {count} |
      | Changed conventions | {count} |
      | Dependency shifts | {count} |
      | Deprecated patterns | {count} |
      | **Total Semantic Items** | {total} |
      ```
      
      ### 5. Update Report and Auto-Proceed
      
      Update {outputFile} frontmatter — append `'semantic-diff'` to `stepsCompleted`. Once the ## Semantic Drift section (or skip notice) has been appended, load, read fully, and execute `{nextStepFile}` (severity classification).
      
      
    • severity-classify.md 6.3 KB
      ---
      nextStepFile: 'step-doc-drift.md'
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      severityRulesFile: '{severityRulesPath}'
      severityClassifyProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-severity-classify.py'
        - '{project-root}/src/shared/scripts/skf-severity-classify.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5: Severity Classification
      
      ## STEP GOAL:
      
      Grade every drift finding from Steps 03 and 04 by severity (CRITICAL/HIGH/MEDIUM/LOW), derive the overall drift score, and produce a categorized findings table with confidence-tier labels.
      
      ## Rules
      
      - Only classify existing findings — do not discover new drift items or suggest remediation
      - Reading each change and assigning its `type` / `category` is judgment; mapping those to a severity, reducing the set to the drift score, and counting per level are deterministic and delegated to the shared helper so the classification cannot drift from {severityRulesFile} between runs
      - The confidence tier (T1 / T1-low / T2) travels with each finding from Steps 03/04 — the helper never touches it
      
      ## MANDATORY SEQUENCE
      
      ### 1. Collect and Categorize Findings
      
      Gather every drift item already recorded in the report:
      
      **From ## Structural Drift (Step 03):** added, removed, changed, and moved exports (plus any Script/Asset Drift rows).
      **From ## Semantic Drift (Step 04, Deep tier only):** new patterns, changed conventions, dependency shifts, deprecated patterns.
      
      For each finding, read {severityRulesFile} and assign the two interpretive fields the rules key on — this is the judgment step, where the nuance of the change lives:
      
      - `type`: `removed` / `added` / `changed` / `moved` / `renamed` / `deprecated` / `semantic`
      - `category`: what the change is about — e.g. `export`, `module`, `class`, `interface`, `signature`, `parameter_count`, `return_type`, `inheritance`, `internal_helper`, `default_value`, `required_parameter`, `implementation`, `optional_parameter`, `function` (for a move), or a LOW bucket (`style`, `convention`, `comment`, `documentation`, `whitespace`, `test`, `private`, `internal`). The category carries the interpretation: a removed helper referenced in a documented pattern is `internal_helper` (→ HIGH); a new private function is `private` (→ LOW).
      
      Build one JSON array of `{type, category, detail, confidence, file, line}` objects — carry each finding's `confidence`, `file`, `line`, and human `detail` through untouched.
      
      ### 2. Classify, Score, and Count (deterministic)
      
      Mapping type/category to a severity, reducing the severities to an overall drift score, and counting per level each have exactly one correct answer for a given finding set — delegate them to the shared helper, which encodes {severityRulesFile} directly.
      
      **Resolve `{severityClassifyHelper}`** from `{severityClassifyProbeOrder}`; first existing path wins.
      
      Pipe the findings array from §1 to the helper on stdin:
      
      ```bash
      echo '{findings_json}' | uv run {severityClassifyHelper} -
      ```
      
      Parse the emitted JSON:
      
      ```json
      {
        "status": "ok",
        "drift_score": "CLEAN|MINOR|SIGNIFICANT|CRITICAL",
        "total_findings": N,
        "by_severity": {"CRITICAL": N, "HIGH": N, "MEDIUM": N, "LOW": N},
        "findings": [ {"type": "...", "category": "...", "detail": "...", "confidence": "...", "severity": "CRITICAL|HIGH|MEDIUM|LOW"} ]
      }
      ```
      
      Consume `by_severity`, `drift_score`, and each finding's assigned `severity` directly — do not recount or recompute the score in prose.
      
      **If `uv`/the helper cannot execute** (e.g. claude.ai web): fall back to classifying in the main thread — apply {severityRulesFile}'s severity levels to each finding's type/category, then reduce with its Overall Drift Score table (CLEAN = no findings; MINOR = LOW only; SIGNIFICANT = any MEDIUM/HIGH, no CRITICAL; CRITICAL = any CRITICAL present).
      
      ### 3. Compile Severity Classification Section
      
      **Rollup inherits from step 3.** If step 3 §5 collapsed ≥ 10 same-kind findings into a single rollup row (deleted source file, renamed module, entire package tree removed), carry that rollup through to the matching severity table as one row — do not re-expand it here. Keep the 6-column severity table shape; the rollup encodes root cause, count, and representative symbols **inline in the `Finding` cell** rather than adding columns, so rollup and per-item rows render cleanly in one table. Changed-signature and cross-file findings remain per-row; they were not eligible for rollup in step 3 and are not eligible here.
      
      **Rollup row form (any severity table):**
      
      | # | Finding | Type | Detail | Location | Confidence |
      |---|---------|------|--------|----------|------------|
      | N | {root cause} (×{Count}; rep: `{sym1}`, `{sym2}`, `{sym3}`, …) | {structural/semantic} | {shared detail} | {root-cause path} | {T1/T2} |
      
      Append to {outputFile}, filling the counts from `by_severity` and each finding's assigned `severity`:
      
      ```markdown
      ## Severity Classification
      
      **Overall Drift Score: {drift_score}**
      
      ### CRITICAL ({by_severity.CRITICAL})
      
      | # | Finding | Type | Detail | Location | Confidence |
      |---|---------|------|--------|----------|------------|
      | 1 | {finding} | {structural/semantic} | {detail} | {file}:{line} | {T1/T2} |
      
      ### HIGH ({by_severity.HIGH})
      
      | # | Finding | Type | Detail | Location | Confidence |
      |---|---------|------|--------|----------|------------|
      | 1 | {finding} | {structural/semantic} | {detail} | {file}:{line} | {T1/T2} |
      
      ### MEDIUM ({by_severity.MEDIUM})
      
      | # | Finding | Type | Detail | Location | Confidence |
      |---|---------|------|--------|----------|------------|
      | 1 | {finding} | {structural/semantic} | {detail} | {file}:{line} | {T1/T2} |
      
      ### LOW ({by_severity.LOW})
      
      | # | Finding | Type | Detail | Location | Confidence |
      |---|---------|------|--------|----------|------------|
      | 1 | {finding} | {structural/semantic} | {detail} | {file}:{line} | {T1/T2} |
      
      ### Classification Summary
      
      | Severity | Count |
      |----------|-------|
      | CRITICAL | {by_severity.CRITICAL} |
      | HIGH | {by_severity.HIGH} |
      | MEDIUM | {by_severity.MEDIUM} |
      | LOW | {by_severity.LOW} |
      | **Total** | {total_findings} |
      ```
      
      ### 4. Update Report and Auto-Proceed
      
      Update {outputFile} frontmatter:
      - Append `'severity-classify'` to `stepsCompleted`
      - Set `drift_score` to `{drift_score}` from the helper
      
      Once the ## Severity Classification section has been appended with all findings classified, load, read fully, and execute `{nextStepFile}` (documentation drift).
      
    • severity-rules.md 2.5 KB
      <!-- Config: communicate in {communication_language}. -->
      
      # Severity Classification Rules
      
      ## Severity Levels
      
      ### CRITICAL — Breaking Changes
      - Removed or renamed public exports (functions, classes, types)
      - Changed function signatures (parameter count, parameter types, return type)
      - Removed or renamed modules/files referenced in skill
      - Changed class inheritance or interface contracts
      - **Impact:** Skill instructions will produce incorrect code if followed
      
      ### HIGH — Significant Drift
      - New public API exports not documented in skill (>3 new exports)
      - Removed internal helpers that are referenced in documented patterns
      - Changed default parameter values that affect documented behavior
      - New required parameters added to documented functions
      - Deprecated APIs still documented as current in skill
      - **Impact:** Skill is incomplete or contains outdated guidance
      
      ### MEDIUM — Moderate Drift
      - Implementation changes behind a stable public API
      - New optional parameters with defaults on documented functions
      - New public exports not in skill (1-3 new exports)
      - Moved functions between files (same API, different location)
      - Changed internal implementation patterns documented in skill conventions
      - **Impact:** Skill is functional but not fully current
      
      ### LOW — Minor Drift
      - Style or convention changes (formatting, naming patterns)
      - Comment or documentation changes in source
      - Whitespace or structural reorganization
      - New private/internal functions not affecting public API
      - Test file changes
      - **Impact:** Cosmetic — skill remains accurate for practical use
      
      ## Overall Drift Score
      
      | Score       | Criteria                                 |
      |-------------|------------------------------------------|
      | CLEAN       | 0 findings at any level                  |
      | MINOR       | LOW findings only, no MEDIUM+            |
      | SIGNIFICANT | Any MEDIUM or HIGH findings, no CRITICAL |
      | CRITICAL    | Any CRITICAL findings present            |
      
      ## Confidence Tier Labels
      
      | Label  | Source                           | Reliability                             |
      |--------|----------------------------------|-----------------------------------------|
      | T1     | AST extraction (ast-grep)        | High — structural truth                 |
      | T1-low | Text pattern matching (no AST)   | Moderate — pattern-based                |
      | T2     | QMD semantic context             | High — evidence-backed temporal context |
      | T3     | External documentation reference | Variable — secondary source             |
      
    • step-doc-drift.md 7.5 KB
      ---
      nextStepFile: 'report.md'
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      # Resolve `{compareDocHashesHelper}` by probing `{compareDocHashesProbeOrder}`
      # in order (installed SKF module path first, src/ dev-checkout fallback); first
      # existing path wins. When neither resolves (uv/python absent), §2 falls back to
      # skipping the doc-drift check with a notice — see the graceful-failure rule.
      compareDocHashesProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-detect-docs.py'
        - '{project-root}/src/shared/scripts/skf-detect-docs.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 5a: Documentation Drift
      
      ## STEP GOAL:
      
      Compare documentation content hashes stored at compile time (in `doc_sources` within metadata.json) against the current upstream state. Produce a drift section that reports which tracked docs have changed, which are unreachable, and which were never hashed. This step is informational — doc drift does not affect the source code drift score.
      
      ## Rules
      
      - Auto-proceed step — no user interaction
      - Graceful failure — if doc fetching fails for any URL, mark as `fetch_failed`, do not block the audit
      - Do not classify severity — doc drift is informational alongside source drift
      - If no `doc_sources` in metadata, skip with notice and auto-proceed
      - Never abort the audit pipeline on any failure in this step
      
      ## MANDATORY SEQUENCE
      
      ### 1. Check for doc_sources
      
      Check the skill metadata loaded at init (step 1 §3 — Load Skill Artifacts) for a `doc_sources` array.
      
      **If `doc_sources` is absent or metadata lacks the field:**
      
      Append to {outputFile}:
      
      ```markdown
      ## Documentation Drift
      
      No doc_sources recorded — skip doc drift check. This skill was compiled before doc tracking was available. Recompile with the current CS pipeline to enable doc drift detection.
      ```
      
      Set `doc_drift_summary = { skipped_entirely: true }` in workflow context. Update {outputFile} frontmatter: append `'doc-drift'` to `stepsCompleted`. Auto-proceed to {nextStepFile}.
      
      **If `doc_sources` is present but an empty array:**
      
      Append to {outputFile}:
      
      ```markdown
      ## Documentation Drift
      
      No documentation sources tracked. The `doc_sources` array is empty — no drift check to perform.
      ```
      
      Set `doc_drift_summary = { total_tracked: 0, skipped_entirely: false }` in workflow context. Update {outputFile} frontmatter: append `'doc-drift'` to `stepsCompleted`. Auto-proceed to {nextStepFile}.
      
      **If `doc_sources` is present and non-empty:** Continue to §2.
      
      ### 2. Fetch and Hash Each Tracked Doc
      
      Fetching each URL, hashing the response bytes, comparing against the stored `content_hash`, and categorizing the outcome is deterministic work — and the model cannot compute a `sha256:{hexdigest}` of fetched bytes natively (it must shell out). Delegate the whole fetch/hash/compare pass to the shared script, which hashes byte-symmetrically with how `doc_sources` hashes were written at compile time and fetches independently of whether a WebFetch tool is wired.
      
      **Resolve `{compareDocHashesHelper}`** from `{compareDocHashesProbeOrder}`; first existing path wins.
      
      Run one deterministic comparison subprocess over the skill's `doc_sources`. The script reads the `doc_sources` array straight out of the skill's `metadata.json` (or accepts a bare `doc_sources` array, or `-` for stdin):
      
      ```bash
      uv run {compareDocHashesHelper} compare-hashes {skill_path}/metadata.json
      ```
      
      For each entry, the script:
      - **`content_hash` is `null`** → records it under `skipped_null_hash` and does **not** fetch the URL (there is no baseline to compare against).
      - **`content_hash` is non-null** → HTTP GETs the URL (15s timeout, same User-Agent as the compile side), computes `sha256:{hexdigest}` of the response body bytes, and compares against the stored `content_hash` (prefix-normalized so a bare-hex writer form still matches).
        - Hashes match → `unchanged`.
        - Hashes differ → `changed`, carrying `old_hash` and `new_hash`.
        - Network error, timeout, or non-200 status → `fetch_failed`, carrying the failure `reason`. Not reported as drift.
      
      Parse the emitted JSON:
      
      ```json
      {
        "changed":           [{"url": "...", "old_hash": "sha256:...", "new_hash": "sha256:..."}],
        "unchanged":         [{"url": "..."}],
        "fetch_failed":      [{"url": "...", "old_hash": "sha256:...", "reason": "..."}],
        "skipped_null_hash": [{"url": "..."}],
        "stats": {"total_tracked": N, "changed": N, "unchanged": N, "fetch_failed": N, "skipped_null_hash": N}
      }
      ```
      
      The script exits 0 on any well-formed input (even when everything drifted) and exits 2 only on malformed args/JSON — it never blocks this informational audit.
      
      If `uv`/`python` is unavailable or the script cannot be resolved — i.e. URL fetching is unavailable in the current environment — skip the doc drift check entirely with:
      
      ```markdown
      ## Documentation Drift
      
      Doc drift check skipped — URL fetching unavailable in current environment.
      ```
      
      Set `doc_drift_summary = { skipped_entirely: true }` and auto-proceed.
      
      ### 3. Build Drift Findings
      
      Read the categories and totals directly from the script's JSON — no manual counting. The four buckets are already computed:
      - **changed:** entries where the stored `content_hash` differs from the newly computed hash (`changed[]`)
      - **unchanged:** entries where the hashes match (`unchanged[]`)
      - **fetch_failed:** entries where the URL could not be reached (`fetch_failed[]`)
      - **skipped_null_hash:** entries where `content_hash` was `null` (`skipped_null_hash[]`)
      
      Take the totals straight from `stats` (do not recount):
      - `total_tracked` = `stats.total_tracked`
      - `changed` = `stats.changed`
      - `unchanged` = `stats.unchanged`
      - `fetch_failed` = `stats.fetch_failed`
      - `skipped_null_hash` = `stats.skipped_null_hash`
      
      ### 4. Append to Drift Report
      
      Append the `## Documentation Drift` section to {outputFile}.
      
      **When drift detected:**
      
      ```markdown
      ## Documentation Drift
      
      | URL | Old Hash | New Hash | Detected At |
      |-----|----------|----------|-------------|
      | {url} | `{old_hash}` | `{new_hash}` | {ISO-8601 timestamp} |
      
      **{changed} of {total_tracked} tracked documentation source(s) have changed since compile.**
      ```
      
      Include rows for ALL entries, in order:
      - Drifted entries: show old and new hash
      - Unchanged entries: omit from table (only drifted entries appear)
      - Fetch-failed entries: `| {url} | \`{old_hash}\` | _(fetch failed: {reason})_ | {timestamp} |`
      - Null-hash entries: `| {url} | _(not recorded)_ | — | — |`
      
      Fetch-failed entries are clearly marked and excluded from the drift count.
      
      **When no drift detected:**
      
      ```markdown
      ## Documentation Drift
      
      No documentation drift detected. All {total_tracked} tracked documentation source(s) match their compile-time hashes.
      ```
      
      If some entries were fetch_failed or skipped_null_hash, append a note after the main message listing those entries.
      
      ### 5. Store Context and Auto-Proceed
      
      Store `doc_drift_summary` in workflow context for report.md to reference:
      
      ```
      doc_drift_summary = {
        total_tracked: N,
        changed: N,
        unchanged: N,
        fetch_failed: N,
        skipped_null_hash: N,
        skipped_entirely: false
      }
      ```
      
      Update {outputFile} frontmatter:
      - Append `'doc-drift'` to `stepsCompleted`
      
      Display: "**Documentation drift check complete. {changed} of {total_tracked} source(s) drifted. Proceeding to report generation...**"
      
      Load, read the full file, then execute {nextStepFile}.
      
      ## CRITICAL STEP COMPLETION NOTE
      
      Only when the ## Documentation Drift section has been appended to {outputFile} and workflow context updated do you then load and read fully `{nextStepFile}` to begin final report generation.
      
    • structural-diff.md 10.2 KB
      ---
      nextStepFile: 'semantic-diff.md'
      outputFile: '{forge_version}/drift-report-{timestamp}.md'
      loadProvenanceProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-load-provenance.py'
        - '{project-root}/src/shared/scripts/skf-load-provenance.py'
      compareFileHashesProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-compare-file-hashes.py'
        - '{project-root}/src/shared/scripts/skf-compare-file-hashes.py'
      structuralDiffProbeOrder:
        - '{project-root}/_bmad/skf/shared/scripts/skf-structural-diff.py'
        - '{project-root}/src/shared/scripts/skf-structural-diff.py'
      ---
      
      <!-- Config: communicate in {communication_language}. -->
      
      # Step 3: Structural Diff
      
      ## STEP GOAL:
      
      Compare the original provenance map extractions from create-skill against the current re-index snapshot from Step 02 to detect structural drift. Identify added, removed, and changed exports with file:line citations and confidence tier labels.
      
      ## Rules
      
      - Focus only on structural comparison — added/removed/changed exports
      - Do not classify severity (Step 05) or suggest remediation (Step 06)
      - Use subprocess Pattern 4 (parallel) when available; if unavailable, compare sequentially
      
      ## MANDATORY SEQUENCE
      
      ### 1. Run the Deterministic Export Diff
      
      The export comparison — canonicalization, set arithmetic (added/removed/moved), and field-level change detection — is fully deterministic and runs in one subprocess. Do **not** diff the two export lists by hand: an LLM comparing dozens or hundreds of exports can silently drop or mis-match entries, which violates this skill's zero-hallucination contract.
      
      **Resolve `{structuralDiffHelper}`** from `{structuralDiffProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      Run one comparison over the baseline provenance map (from step 1) and the current extraction snapshot (`{extractionSnapshot}`, written to disk by step 2 §3):
      
      ```bash
      uv run {structuralDiffHelper} {provenanceMap} {extractionSnapshot}
      ```
      
      The helper reads both shapes directly — the provenance map's `entries[]` (with `export_name`/`export_type`/`source_file`/`source_line`) and the snapshot's `exports[]` — and aliases the field names, so no manual projection is needed.
      
      **Canonicalization is applied inside the helper, symmetrically to both sides**, before name-keyed matching — so cosmetic extractor differences do not surface as false-positive "Changed"/"Removed"/"Added" entries:
      
      - **Quote style on string defaults** — `kind: str = "Hnsw"` ↔ `kind: str = 'Hnsw'`.
      - **Stdlib module qualification** — `typing.Optional[...]` → `Optional[...]`, `dataclasses.field(...)` → `field(...)` (user-defined namespaces are never collapsed).
      - **Public-API re-export resolution** — a renamed public re-export (`_Impl` → `Public`) matches the baseline entry instead of splitting into "Removed `_Impl`" + "Added `Public`". The re-export map is **auto-derived from the provenance map** (identical to the `{reexport_map}` projection `skf-load-provenance.py normalize` produced in step 1 §4). Pass `--reexport-map {file}` only to override with a custom map.
      
      Parse the emitted JSON:
      
      ```
      {
        "summary": {"added": N, "removed": N, "changed": N, "moved": N, "unchanged": N},
        "added":   [ <entry>, ... ],   // in current snapshot, NOT in provenance map
        "removed": [ <entry>, ... ],   // in provenance map, NOT in current snapshot
        "changed": [ {"name", "field", "baseline_value", "current_value"}, ... ],
        "moved":   [ {"name", "previous_file", "current_file"}, ... ],
        "unchanged_count": N,
        "applied_transforms": [ {"transform": "quote-style|stdlib-prefix|reexport-resolution", "count": N}, ... ]
      }
      ```
      
      Stash `applied_transforms` in workflow context — step 6 surfaces it in the Provenance section so a reviewer can tell which cosmetic differences the diff collapsed and which changes were real.
      
      **If `uv` / the helper cannot execute** (e.g. claude.ai web): fall back to comparing the two lists by hand — match by canonicalized export name (apply the three transforms above to both sides), then read off added (current-only), removed (baseline-only), moved (same name, different `file`), and changed (matched name, differing type/signature/line). Compare a field only when it is present on both sides.
      
      ### 2. Read Added / Removed / Moved from the Diff
      
      These sets come straight from the helper's JSON — no further set arithmetic:
      
      - **Added** — `added[]`: exports in the current snapshot but not the provenance map. Each carries name, type, signature, file, line, confidence.
      - **Removed** — `removed[]`: exports in the provenance map but not the current snapshot. Same fields (in provenance-map field names).
      - **Moved** — `moved[]`: matched exports whose file path changed (`previous_file` → `current_file`). A move is **not** a removal.
      
      Confidence tier for each entry is the `confidence` field the extractor recorded (T1 if AST-backed, T1-low if text-based).
      
      ### 3. Read Changed Exports from the Diff
      
      `changed[]` lists per-field differences for exports present in BOTH sets. Each item names the export, the `field` that changed (type / signature / line / confidence), and its `baseline_value` → `current_value`. Group items by export name when compiling the report, and pair with the export's `moved[]` entry (if any) to describe location changes.
      
      ### 4b. Detect Script/Asset Drift
      
      **Only execute if provenance-map.json contains `file_entries`.**
      
      **Resolve `{compareFileHashesHelper}`** from `{compareFileHashesProbeOrder}`; first existing path wins. HALT if no candidate exists.
      
      Run one deterministic comparison subprocess — it walks tracked file_entries[] AND the inverse direction (source-tree → candidate set in standard script/asset/doc directories) so the LLM does not orchestrate per-file hashing:
      
      ```bash
      uv run {compareFileHashesHelper} compare {provenanceMap} {sourceRoot}
      ```
      
      Parse the emitted JSON:
      
      ```
      {
        "added":   ["<rel-path>", ...],   // present on disk in tracked dirs, NOT in file_entries
        "removed": ["<rel-path>", ...],   // in file_entries, missing on disk
        "changed": [{"path": "...", "stored_hash": "sha256:...", "current_hash": "sha256:..."}],
        "stats":   {"added": N, "removed": N, "changed": N, "unchanged": N}
      }
      ```
      
      Hash-prefix normalization (writer-vs-reader compatibility — `skf-create-skill` writes `content_hash` with a `"sha256:"` prefix, a bare-hex hash from `hashlib` would otherwise never match) is handled inside the script. Downstream consumers read `added`/`removed`/`changed` directly with no further normalization.
      
      Append the three lists into the Structural Drift section under a `### Script/Asset Drift (added {stats.added}, removed {stats.removed}, changed {stats.changed})` heading — take each count straight from `stats`, no recount.
      
      **If `uv`/the helper cannot execute** (e.g. claude.ai web): skip the script/asset drift check with a `### Script/Asset Drift — skipped (hashing helper unavailable)` note rather than blocking the audit. This check is supplementary to the export diff, which has its own by-hand fallback in §1.
      
      ### Stack-Specific Structural Diff
      
      If `{is_stack_skill}` is true:
      
      **For v2 provenance (per-export entries with `source_library`):**
      - Group entries by `source_library`
      - For each library, run the same deterministic diff as the single-skill path (§1) — pass the per-library baseline slice and the matching current snapshot to `{structuralDiffHelper}`
      - Report per-library diff results
      
      **For code-mode stacks:** Re-extract from each source repo and compare per-library entries.
      
      **For compose-mode stacks:** Compare current constituent skill exports against the entries recorded at compose time. Use the `source_library` field to match entries to constituents.
      
      **For v1 legacy provenance:** Report library-level summary only (export counts, extraction methods). Note that per-export drift detection requires re-composition with v2 provenance.
      
      **Integration drift:** For each integration in `integrations[]`, verify that co-import files still contain the detected patterns (code-mode) or that constituent skills still document the integration (compose-mode).
      
      ### 5. Compile Structural Drift Section
      
      **Rollup for high-volume uniform findings.** When ≥ 10 findings in the same table share one root cause (deleted source file, renamed module, entire package tree removed), you may collapse them into one row per root cause. Rollup rows replace the per-symbol `Export`/`Signature` columns with `Count` and `Representative symbols` (up to 3 names, `…` if more). Rollup applies to **Added Exports**, **Removed Exports**, and **Script/Asset Drift** tables — **not** to Changed Exports, which are heterogeneous by construction (signature changes and cross-file changes are inspected per-finding). Record which groupings were collapsed in workflow context for reviewer traceability.
      
      **Rollup row form (Added / Removed Exports):**
      
      | Root Cause | Count | Representative symbols | Location | Confidence |
      |------------|-------|------------------------|----------|------------|
      | {deleted/renamed path or similar} | {N} | `{sym1}`, `{sym2}`, `{sym3}`, … | {root-cause path} | {T1/T1-low} |
      
      Append to {outputFile}:
      
      ```markdown
      ## Structural Drift
      
      **Comparison:** Provenance map ({provenance_date}) vs Current scan ({scan_date})
      **Method:** {Quick: text-diff / Forge: AST structural / Deep: AST structural}
      
      ### Added Exports ({count})
      
      | Export | Type | Signature | Location | Confidence |
      |--------|------|-----------|----------|------------|
      | {name} | {type} | {signature} | {file}:{line} | {T1/T1-low} |
      
      ### Removed Exports ({count})
      
      | Export | Type | Original Signature | Original Location | Confidence |
      |--------|------|-------------------|-------------------|------------|
      | {name} | {type} | {signature} | {file}:{line} | {T1/T1-low} |
      
      ### Changed Exports ({count})
      
      | Export | Change Type | Before | After | Location | Confidence |
      |--------|------------|--------|-------|----------|------------|
      | {name} | {signature/type/location} | {old} | {new} | {file}:{line} | {T1/T1-low} |
      
      ### Summary
      
      | Category | Count |
      |----------|-------|
      | Added | {added_count} |
      | Removed | {removed_count} |
      | Changed | {changed_count} |
      | **Total Drift Items** | {total} |
      ```
      
      ### 6. Update Report and Auto-Proceed
      
      Update {outputFile} frontmatter — append `'structural-diff'` to `stepsCompleted`. Once the ## Structural Drift section has been appended, load, read fully, and execute `{nextStepFile}` (semantic diff).
      
      
  • customize.toml 1.9 KB
    # DO NOT EDIT -- overwritten on every update.
    #
    # Workflow customization surface for skf-audit-skill.
    # Team overrides:     _bmad/custom/skf-audit-skill.toml (under {project-root})
    # Personal overrides: _bmad/custom/skf-audit-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 audit 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
    # (house style, severity-threshold policies, audit guardrails).
    # Overrides append.
    #
    # Each entry is either:
    #   - a literal sentence, e.g. "Critical drift requires CODEOWNERS notification."
    #   - a file reference prefixed with `file:`, e.g.
    #     "file:{project-root}/docs/audit-policy.md" (globs supported; file
    #     contents are loaded and treated as facts).
    
    persistent_facts = [
      "file:{project-root}/**/project-context.md",
    ]
    
    # --- Optional asset overrides ---
    #
    # Lift the canonical asset paths so orgs can substitute house-style copies
    # without forking the skill. Empty string = use the bundled default.
    
    drift_report_template_path = ""
    severity_rules_path = ""
    
    # Optional post-audit hook. When non-empty, invoked with
    # `--result-path=<path>` after the drift report and result JSON are finalized.
    # Failures are logged to workflow_warnings[] but never fail the workflow.
    
    on_complete = ""
    
  • SKILL.md 8.9 KB
    ---
    name: skf-audit-skill
    description: Drift detection between skill and current source code. Use when the user requests to "audit a skill" or "audit skill" for drift.
    ---
    
    # Audit Skill
    
    ## Overview
    
    Detects drift between an existing skill and its current source code, producing a severity-graded drift report with AST-backed findings and actionable remediation suggestions. Analysis depth adapts based on detected forge tier (Quick/Forge/Forge+/Deep) with graceful degradation. Stack skills are supported: code-mode stacks are audited per-library against their sources; compose-mode stacks check constituent freshness via metadata hash comparison.
    
    ## 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.
    
    ## Role
    
    You are a skill auditor in Ferris Audit mode: a deterministic drift-detection workflow where the source code is the ground truth and every finding traces back to it.
    
    ## Workflow Rules
    
    These rules apply to every step in this workflow:
    
    - Never fabricate findings — all data must trace to source code with file:line citations
    - Only load one step file at a time — never preload future steps
    - Update `stepsCompleted` in output file frontmatter before loading next step
    - 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 & Baseline | references/init.md | No (confirm) |
    | 2 | Re-Index Source | references/re-index.md | Yes |
    | 3 | Structural Diff | references/structural-diff.md | Yes |
    | 4 | Semantic Diff | references/semantic-diff.md | Yes (skip at non-Deep) |
    | 5 | Severity Classification | references/severity-classify.md | Yes |
    | 5a | Doc Drift | references/step-doc-drift.md | Yes |
    | 6 | Report | references/report.md | Yes |
    | 7 | Workflow Health Check | references/health-check.md | Yes |
    
    ## Invocation Contract
    
    | Aspect | Detail |
    |--------|--------|
    | **Inputs** | `skill_name` [required], `skill_path` [optional override — full path to skill directory; bypasses manifest/symlink resolution], `tier_override` [optional: Quick / Forge / Forge+ / Deep — overrides detected tier], `degraded` [optional bool — pre-confirm degraded-mode opt-in when no provenance map exists], `upstream_drift_choice` [optional: C / S / X — pre-supplied answer for the upstream-drift gate at init.md §5b], `dirty_worktree_choice` [optional: T / A / F — pre-supplied answer for the dirty-worktree sub-gate at init.md §5b], `force` [optional bool — when paired with `dirty_worktree_choice=F` or used for any future destructive-action gate, signals consent to skip the confirmation] |
    | **Gates** | step 1: Manifest-vs-Symlink Gate [N] · Upstream-Drift Gate [C/S/X] · Dirty-Worktree Sub-Gate [T/A/F] · Degraded-Mode Gate [D/X] · Baseline Confirm Gate [C] |
    | **Outputs** | `drift-report-{timestamp}.md` at `{forge_version}/` with `drift_score` and `nextWorkflow` frontmatter; per-run result contract at `{forge_version}/audit-skill-result-{timestamp}.json` plus `-latest.json` copy; final `SKF_AUDIT_RESULT_JSON` line on stdout when `{headless_mode}` is true |
    | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true; pre-supplied inputs (`upstream_drift_choice`, `dirty_worktree_choice`, `degraded`, `tier_override`) consumed at the gates that would otherwise prompt |
    | **Exit codes** | See "Exit Codes" below |
    
    ## Exit Codes
    
    Every hard halt in this workflow exits with a stable code so headless automators can branch on the failure class without grepping message text:
    
    | Code | Meaning              | Raised by                                                                                                          |
    | ---- | -------------------- | ------------------------------------------------------------------------------------------------------------------ |
    | 0    | success              | step 7 (terminal health-check)                                                                                     |
    | 2    | input-missing        | step 1 §1 — no `skill_name` supplied in headless mode (interactive prompt cannot resolve)                          |
    | 3    | resolution-failure   | step 1 §1 (skill not found at resolved path: missing `SKILL.md`); step 1 §2 (`forge-tier.yaml` missing — setup-forge not run); step 1 §5 (source directory from provenance map no longer exists / inaccessible) |
    | 4    | write-failure        | step 1 §6 / step 6 §3 (drift report write failed: read-only mount, disk full, permissions denied)                 |
    | 6    | user-cancelled       | step 1 §1 manifest-vs-symlink gate `[X]` · step 1 §4 degraded-mode gate `[X]` · step 1 §5b upstream-drift gate `[X]` · step 1 §5b dirty-worktree sub-gate `[A]` (and `[A]` headless default) |
    
    ## Result Contract (Headless)
    
    When `{headless_mode}` is true, step 6 emits a single-line JSON envelope on **stdout** before chaining to step 7, and every hard halt emits the same envelope shape on **stderr** with `status: "error"`:
    
    ```
    SKF_AUDIT_RESULT_JSON: {"status":"success|error","skill_name":"…","drift_score":"CLEAN|MINOR|SIGNIFICANT|CRITICAL|null","report_path":"…|null","next_workflow":"update-skill|null","audit_ref":"…|null","exit_code":0,"halt_reason":null}
    ```
    
    `status` is `"success"` on the terminal happy path, `"error"` on any halt. `drift_score` is `null` when the workflow halted before severity classification ran. `next_workflow` is `"update-skill"` when CRITICAL or HIGH findings exist, otherwise `null`. `halt_reason` is one of: `null` (success), `"input-missing"`, `"skill-not-found"`, `"forge-tier-missing"`, `"source-dir-missing"`, `"write-failed"`, `"user-cancelled"`. `exit_code` matches the table above.
    
    ## 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`
       - Generate and store `timestamp` as `YYYYMMDD-HHmmss` format. This value is fixed for the entire workflow run.
    
    2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
    
    3. **Resolve workflow customization.** Run:
    
       ```bash
       python3 {project-root}/_bmad/scripts/resolve_customization.py \
           --skill {skill-root} --key workflow
       ```
    
       The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append):
    
       - `{skill-root}/customize.toml` — bundled defaults
       - `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)
       - `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)
    
       If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly — the bundled defaults are an empty string for each path scalar.
    
       Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each of the scalars, if the merged value is empty or absent, use the bundled default:
    
       - `{driftReportTemplatePath}` ← `workflow.drift_report_template_path` if non-empty, else `assets/drift-report-template.md`
       - `{severityRulesPath}` ← `workflow.severity_rules_path` if non-empty, else `references/severity-rules.md`
       - `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty (no-op — report.md skips the hook invocation entirely)
    
       Stash all three as workflow-context variables. Stage files reference `{driftReportTemplatePath}` / `{severityRulesPath}` / `{onCompleteCommand}` directly.
    
       Also apply the array surfaces (not silent no-ops): run `workflow.activation_steps_prepend` now, treat `workflow.persistent_facts` as standing context for the run (`file:`-prefixed entries load their file/glob contents as facts — the bundled default globs any `project-context.md`), then run `workflow.activation_steps_append` after activation.
    
    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