bmad-module-skill-forge 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +3 -3
  3. package/docs/_data/pinned.yaml +19 -13
  4. package/docs/architecture.md +2 -2
  5. package/docs/concepts.md +4 -4
  6. package/docs/examples.md +32 -25
  7. package/docs/getting-started.md +3 -3
  8. package/docs/how-it-works.md +2 -2
  9. package/docs/index.md +1 -1
  10. package/docs/skill-model.md +41 -39
  11. package/docs/verifying-a-skill.md +41 -20
  12. package/docs/why-skf.md +1 -1
  13. package/docs/workflows.md +2 -2
  14. package/package.json +1 -1
  15. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
  16. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  17. package/src/shared/scripts/skf-build-change-manifest.py +881 -168
  18. package/src/shared/scripts/skf-count-tokens.py +54 -10
  19. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  20. package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
  21. package/src/shared/scripts/skf-extract-public-api.py +439 -107
  22. package/src/shared/scripts/skf-extraction-inventory.py +10 -6
  23. package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
  24. package/src/shared/scripts/skf-parse-gaps.py +135 -26
  25. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  26. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  27. package/src/shared/scripts/skf-shard-body.py +239 -41
  28. package/src/shared/scripts/skf-validate-output.py +12 -3
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +470 -38
  30. package/src/shared/tool-requirements.yaml +8 -2
  31. package/src/skf-analyze-source/SKILL.md +2 -2
  32. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  33. package/src/skf-audit-skill/SKILL.md +2 -2
  34. package/src/skf-audit-skill/references/re-index.md +2 -2
  35. package/src/skf-brief-skill/SKILL.md +2 -2
  36. package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
  37. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  38. package/src/skf-campaign/SKILL.md +2 -2
  39. package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
  40. package/src/skf-create-skill/SKILL.md +2 -2
  41. package/src/skf-create-skill/assets/skill-sections.md +1 -1
  42. package/src/skf-create-skill/references/extract.md +2 -1
  43. package/src/skf-create-skill/references/extraction-patterns.md +3 -3
  44. package/src/skf-create-skill/references/report.md +6 -1
  45. package/src/skf-create-stack-skill/SKILL.md +2 -2
  46. package/src/skf-create-stack-skill/references/validate.md +1 -1
  47. package/src/skf-drop-skill/SKILL.md +2 -2
  48. package/src/skf-export-skill/SKILL.md +2 -2
  49. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  50. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  51. package/src/skf-export-skill/references/load-skill.md +1 -1
  52. package/src/skf-export-skill/references/token-report.md +29 -9
  53. package/src/skf-forger/SKILL.md +2 -2
  54. package/src/skf-quick-skill/SKILL.md +2 -2
  55. package/src/skf-refine-architecture/SKILL.md +6 -8
  56. package/src/skf-rename-skill/SKILL.md +2 -2
  57. package/src/skf-setup/SKILL.md +2 -2
  58. package/src/skf-test-skill/SKILL.md +2 -2
  59. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  60. package/src/skf-test-skill/references/coherence-check.md +4 -3
  61. package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
  62. package/src/skf-test-skill/references/coverage-check-tiers.md +1 -1
  63. package/src/skf-test-skill/references/coverage-check.md +45 -69
  64. package/src/skf-test-skill/references/init.md +2 -1
  65. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  66. package/src/skf-test-skill/references/report.md +2 -2
  67. package/src/skf-test-skill/references/scoring-rules.md +5 -4
  68. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  69. package/src/skf-test-skill/scripts/gap-ledger.py +378 -163
  70. package/src/skf-test-skill/scripts/load-coverage-inputs.py +171 -70
  71. package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
  72. package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
  73. package/src/skf-test-skill/scripts/score-signatures.py +329 -54
  74. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  75. package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
  76. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  77. package/src/skf-update-skill/SKILL.md +2 -2
  78. package/src/skf-update-skill/references/detect-changes.md +1 -1
  79. package/src/skf-update-skill/references/gap-driven.md +36 -21
  80. package/src/skf-update-skill/references/merge.md +15 -10
  81. package/src/skf-update-skill/references/re-extract.md +3 -1
  82. package/src/skf-update-skill/references/report.md +5 -4
  83. package/src/skf-update-skill/references/write.md +39 -18
  84. package/src/skf-verify-stack/SKILL.md +2 -2
  85. package/tools/cli/lib/tool-check.js +158 -2
@@ -28,7 +28,7 @@ hashContentProbeOrder:
28
28
 
29
29
  ## STEP GOAL:
30
30
 
31
- Merge freshly extracted export data into the existing SKILL.md content while preserving all [MANUAL] sections. Detect and resolve conflicts where regenerated content overlaps developer-authored content.
31
+ Merge freshly extracted export data into the existing SKILL.md and its references/*.md while preserving all [MANUAL] sections. Detect and resolve conflicts where regenerated content overlaps developer-authored content.
32
32
 
33
33
  ## Rules
34
34
 
@@ -67,9 +67,11 @@ From the [MANUAL] inventory captured in step 1:
67
67
 
68
68
  ### 3. Apply Merge by Priority Order
69
69
 
70
- Read this step's inputs from disk, never from memory: the change manifest from `{run_dir}/change-manifest.json` (step 2, or gap-driven.md §1 in gap-driven mode), the extraction or verification records from `{run_dir}/reextract-records.json` (step 3, or gap-driven.md §4), and the documents step 2 §1b promoted from `{run_dir}/promoted-docs.json` when it wrote that file. A compacted context still merges what those steps found.
70
+ Read this step's inputs from disk, never from memory: the change manifest from `{run_dir}/change-manifest.json` (step 2, or gap-driven.md §1 in gap-driven mode), the extraction or verification records from `{run_dir}/reextract-records.json` (step 3, or gap-driven.md §4), and the documents step 2 §1b promoted from `{run_dir}/promoted-docs.json` when it wrote that file. A compacted context still merges what those steps found. A gap-driven record in `files` that carries a `local` name was matched by its public name (a renamed re-export): document it under that public name, its `name`, saying it re-exports the declared `local` name.
71
71
 
72
- Apply merge in the following priority order:
72
+ Apply merge in the following priority order.
73
+
74
+ **A split-body skill** (one whose `references/*.md` files document exports, the `## Full` sections a body split moved among them): Priorities 1 to 5 apply their change to every file that documents the export, SKILL.md and each `references/*.md` alike. Priority 1 removes the export's content from each file, and deletes a reference file it leaves documenting nothing and holding no `[MANUAL]` block, with SKILL.md's `> See` pointer to that file; Priority 2 moves its citations (`[AST:]` and `[SRC:]` alike) in each, Priority 3 renames it in each, Priority 4 replaces its content in each, and Priority 5 adds a new export where the skill documents that kind of export (a `references/*.md` file when the skill keeps that kind there) and corrects any statement, in SKILL.md or a `references/*.md` file, that calls the new export not covered. [MANUAL] blocks are inventoried and verified in SKILL.md only: nothing re-checks one in a `references/*.md` file yet, so the merge leaves such a block untouched, as it never edits inside one in SKILL.md without the user's §4 decision. §6b writes each `references/*.md` file this changes or adds, and deletes each one it removes.
73
75
 
74
76
  **Priority 1 — Process DELETED exports:**
75
77
  - Remove generated content for deleted exports
@@ -91,6 +93,8 @@ Apply merge in the following priority order:
91
93
 
92
94
  **Priority 4 — Process MODIFIED exports:**
93
95
  - Replace generated content for the export with fresh extraction
96
+ - **Gap-driven `MODIFIED_EXPORT`:** its fresh extraction is its record in `files` of `{run_dir}/reextract-records.json`, which gap-driven.md §4 copied from §4a's extraction at the line its spot-check located (`verified` or `moved`): document it from that record, as a `re-extracted` one is, taking its signature, parameters and return type from the record, never from the old content or the gap's text, so SKILL.md names the signature step 5's `apply` writes to the provenance map. The record holds no docstring (§4 asks none for it), so keep the export's description. With no record there (its verification record's `extraction_method` is null), repair it from the source line its spot-check read.
97
+ - **Gap-driven `MODIFIED_EXPORT` recorded `unknown` or `missing`** (gap-driven.md §4 located no line for it, and step 5's `apply` warns with the cause, `cited-elsewhere` among them): keep its documented content as it stands. Never rewrite it from the gap's text (its title, issue or remediation), which may describe another file's declaration.
94
98
  - Preserve [MANUAL] blocks adjacent to the export
95
99
  - Check for position conflicts (new content shifts [MANUAL] block)
96
100
  - If position conflict: flag as POSITION conflict
@@ -100,7 +104,8 @@ Apply merge in the following priority order:
100
104
  - Place before any [MANUAL] blocks at section boundary
101
105
  - No conflicts expected (new content, no existing [MANUAL])
102
106
  - **A `public-api` skill's public surface (normal mode only):** document no new export, and no export of an ADDED or MOVED file, whose record in `{run_dir}/reextract-records.json` is marked `public: false`, unless the provenance map holds the export (by name at its `old_path` for a MOVED file) or the old name it renames, or this update removes an entry of its name (a `DELETED_EXPORT`, a DELETED file's entry): `records` (re-extract.md §4) found it off the public surface, and step 5's `apply` adds no entry for it, so SKILL.md, the provenance map and metadata.json's `exports[]` name the same exports. A record with no `public` mark is documented as before, and an export the map already holds keeps its content (Priority 4) whatever its mark. In gap-driven and degraded mode document every record whatever its mark: `apply` maps them all.
103
- - **Gap-driven cited `NEW_EXPORT` whose spot-check pinned a line** (gap-driven.md §4 recorded `verified` or `moved` for an export the provenance map does not hold): cite it as `[SRC:{source_file}:L{line}]`, where `{line}` is the citation's line for `verified` and the `new_location` line for `moved`: the line write.md §3 records in its new `source-read` entry. The spot-check found that line by the verifier's text rules, not by an ast-grep recipe, so the prefix is `SRC`, never `AST`.
107
+ - **Gap-driven cited `NEW_EXPORT` whose spot-check pinned a line** (gap-driven.md §4 recorded `verified` or `moved` for an export the provenance map does not hold): cite it as `[SRC:{source_file}:L{line}]`, where `{line}` is the citation's line for `verified` and the `new_location` line for `moved`: the line write.md §3 records in its new `source-read` entry. The spot-check found that line by the verifier's text rules, not by an ast-grep recipe, so the prefix is `SRC`, never `AST`. Document it from its record in `files` of `{run_dir}/reextract-records.json`, as a `re-extracted` one is, when §4 copied one there from §4a's extraction at that line (its verification record's `extraction_method` is set): its signature, parameters and return type come from that record, never from the gap's text, and the record holds no docstring (§4 asks none for it). It keeps its `[SRC:]` citation and its T1-low label whatever its record's tool.
108
+ - **Gap-driven `NEW_EXPORT` recorded `unknown` with `cited-elsewhere`** (gap-driven.md §4: the provenance map already holds its name at another file than the gap cites): add nothing for it, and never document it from the gap's text (its title, issue or remediation), which may describe another file's declaration.
104
109
 
105
110
  **Priority 6 — Process script/asset file changes (from Category D in change manifest):**
106
111
 
@@ -139,7 +144,7 @@ For each entry of `promoted_docs_new[]`, read from `{run_dir}/promoted-docs.json
139
144
  For each `STRUCTURAL_FIX` entry gap-driven.md §4 forwarded:
140
145
 
141
146
  - Apply the surgical edit described in the entry's `remediation` text to the **generated output file only** (e.g., escape an unescaped `|` inside a code span, balance a fence, repair a broken intra-skill anchor in SKILL.md or a `references/*.md`).
142
- - **A split-body consistency finding** (rule R2: the SKILL.md body and a `references/*.md` file document one export differently): edit the `references/*.md` file so it documents the export as the SKILL.md body does, whichever of the two files the gap's `Source:` names. The body is authoritative (test-skill coverage-check §1b), so never change the body to match the reference file.
147
+ - **A split-body consistency finding** (rule R2: the SKILL.md body and a `references/*.md` file document one export differently): edit the file the entry's `package_citation` names, the side the pinned source contradicts (test-skill coverage-check §5b), so it documents the export as the other file does. Without a `package_citation`, edit the file the remediation names to change; when neither names one, edit the `references/*.md` file to match the SKILL.md body. Change only what the remediation names as contradicting (a parameter, a type, the return type), taking it from the other file, and nothing more when it says both forms agree with the source. Keep each file's level of detail: the body keeps its brief form, and a `|` the change puts inside a table cell is escaped (`\|`). Read no source. When the manifest also holds a `MODIFIED_EXPORT` for the same export, Priority 4's rewrite settles the body: skip this edit of SKILL.md for that export.
143
148
  - Do **not** add, modify, or remove any provenance `entries[]` row — STRUCTURAL_FIX never touches the provenance map.
144
149
  - Preserve any [MANUAL] blocks; if the fix location overlaps a [MANUAL] block, flag as a POSITION conflict instead of editing.
145
150
  - Record in the update report: `"Structural fix: {remediation summary} at {file}:{line}"`.
@@ -150,7 +155,7 @@ For each `STRUCTURAL_FIX` entry gap-driven.md §4 forwarded:
150
155
 
151
156
  For each `metadata update` entry gap-driven.md §4 forwarded:
152
157
 
153
- - Queue the surgical metadata patch described in the entry's `remediation` (e.g., reconcile a divergent `stats` count, add an explanatory stat) in workflow context as `metadata_patches[]` for write.md §2 to apply **before** its automatic stat recount.
158
+ - Queue the surgical metadata patch described in the entry's `remediation` in workflow context as `metadata_patches[]` for write.md §2 to apply **before** its automatic stat recount.
154
159
  - Touch no provenance `entries[]` row and no generated markdown — this priority only stages the patch; write.md §2 applies it.
155
160
  - Record in the update report: `"Metadata patch queued: {remediation summary}"`.
156
161
 
@@ -214,7 +219,7 @@ Merge Results:
214
219
  manual_orphans_removed: [count]
215
220
  ```
216
221
 
217
- In gap-driven mode, write what step 5's `apply` reads from merge to `{run_dir}/merge-records.json`: for each `NEW_EXPORT` or `MODIFIED_EXPORT` the provenance map does not hold and no `re-extracted` record covers (a cited export whose spot-check pinned a line, and a `Medium`, `Low` or `Info` one recorded `unknown`), the `export_type`, `params` and `return_type` its merged documentation gives, leaving out each one it does not:
222
+ In gap-driven mode, write what step 5's `apply` reads from merge to `{run_dir}/merge-records.json`: for each `NEW_EXPORT` or `MODIFIED_EXPORT` the provenance map does not hold and no `re-extracted` record covers (a cited export whose spot-check pinned a line, and a `Medium`, `Low` or `Info` one recorded `unknown`), the `export_type`, `params` and `return_type` its merged documentation gives, leaving out each one it does not. Leave out each part a record gives, and leave out an export whose record gives all three: when §4a found an export's record (its verification record in `{run_dir}/reextract-records.json` has an `extraction_method`), `apply` takes its whole signature from that record, a part the record gives as null included, and reads merge's only for an export with no record:
218
223
 
219
224
  ```bash
220
225
  cat > "{run_dir}/merge-records.json" <<'SKF_JSON'
@@ -279,7 +284,7 @@ uv run {runLockHelper} acquire \
279
284
 
280
285
  Bind `{version_staging}` ← `staging`: the empty folder `{skill_group}/{new_version}.skf-tmp` it created (it clears one an interrupted run left).
281
286
  3. **Copy the current package** `{skill_package}` to `{version_staging}/{skill_name}`, naming both without a trailing `/` — equivalent to `cp -a "{skill_package}" "{version_staging}/{skill_name}"`: SKILL.md, metadata.json, context-snippet.md, `references/`, `scripts/`, `assets/` and every other entry, each link copied as a link and never followed. The files this update does not rewrite carry over to the new version this way.
282
- 4. **Write and verify the merged SKILL.md in the staged package**, before anything is published. Use the `Edit` or `Write` tool to write the merged SKILL.md content (UTF-8) to `{version_staging}/{skill_name}/SKILL.md`, and any `references/*.md` the merge changed into `{version_staging}/{skill_name}/references/`. Then, from `{project-root}`, check the [MANUAL] blocks against the inventory §4 amended:
287
+ 4. **Write and verify the merged SKILL.md in the staged package**, before anything is published. Use the `Edit` or `Write` tool to write the merged SKILL.md content (UTF-8) to `{version_staging}/{skill_name}/SKILL.md`, and each `references/*.md` the merge changed or added, at any priority, into `{version_staging}/{skill_name}/references/`, and delete from there each one the merge removed. Then, from `{project-root}`, check the [MANUAL] blocks against the inventory §4 amended:
283
288
 
284
289
  ```bash
285
290
  uv run {hashContentHelper} manual-verify "{version_staging}/{skill_name}/SKILL.md" \
@@ -302,7 +307,7 @@ If no `{atomicWriteProbeOrder}` candidate resolves, or the stage, copy, write, p
302
307
  **Write the merged files in place** (gap-driven mode only; every other mode wrote them in step 4 above):
303
308
 
304
309
  1. **The rescopes' brief amendments.** For each manifest entry with a `rescope` (rule R1), append its `scope.amendments[]` entry and add its path to `scope.exclude` in `{forge_data_folder}/{skill_name}/skill-brief.yaml`, preserving every other field; skip an amendment the brief already holds (same `action`, `category` and `path`) and a path `scope.exclude` already lists, so a re-run never adds them twice.
305
- 2. **Stage and verify SKILL.md in the run folder.** Write the merged SKILL.md (UTF-8) to `{run_dir}/SKILL.md`, then, from `{project-root}`:
310
+ 2. **Stage and verify SKILL.md in the run folder.** Write the merged SKILL.md (UTF-8) to `{run_dir}/SKILL.md`, and each `references/*.md` the merge changed or added, at any priority, to `{run_dir}/references/`, and list each one it removed, one `references/<name>.md` path a line, in `{run_dir}/references-removed.txt`, then, from `{project-root}`:
306
311
 
307
312
  ```bash
308
313
  uv run {hashContentHelper} manual-verify "{run_dir}/SKILL.md" \
@@ -310,7 +315,7 @@ If no `{atomicWriteProbeOrder}` candidate resolves, or the stage, copy, write, p
310
315
  ```
311
316
 
312
317
  On `ok` false, or a failing command: HALT with status `halted-for-manual-mismatch` before the package changes (halt procedure: `phase: "merge:verify-manual-integrity"`, `path: "{run_dir}/SKILL.md"`, `reason: "[MANUAL] blocks differ from the approved inventory: modified {modified}; missing {missing}"`); its rollback restores the brief step 1 amended.
313
- 3. **Copy it over the package.** Copy `{run_dir}/SKILL.md` to `{skill_package}/SKILL.md`, and write any `references/*.md` the merge changed (merge Priority 8) into `{skill_package}/references/`.
318
+ 3. **Copy it over the package.** Copy `{run_dir}/SKILL.md` to `{skill_package}/SKILL.md`, and each file in `{run_dir}/references/`, every `references/*.md` the merge changed or added at any priority, into `{skill_package}/references/`, and delete from the package each path `{run_dir}/references-removed.txt` lists.
314
319
 
315
320
  **Do NOT write here:**
316
321
  - `metadata.json`, `provenance-map.json`, `evidence-report.md`: derived from the merge output, written by step 5 sections 2–4 (the version folder above only copies the previous version's files, which step 5 then rewrites)
@@ -66,7 +66,7 @@ The halt leaves `{run_dir}` in place.
66
66
  **Forge tier (AST structural extraction):**
67
67
 
68
68
  - Step 2's Category B ran the recipe runner (`{extractPublicApiHelper}` `--mode full`, the recipes of the AST Extraction Protocol in `{extractionPatternsData}`) over every file to extract, `{run_dir}/extract-files.json`, and wrote `{run_dir}/extraction.json`: never run it again here. §4's helper takes each file's exports from its `exports[]`, each at the line of its name, never a decorator or `export` line above it: T1 (AST-verified structural truth) with `extraction_method: ast-grep` and the recipe's labels.
69
- - Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names (not UTF-8, or code the parser could not read, where a recipe can miss an export), a name `entry_point_diff.extraction_gaps[]` lists, a map entry `{run_dir}/baseline-gaps.json` lists (a module, an alias that renames, `__version__` or another underscore name the runner never reports, which its file still declares), and each file step 2 told the user it read by eye. Step 2 already read those of the modified and added files into `{run_dir}/export-details.json` (its entries with an `export_type`), and §4's helper takes them from there. An export read by eye is T1-low with `extraction_method: source-read` and `ast_node_type: null`.
69
+ - Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names (not UTF-8, or code the parser could not read, where a recipe can miss an export), a name `entry_point_diff.extraction_gaps[]` lists, a map entry `{run_dir}/baseline-gaps.json` lists (a module, an alias that renames, `__version__` or another Python or Rust underscore name the runner never reports, which its file still declares), and each file step 2 told the user it read by eye. Step 2 already read those of the modified and added files into `{run_dir}/export-details.json` (its entries with an `export_type`), and §4's helper takes them from there. An export read by eye is T1-low with `extraction_method: source-read` and `ast_node_type: null`.
70
70
 
71
71
  **Tier degradation handling (Forge/Forge+/Deep):** If step 2's runner could run no ast-grep, or left files unread (step 2 told the user which), follow `{tierDegradationRulesData}` for fallback strategy and user notification requirements. Silent degradation is forbidden: the user must always know which files were read by eye instead of matched, and why.
72
72
 
@@ -116,6 +116,8 @@ Launch a subprocess that loads qmd_bridge and for each changed export:
116
116
  2. Searches for usage patterns, documentation references, temporal history
117
117
  3. Returns T2 evidence per export (usage frequency, context snippets, related concepts), as patches §4 merges: write one `{"file_path", "exports": [{"name", "qmd_evidence"}]}` block per file to its own file in `{run_dir}/reextract-patches/`
118
118
 
119
+ Then bind `{qmd_version}` ← the version `qmd --version` prints (`qmd 2.0.1` gives `2.0.1`): write.md §2 records it as `tool_versions.qmd`.
120
+
119
121
  **If no matching collection found in registry:**
120
122
  Log: "No QMD extraction collection found for {skill_name}. T2 enrichment skipped. Re-run [CS] Create Skill to generate the collection."
121
123
  Continue without T2 enrichment: extraction still produces its structural results, labeled by tool.
@@ -46,7 +46,7 @@ When `{unconsumed_test_report}` is bound (step 1 §4b found a test report newer
46
46
  - `fail`: "**Recommendation:** The source has not changed, but test report `{its file name}` (fail) has not been applied to this skill. Run `@Ferris US {skill_name} --from-test-report` to repair the gaps it lists, then re-run test-skill."
47
47
  - `pass-with-drift`: "**Recommendation:** The source has not changed, but test report `{its file name}` passed only under `--allow-workspace-drift`: test-skill read a workspace HEAD other than the commit this skill is pinned to. Once the workspace holds the pinned commit, re-run test-skill without `--allow-workspace-drift` before exporting."
48
48
 
49
- In gap-driven mode (gap-driven.md §1 translated none of the report's gaps), replace the sentence that starts "Source code matches provenance map exactly" with "Test report `{its file name}` lists no gap update-skill repairs.", followed by each gap it did not route (`{id}: {title} ({category})`), and the recommendation with "Repair the listed gaps by hand, or re-run test-skill once the skill changes."
49
+ In gap-driven mode (gap-driven.md §1 translated none of the report's gaps), replace the sentence that starts "Source code matches provenance map exactly" with "Test report `{its file name}` lists no gap update-skill repairs.", followed by each gap it did not route (`{id}: {title} ({category}): {reason}`, with the `reason` gap-driven.md §1's `not_routed[]` gives it), and the recommendation with "Repair the listed gaps by hand, or re-run test-skill once the skill changes."
50
50
 
51
51
  When `{source_moved}` is true, add before the recommendation: "Upstream moved to `{target_commit}`, but no file this skill tracks changed, so nothing was written and the skill stays pinned at `{source_commit}`." When `{target_ref_override}` is set and `{target_ref}` differs from `{source_ref}`, also add "**The re-pin to `{target_ref}` was not recorded** — an update records a new ref only when it writes, and `{target_ref}` changes no file this skill tracks." and add `target-ref-not-recorded: {target_ref} changes no file {skill_name} tracks; the skill still records {source_ref}` to `warnings[]`.
52
52
 
@@ -155,7 +155,7 @@ In `{headless_mode}`, print the line as §5b says, with `status: "dry-run"` and
155
155
 
156
156
  {when step 2 wrote `{run_dir}/new-files.json`: **New scripts and assets:** {the length of its `new_files[]`} new; set aside: {the length of `tracked_code[]`} code files the map cites, {the length of `out_of_scope[]`} outside the brief's scope, {the length of `intent_none[]`} of a kind whose intent is `none` (counted from the lists, never from `stats`)}
157
157
 
158
- {in gap-driven mode, when gap-driven.md §1 left gaps unrouted: **Not repaired by this run:** each `{id}: {title} ({category})`}
158
+ {in gap-driven mode, when gap-driven.md §1 left gaps unrouted: **Not repaired by this run:** each `{id}: {title} ({category}): {reason}`, with the `reason` gap-driven.md §1's `not_routed[]` gives it}
159
159
 
160
160
  ### Export Changes
161
161
 
@@ -250,7 +250,7 @@ The shared emitter writes the result contract and prints the line; never type ei
250
250
 
251
251
  ```bash
252
252
  cat > "{run_dir}/result-context.json" <<'SKF_JSON'
253
- {"status": "success", "skill_name": "{skill_name}", "version": "{new_version}", "previous_version": "{baseline_version}", "update_mode": "{update_mode}", "files_written": [<each artifact write.md wrote and verified: SKILL.md, metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, active-symlink>], "error": null,
253
+ {"status": "success", "skill_name": "{skill_name}", "version": "{new_version}", "previous_version": "{baseline_version}", "update_mode": "{update_mode}", "files_written": [<each artifact write.md wrote and verified: SKILL.md, references (when this run wrote, added or removed a references/*.md file, as the files on disk show), metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, active-symlink>], "error": null,
254
254
  "result_contract": {"skill": "skf-update-skill", "status": "success", "outputs": [{"type": "skill", "path": "<each modified file's path>"}], "summary": {"update_status": "success", "exports_affected": <total_export_changes, or the gap count in gap-driven mode>, "files_modified": <the files written>, "validation_status": "<passed, warnings or failures>"}}}
255
255
  SKF_JSON
256
256
  ```
@@ -270,9 +270,10 @@ SKF_UPDATE_RESULT_JSON: {"skf_update": {"status": "success", "skill_name": ...,
270
270
  In `{headless_mode}`, bind `{result_envelope_line}` to that line and do not display it here: the shared health check displays it verbatim as the run's last line. An interactive run runs the command for its result files and need not show the line. When the emitter exits non-zero and its `message` names the payload, fix the payload once and run it again; a write it could not make adds `result_file_write_failed` to the line's `warnings[]`.
271
271
 
272
272
  - `status`: `"success"` here; §1 stages `"no-changes"`, §1a `"detect-only"` and §1b `"dry-run"`; a halt prints its own `halted-for-*` or `blocked` line through its step's halt procedure and never reaches this step. The full enum lives in the schema.
273
+ - `files_written[]`: `references` stands for every `references/*.md` file of the package this run wrote, added or removed, one value however many: merge.md §6b's merge, at any priority, write.md §6's provenance fixes and write.md §7's body split. Read it from the files on disk, never from memory: a `references/` path in the `files_written[]` write.md §6's provenance `fix` printed; a file the `Body split` row lists in this run's `## Update Operation` section of `{forge_version}/evidence-report.md`, the one write.md §4 appended, never an earlier update's; outside gap-driven mode, a `references/*.md` file that `{skill_package}` and the previous version's package (`{skill_group}/{baseline_version}/{skill_name}/references/`) do not hold alike, one lacking it or the two differing; in gap-driven mode, which repairs the version in place, a file in `{run_dir}/references/`, where merge.md §6b staged each one it wrote, or a path `{run_dir}/references-removed.txt` lists.
273
274
  - `headless_decisions[]`: every gate's record in the run's decision log (init.md §4 degraded-rebuild and §8 confirmation, detect-changes.md §1b / §1c / §2.2, gap-driven.md §1 rule R1, merge.md §8). Each entry `{gate, default_action, taken_action, reason, evidence?}`. Empty when no gate auto-resolved.
274
275
  - `error`: null on every exit this step prints. A halt's line carries `{phase, path?, reason}`, and pipelines branch on `error !== null` for non-zero exit semantics.
275
- - `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `new-files-not-checked` (Category D: a skill with no brief, so no new script or asset was looked for), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category})` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished`, `public-api-counted-by-hand:` (write.md §2: the recipe runner exited 2, so the public names were found without it; it quotes the runner's error line) and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
276
+ - `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `new-files-not-checked` (Category D: a skill with no brief, so no new script or asset was looked for), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category}): {reason}` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished`, `public-api-counted-by-hand:` (write.md §2: the recipe runner exited 2, so the public names were found without it; it quotes the runner's error line) and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
276
277
 
277
278
  **Post-finalization hook.** A finished run, this one or §1's no-change exit, fires the hook; `--detect-only`, `--dry-run` and a halt never do (customize.toml says so). If `{onCompleteCommand}` (resolved in SKILL.md On Activation §4 from `workflow.on_complete`) is non-empty, invoke it after the emitter wrote both result files:
278
279
 
@@ -89,6 +89,14 @@ compareDocHashesProbeOrder:
89
89
  validateOutputProbeOrder:
90
90
  - '{project-root}/_bmad/skf/shared/scripts/skf-validate-output.py'
91
91
  - '{project-root}/src/shared/scripts/skf-validate-output.py'
92
+ # Resolve `{shardBodyHelper}` to the first existing path. §7 splits an
93
+ # oversized body with it when skill-check reports `body.max_lines` or
94
+ # `body.max_tokens`. If neither exists, §7 falls back to `skill-check
95
+ # split-body` for `body.max_lines` when SKILL.md holds no [MANUAL] block,
96
+ # and otherwise splits nothing and records a WARN.
97
+ shardBodyProbeOrder:
98
+ - '{project-root}/_bmad/skf/shared/scripts/skf-shard-body.py'
99
+ - '{project-root}/src/shared/scripts/skf-shard-body.py'
92
100
  # `{extractPublicApiHelper}`: §2's public API counts, the runner's or those
93
101
  # `{entryPointsByHandData}` has it make; if neither exists, §2 halts.
94
102
  extractPublicApiProbeOrder:
@@ -135,7 +143,7 @@ A `Write` or `Edit` call in §2 to §5 that errors (permission denied, disk full
135
143
 
136
144
  ### 0. Description Guard Protocol
137
145
 
138
- **Used by:** §7 (`skill-check check --fix` and `skill-check split-body --write`).
146
+ **Used by:** §7 (`skill-check check --fix`, the `{shardBodyHelper}` body split and `skill-check split-body --write`).
139
147
 
140
148
  The deterministic phases are executed via `{descriptionGuardHelper}`: §7 invokes the helper at the capture and verify-restore points around every `skill-check` call.
141
149
 
@@ -184,7 +192,7 @@ uv run {runLockHelper} acquire \
184
192
  `{lock_recovery}` is, outside gap-driven mode, "The previous version is unchanged, and the halt removed `{skill_group}/{new_version}/` and `{forge_data_folder}/{skill_name}/{new_version}/`, which step 4 created for this update: re-run update-skill once no other update of {skill_name} runs." and, in gap-driven mode, "This repair already wrote SKILL.md in place; the halt restored the package and the skill brief from the snapshot step 4 took, so the skill is as it was: re-run update-skill once no other update of {skill_name} runs."
185
193
 
186
194
  The `metadata` call below writes `{skill_package}/metadata.json`, never by hand:
187
- - **First, apply any queued `metadata_patches[]`** (staged by merge Priority 8b from gap-driven `metadata update` entries) to `metadata.json`, before §3's `apply` reads it: each surgical patch described in the gap's remediation (reconcile a divergent count, add an explanatory stat, etc.). The `metadata` call keeps a patch in any field but those its `--help` lists (e.g., `exports_documented`), whose value it writes; apply none to `exports[]`, which §3's `apply` builds from the list it reads. Log each patch the call overwrites or you leave out, so a still-stale value surfaces in the report rather than being silently overwritten.
195
+ - **First, apply any queued `metadata_patches[]`** (staged by merge Priority 8b from gap-driven `metadata update` entries) to `metadata.json`, before §3's `apply` reads it: each surgical patch described in the gap's remediation. The `metadata` call keeps a patch in any field but those its `--help` lists (e.g., `exports_documented`), whose value it writes; apply none to `exports[]`, which §3's `apply` builds from the list it reads. Log each patch the call overwrites, drops (its `dropped_stats`) or you leave out, so a still-stale value surfaces in the report rather than being silently overwritten.
188
196
  - **For gap-driven rescopes** (`DELETED_EXPORT` / verification `rescoped`): §3's `apply` drops the removed exports from `exports`, and `stats` recompute from that reduced surface, which the `brief.scope.exclude` + `scope.amendments[]` (`action: "excluded"`) step 4 §6b wrote from gap-driven.md §1's rule R1 justify.
189
197
  - `version` is `{new_version}`, the version step 4 §6b chose: **if `update_mode == "gap-driven"`, that is the unchanged version**: the skill is being repaired against the same source commit, so of the fields that mark an update only `generation_date` below changes, and step 4 wrote into the existing version directory (step 1 §6c records no source version in gap-driven mode). Otherwise it is the version whose folder step 4 §6b created: `{source_version_detected}` when step 1 §6c recorded `source_version_detected`, else the next patch version. Never pick another value here: `version` must name the folder `metadata.json` sits in, the one §8 points the `active` link at. From here on `{version}` is `{new_version}`.
190
198
  - Bind `{generation_date}` ← the current UTC time to the second, read from the clock with `date -u +%Y-%m-%dT%H:%M:%SZ`, never typed: step 1 §4b offers a test report only when it is newer. It is the only update bookkeeping metadata.json carries, in every mode: §3's update operation block records `last_update` and `update_type` in provenance-map.json, and the `metadata` call removes either key an older SKF version left in metadata.json.
@@ -215,9 +223,9 @@ The `metadata` call below writes `{skill_package}/metadata.json`, never by hand:
215
223
  | uv run {renderMetadataStatsHelper} {forge_version}/provenance-map.json --shape reference-app -o "{run_dir}/stats.json"
216
224
  ```
217
225
 
218
- If its `coherence.ok` (in `{run_dir}/stats.json`, which the `metadata` call reads) is false, fix the provenance map and re-run the helper; never hand-edit the stats. A `confidence_distribution` violation means some provenance entries carry a missing or unrecognized `signature_source` (§3 must write it on every entry).
226
+ If its `coherence.ok` (in `{run_dir}/stats.json`, which the `metadata` call reads) is false, fix the provenance map and re-run the helper; never hand-edit the stats. A `confidence_distribution` violation means some provenance entries carry a missing or unrecognized `signature_source`, which only an edit made after §3's `apply` leaves: `apply` gives every entry one, so set each the way it does (its `confidence` when that is `T1-low`, `T2` or `T3`, else `T1` when its `extraction_method` is `ast-grep` or `ast_bridge`, else `T1-low`).
219
227
 
220
- **Label violations.** A violation whose `field` is `provenance.entries[<i>].<field>` means entry `<i>` (its `export_name` is in the violation) carries a label its `extraction_method` does not allow: relabel it by the `## Relabel Rule` section of `{extractionPatternsData}`, with `{source_root}` as the tree its node-kind lookup reads. **Under the drift override** (`{workspace_drift_status}` is `overridden`, gap-driven.md §3) run no recipe at `{source_root}`, so no `kind-at`: HEAD is not the pinned commit, so a kind read there says nothing about the entry. Leave each violation that needs a kind from the tree in place and list it as a WARN ending ` (drift override: HEAD {head_short_sha} is not pinned {pinned_short_sha})`; a relabel that reads nothing from the tree still applies. With the status `ok` or `skipped`, run the lookup as the rule says. Rewrite `{forge_version}/provenance-map.json` with the relabeled entries and re-run the helper with the same payload, `--shape` and `-o`, whose file the `metadata` call reads: a relabeled `signature_source` moves the distribution, and a violation left as a WARN does not block the write. This relabel applies to every entry the helper flags, including a gap-driven `verified` entry (§3). Record the relabeled export names, and each WARN left, as `{provenance_relabels}` in workflow context: the update report lists them under its Confidence Tier Breakdown (report.md), so no entry moves from T1 to T1-low unannounced.
228
+ **Label violations.** A violation whose `field` is `provenance.entries[<i>].<field>` means entry `<i>` (its `export_name` is in the violation) carries a label its `extraction_method` does not allow: relabel it by the `## Relabel Rule` section of `{extractionPatternsData}`, with `{source_root}` as the tree its node-kind lookup reads. **Under the drift override** (`{workspace_drift_status}` is `overridden`, gap-driven.md §3) run no recipe at `{source_root}`, so no `kind-at`: HEAD is not the pinned commit, so a kind read there says nothing about the entry. Leave each violation that needs a kind from the tree in place and list it as a WARN ending ` (drift override: HEAD {head_short_sha} is not pinned {pinned_short_sha})`; a relabel that reads nothing from the tree still applies. With the status `ok` or `skipped`, run the lookup as the rule says. Rewrite `{forge_version}/provenance-map.json` with the relabeled entries and re-run the helper with the same payload, `--shape` and `-o`, whose file the `metadata` call reads: a relabeled `signature_source` moves the distribution, and a violation left as a WARN does not block the write. This relabel applies to every entry the helper flags, including a gap-driven `verified` entry (§3). Record the relabeled export names, each WARN left, and each name §3's `apply` listed in `relabeled`, as `{provenance_relabels}` in workflow context: the update report lists them under its Confidence Tier Breakdown (report.md), so no entry moves from T1 to T1-low unannounced.
221
229
  - **Write metadata.json** once the stats helper's last run wrote `{run_dir}/stats.json`. From `{project-root}`, run:
222
230
 
223
231
  ```bash
@@ -227,9 +235,13 @@ The `metadata` call below writes `{skill_package}/metadata.json`, never by hand:
227
235
  --stats "{run_dir}/stats.json" \
228
236
  --version "{new_version}" \
229
237
  --generation-date "{generation_date}" \
238
+ --skf-root "{project-root}/_bmad/skf" \
239
+ [--extraction "{run_dir}/extraction.json"] [--qmd-version "{qmd_version}"] \
230
240
  [--source-commit "{target_commit}"] [--source-ref "{target_ref}"]
231
241
  ```
232
242
 
243
+ It sets `tool_versions` to the toolchain this run used, as create-skill stamps it: `skf` read from the installed SKF under `--skf-root`, `ast_grep` from the `ast_grep.version` of `--extraction`, `qmd` from `--qmd-version`; a tool this run did not use keeps the version metadata.json records. Pass `--extraction` when step 2 wrote `{run_dir}/extraction.json`, and `--qmd-version` when re-extract.md §3 queried QMD and bound `{qmd_version}`. When the `effective_denominator` this run stores differs from the one metadata.json records, or only one of them has one, it drops each `effective_denominator_*` stats key, which explained the old count; `dropped_stats` lists each key it drops, a stats helper field this run's stats left out included. On exit 0, add each of its `warnings` to `warnings[]`, recorded as **Warnings go to the run log** says, so the report lists each dropped key, each `unknown` tool version and each stats key create-skill does not write that an earlier run left. Leave out only a warning that a stats key was `kept from an earlier run` when this run's `metadata_patches[]` wrote that key; a dropped key's warning always goes to `warnings[]`.
244
+
233
245
  On exit 1, fix the input its stderr names and run it again; when it fails again, prints no JSON or no candidate resolves, HALT with status `halted-for-write-failure` (halt procedure: `phase: "write:metadata"`, `path: "{skill_package}/metadata.json"`, `reason: "<its stderr>"`).
234
246
 
235
247
  **A docs-only skill's document hashes.** When step 2 wrote `{run_dir}/doc-hashes.json` (its docs-only branch), record the new hash of each document it found changed, after the writes above, so the next update compares against the documents this one read. Resolve `{compareDocHashesHelper}` ← first existing path in `{compareDocHashesProbeOrder}` and, from `{project-root}`, run:
@@ -273,11 +285,11 @@ uv run {buildChangeManifestHelper} apply \
273
285
  -o "{forge_version}/provenance-map.json"
274
286
  ```
275
287
 
276
- `{update_type}` follows the run's mode as report.md §2 names it (`{update_mode}`): `incremental` for `normal`, `gap-driven` for `gap-driven`, `full` for `degraded` (a degraded run has no map: leave out `--provenance-map`, and `apply` starts a new one). Pass each `{run_dir}` file the run wrote: the change manifest and the records in every mode, `category-c.json` and `ccc-pairs.json` (step 2's Category C and its CCC check), `extraction.json` and `export-details.json` when step 2 wrote them, `merge-records.json` when step 4 did, the Category D files (`category-d-compare.json`, and `new-files.json` when step 2 wrote it: a skill with no brief has none) with `--source-root` when step 2 ran Category D, and `promoted-docs.json` when step 2 §1b wrote it. `{generation_date}` is the value §2 bound, `--test-report-run-id` goes only with a gap-driven run (the `{test_report_run_id}` of the report it applied), `{manual_sections_preserved}` is `len(preserved) + len(moved)` from the §1 `manual-verify` verdict (never a marker count), and `{map_source_commit}` and `{map_source_ref}` are the `source_commit` and `source_ref` §2 writes to `metadata.json` (an empty string for a null one): audit-skill reads its baseline commit from the map's top-level `source_commit` and `source_ref`. Pass `--drift-head` and `--drift-pinned` only when `{workspace_drift_status}` is `overridden` (gap-driven.md §3). Pass `--metadata-in` and `--lists-out` in every mode: `apply` writes there the lists §2's `metadata` call reads. Every entry `apply` writes carries `signature_source`, which §2's stats helper bins, and `apply` sets the map's update operation block (`"last_update"`, `"update_type"` and its other keys), replacing an earlier update's.
288
+ `{update_type}` follows the run's mode as report.md §2 names it (`{update_mode}`): `incremental` for `normal`, `gap-driven` for `gap-driven`, `full` for `degraded` (a degraded run has no map: leave out `--provenance-map`, and `apply` starts a new one). Pass each `{run_dir}` file the run wrote: the change manifest and the records in every mode, `category-c.json` and `ccc-pairs.json` (step 2's Category C and its CCC check), `extraction.json` and `export-details.json` when step 2 wrote them, `merge-records.json` when step 4 did, the Category D files (`category-d-compare.json`, and `new-files.json` when step 2 wrote it: a skill with no brief has none) with `--source-root` when step 2 ran Category D, and `promoted-docs.json` when step 2 §1b wrote it. `{generation_date}` is the value §2 bound, `--test-report-run-id` goes only with a gap-driven run (the `{test_report_run_id}` of the report it applied), `{manual_sections_preserved}` is `len(preserved) + len(moved)` from the §1 `manual-verify` verdict (never a marker count), and `{map_source_commit}` and `{map_source_ref}` are the `source_commit` and `source_ref` §2 writes to `metadata.json` (an empty string for a null one): audit-skill reads its baseline commit from the map's top-level `source_commit` and `source_ref`. Pass `--drift-head` and `--drift-pinned` only when `{workspace_drift_status}` is `overridden` (gap-driven.md §3). Pass `--metadata-in` and `--lists-out` in every mode: `apply` writes there the lists §2's `metadata` call reads. Every entry `apply` writes carries `signature_source`, which §2's stats helper bins (an entry the map held with none, or with a value other than `T1`, `T1-low`, `T2` or `T3`, as a map written before the key existed does, takes its `confidence` when that is `T1-low`, `T2` or `T3`, else `T1` when its `extraction_method` is `ast-grep` or `ast_bridge`, else `T1-low`), and `apply` sets the map's update operation block (`"last_update"`, `"update_type"` and its other keys), replacing an earlier update's.
277
289
 
278
290
  Dispatch on its exit code:
279
291
 
280
- - **0:** it printed `{status: "written", entries, not_public, file_entries, warnings, map}`. Bind `{provenance_spot_check_warnings}` ← its `warnings`, and add each to `warnings[]`; the update report lists each as a WARN (report.md §2). `not_public` lists, `{name, file}`, each new export of a `public-api` skill it did not add in normal mode as off the public surface (the `public: false` mark of `reextract-records.json`, else the same rule over the `scope.type` and `entry_point_diff` of `extraction.json`), and one of its `warnings` counts them; it also wrote that list to `{run_dir}/not-public.json` (`[]` when it left none out). When `extraction.json` names `public-api` but gives no surface the rule can read (Quick tier, a runner failure, an incomplete run), it adds every name, with one warning that says so.
292
+ - **0:** it printed `{status: "written", entries, not_public, relabeled, signature_source_backfilled, file_entries, warnings, map}`. Bind `{provenance_spot_check_warnings}` ← its `warnings`, and add each to `warnings[]`; the update report lists each as a WARN (report.md §2). In gap-driven mode a `NEW_EXPORT` or `MODIFIED_EXPORT` the map holds whose spot-check ended `missing` or `unknown` keeps its entry unchanged, whatever its severity, with one of these warnings, which names the cause of an `unknown` (several map entries of its name, no definition line or several at the entry, a file no rule covers, a failed or skipped check, a citation of another file than the map's entries of its name); for a blocking gap (any severity but `Medium`, `Low` or `Info`) it also names the gap and the one fix a re-run acts on (gap-driven.md §4 bullet 1): fix the entry by hand, or, for several entries of its name, give the gap a `file:line` Source at the definition when that file holds only one of them, then re-run update-skill. A `cited-elsewhere` gap, whatever its severity, takes its own warning and fix in their place (gap-driven.md §4 bullet 1). `not_public` lists, `{name, file}`, each new export of a `public-api` skill it did not add in normal mode as off the public surface (the `public: false` mark of `reextract-records.json`, else the same rule over the `scope.type` and `entry_point_diff` of `extraction.json`), and one of its `warnings` counts them; it also wrote that list to `{run_dir}/not-public.json` (`[]` when it left none out). When `extraction.json` names `public-api` but gives no surface the rule can read (Quick tier, a runner failure, an incomplete run), it adds every name, with one warning that says so. `relabeled` lists, `{name, file, from, to}`, each entry whose `signature_source` a gap-driven repair changed when it wrote the signature §4a's record gave (a record read by eye moves an `ast-grep` entry from T1 to T1-low), each with one of its `warnings`: §2 adds each name to `{provenance_relabels}`. `signature_source_backfilled` counts, by value, the entries it gave a `signature_source` because they lacked or held an unrecognized one (`{"T1-low": 2}`, `{}` when none), with one of its `warnings`, which also lists each unrecognized value it replaced (`name: old -> new`); it does not list them in `entries.updated`. In a map naming several libraries, one of its `warnings` counts and names each new entry it gave the map's first `source_library` because the entry's file or folder did not settle which (`<name> in <file>`, or `<name> (no file)`): check that entry's `source_library` in `{forge_version}/provenance-map.json` and correct it by hand when it names the wrong library.
281
293
  - **3** (`status` `refused`): a blocking gap reached the provenance write with no source line (`blocking_unresolved[]`: a `NEW_EXPORT` the map does not hold, `unknown`, with a severity other than `Medium`, `Low` or `Info`, a missing one included). gap-driven.md §4 sends every such gap to its §4a and halts with `halted-for-remediation-path` before merge when §4a does not find it, so this happens only when that check was skipped or bypassed: `apply` wrote nothing, and no null citation is written. HALT with status `blocked` and write no `metadata.json`, `provenance-map.json` or other artifact. Tell the user: "**A blocking gap reached the provenance write with no source line:** {each export name, with its severity or "no severity"}. gap-driven.md §4 should have stopped on it with `halted-for-remediation-path`. The halt restored SKILL.md and references/ from the snapshot step 4 took; give each gap a `file:line` Source or name the file that defines its export in its Remediation (or downgrade it to Medium, Low or Info), then re-run update-skill." The halt procedure takes `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "blocking-gap-unresolved: {export names}: gap-driven.md §4 should have halted with halted-for-remediation-path"`.
282
294
  - **Exit 1, no JSON, or no candidate resolves:** HALT with status `halted-for-write-failure` (halt procedure: `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "<its stderr, or skf-build-change-manifest.py is missing; re-install SKF>"`).
283
295
 
@@ -320,6 +332,7 @@ Append update operation section to `{forge_version}/evidence-report.md` (create
320
332
 
321
333
  ### Validation Summary
322
334
  - Spec compliance: {PASS/WARN/FAIL/SKIP} (quality score {score}/100)
335
+ - Body split: {each section §7 moved and the references/ file it added, or none}
323
336
  - [MANUAL] integrity: {PASS/FAIL}
324
337
  - Confidence tiers: {PASS/WARN}
325
338
  - Provenance: {PASS/WARN}
@@ -340,7 +353,7 @@ Append update operation section to `{forge_version}/evidence-report.md` (create
340
353
 
341
354
  **Scope and Targeted Re-Extraction population:** read `{run_dir}/evidence-records.jsonl`, where step 2 (§1b, §1c and §2.2) and gap-driven.md §4 appended their records, one JSON line each. Write each record under its key as it was recorded, and `none` for a key no line holds (or when the file does not exist), so a headless decision such as a deletion-ratio continue stays visible after the run.
342
355
 
343
- **Validation Summary population:** §4 writes `[MANUAL] integrity` from §1's `manual-verify` verdict (`PASS` when `ok`) and `Confidence tiers` from §2 (`PASS` when the stats helper reported no label violation, else `WARN` naming `{provenance_relabels}`), with placeholders for the rest. §6 fills `Provenance`, and §7 fills `Spec compliance`, `Diff` and `Security`, each in the on-disk report, as §5 fills the Context Snippet sub-block.
356
+ **Validation Summary population:** §4 writes `[MANUAL] integrity` from §1's `manual-verify` verdict (`PASS` when `ok`) and `Confidence tiers` from §2 (`PASS` when the stats helper reported no label violation, else `WARN` naming `{provenance_relabels}`), with placeholders for the rest. §6 fills `Provenance`, and §7 fills `Spec compliance`, `Diff` and `Security`, and `Body split` (`none` when no split ran), each in the on-disk report, as §5 fills the Context Snippet sub-block.
344
357
 
345
358
  **Description Guard population** (used by §7 Post-Write Validation when the §0 protocol fires): fill all four fields from context when `description_guard_restored == true` (triggering tool, whether restore succeeded, and what changed, based on the recorded `description_guard_diff_kind`). When `Restored: false`, the other three fields are `—` — this is the clean-run expected state — except when `description_guard_refused == "empty-capture"` (§0's empty-snapshot rule): then set `Triggering tool` to the recorded tool name, `Original description preserved: false`, and `Notes: guard refused — empty captured snapshot (empty-capture)`, so a refused restore is distinguishable from a run where the guard never fired. Same field semantics and populator logic as create-skill step 6 §8.
346
359
 
@@ -350,23 +363,24 @@ Append update operation section to `{forge_version}/evidence-report.md` (create
350
363
 
351
364
  **Regenerate `context-snippet.md` if stale:**
352
365
 
353
- `context-snippet.md` is a `{skill_package}` deliverable that goes stale whenever **headline exports**, **version**, or **gotchas** change in this run. Regenerate it only when at least one of these triggers fired; otherwise skip — a skip is the correct outcome for gap-driven repairs and other runs that touch internals below the snippet's surface, where regenerating would produce byte-identical content.
366
+ `context-snippet.md` is a `{skill_package}` deliverable that goes stale whenever **headline exports**, **version**, **gotchas** or **section anchors** change in this run. Regenerate it only when at least one of these triggers fired; otherwise skip: a skip is the correct outcome for gap-driven repairs and other runs that touch internals below the snippet's surface, where regenerating would produce byte-identical content.
354
367
 
355
368
  **Staleness triggers:**
356
369
 
357
- - **Headline exports changed** — the top-K exports surfaced in the snippet differ from the prior snippet (a `NEW_EXPORT` was promoted into a headline slot, or a `MODIFIED_EXPORT` changed the signature/shape of a surfaced export).
370
+ - **Headline exports changed**: the api names `skf-export-skill/references/generate-snippet.md` §3b ranks for this run differ from the prior snippet's `|api:` line (a listed name left `exports` or is now deprecated, a `NEW_EXPORT` took a free slot, or a `MODIFIED_EXPORT` changed the kind of a listed name). In gap-driven mode it fires only when a name of the prior line left `exports` or SKILL.md now marks it deprecated or removed: a line that differs by the ranking alone (its order, a free slot or the tier's limit) waits for the next export.
358
371
  - **Version changed** — §2 bumped `version` (normal mode with detected source drift; never fires in gap-driven mode per §2's carve-out).
359
- - **Gotchas changed** — new gotchas surfaced from this run's evidence that were not in the prior snippet, or a prior gotcha was invalidated and removed.
372
+ - **Gotchas changed**: the gotchas line `skf-export-skill/references/generate-snippet.md` §3a resolves from the merged SKILL.md and this run's evidence report (at most 3 pitfalls, in its order) differs from the prior snippet's `|gotchas:` line, `[CARRIED]` markers aside.
373
+ - **Anchors changed**: the prior snippet's `|quick-start:` or `|key-types:` anchor differs from the one `generate-snippet.md` §3b item 4 gives for the merged SKILL.md (an anchor points into `references/` although SKILL.md has the heading, or a heading moved). Record it as `anchors` in `Triggers fired`.
360
374
 
361
375
  **Record the decision on the on-disk evidence report:** open `{forge_version}/evidence-report.md` (written by §4 with placeholder values in the `### Context Snippet` sub-block) and update that sub-block under the Update Operation section just written. Set `Regenerated: true|false`, fill `Triggers fired:` with the list of triggers that fired (or `—` when none), and write a one-sentence `Notes:` entry. See §4's "Context Snippet population" note for field semantics.
362
376
 
363
377
  **If no trigger fired:** skip regeneration: do not touch `context-snippet.md` on disk. The snippet remains valid against the prior run's surface. Continue to §6.
364
378
 
365
- **If at least one trigger fired:** regenerate the snippet using the format from `skf-create-skill/assets/skill-sections.md` (pipe-delimited indexed format).
379
+ **If at least one trigger fired:** regenerate the snippet. Its content comes from `skf-export-skill/references/generate-snippet.md` §3a and §3b, as the paragraph below says; `skf-create-skill/assets/skill-sections.md` gives only its line format (pipe-delimited indexed format).
366
380
 
367
381
  Use the **flat draft form** for the `root:` path in the draft snippet: `root: skills/{skill-name}/`. The per-IDE skill root (e.g., `.claude/skills/`, `.windsurf/skills/`, `.github/skills/` — see `skf-export-skill/assets/managed-section-format.md`) is applied later by `export-skill` step 3 when the skill is exported. Do not choose an IDE-specific prefix in update-skill — that is an export-time decision that depends on config.yaml.
368
382
 
369
- Pull values for the regenerated snippet from the updated metadata.json (version, top exports), the merged SKILL.md (section anchors, inline summaries), and the evidence report (new gotchas). If gotchas cannot be derived from the updated evidence but the prior snippet has a `|gotchas:` line, carry forward the prior line with the `[CARRIED]` marker — see `skf-export-skill/references/generate-snippet.md` for the carry-forward protocol (one-cycle limit).
383
+ Take the version from the updated metadata.json and the inline summaries from the merged SKILL.md. Rank the api names and verify the section anchors as `skf-export-skill/references/generate-snippet.md` §3b states, and derive the gotchas as its §3a states: the merged SKILL.md, this run's evidence report (`{forge_version}/evidence-report.md`) and the provenance map §3 wrote (`{forge_version}/provenance-map.json`, its `{forge_provenance_map}`) are the current sources, and the prior `context-snippet.md` gives `prior_api` and `prior_gotchas` (its §2.5). A prior pitfall no current source states takes a free slot with the `[CARRIED]` marker, or is dropped when already carried, by that file's carry-forward protocol (one-cycle limit).
370
384
 
371
385
  Write the regenerated snippet to `{skill_package}/context-snippet.md`, preserving file permissions.
372
386
 
@@ -389,7 +403,7 @@ The helper reads its `--source-root` (falling back to the provenance map's own `
389
403
  - `missing[]` — documented exports (metadata `exports[]`) with no provenance entry: a coverage gap.
390
404
  - `orphaned[]` — provenance `entries[].export_name` whose export was removed but the entry remains.
391
405
  - `summary.set_diff`: `not-applicable` for a reference app (`scope_type: reference-app` in `{skill_package}/metadata.json`), whose `exports[]` is empty by design while its entries follow each citation: the helper then diffs no sets, `missing[]` and `orphaned[]` stay empty, and its `status` comes from `stale[]`, `citations[]` and `node_kinds[]` alone. `checked` for every other skill.
392
- - `stale[]`: entries whose `source_file:source_line` no longer resolves or is not the line that defines the export; each carries a `reason` of `file-missing`, `line-out-of-bounds`, `line-invalid` or `line-not-definition`. A `line-not-definition` item lists in `definition_lines` every line of the file that defines the export (the `def` or declaration line itself, not a decorator or blank line above it). An empty list means the rules found no definition line, which may be a shape they do not cover: the entry is unverified, not gone. Internal names are canonicalized through `reexport_map` before the diff, so a barrel-renamed export does not read as missing or orphaned.
406
+ - `stale[]`: entries whose `source_file:source_line` no longer resolves or is not the line that defines the export; each carries a `reason` of `file-missing`, `line-out-of-bounds`, `line-invalid` or `line-not-definition`. A `line-not-definition` item lists in `definition_lines` every line of the file that defines the export (the `def` or declaration line itself, not a decorator or blank line above it), except that a Python `@overload` group gives its first stub alone. An empty list means the rules found no definition line, which may be a shape they do not cover: the entry is unverified, not gone. Internal names are canonicalized through `reexport_map` before the diff, so a barrel-renamed export does not read as missing or orphaned.
393
407
  - `citations[]`: `[AST:]` / `[SRC:]` citations in SKILL.md and `references/` whose prefix disagrees with the entry they cite (`prefix-mismatch`: `ast-grep` gives `[AST:]`, `source-read` gives `[SRC:]`), and `[AST:]` citations in a skill whose map has no `ast-grep` entry (`ast-without-ast-grep`). Each gives the markdown `file` and `line`, the `citation` text and its `expected_prefix`.
394
408
  - `summary.stale_check` — `"checked"` when citations were resolved against the source tree, or `"skipped-no-source-root"` when no source root resolved on disk (the completeness + orphan diffs still ran; `stale` is empty by construction, not clean-by-verification).
395
409
  - `summary.skill_citations_scanned` and `summary.skill_citations_matched`: when citations were scanned and none matched, no citation names a path and line the map records (the citations use another root), so the prefix check compared nothing. List that as a WARN.
@@ -414,7 +428,7 @@ When the helper exits 2 it writes no JSON: record `Provenance: WARN (not run: ve
414
428
  - **`manual_verify.ok` is false** (a [MANUAL] block changed): HALT with status `halted-for-manual-mismatch`. The halt procedure takes `phase: "write:verify-manual-integrity"`, `path: "{skill_package}/SKILL.md"`, `reason: "[MANUAL] blocks changed after the provenance fixes: ..."`, as §1's does. Alert the user: "**[MANUAL] section integrity failure after the provenance fixes.** Blocks modified (interior changed): {modified}. Blocks missing (markers lost): {missing}. Verified against the inventory `{manual_inventory}`. {manual_recovery}", with `{manual_recovery}` as §1 gives it: §8 has not pointed the `active` link at a new version yet, so this halt rolls back as §1's does.
415
429
  - **Exit 2** (no JSON; its stderr line names any file it already wrote): record `Provenance: WARN (not fixed: verifier error)` with that line, re-read each file it names into context, and go on to step 2.
416
430
  - **Otherwise:** re-read each file in `files_written[]` into the in-context copies (the merged SKILL.md and `references/` content and §3's provenance map), so later sections and the report read the fixed text, and add `SKILL.md` and `provenance-map.json` to `files_written[]` (report.md §5b) when `fix` wrote them.
417
- 2. **Node kinds.** For each `node_kinds[]` item, take the entry at its `entry_index` in `{forge_version}/provenance-map.json` as step 1 left it (its `export_name` and `source_file` confirm it). When this run's extraction record (a `files` record of `{run_dir}/reextract-records.json`, which re-extract.md §4 or gap-driven.md §4 wrote) names the recipe that matched that export (`ast_recipe`), set the entry's `ast_node_type` to the `kind` that recipe declares in `{extractionPatternsData}` (the ast-grep Patterns table in create-skill's `extraction-patterns-by-hand.md`, which that section of `{extractionPatternsData}` points to, gives the kind of a `find_code` pattern). When no record names one (a `verified` or `moved` entry has none), look the kind up at the entry's `source_file` and `source_line`: from `{project-root}`, run `uv run {verifyProvenanceCompletenessHelper} kind-at --source-root "{source_root}" --file "{source_file}" --line {source_line} --name "{export_name}" --recipes "{extractionPatternsData}"` and set the `kind` it prints when `status` is `found`. List a WARN, with the status, for any other status or an exit 2, when there is no local source tree, or when `{workspace_drift_status}` is `overridden` (gap-driven.md §3: the tree read is not the recorded commit, so run no `kind-at`). Never invent a kind, and never change `extraction_method` to clear the finding. Write the map with `python3 {atomicWriteHelper} write --target {forge_version}/provenance-map.json` (resolve it ← first existing path in `{atomicWriteProbeOrder}`, the new map on stdin); if neither path resolves, fix no node kind and list each one as a WARN. Apply the same change to §3's in-context map, and add `provenance-map.json` to `files_written[]`.
431
+ 2. **Node kinds.** For each `node_kinds[]` item, take the entry at its `entry_index` in `{forge_version}/provenance-map.json` as step 1 left it (its `export_name` and `source_file` confirm it). When this run's extraction record (a `files` record of `{run_dir}/reextract-records.json`, which re-extract.md §4 or gap-driven.md §4 wrote) names the recipe that matched that export (`ast_recipe`), set the entry's `ast_node_type` to the `kind` that recipe declares in `{extractionPatternsData}` (the ast-grep Patterns table in create-skill's `extraction-patterns-by-hand.md`, which that section of `{extractionPatternsData}` points to, gives the kind of a `find_code` pattern). When no record names one (a `verified` or `moved` entry has none unless gap-driven.md §4 copied the record §4a found at its line), look the kind up at the entry's `source_file` and `source_line`: from `{project-root}`, run `uv run {verifyProvenanceCompletenessHelper} kind-at --source-root "{source_root}" --file "{source_file}" --line {source_line} --name "{export_name}" --recipes "{extractionPatternsData}"` and set the `kind` it prints when `status` is `found`. List a WARN, with the status, for any other status or an exit 2, when there is no local source tree, or when `{workspace_drift_status}` is `overridden` (gap-driven.md §3: the tree read is not the recorded commit, so run no `kind-at`). Never invent a kind, and never change `extraction_method` to clear the finding. Write the map with `python3 {atomicWriteHelper} write --target {forge_version}/provenance-map.json` (resolve it ← first existing path in `{atomicWriteProbeOrder}`, the new map on stdin); if neither path resolves, fix no node kind and list each one as a WARN. Apply the same change to §3's in-context map, and add `provenance-map.json` to `files_written[]`.
418
432
 
419
433
  These fixes apply to any entry, a gap-driven `verified` or `moved` entry included (§3). Fix nothing else: each `left_as_warn[]` item stays for a person to decide, and so do `missing` / `orphaned` exports. A `line-not-definition` item with an empty `definition_lines` is never fixed: list its export as unverified.
420
434
 
@@ -426,16 +440,16 @@ Map the re-run's `status` to the `Provenance:` line of the §4 evidence report's
426
440
 
427
441
  ### 7. Run Post-Write Validation
428
442
 
429
- skill-check runs once per update, here, against the written package. Check that it is available, never assume it: run `npx skill-check -h`.
443
+ skill-check runs once per update, here, against the written package, and once more after a body split (below) to read the score the split left. Check that it is available, never assume it: run `npx skill-check -h`.
430
444
 
431
- **Description Guard Protocol:** every invocation below that may modify SKILL.md (`skill-check check --fix` and any `split-body` write) must run inside the four-phase guard defined in §0. Invoke `{descriptionGuardHelper}` at the capture and verify-restore points around each call:
445
+ **Description Guard Protocol:** every invocation below that may modify SKILL.md (`skill-check check --fix`, the `{shardBodyHelper}` body split and any `split-body` write) must run inside the four-phase guard defined in §0. Invoke `{descriptionGuardHelper}` at the capture and verify-restore points around each call:
432
446
 
433
447
  ```bash
434
448
  # Phase 1: capture before any frontmatter-touching tool call
435
449
  uv run {descriptionGuardHelper} capture "{skill_package}/SKILL.md"
436
450
  # stash returned `description` as `guarded_description`
437
451
 
438
- # Phase 2: run the tool (skill-check --fix, split-body --write)
452
+ # Phase 2: run the tool (skill-check --fix, the body split, split-body --write)
439
453
 
440
454
  # Phases 3+4: verify and restore after the tool call
441
455
  uv run {descriptionGuardHelper} verify-restore "{skill_package}/SKILL.md" \
@@ -448,7 +462,14 @@ Do not rely on per-call ad-hoc preservation logic: use the helper.
448
462
 
449
463
  - Run `npx skill-check check "{skill_package}" --fix --format json` **inside the §0 guard**. One call validates the package against the spec, scores it, fixes what has one answer and runs the security scan (on unless `--no-security-scan` is passed). Read the JSON, not the exit code: the quality score (`scores[].score`, the entry matching the skill, or a top-level `qualityScore` on an older skill-check), the remaining `diagnostics[]`, `fixed[]` and the security findings. When the scan could not run (no `SNYK_TOKEN`), record `Security: SKIP (SNYK_TOKEN not configured)`.
450
464
  - **Context sync after --fix:** if `fixed[]` is non-empty, re-read the SKILL.md `--fix` changed into the in-context copy, so the report does not work from a copy that differs from the file on disk. The §0 guard has already restored `description` if it diverged.
451
- - If `body.max_lines` is reported, prefer a selective split: extract only the largest Tier 2 section(s) to `references/`, keeping Tier 1 inline (inline passive context achieves 100% task accuracy vs 79% for on-demand retrieval). **Never move a section that holds a `[MANUAL]` block:** init.md §5 inventories SKILL.md only, so a block moved to `references/` would leave the next update's inventory. Fall back to `npx skill-check split-body "{skill_package}" --write`, **inside the §0 guard** (it can also touch frontmatter), only when SKILL.md holds no `[MANUAL]` block (§1's verdict lists none): it moves every section. Verify anchors resolve after a split.
465
+ - If `body.max_lines` or `body.max_tokens` is reported, split the body selectively with `{shardBodyHelper}` (resolve it ← first existing path in `{shardBodyProbeOrder}`), **inside the §0 guard**: from `{project-root}`, run `uv run {shardBodyHelper} "{skill_package}/SKILL.md" --budget 400 --word-budget 4500 --skip-manual`, with `--budget 500` in place of `--budget 400` when only `body.max_tokens` is reported. It moves the largest Tier 2 (`## Full`) sections to `references/`, each replaced by a `> See` pointer to a file it never overwrites, until the body is within that many lines and 4,500 words, the same headroom under skill-check's 5,000 that 400 lines leave under its 500, and keeps Tier 1 inline (inline passive context achieves 100% task accuracy vs 79% for on-demand retrieval). **Never move a section that holds a `[MANUAL]` block:** init.md §5 inventories SKILL.md only, so a block moved to `references/` would leave the next update's inventory; `--skip-manual` keeps the helper off every section a block reaches into. No Tier 1 content is trimmed, and `split-body` never runs after the helper. Add each WARN below to `warnings[]` as well. Act on its report:
466
+ - **Exit 1 or 2, or no JSON:** HALT with status `blocked` (halt procedure: `phase: "write:body-split"`, `path: "{skill_package}/SKILL.md"`, `reason: "body split failed: {its stderr line}"`). On exit 2 that line ends `already written: {each references/*.md file it wrote before the failure, or none}`: name each of those files in the halt message.
467
+ - **`tier1_preserved` false (it then wrote nothing) or `xref_ok` false:** HALT with status `blocked` (halt procedure: `phase: "write:body-split"`, `path: "{skill_package}/SKILL.md"`, `reason: "body split: tier1_missing {tier1_missing}; xref_ok {xref_ok}"`).
468
+ - **`under_budget` false:** record `Spec compliance: WARN (body.max_lines: no Tier 2 section without a [MANUAL] block left to move)`.
469
+ - **`under_word_budget` false:** record `Spec compliance: WARN (body.max_tokens: no Tier 2 section without a [MANUAL] block left to move)`.
470
+ - **No `{shardBodyProbeOrder}` candidate resolves:** when `body.max_lines` is reported and SKILL.md holds no `[MANUAL]` block (§1's verdict lists none), fall back to `npx skill-check split-body "{skill_package}" --write`, **inside the §0 guard** (it can also touch frontmatter): it moves every section. When `body.max_lines` is reported and SKILL.md holds a `[MANUAL]` block, split nothing and record `Spec compliance: WARN (body.max_lines: skf-shard-body.py missing)`; when only `body.max_tokens` is reported, split nothing and record `Spec compliance: WARN (body.max_tokens: skf-shard-body.py missing)`.
471
+
472
+ After a split (the helper's `sections_extracted` is not empty, or the fallback ran), re-read SKILL.md and each new `references/*.md` file into the in-context copy, as after `--fix`; re-run `npx skill-check check "{skill_package}" --format json --no-security-scan` and take the quality score and the remaining `diagnostics[]` from it; list in the Validation Summary's `Body split` row each section moved and the `references/` file it added; and verify anchors resolve.
452
473
  - When a previous version is on disk (outside gap-driven mode), run `npx skill-check diff "{skill_group}/{baseline_version}/{skill_name}" "{skill_package}"` and record its new and fixed issues as informational; otherwise record `Diff: SKIP`.
453
474
 
454
475
  **Re-check the [MANUAL] blocks** once `--fix` and any split have run, from `{project-root}`:
@@ -84,10 +84,10 @@ When `{headless_mode}` is true, step 6 prints one `SKF_VERIFY_STACK_RESULT_JSON:
84
84
 
85
85
  **Bind the inputs** in every mode: `{architecture_doc_path}` ← the `--architecture-doc` value, `{prd_path}` ← `--prd` and `{previous_report_path}` ← `--previous-report`, each null when its flag is absent. init.md §1 asks only for those still null.
86
86
 
87
- 4. **Resolve workflow customization.** Run:
87
+ 4. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
88
88
 
89
89
  ```bash
90
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
90
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
91
91
  ```
92
92
 
93
93
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-verify-stack.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran) for init.md to record once it has created `{run_dir}`.