bmad-module-skill-forge 8.0.0 → 8.1.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.
@@ -87,7 +87,7 @@ These rules apply to every step in this workflow:
87
87
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
88
88
  ```
89
89
 
90
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-analyze-source.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 step 5 to record in the run sink.
90
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-analyze-source.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, `SKF helper skf-resolve-customization.py 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 step 5 to record in the run sink.
91
91
 
92
92
  Then bind, so stage files use the variable with no conditional at the usage site:
93
93
 
@@ -79,7 +79,7 @@ Stage 1b returns to init.md at §5b's **Record for report**, then §6. Step 1 §
79
79
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
80
80
  ```
81
81
 
82
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-audit-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. Then keep the reason as `{customization_resolver_unavailable}`, which step 6 hands to the emitter as a warning.
82
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-audit-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, `SKF helper skf-resolve-customization.py 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. Then keep the reason as `{customization_resolver_unavailable}`, which step 6 hands to the emitter as a warning.
83
83
 
84
84
  Bind the two scalars the stages read, each to its bundled default when the merged value is empty or absent:
85
85
 
@@ -65,7 +65,7 @@ Give the halt's own `halt_reason`: the helper derives `exit_code` from it. `skil
65
65
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
66
66
  ```
67
67
 
68
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-brief-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 add `customization_resolver_unavailable: <reason>` to `workflow_warnings[]` as the run's first warning.
68
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-brief-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, `SKF helper skf-resolve-customization.py 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 add `customization_resolver_unavailable: <reason>` to `workflow_warnings[]` as the run's first warning.
69
69
 
70
70
  Bind the values the stage files use, taking the bundled default when the merged value is empty or absent:
71
71
 
@@ -37,7 +37,7 @@ Run these steps once, in order, before dispatching to Mode Routing.
37
37
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
38
38
  ```
39
39
 
40
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-campaign.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, unless the invocation is `campaign status` (which writes nothing), log `customization_resolver_unavailable: <reason>` as an `event` in the decision log once `{campaignWorkspacePath}` is bound below (`references/campaign-contracts.md`, Decision Log).
40
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-campaign.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, `SKF helper skf-resolve-customization.py 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, unless the invocation is `campaign status` (which writes nothing), log `customization_resolver_unavailable: <reason>` as an `event` in the decision log once `{campaignWorkspacePath}` is bound below (`references/campaign-contracts.md`, Decision Log).
41
41
 
42
42
  Resolve each scalar now (so step files never repeat the conditional) and stash as workflow-context variables:
43
43
 
@@ -82,7 +82,7 @@ These rules apply to every step in this workflow:
82
82
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
83
83
  ```
84
84
 
85
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-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 step 1 §0 to record in each brief's run folder.
85
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-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, `SKF helper skf-resolve-customization.py 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 step 1 §0 to record in each brief's run folder.
86
86
 
87
87
  Apply the resolved values so the surface is not a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts), except a `!` entry and the entry it names (`!file:{project-root}/**/project-context.md` drops the bundled project-context default); and stash `{onCompleteCommand}` ← `workflow.on_complete` (empty string = no-op) for step 8 to invoke once each brief's result contract is written. After activation completes, execute each entry in `workflow.activation_steps_append` in order.
88
88
 
@@ -87,7 +87,7 @@ The sink, `{run_dir}/warnings.jsonl`, is the list: step 7 §8b lists it in `evid
87
87
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
88
88
  ```
89
89
 
90
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-stack-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): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
90
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-stack-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, `SKF helper skf-resolve-customization.py 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): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
91
91
 
92
92
  Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each of the two scalars, if the merged value is empty or absent, use the bundled default:
93
93
 
@@ -66,7 +66,7 @@ Headless callers and pipelines: the inputs, flags, gate map, exit codes and `SKF
66
66
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
67
67
  ```
68
68
 
69
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-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. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
69
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-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, `SKF helper skf-resolve-customization.py 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. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
70
70
 
71
71
  Bind each scalar to its bundled default when the merged value is empty or absent:
72
72
 
@@ -65,7 +65,7 @@ Headless callers and pipelines: the inputs, flags, gates, outputs and exit codes
65
65
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
66
66
  ```
67
67
 
68
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-export-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. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
68
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-export-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, `SKF helper skf-resolve-customization.py 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. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
69
69
 
70
70
  Bind each scalar to its bundled default when the merged value is empty or absent:
71
71
 
@@ -93,7 +93,7 @@ Run these steps once, in order, before the first reply.
93
93
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key agent
94
94
  ```
95
95
 
96
- 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.
96
+ 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, `SKF helper skf-resolve-customization.py 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.
97
97
 
98
98
  The bundled arrays are empty and the roster values stay as shipped: the persona and the menu are this file's. An override may add to the arrays, and each one applies: run `agent.activation_steps_prepend` in order now; treat each `agent.persistent_facts` entry as standing context for the session (a `file:` entry loads its file or glob contents as facts; an entry prefixed `!` drops each earlier entry it names and loads nothing itself); then run `agent.activation_steps_append` once step 6 has greeted, before it dispatches a pick.
99
99
 
@@ -86,7 +86,7 @@ These rules apply to every step in this workflow:
86
86
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
87
87
  ```
88
88
 
89
- 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.
89
+ 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, `SKF helper skf-resolve-customization.py 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.
90
90
 
91
91
  Bind these three as workflow-context variables, taking the default when the merged value is empty or absent:
92
92
 
@@ -77,7 +77,7 @@ When `{headless_mode}` is true, step 6 prints one `SKF_REFINE_ARCHITECTURE_RESUL
77
77
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
78
78
  ```
79
79
 
80
- 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.
80
+ 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, `SKF helper skf-resolve-customization.py 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.
81
81
 
82
82
  Bind each path scalar, taking the bundled default when the merged value is empty or absent:
83
83
 
@@ -70,7 +70,7 @@ Headless callers: the inputs, flags, gate map, outputs, concurrency rule and `SK
70
70
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
71
71
  ```
72
72
 
73
- 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}`.
73
+ 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, `SKF helper skf-resolve-customization.py 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}`.
74
74
 
75
75
  Apply the scalar fallback now so stage files don't have to repeat the conditional logic. For each of the two scalars, if the merged value is empty or absent, the bundled default applies:
76
76
 
@@ -72,7 +72,7 @@ You are a system executor: run each step in sequence, write the configuration fi
72
72
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
73
73
  ```
74
74
 
75
- 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.
75
+ 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, `SKF helper skf-resolve-customization.py 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.
76
76
 
77
77
  Apply the resolved values, with no narration of your own when `{quiet_mode}` is true: run each `workflow.activation_steps_prepend` entry now; treat each `workflow.persistent_facts` entry as standing context (a `file:` entry loads its file or glob contents); bind `{onCompleteCommand}` ← `workflow.on_complete` (empty: no hook) for `references/report.md` §5; then run each `workflow.activation_steps_append` entry, before the first stage.
78
78
 
@@ -70,7 +70,7 @@ Headless callers: the full argument set (`Inputs`), the outputs, the exit codes
70
70
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
71
71
  ```
72
72
 
73
- 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.
73
+ 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, `SKF helper skf-resolve-customization.py 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.
74
74
 
75
75
  Apply the scalar fallbacks now so stage files don't have to repeat the conditional logic. For each scalar, if the merged value is empty or absent, use the bundled default:
76
76
 
@@ -48,6 +48,16 @@ persistent_facts = [
48
48
  #
49
49
  # Lift the canonical asset paths so orgs can substitute house-style copies
50
50
  # without forking the skill. Empty string = use the bundled default.
51
+ #
52
+ # The test report template init copies to the run's report. Default:
53
+ # templates/test-report-template.md. A house-style copy may reword the prose,
54
+ # and must keep:
55
+ # - the title line `# Test Report: {{skillName}}`, which init fills with
56
+ # the skill name;
57
+ # - the six `## ...` section headings, each with the `<!-- Populated by`
58
+ # comment under it, since each stage replaces its own placeholder.
59
+ # The report check halts the run (`report-anchor-missing`) on a report
60
+ # without them.
51
61
 
52
62
  test_report_template_path = ""
53
63
 
@@ -320,7 +320,7 @@ nextWorkflow: ''
320
320
  ---
321
321
  ```
322
322
 
323
- Replace the template's frontmatter with this block (the two hold the same keys). `toolingStatus` is the `tooling_status` §3b set: score.md §3a reads it back for Cap 1. `threshold` and `thresholdSource` are the pass threshold §1b resolved and its source: score.md §1 reads them back, and score.md §7 rewrites `threshold` with the threshold the verdict was settled against. `analysisConfidence` stays empty until score.md §7, or step-hard-gate.md §3 on a run the hard gate blocks, writes the source access coverage-check resolved: `full`, `provenance-map`, `metadata-only`, `remote-only` or `docs-only`. report.md §4c halts on a report that holds none of them.
323
+ Replace the template's frontmatter with this block (the two hold the same keys). Then replace `{{skillName}}` in the template's H1 with `{skill_name}`, so the report's title is `# Test Report: {skill_name}`: report.md §4c halts on a report without that line. `toolingStatus` is the `tooling_status` §3b set: score.md §3a reads it back for Cap 1. `threshold` and `thresholdSource` are the pass threshold §1b resolved and its source: score.md §1 reads them back, and score.md §7 rewrites `threshold` with the threshold the verdict was settled against. `analysisConfidence` stays empty until score.md §7, or step-hard-gate.md §3 on a run the hard gate blocks, writes the source access coverage-check resolved: `full`, `provenance-map`, `metadata-only`, `remote-only` or `docs-only`. report.md §4c halts on a report that holds none of them.
324
324
 
325
325
  ### 7. Report Initialization Status
326
326
 
@@ -186,9 +186,10 @@ for heading in '## Test Summary' '## Coverage Analysis' '## Coherence Analysis'
186
186
  printf '%s: ' "$heading"; grep -cxF "$heading" "{outputFile}"
187
187
  done
188
188
  printf 'placeholders: '; grep -c '^<!-- Populated by' "{outputFile}"
189
+ printf 'title: '; grep -cxF '# Test Report: {skill_name}' "{outputFile}"
189
190
  ```
190
191
 
191
- Each heading must print 1, standing alone on exactly one line, and `placeholders` must print 0. A run the hard gate blocked has all six: step 4c §3 wrote the Completeness Score section in scoring's place. On a heading whose count is not 1, or a placeholder left, HALT (`halt_reason: "report-anchor-missing"`, phase `report:anchors`) with "report anchor missing: {each heading whose count is not 1, with its count}, or {N} placeholder comments left; the section was not written once by its owning step".
192
+ Each heading must print 1, standing alone on exactly one line, `placeholders` must print 0, and `title` must print 1 (the H1 init.md §6c filled). A run the hard gate blocked has all six: step 4c §3 wrote the Completeness Score section in scoring's place. On a heading whose count is not 1, a placeholder left, or a `title` count other than 1, HALT (`halt_reason: "report-anchor-missing"`, phase `report:anchors`) with "report anchor missing: {each heading whose count is not 1, with its count}, {placeholder_count} placeholder comments left, or a `title` count of {title_count} (the report does not hold `# Test Report: {skill_name}` once); the section or title was not written once by its owning step".
192
193
 
193
194
  **Find the health check** unless `no_health_check` is true (the `--no-health-check` flag): resolve `{healthCheckFile}` ← first existing path in `{healthCheckProbeOrder}`. It ends the run (§7), so check it now, while a HALT still leaves the previous run's report and result files in place. If neither candidate exists, HALT (`halt_reason: "health-check-missing"`, phase `report:health-check`):
194
195
 
@@ -19,7 +19,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
19
19
  | Severity | Category | Criteria |
20
20
  |----------|-----------------------------|----------|
21
21
  | Critical | `signature-mismatch` | Wrong signature: a documented signature that differs from the source in its parameters (name, type, order, optionality) or its return type, as signature-comparison-rules.md reads each form (a component by its props; an annotation either side leaves out is not compared), in any way but those the Low `signature-mismatch` row and the Info `observation` row grade, such as a renamed Python parameter (the script cannot see which parameters a caller must pass by position), an optional parameter left out of a form the skill does not label partial, or a mismatch with no `difference` (coverage-check §2 and §2b) |
22
- | Critical | `fabricated-signature` | Fabricated signature: a documented export absent at the source line the skill cites for it. Decided only where ast-grep enumerated the exports at the pinned commit: the name is outside that surface, and the cited file is missing or defines no such name, and no item of an export list (`export { L as NAME }`, `export type { ... }`) or of a Python `from X import` clause elsewhere renames to it a name that file declares; any other rename (`export { default as NAME }`, `const NAME = L`, `type NAME = L`, an import then an export, Python `NAME = L`) stays fabricated (coverage-check §2c) |
22
+ | Critical | `fabricated-signature` | Fabricated signature: a documented export absent at the source line the skill cites for it. Decided only where ast-grep enumerated the exports at the pinned commit: the name is outside that surface, and the cited file is missing or defines no such name, and no item of an export list (`export { L as NAME }`, `export type { ... }`) or of a Python `from X import` clause elsewhere renames to it a name that file declares, nor does any other rename the `aliased` clause of the Info `observation` row clears, directly or through a chain of renames; a rename that clause does not clear stays fabricated (coverage-check §2c) |
23
23
  | Critical | `broken-reference` | Broken reference (contextual mode): a reference whose target does not exist, such as a file not at the stated path, a skill not in the skills output folder, a type no module declares, or a `scripts/` or `assets/` file the skill names (coherence-check §4) |
24
24
  | High | `inaccurate-reference` | Inaccurate reference (contextual mode): the target exists but does not match, such as a type the cited module does not export, a signature that differs, or an integration pattern that does not match the implementation (coherence-check §4) |
25
25
  | High | `reference-escape` | A reference whose real path leaves the skill, its source and, for a stack, the skills output folder (coherence-check §4) |
@@ -45,7 +45,7 @@ This table is the single source of truth for classifying a gap. Every stage reco
45
45
  | Low | `metadata` | Missing optional metadata or examples |
46
46
  | Low | `description` | Description trigger optimization recommended (third-person voice, negative triggers, or keyword coverage gaps), a Tessl Review description suggestion included |
47
47
  | Low | `external-validator` | A skill-check diagnostic or a Tessl Review validation finding the run left unresolved (external-validators §5b) |
48
- | Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it; an `aliased` name, which a line elsewhere renames from the declaration its cited file holds (an export-list item `L as NAME` (`export { default as NAME } from` included, a whole-string side unquoted, a comment allowed before `export`), a plain `export { NAME }` of an import that renames (`import { L as NAME }`, `import NAME from`), a `const`, `let`, `var` or `type NAME = L` with a bare identifier `L`, a Python `from X import L as NAME` at column 0 or one level in a module-level `try`, `except`, `else`, `if` or `elif` block, or a Python `NAME = L`), at that line, a multi-line list's item at its own line; when the rename's relative or in-package `from` module, or the import that binds `L`, resolves to a file that declares `L`, that file must be the cited one, so a rename of another file's same-named declaration stays a fabricated signature, as do a type assertion, any other expression and an import inside a function or a class), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c). Also a partial signature: a mismatch the signature judge tags `omitted-optional` (defined in signature-comparison-rules.md, Difference tags) on a form the skill labels partial (simplified, key parameters, a `...` that stands in for omitted members, not rest or variadic syntax), which the inventory marks `signature_form: "partial"`, as score-signatures.py checks: `Partial signature: {name}`, at the declaration, counted as matching in Signature Accuracy (coverage-check §2 and §2b). Also a return annotation the body contradicts: a mismatch the signature judge tags `annotation-contradicted` (defined in signature-comparison-rules.md, Difference tags) whose documented signature states a return type and writes the source's parameters, and whose `evidence_line`, the body line that returns the documented type, comes after the declaration and before the next one and, with the local source, starts with `return` (or is a one-line declaration's own expression body), as score-signatures.py checks: `Annotation contradicted: {name}`, at the declaration, counted as matching in Signature Accuracy, with no change to the skill (the annotation is reported upstream) (coverage-check §2 and §2b) Also a documentation error a Deep-tier run sees that no script checks (an import path whose module does not export the name, a type member that differs from its declaration): `Not checked by script: {name}`, at the documenting line, written by hand (coverage-check §2 and §5b) |
48
+ | Info | `observation` | Style suggestions, non-blocking observations, and a documented extra: a documented name outside the enumerated surface that is not a fabricated signature and that the source still declares at column 0 (a module so named counts, unless the inventory gives the name a `function`, `hook`, `class`, `interface`, `enum` or `type` kind, no provenance map entry of the name types it a `module` or `package` and the module is not a TS/JS file that is itself the export, with an `export default` or a `module.exports =`; among several declaring files, the one the skill cites on a line naming it; an `aliased` name, which a line elsewhere renames from the declaration its cited file holds, directly or through a chain of renames (an export-list item `L as NAME` (`export { default as NAME } from` included, a whole-string side unquoted, a comment allowed before `export`), a plain `export { NAME }` of an import that renames (`import { L as NAME }`, `import NAME from`), a `const`, `let`, `var` or `type NAME = L` with a bare identifier `L` (a `const`, `let` or `var` annotated with a one-line type that holds no `;` and no `=` but in `=>` included, and a generic `type NAME<T> = L<T>` that passes its bare parameters on in the same order), an `export default L` at column 0 (a rename of `L` to `default`) whose bare identifier `L` an import or a declaration of the same file binds, the statement ending on its line, a Python `from X import L as NAME` at column 0, one level in a module-level `try`, `except`, `else`, `if`, `elif`, `with` or `finally` block or as such a block header's one-line body (`try: from X import L as NAME`), or a Python `NAME = L` or `NAME: <type> = L` whose type holds no `;` or `=`), at that line, a multi-line list's item at its own line; when the rename's relative or in-package `from` module, or the import that binds `L`, resolves to a file that declares `L`, that file must be the cited one or one that renames `L` in turn, by one of these forms or by a Python `L = M.L2` whose module `M` a column-0 `from X import M` binds, a chain classify-stale follows file by file until it reaches the cited declaration, so a rename of another file's same-named declaration stays a fabricated signature, as do a chain that leaves the files classify-stale resolves, loops or ends at another declaration, a generic alias whose parameters are constrained, defaulted, swapped or instantiated, a type assertion, any other expression (`NAME = a.b` included) and an import inside a function or a class), found only in a Python or TS/JS source where classify-stale ran (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and `surface.json` has a complete `extraction`: `Documented extra: {name}`, at that line, which update-skill never routes (coverage-check §2c). Also a partial signature: a mismatch the signature judge tags `omitted-optional` (defined in signature-comparison-rules.md, Difference tags) on a form the skill labels partial (simplified, key parameters, a `...` that stands in for omitted members, not rest or variadic syntax), which the inventory marks `signature_form: "partial"`, as score-signatures.py checks: `Partial signature: {name}`, at the declaration, counted as matching in Signature Accuracy (coverage-check §2 and §2b). Also a return annotation the body contradicts: a mismatch the signature judge tags `annotation-contradicted` (defined in signature-comparison-rules.md, Difference tags) whose documented signature states a return type and writes the source's parameters, and whose `evidence_line`, the body line that returns the documented type, comes after the declaration and before the next one and, with the local source, starts with `return` (or is a one-line declaration's own expression body), as score-signatures.py checks: `Annotation contradicted: {name}`, at the declaration, counted as matching in Signature Accuracy, with no change to the skill (the annotation is reported upstream) (coverage-check §2 and §2b) Also a documentation error a Deep-tier run sees that no script checks (an import path whose module does not export the name, a type member that differs from its declaration): `Not checked by script: {name}`, at the documenting line, written by hand (coverage-check §2 and §5b) |
49
49
  | Info | `provenance-unverified` | Provenance line not verified: the line-check rules found no definition line, or the recorded line is not a line number (`line-invalid`) (coverage-check §4c) |
50
50
  | Info | `migration-section` | Historical migration content in Section 4b with no T2-future annotation (Case 2 of `migration-section-rules.md`) |
51
51
  | Info | `multi-denominator` | Multi-denominator reporting: barrel and documented-surface clusters diverge by design (>10% cross-cluster) |
@@ -126,23 +126,28 @@ cites (never read; default `metadata.json`):
126
126
  first that writes it as an export, the first tier that holds
127
127
  the name winning: (1) its first origin in the documented
128
128
  inventory's floor (load-coverage-inputs.py floor); (2) a
129
- column-0 declaration of it in a fenced block of a language
130
- floor's `_declared_names` reads (JS/TS, Python, Rust, Go),
131
- cited or not, outside an HTML comment and a data block (json, yaml...);
132
- (3) a heading, or the first cell of a table whose header's
133
- first cell names exports (floor's table headers), whose text is
134
- the name or holds a code span floor reads as the name; (4) a
135
- line whose inline code span is the name, or the name directly
136
- followed by `(`. Tiers 2 to 4 read SKILL.md, then every .md file
137
- under references/, subfolders included, in path order, outside
138
- the frontmatter (3 and 4 outside fenced code and HTML comments
139
- too); within a tier the first file, then its lowest line, wins.
140
- A bare word, or a span that holds the name among other words
141
- (`uitripled add`), documents nothing; with no such line, or
142
- without --skill-dir, the Source is `SKILL.md`. Floor's
143
- InputError (no SKILL.md, or a top-level file it cannot read or
144
- decode) leaves tier 1 empty, and a skill file that cannot be
145
- read leaves tiers 2 to 4 empty. One whose
129
+ heading, or the first cell of a table whose header's first
130
+ cell names exports (floor's table headers), whose text is the
131
+ name or holds a code span floor reads as the name; (3) a line
132
+ whose inline code span is the name, or the name directly
133
+ followed by `(`; (4) a column-0 declaration of it in a fenced
134
+ block of a language floor's `_declared_names` reads (JS/TS,
135
+ Python, Rust, Go), cited or not, so a def an example writes
136
+ under the name is cited only when nothing above documents it;
137
+ (5) a line of a fenced block that holds the name as a whole
138
+ quoted string (`"name"` or `'name'`), such as an `__all__` item.
139
+ Tiers 2 to 5 read SKILL.md, then every .md file under
140
+ references/, subfolders included, in path order, outside the
141
+ frontmatter (2 and 3 outside fenced code and HTML comments, 4
142
+ and 5 inside a fence outside an HTML comment and a data block
143
+ (json, yaml...)); within a tier the first file, then its lowest
144
+ line, wins. A bare word, a span that holds the name among other
145
+ words (`uitripled add`), or a quoted string outside a fence or
146
+ holding other words too (`"add x"`) documents nothing; with no
147
+ such line, or without --skill-dir, the Source is `SKILL.md`.
148
+ Floor's InputError (no SKILL.md, or a top-level file it cannot
149
+ read or decode) leaves tier 1 empty, and a skill file that
150
+ cannot be read leaves tiers 2 to 5 empty. One whose
146
151
  `excluded.outsideScope` row says the brief
147
152
  scopes its file out (`excludeGlob`, the `scope.exclude` glob
148
153
  that matches it, or `outsideInclude`, no `scope.include` glob
@@ -406,6 +411,9 @@ EFFORT = {
406
411
  _ID_RE = re.compile(r"^GAP-(\d{3,})$")
407
412
  _STAGE_RE = re.compile(r"^[a-z][a-z0-9-]*$")
408
413
  _RUN_ID_FROM_NAME = re.compile(r"^test-findings-(.+)\.json$")
414
+ # A whole quoted string, `"name"` or `'name'`. The lookahead lets matches overlap, so an apostrophe earlier on the
415
+ # line (`don't 'add'`) does not swallow the next string; the text between two strings is a harmless extra key.
416
+ _QUOTED_RE = re.compile(r"""(?=(["'])([^"'\n]+)\1)""")
409
417
 
410
418
 
411
419
  class LedgerError(Exception):
@@ -913,26 +921,33 @@ def _documenting_lines(skill_dir: Path) -> dict[str, str]:
913
921
  1. its first origin in the documented inventory's floor
914
922
  (load-coverage-inputs.py `floor`: SKILL.md and the top-level
915
923
  references/*.md);
916
- 2. a column-0 public declaration of it in a fenced code block (floor's
917
- `_declared_names`), cited or not, outside an HTML comment and a data
918
- block;
919
- 3. a heading, or the first cell of a table whose header's first cell is
924
+ 2. a heading, or the first cell of a table whose header's first cell is
920
925
  one of floor's `FLOOR_TABLE_HEADERS`, whose stripped text is the name
921
926
  or holds a code span floor's `_floor_name` reads as the name;
922
- 4. a line whose inline code span, stripped, is the name, or the name
923
- directly followed by `(`.
924
-
925
- Tiers 2 to 4 read SKILL.md, then every .md file under references/,
927
+ 3. a line whose inline code span, stripped, is the name, or the name
928
+ directly followed by `(`;
929
+ 4. a column-0 public declaration of it in a fenced code block (floor's
930
+ `_declared_names`), cited or not, so a def an example writes under
931
+ the name (a connector author's own `mount_table_target`) is cited
932
+ only when nothing above documents it;
933
+ 5. a raw (unmasked) line of a fenced code block that holds the name as a
934
+ whole quoted string (`"name"` or `'name'`), such as an `__all__`
935
+ item; it only replaces the bare `SKILL.md` fallback.
936
+
937
+ Tiers 2 to 5 read SKILL.md, then every .md file under references/,
926
938
  subfolders included, in path order (locate-export-segments.py
927
- `skill_files`), never the frontmatter, tiers 3 and 4 outside fenced code
928
- and HTML comments; within a tier the first file, then its lowest line,
929
- wins. A bare word, or a span that holds the name among other words
930
- (`uitripled add`), documents nothing. An input a tier cannot read is
931
- skipped: floor's InputError (no SKILL.md, a file that is not UTF-8)
932
- leaves tier 1 empty, an unreadable file tiers 2 to 4."""
939
+ `skill_files`), never the frontmatter: tiers 2 and 3 outside fenced code
940
+ and HTML comments, tiers 4 and 5 inside a fence outside an HTML comment
941
+ and a data block (json, yaml...). Within a tier the first file, then its
942
+ lowest line, wins. A bare word, a span that holds the name among other
943
+ words (`uitripled add`), or a quoted string outside a fence or holding
944
+ other words too (`"add x"`) documents nothing. An input a tier cannot
945
+ read is skipped: floor's InputError (no SKILL.md, a file that is not
946
+ UTF-8) leaves tier 1 empty, an unreadable file tiers 2 to 5."""
933
947
  coverage = _sibling("load-coverage-inputs.py")
934
948
  segments = _sibling("locate-export-segments.py")
935
- tiers: list[dict[str, str]] = [{}, {}, {}, {}]
949
+ # In merge order: floor, heading or export-table cell, code span, fenced declaration, quoted string.
950
+ tiers: list[dict[str, str]] = [{}, {}, {}, {}, {}]
936
951
  try:
937
952
  for entry in coverage.floor(str(skill_dir))["names"]:
938
953
  origin = entry["origins"][0]
@@ -943,8 +958,9 @@ def _documenting_lines(skill_dir: Path) -> dict[str, str]:
943
958
  files = segments.skill_files(skill_dir)
944
959
  except OSError:
945
960
  files = []
946
- # floor's fence filter: a fence in an HTML comment or a data block declares nothing, and a Svelte block's
947
- # `export let` is a component prop. Unlike floor, an unheaded fence counts.
961
+ # floor's fence filter: a fence in an HTML comment or a data block declares and quotes nothing, and a Svelte
962
+ # block's `export let` is a component prop. Unlike floor, an unheaded fence counts; a Svelte, shell or untagged
963
+ # fence's quoted strings count too.
948
964
  js_family = segments.JS_FAMILY - {"svelte"}
949
965
  for rel, lines in files:
950
966
  start = segments._frontmatter_end(lines)
@@ -954,9 +970,13 @@ def _documenting_lines(skill_dir: Path) -> dict[str, str]:
954
970
  language = segments._language(info)
955
971
  if opening in comments or language in segments.DATA_LANGUAGES:
956
972
  continue
957
- masked = segments.mask_code("\n".join(lines[opening + 1:closing]), language).split("\n")
973
+ raw = lines[opening + 1:closing]
974
+ masked = segments.mask_code("\n".join(raw), language).split("\n")
958
975
  for offset, declared in coverage._declared_names(masked, language, js_family):
959
- tiers[1].setdefault(declared, f"{rel}:{opening + offset + 2}")
976
+ tiers[3].setdefault(declared, f"{rel}:{opening + offset + 2}")
977
+ for offset, line in enumerate(raw):
978
+ for match in _QUOTED_RE.finditer(line):
979
+ tiers[4].setdefault(match.group(2), f"{rel}:{opening + offset + 2}")
960
980
  headings, prose = segments._prose(lines, start, code)
961
981
  shaped = [(index, text) for index, _level, text, _above in headings]
962
982
  written = list(shaped)
@@ -968,12 +988,12 @@ def _documenting_lines(skill_dir: Path) -> dict[str, str]:
968
988
  for index, text in sorted(shaped):
969
989
  for key in [text.strip()] + [coverage._floor_name(span) for span in segments._inline_code(text)]:
970
990
  if key:
971
- tiers[2].setdefault(key, f"{rel}:{index + 1}")
991
+ tiers[1].setdefault(key, f"{rel}:{index + 1}")
972
992
  for index, text in sorted(written):
973
993
  for span in map(str.strip, segments._inline_code(text)):
974
994
  for key in (span, span.split("(", 1)[0] if "(" in span else None):
975
995
  if key:
976
- tiers[3].setdefault(key, f"{rel}:{index + 1}")
996
+ tiers[2].setdefault(key, f"{rel}:{index + 1}")
977
997
  found: dict[str, str] = {}
978
998
  for tier in tiers:
979
999
  for key, line in tier.items():
@@ -1578,7 +1598,9 @@ def _alias_fix(name: str, file: str, item: dict) -> str:
1578
1598
  alias_file, _, alias_line = alias.rpartition(":")
1579
1599
  named = isinstance(local, str) and bool(local)
1580
1600
  renamed = f"`{local}`" if named else "a name a cited file declares"
1581
- head = f"`{name}` is an alias: `{alias}` renames {renamed} to it, and no line of `{file}` defines `{name}`. "
1601
+ of = f"{renamed}, which a cited file declares" if named else renamed
1602
+ head = (f"`{name}` is an alias of {of}; the source declares the alias at `{alias}`, and no line of `{file}` "
1603
+ f"defines `{name}`. ")
1582
1604
  cite = (f"the alias: set `source_file` to `{alias_file}` and `source_line` to `{alias_line}`, and move the "
1583
1605
  f"citations of `{name}` there.")
1584
1606
  if local == "default":
@@ -25,9 +25,10 @@ nextWorkflow: ''
25
25
  Each stage writes its section in place of the heading below that it owns and
26
26
  the placeholder comment under that heading, so every heading occurs once.
27
27
  Before the result files are written, report.md §4c checks that each heading
28
- occurs exactly once and that no placeholder comment is left: a stage that
29
- skipped its section stops the run there. Do not reorder or delete the
30
- headings or their placeholder comments.
28
+ occurs exactly once, that no placeholder comment is left and that the title
29
+ above names the skill (init.md §6c fills it): a stage that skipped its
30
+ section stops the run there. Do not change the title line, and do not
31
+ reorder or delete the headings or their placeholder comments.
31
32
 
32
33
  Heading / owning step:
33
34
  Test Summary → detect-mode §2
@@ -86,7 +86,7 @@ Headless callers: the inputs, flags, gate map, outputs and the `SKF_UPDATE_RESUL
86
86
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
87
87
  ```
88
88
 
89
- It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-update-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. Record the warning in the run log at once: write `customization_resolver_unavailable: <reason>` to `{run_dir}/resolver-warning.txt` with a file write (a resolver error can hold quotes or `$( )`, so never echo it or type it into an argument), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"` from `{project-root}`.
89
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-update-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, `SKF helper skf-resolve-customization.py 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. Record the warning in the run log at once: write `customization_resolver_unavailable: <reason>` to `{run_dir}/resolver-warning.txt` with a file write (a resolver error can hold quotes or `$( )`, so never echo it or type it into an argument), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"` from `{project-root}`.
90
90
 
91
91
  Apply the resolved values so no surface is a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now; treat every entry in `workflow.persistent_facts` as standing context for the whole run (entries prefixed `file:` are paths or globs whose contents load as facts: the bundled default loads 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); resolve `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty string, and stash it in workflow context (`references/report.md` §5b runs it after a finished run's result files, never in `--detect-only`, `--dry-run` or a halt; empty = no-op). After activation completes, execute each entry in `workflow.activation_steps_append` in order before `init.md` runs.
92
92
 
@@ -91,7 +91,7 @@ When `{headless_mode}` is true, step 6 prints one `SKF_VERIFY_STACK_RESULT_JSON:
91
91
  uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
92
92
  ```
93
93
 
94
- 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}`.
94
+ 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, `SKF helper skf-resolve-customization.py 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}`.
95
95
 
96
96
  Apply the path-scalar fallback now so stage files don't repeat it: `{reportTemplatePath}` ← `workflow.report_template_path` if non-empty, else `assets/feasibility-report-template.md`. An empty or absent value falls through to that default, and init.md §4 loads the variable as it is. Bind `{outputFolderPath}` ← `{forge_data_folder}`, always (no setting moves it): the report and its `-latest` copy go where create-stack-skill and refine-architecture look for them, and step 1 finds the earlier reports there.
97
97