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
@@ -17,7 +17,7 @@ To calculate approximate token counts for all exported artifacts and present a c
17
17
 
18
18
  - Focus only on token counting and reporting — read-only measurement
19
19
  - Auto-proceed when complete
20
- - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), run the helper once per skill, then present one aggregate table with one row per skill (context-snippet.md, SKILL.md, metadata.json, references/, package total). The `managed_section` value is identical across runs (same target files) — measure and report it once for the run. See step 1 §1c.
20
+ - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), run the helper once per skill, then present one aggregate table with one row per skill (context-snippet.md, SKILL.md, metadata.json, references/, package total) and the Context Budget Impact per skill (its `context_budget.on_trigger`, `full_disclosure` and `snippet`). The `managed_section` and `context_budget.always_on` values are identical across runs (same target files): measure and report them once for the run. See step 1 §1c.
21
21
 
22
22
  ## MANDATORY SEQUENCE
23
23
 
@@ -37,44 +37,64 @@ The helper emits JSON:
37
37
  {
38
38
  "files": [{"path": "…", "role": "…", "exists": true, "words": 0, "tokens": 0}],
39
39
  "references_total": {"count": 0, "words": 0, "tokens": 0},
40
- "managed_section": {"present": true, "source_file": "…", "words": 0, "tokens": 0},
41
- "package_total": {"words": 0, "tokens": 0}
40
+ "managed_section": {"present": true, "source_file": "…", "target_files": ["…"], "skills": 0, "words": 0, "tokens": 0},
41
+ "package_total": {"words": 0, "tokens": 0},
42
+ "context_budget": {"always_on": 0, "on_trigger": 0, "full_disclosure": 0, "snippet": 0}
42
43
  }
43
44
  ```
44
45
 
45
46
  **Read the JSON — do not re-count words in-prompt.** Each `tokens` value is a char-over-four estimate (`len(text)//4`, the SKF-wide convention shared with `skf-validate-output.py`); each `words` value is the whitespace-split count for the report's Words column. The `files` array carries one row per artifact — `role` is one of `context-snippet`, `skill-md`, `metadata`, `reference`. `package_total` already sums context-snippet.md + SKILL.md + metadata.json + `references_total` and **excludes** the `managed_section` row: the managed section is a shared all-skills cost (it bundles this skill's snippet plus every other skill's snippet), so folding it in would double-count this skill's snippet and pull in unrelated skills. It is reported separately under Context Budget Impact.
46
47
 
47
- **If `passive_context` was disabled:** the helper reports the `context-snippet.md` row with `exists: false` and `managed_section.present: false` — render both as "N/A (disabled)".
48
+ `managed_section.skills` counts the skill rows (each `[name v…]` row header, as the managed-section rebuild counts them) in the measured block. `managed_section.target_files` lists every target file that holds a block, each once, and `managed_section.source_file` names the one the helper measured, the first of them; a target file that cannot be read is skipped. `context_budget` gives the Context Budget Impact values, in tokens: `always_on` is the managed section's (null when no target file holds a block: render it as N/A), `on_trigger` SKILL.md's, `full_disclosure` SKILL.md's plus `references_total.tokens` (what an agent loads when it reads every reference too: neither the snippet nor metadata.json, so it is not `package_total`), and `snippet` the context-snippet.md row's.
49
+
50
+ **If `passive_context` was disabled:** the helper reports the `context-snippet.md` row with `exists: false` and `managed_section.present: false`: render both rows, the always-on cost and the benchmark as "N/A (disabled)".
48
51
 
49
52
  ### 2. Present Token Report
50
53
 
51
- Render the table straight from the helper JSON — no re-counting.
54
+ Render the report straight from the helper JSON, with no re-counting. In each table row, `{n}` ← the row's `words` and `{t}` ← its `tokens`:
52
55
 
53
56
  - `context-snippet.md` row → `files[].tokens` / `.words` for `role: context-snippet`
54
- - `Managed section` row → `managed_section.words` / `.tokens` (report once for the run)
57
+ - `Managed section` row → `managed_section.words` / `.tokens`; `{target-file-list}` ← `managed_section.target_files`, joined with commas; `{skill-count}` ← `managed_section.skills` (report the row once for the run)
55
58
  - `SKILL.md` row → `files[]` for `role: skill-md`
56
59
  - `metadata.json` row → `files[]` for `role: metadata`
57
- - `references/` row → `references_total.words` / `.tokens`; `{count}` files = `references_total.count`
60
+ - `references/` row → `references_total.words` / `.tokens`; `{count}` ← `references_total.count`
58
61
  - `Package total` row → `package_total.words` / `.tokens`
59
62
 
63
+ The Context Budget Impact lines, per skill in multi-skill mode (the always-on cost once for the run):
64
+
65
+ - `{managed-section-tokens}` ← `context_budget.always_on`, the managed section's tokens (N/A when null)
66
+ - `{measured-file}` ← `managed_section.source_file`, the target file the helper measured
67
+ - `{skill-tokens}` ← `context_budget.on_trigger`, SKILL.md's tokens
68
+ - `{total-tokens}` ← `context_budget.full_disclosure`, SKILL.md's tokens plus `references_total.tokens` (never `package_total`, which also counts the snippet and metadata.json)
69
+ - `{snippet-tokens}` ← `context_budget.snippet`, the context-snippet.md row's tokens
70
+ - `{skill-name}` ← `skill_package`, its last folder (multi-skill mode only)
71
+
60
72
  "**Token Report**
61
73
 
62
74
  | Artifact | Words | Est. Tokens | Notes |
63
75
  |----------|-------|-------------|-------|
64
76
  | context-snippet.md | {n} | ~{t} | Passive context (always-on) |
65
- | Managed section (all skills) | {n} | ~{t} | In {target-file-list}, all {count} skills |
77
+ | Managed section (all skills) | {n} | ~{t} | In {target-file-list}, all {skill-count} skills |
66
78
  | SKILL.md | {n} | ~{t} | Active skill (on-trigger) |
67
79
  | metadata.json | {n} | ~{t} | Machine-readable |
68
80
  | references/ | {n} | ~{t} | {count} files |
69
81
  | **Package total** | **{n}** | **~{t}** | **This skill's own artifacts (snippet + SKILL.md + metadata + references); excludes the shared Managed section row** |
70
82
 
71
83
  **Context Budget Impact:**
72
- - **Always-on cost:** ~{managed-section-tokens} tokens (managed section in {target-file-list})
84
+ - **Always-on cost:** ~{managed-section-tokens} tokens (measured in {measured-file}; the managed section is in {target-file-list})
73
85
  - **On-trigger cost:** ~{skill-tokens} tokens (when SKILL.md is loaded)
74
86
  - **Full disclosure cost:** ~{total-tokens} tokens (if references/ also loaded)
75
87
 
76
88
  **Benchmark:** Target is ~80-120 tokens per skill in managed section. Current: ~{snippet-tokens} tokens."
77
89
 
90
+ **In multi-skill mode**, render the Context Budget Impact and the Benchmark this way instead: the always-on cost once for the run, then one line per skill, each from that skill's own run:
91
+
92
+ "**Context Budget Impact:**
93
+ - **Always-on cost:** ~{managed-section-tokens} tokens (measured in {measured-file}; the managed section is in {target-file-list})
94
+ - **{skill-name}:** on-trigger ~{skill-tokens} tokens, full disclosure ~{total-tokens} tokens, snippet ~{snippet-tokens} tokens
95
+
96
+ **Benchmark:** Target is ~80-120 tokens per skill in managed section."
97
+
78
98
  ### 3. Proceed to Summary
79
99
 
80
100
  Display: "**Proceeding to export summary...**"
@@ -86,10 +86,10 @@ Run these steps once, in order, before the first reply.
86
86
  - No JSON object with a `status`: HARD HALT with what the call printed. When `uv` itself is missing, add that SKF runs its scripts with `uv`, which installs from <https://docs.astral.sh/uv/getting-started/installation/>.
87
87
  - `status` `ok`: bind `{project_name}`, `{user_name}`, `{communication_language}` and `{document_output_language}` from `config`, and each folder from its absolute path: `{output_folder}` from `config.output_folder_resolved`, `{skills_output_folder}` from `config.skills_output_folder_resolved`, `{forge_data_folder}` from `config.forge_data_folder_resolved` and `{sidecar_path}` from `config.sidecar_path_resolved`. The greeting reads `derived`. `sidecar.preferences_error` and `sidecar.forge_tier_error` appear only for a file that is there but did not load: name that file in one line of the greeting and go on with its defaults.
88
88
 
89
- 3. **Resolve agent customization.** Run:
89
+ 3. **Resolve agent 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:
90
90
 
91
91
  ```bash
92
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key agent
92
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key agent
93
93
  ```
94
94
 
95
95
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-forger.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. Ferris keeps no run log of his own, so that line opens the greeting.
@@ -79,10 +79,10 @@ These rules apply to every step in this workflow:
79
79
 
80
80
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `{sidecar_path}/preferences.yaml` sets `headless_mode: true`. Default: false.
81
81
 
82
- 3. **Resolve workflow customization.** Run:
82
+ 3. **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:
83
83
 
84
84
  ```bash
85
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
85
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
86
86
  ```
87
87
 
88
88
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-quick-skill.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) and, when `{emitEnvelopeHelper}` has a path, record it in the run sink: write `customization_resolver_unavailable: {customization_resolver_unavailable}` to `{run_dir}/resolver-warning.txt` with a file write, never `echo` (the reason can hold quotes or `$( )`), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"`. Under `--batch`, `references/batch-mode.md` §2 records that file in each target's run folder.
@@ -24,8 +24,6 @@ You are an architecture refinement analyst in Ferris Architect mode; the user br
24
24
 
25
25
  ## Workflow Rules
26
26
 
27
- These rules apply to every step in this workflow:
28
-
29
27
  - Never speculate — every gap, issue, or improvement must cite specific APIs, types, or function signatures from the generated skills
30
28
  - Only load one step file at a time — never preload future steps
31
29
  - Always communicate in `{communication_language}`
@@ -67,15 +65,15 @@ When `{headless_mode}` is true, step 6 prints one `SKF_REFINE_ARCHITECTURE_RESUL
67
65
  - `skills_output_folder`, `forge_data_folder`, `output_folder`, `sidecar_path`
68
66
 
69
67
  2. **Compute run-scoped variables:**
70
- - `timestamp` ← the output of `date -u +%Y%m%d-%H%M%S`, run once now: it names the run folder and the copy an earlier refined document is renamed to.
71
- - `run_dir` ← `{project-root}/_bmad-output/.skf-run/skf-refine-architecture-{timestamp}`, the run folder §5 creates
68
+ - `timestamp` ← the output of `date -u +%Y%m%d-%H%M%S`, run once now.
69
+ - `run_dir` ← `{project-root}/_bmad-output/.skf-run/skf-refine-architecture-{timestamp}`
72
70
 
73
71
  3. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
74
72
 
75
- 4. **Resolve workflow customization.** Run:
73
+ 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:
76
74
 
77
75
  ```bash
78
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
76
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
79
77
  ```
80
78
 
81
79
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-refine-architecture.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}` for §5 to record.
@@ -86,7 +84,7 @@ When `{headless_mode}` is true, step 6 prints one `SKF_REFINE_ARCHITECTURE_RESUL
86
84
  - `{outputFolderPath}` ← `workflow.output_folder_path` if non-empty, else `{output_folder}`
87
85
  - `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty
88
86
 
89
- Then 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 loads every `project-context.md` under `{project-root}`; an entry prefixed `!` drops each earlier entry it names and loads nothing itself, so an override's `"!file:{project-root}/**/project-context.md"` turns that default off), then run `workflow.activation_steps_append` once §5's pre-flight has passed, before §6 loads the first stage.
87
+ Then run `workflow.activation_steps_prepend` now, treat `workflow.persistent_facts` as standing context for the run (`file:` entries load their files or globs as facts: the bundled default loads every `project-context.md` under `{project-root}`; an entry prefixed `!` drops each earlier entry it names and loads nothing itself, so an override's `"!file:{project-root}/**/project-context.md"` turns that default off), then run `workflow.activation_steps_append` once §5's pre-flight has passed, before §6 loads the first stage.
90
88
 
91
89
  5. **Pre-flight: the emitter, config, write probe and run folder.** In that order, so every halt can print its envelope and an empty path halts as missing config (exit 3), not as a write failure.
92
90
 
@@ -114,7 +112,7 @@ When `{headless_mode}` is true, step 6 prints one `SKF_REFINE_ARCHITECTURE_RESUL
114
112
 
115
113
  On any non-zero exit: HALT (exit code 4, `halt_reason: "write-failed"`) at phase `on-activation:write-probe`, with `"path"` set to the folder that failed.
116
114
 
117
- **Run folder (exit 4).** Create the run folder:
115
+ **Run folder (exit 4).**
118
116
 
119
117
  ```bash
120
118
  mkdir -p "{project-root}/_bmad-output/.skf-run" && mkdir "{run_dir}"
@@ -63,10 +63,10 @@ Headless callers: the inputs, flags, gate map, outputs, concurrency rule and `SK
63
63
  - `{dry_run}`: true if `--dry-run` was passed, else false.
64
64
  - `{old_name_arg}` and `{new_name_arg}`: the current name and the new name the invocation supplied, as arguments or in a request such as "rename cognee to cognee-ai", each null when absent. select.md §4 and §5 take a supplied name in either mode instead of asking for it.
65
65
 
66
- 3. **Resolve workflow customization.** Run:
66
+ 3. **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:
67
67
 
68
68
  ```bash
69
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
69
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
70
70
  ```
71
71
 
72
72
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-rename-skill.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 select.md §1 to record once it has created `{run_dir}`.
@@ -65,10 +65,10 @@ You are a system executor: run each step in sequence, write the configuration fi
65
65
 
66
66
  5. **Reconcile `{headless_mode}`**: OR it with `derived.headless_mode` (`headless_mode: true` in `preferences.yaml`). Then, if `{headless_mode}` is true, set `{quiet_mode}` to true.
67
67
 
68
- 6. **Resolve workflow customization.** Run:
68
+ 6. **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:
69
69
 
70
70
  ```bash
71
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
71
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
72
72
  ```
73
73
 
74
74
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-setup.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. Bind `{customization_resolver_unavailable}` ← that reason (null when the resolver ran), which `references/report.md` and every later halt hand to the emitter; print the warning only when `{quiet_mode}` is false.
@@ -63,10 +63,10 @@ Headless callers: the full argument set (`Inputs`), the outputs, the exit codes
63
63
 
64
64
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
65
65
 
66
- 3. **Resolve workflow customization.** Run:
66
+ 3. **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:
67
67
 
68
68
  ```bash
69
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
69
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
70
70
  ```
71
71
 
72
72
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-test-skill.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}`: report.md §4c hands it to the result as a warning.
@@ -7,6 +7,7 @@
7
7
 
8
8
  **Mode:** Naive (structural validation only)
9
9
  **Coherence category:** Not scored (weight redistributed)
10
+ **Migration Section:** {the note migration-section-rules.md Case 4 records; leave this line out when it records none}
10
11
 
11
12
  ### Structural Findings
12
13
 
@@ -19,13 +20,13 @@
19
20
 
20
21
  ## Coherence Analysis — Naive Mode: Reference Consistency (split-body)
21
22
 
22
- Only rendered when `references/` directory exists alongside SKILL.md.
23
+ Only rendered when `references/` directory exists alongside SKILL.md. Each row is a split-body gap coverage-check recorded, with its Source at `SKILL.md:{SKILL.md Line}` when the pinned source contradicts the body, else at `{Reference File}:{Reference Line}`. **Contradicted Side** is the side the source contradicts (`SKILL.md` or `reference`), `none` when both forms agree with it, or `not settled` when no source settled it.
23
24
 
24
25
  ```markdown
25
26
  ### Reference Consistency (split-body)
26
27
 
27
- | # | Reference File | Export | Issue | SKILL.md Line | Reference Line |
28
- |---|---------------|--------|-------|---------------|---------------|
28
+ | # | Reference File | Export | Issue | SKILL.md Line | Reference Line | Contradicted Side |
29
+ |---|---------------|--------|-------|---------------|---------------|-------------------|
29
30
  | {per-mismatch rows} |
30
31
 
31
32
  **Exports Cross-Checked:** {count}
@@ -41,6 +42,7 @@ Only rendered when `references/` directory exists alongside SKILL.md.
41
42
  **References Found:** {count}
42
43
  **References Valid:** {count}
43
44
  **Broken References:** {count}
45
+ **Migration Section:** {the note migration-section-rules.md Case 4 records; leave this line out when it records none}
44
46
 
45
47
  ### Reference Validation
46
48
 
@@ -78,7 +80,7 @@ Every gap a stage finds is one JSON record in the run's gap ledger, `{forge_vers
78
80
  ```
79
81
 
80
82
  - `severity` and `category` come from one row of the Gap Severity table in `references/scoring-rules.md`. The script refuses a category outside its vocabulary, which `uv run scripts/gap-ledger.py categories` lists.
81
- - `source` is a `file:line` pair whenever a line applies, with nothing after it: update-skill reads the pair as the gap's citation. A gap inside the skill package cites `SKILL.md:{line}` or `references/{file}.md:{line}`.
83
+ - `source` is a `file:line` pair whenever a line applies, with nothing after it: update-skill reads the pair as the gap's citation. A gap inside the skill package cites `SKILL.md:{line}` or `references/{file}.md:{line}`. A split-body record, titled `Split-body mismatch: {export} ({reference_file})`, cites the side the pinned source contradicts: `SKILL.md:{skill_md_line}` when the reference agrees with the source declaration and the body does not, else `{reference_file}:{reference_line}`, also when no source settled it. Its remediation names the file to edit and the file and line whose form it takes, and update-skill changes only what it names as contradicting.
82
84
  - `remediation` follows the Remediation Quality Rules below and names each file it touches.
83
85
  - Leave out `id`, `group` and `stage`: the script assigns them (`GAP-{NNN}` in the order the gaps were recorded, the group from the category, the stage from `--stage`).
84
86
 
@@ -77,7 +77,7 @@ uv run {scanSkillMdStructureHelper} usage-scope "{resolved_skill_package}/SKILL.
77
77
  The script decides split-body from the `references/` folder alone; whether the `## Full*` sections are stubs is your judgment, so pass `--body single` when a `references/` directory exists but those sections carry real content. Read `zero_usage[]` from the output:
78
78
  - **Zero occurrences across the entire scope → Medium severity** finding, one per `zero_usage[]` entry: `naive-coherence: exported {kind} \`{name}\` is not referenced in any usage-family section or reference file`. This catches the "documented but unused" failure mode that trivially fails discovery testing. Like a missing export, it is a completeness gap, so it does not block the hard gate.
79
79
 
80
- **2.5 Async/sync consistency.** `{locateExportSegmentsScript}` reports the facts: for each export the step 3 inventory lists with `kind` `function` or `method`, where the skill describes it (`descriptions[]`: the heading sections, table rows, list items and paragraphs that name it in code, and the signature lines that declare it) and each call to it in fenced code, with whether the call awaits it. It reads the names from the validated inventory file itself, keeping the `function` and `method` entries, so no name is copied by hand. Skip this check when the inventory has no such export; otherwise run the script (it resolves relative to the skill root):
80
+ **2.5 Async/sync consistency.** `{locateExportSegmentsScript}` reports the facts: for each export the step 3 inventory lists with `kind` `function` or `method`, where the skill describes it (`descriptions[]`: the heading sections, table rows, list items and paragraphs that name it in code, and the signature lines that declare it) and each call to it in fenced code, with whether the call awaits it (`awaited`) and how (`awaitedVia`): `direct` when the await applies to the call itself (`directlyAwaited`), `chain` when it applies to a member, subscript, call or group around the call, `consumer` for an async loop or context manager, and `argument` or `runner` when the call sits in the arguments of an awaited call or of a runner, whose callee `enclosingCallee` names. It reads the names from the validated inventory file itself, keeping the `function` and `method` entries, so no name is copied by hand. Skip this check when the inventory has no such export; otherwise run the script (it resolves relative to the skill root):
81
81
 
82
82
  ```bash
83
83
  uv run {locateExportSegmentsScript} --inventory "{run_dir}/inventory.json" --skill-dir "{resolved_skill_package}"
@@ -92,7 +92,8 @@ The script judges no meaning. For each `exports[]` entry, read its `descriptions
92
92
  Then, for each export:
93
93
 
94
94
  - **async**, no call is awaited (`awaitedCount` is 0) and no unawaited call hands its result on → **High severity** finding: `naive-coherence: \`{name}\` described as async but example lacks \`await\`` (cite the first call's `file` and `line`). A call hands its result on when its `chainedMember` waits for it (`then`, `catch`, `join`, `get`), the example returns it (`return fetchData(x)` in an async function) or stores it and awaits it later: read the call lines before raising the finding.
95
- - **sync** and `awaitedCount` is above 0 → **High severity** finding: `naive-coherence: \`{name}\` described as sync but example awaits it` (cite the first awaited call).
95
+ - **sync** and `directlyAwaitedCount` is above 0 → **High severity** finding: `naive-coherence: \`{name}\` described as sync but example awaits it` (cite the first directly awaited call).
96
+ - **sync**, `directlyAwaitedCount` is 0 and a call sits in the arguments of a call that needs awaitable arguments → the same **High severity** finding (cite the first such call's `file` and `line`): its `awaitedVia` is `runner` (a runner such as Python `asyncio.run` or `loop.run_until_complete`, Rust `block_on`), or `argument` with an `enclosingCallee` of Python `asyncio.gather`, `asyncio.wait` or `asyncio.wait_for`. JavaScript `Promise.all`, `Promise.race` and `Promise.allSettled` accept plain values, so a call in their arguments raises nothing, and neither does any other call that is `awaited` but not `directlyAwaited` (`awaitedVia` `chain`, `consumer`, or `argument` of another callee).
96
97
  - Otherwise no finding. The names in `notCalled[]` appear in no example, so they raise none.
97
98
 
98
99
  **2.6 Table syntax.** Read `table_drift[]` from the second JSON blob (§2.0). The script normalizes escaped pipes (`\|`, used inside TypeScript union types such as `string \| undefined`) before splitting and compares each row against its header's column count, so a plain `split on |` false-positive cannot occur here. For each entry, emit a **Medium severity** finding: `naive-coherence: table row at line {entry.line} has {entry.actual_cols} columns; header has {entry.expected_cols}` (the `entry.section` and `entry.row` fields populate the detail when present).
@@ -238,7 +239,7 @@ Then record every coherence gap in the gap ledger `{ledgerFile}`: the hard gate
238
239
  uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coherence-check --from structure --input "{run_dir}/references.json"
239
240
  ```
240
241
 
241
- Then write the records no script wrote, each in the Ledger Record Format of `{outputFormatsFile}`: the §2.5 findings, titled with their `naive-coherence:` text and with their Source at `SKILL.md:{line}`; each §5 incomplete integration pattern; and in both modes a §2b / §5b migration section finding (Case 1 and Case 3 are Medium `migration-section` gaps, a Low `migration-section` gap when the reviewer downgraded Case 3 with an inline justification, and Case 2 is an Info `migration-section` gap). The split-body rows of the Reference Consistency table are not recorded here: coverage-check §1b recorded them. Write them as one JSON array on the lines between the two markers, exactly as they are: the quoted marker hands them to the script unchanged, quotes, apostrophes and `$` included. Run the command even when the array is empty (`[]`): the hard gate refuses to decide until every stage before it has recorded, with gaps or without.
242
+ Then write the records no script wrote, each in the Ledger Record Format of `{outputFormatsFile}`: the §2.5 findings, titled with their `naive-coherence:` text and with their Source at the cited call's `{file}:{line}`; each §5 incomplete integration pattern; and in both modes a §2b / §5b migration section finding (Case 1 and Case 3 are Medium `migration-section` gaps, a Low `migration-section` gap when the reviewer downgraded Case 3 with an inline justification, and Case 2 is an Info `migration-section` gap). The split-body rows of the Reference Consistency table are not recorded here: coverage-check §1b recorded them. Write them as one JSON array on the lines between the two markers, exactly as they are: the quoted marker hands them to the script unchanged, quotes, apostrophes and `$` included. Run the command even when the array is empty (`[]`): the hard gate refuses to decide until every stage before it has recorded, with gaps or without.
242
243
 
243
244
  ```bash
244
245
  uv run {gapLedgerScript} append --ledger "{ledgerFile}" --stage coherence-check <<'SKF_GAPS'
@@ -0,0 +1,61 @@
1
+ # Coverage Check: Branch Sections
2
+
3
+ Load this file only from the coverage-check.md sentence that names one of its sections, and follow that section alone: **Inventory Failures** from coverage-check.md §1a when the inventory check fails; **States 2 to 4** from coverage-check.md §2 when the source is not local, and only its State 2 command from coverage-check.md §2b when `rebuild` is `provenance-fold`; **Scalar and Stack Branches** from coverage-check.md §2c on the scalar or stack branch; **Denominator Candidates** from coverage-check.md §4 when `denominator.clause` is `stratified-scope` or `multi-entry`. Load it once: when another sentence names one of its sections, read that section from the copy already loaded. A bare § in a section is coverage-check.md's. coverage-check.md's frontmatter bindings, its run files, its per-file results and its §2 **Exits** apply here unchanged. When the section is done, continue in coverage-check.md right after the sentence that loaded it.
4
+
5
+ ## Inventory Failures
6
+
7
+ - **`valid` false (exit 2):** delete the response `{validateInventoryScript}` refused, so a stale one is never validated twice:
8
+
9
+ ```bash
10
+ rm -f "{run_dir}/inventory-response.txt"
11
+ ```
12
+
13
+ Re-dispatch the §1 subagent once, with the script's `violations[]` appended to its instructions (an absent name as a name the skill does not write): it writes its new response to that file, or returns it for you to save there, as §1 says. Run the §1a command again.
14
+ - **Exit 1** (no input, a file that cannot be read, or a package with no readable SKILL.md): correct the file and run it again.
15
+
16
+ ## States 2 to 4
17
+
18
+ - **State 2** (`{forge_provenance_map}`, the provenance map init.md §2 bound): the union of its named exports and the `metadata.json` `exports[]` names, with `--fold`, `--keep` and `--fold-prefix` as the Source API Surface Definition's canonicalization says:
19
+
20
+ ```bash
21
+ uv run {coverageInputsScript} surface --provenance "{forge_provenance_map}" --metadata "{resolved_skill_package}/metadata.json" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] [--fold] [--keep <variant>] [--fold-prefix <prefix>] [--monorepo] [--pattern-reference-intent] [--curated-subset] --output "{run_dir}/surface.json"
22
+ ```
23
+
24
+ Step 5's scoring script reads its `state2` counts, and the report its fold summary, `canonical.summary`.
25
+ - **State 3**: the `metadata.json` `exports[]` names: `surface --metadata "{resolved_skill_package}/metadata.json" [--monorepo] [--pattern-reference-intent] [--curated-subset] --output "{run_dir}/surface.json"`.
26
+ - **State 4**: the names read remotely by eye, as per-file results: `surface --per-file "{run_dir}/per-file-<n>.json" [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"`.
27
+
28
+ ## Scalar and Stack Branches
29
+
30
+ Run the command of the branch §2b picked; §2c's exits apply to it:
31
+
32
+ - **Scalar**, over the `coverage-inputs.json` scalar and §2b's set (`denominator.surfaceSet`); `--verified` makes §2b's `verified` count the numerator when the count was inflated and the branch counts no set:
33
+
34
+ ```bash
35
+ uv run {reconcileScript} --denominator-source scalar --coverage-inputs "{run_dir}/coverage-inputs.json" --inventory "{run_dir}/inventory.json" --surface "{run_dir}/surface.json" --surface-set <set> --skill-dir "{resolved_skill_package}" --verified "{run_dir}/numerator.json" --output "{run_dir}/coverage.json"
36
+ ```
37
+
38
+ - **Stack** (`metadata.json.skill_type == "stack"`):
39
+
40
+ ```bash
41
+ uv run {reconcileScript} --denominator-source stack --coverage-inputs "{run_dir}/coverage-inputs.json" --skill-dir "{resolved_skill_package}" --output "{run_dir}/coverage.json"
42
+ ```
43
+
44
+ **The scalar branch counts §2b's set** only when `surface.json` `denominator.setCounted` is true: a scope set (not `all`) whose every name sits at the file that defines it (a complete runner extraction, the fallback scan, or quick output that traces each name), read whole. Then `numeratorSource` is `surface`: `documented` counts the documented names the set holds (`documentedNames`), so a documented name the set lacks never offsets an undocumented one; `missing` lists the set's undocumented names; §5's render draws the per-export table; and the denominator stays the stored count, so when the set's size (`surfaceSetSize`) differs from it, `warnings` names both. Otherwise (`all`, a set read by eye or from an incomplete extraction, or an empty set, which warns) the branch looks each documented name up in the skill, `--verified` applies, and `missing` lists no name. On any surface read from the source (`guards.deflation.applicable` true), `stale` lists the documented names the whole surface (the set and the `all` set) lacks, with `staleApplicable` true, so §2c's Stale names bullet applies: a documented name outside the set that the source still exports is neither counted nor stale. §5's render lists each `warnings` entry.
45
+
46
+ **A surplus is a signal, not a pass.** Where the numerator and denominator count independent sets (the stack branch, and the scalar branch when it counts no set), `documented` can exceed `denominator`, as it can when a counted set holds more names than the stored count: `coverage.json` then reports `numeratorSurplus` above 0 and `coverageCapped: true`, holds `exportCoverage` at 100 and keeps the raw ratio in `coverageUncapped`, and §5's render states the surplus beside both counts. A large surplus is the deflated-denominator signature: check `guards.deflation`, whose `basis` names the set it measured the stored count against, before accepting the 100.
47
+
48
+ coverage-check.md §5b records this gap from the run files:
49
+
50
+ - **Missing count** (scalar or stack branch, `missingCount` above 0): one Medium `missing-export` gap for the count. A scalar result that counts its set records its `missing` names as §2c's Missing names bullet says instead, plus one gap for the rest when `missingCount` exceeds them. When §2b's numerator check found the count inflated and the scalar branch counted no set, its `absent[]` names replace this gap, one each: update-skill documents a named export, not a count.
51
+
52
+ ## Denominator Candidates
53
+
54
+ ```markdown
55
+ **Denominator Candidates** (stratified-scope audit trail):
56
+ - `stats.effective_denominator`: {statsEffectiveDenominator | absent} {← chosen on the scalar branch}
57
+ - `scope.tier_a_include` union: {tierAIncludeUnion | absent} {← chosen if the barrel branch's set}
58
+ - `scope.include` union: {scopeIncludeUnion | absent} {← chosen if the barrel branch's set}
59
+ - exports-map subpath union: {subpathUnion | absent} {← chosen if the barrel branch's set}
60
+ - root barrel: {rootBarrel | absent} {secondary candidate: root-barrel-vs-subpath-union audit}
61
+ ```
@@ -26,7 +26,7 @@ The recipe runner cannot be used, so subagents read the files in scope one at a
26
26
  uv run {scoreSignaturesScript} plan --inventory "{run_dir}/inventory.json" --output "{run_dir}/signature-plan.json"
27
27
  ```
28
28
 
29
- For each source file that defines public API exports, delegate a subagent that gets that map with the file. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, compares each with the map's entry (params, return type), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...]}`, each mismatch in the shape §2's signature comparison returns. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
29
+ For each source file that defines public API exports, delegate a subagent that gets that map with the file. It extracts the symbols the file exports, with their full signatures, with ast-grep (or reads the file when `extraction.fallback.reason` says no ast-grep can run; list those files in the Coverage Analysis section as read by eye), leaves out a name the file only passes on from another module of the source being tested (`export { x } from './x'`, `export * from`, an `__init__.py` import, a Rust `pub use`), which that module's own result counts, keeps a name it passes on from another package (`export { z } from 'zod'`), which no other result counts, compares each with the map's entry (params, return type), gives each mismatch the `line` that defines the export, and returns only `{"file", "exports_found": [...], "types_found": [<the exported interfaces, type aliases, enums and classes>], "signature_mismatches": [...]}`, each mismatch in the shape §2's signature comparison returns. Tell it the overload rule of coverage-check.md §2: a declaration opening an overload set (Python `@overload` stubs, TS bodiless signatures) is read through its implementation, and a documented signature matching the implementation or any overload is no mismatch. Save each response to `{run_dir}/per-file-<n>.txt` with the Write tool and build the surface from them, one `--per-file` each and `--fallback-scan` (each names the file that defines its exports, so a `scope.tier_a_include` list that counts none of them does not turn the guards off), with the same baselines as at Quick tier, and `--extraction` when the runner's JSON sent you here (`extraction.fallback.needed`): it adds no name, only its scope and the globs it lists as matching no file, so the guards still run:
30
30
 
31
31
  ```bash
32
32
  uv run {coverageInputsScript} surface --per-file "{run_dir}/per-file-<n>.txt" --fallback-scan [--extraction "{run_dir}/extract-full.json"] [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] --metadata "{resolved_skill_package}/metadata.json" [--provenance "{forge_provenance_map}"] [--monorepo] [--pattern-reference-intent] [--curated-subset] [--barrel <state>] --output "{run_dir}/surface.json"