bmad-module-skill-forge 7.0.0 → 8.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +1 -1
  3. package/docs/_data/pinned.yaml +9 -10
  4. package/docs/campaign.md +2 -2
  5. package/docs/skill-model.md +3 -3
  6. package/docs/verifying-a-skill.md +17 -18
  7. package/docs/workflows.md +7 -7
  8. package/package.json +1 -1
  9. package/src/shared/references/description-guard-protocol.md +12 -10
  10. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +1 -1
  11. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +1 -1
  12. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +38 -0
  13. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +5 -0
  14. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +1 -1
  15. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +1 -1
  16. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +8 -3
  17. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +1 -1
  18. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +6 -1
  19. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
  20. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +7 -2
  21. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +1 -1
  22. package/src/shared/scripts/skf-parse-gaps.py +4 -1
  23. package/src/shared/scripts/skf-quick-batch.py +8 -4
  24. package/src/shared/scripts/skf-verify-provenance-completeness.py +727 -179
  25. package/src/skf-audit-skill/customize.toml +4 -3
  26. package/src/skf-audit-skill/references/report.md +1 -1
  27. package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
  28. package/src/skf-campaign/references/campaign-contracts.md +1 -1
  29. package/src/skf-campaign/references/step-06-batch.md +2 -2
  30. package/src/skf-campaign/references/step-07-capstone.md +1 -1
  31. package/src/skf-campaign/references/step-08-verify.md +1 -1
  32. package/src/skf-campaign/references/step-09-refine.md +1 -1
  33. package/src/skf-campaign/references/step-11-maintenance.md +1 -1
  34. package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
  35. package/src/skf-campaign/scripts/campaign-report.py +60 -4
  36. package/src/skf-campaign/scripts/campaign-state.py +67 -19
  37. package/src/skf-campaign/templates/campaign-report-template.md +4 -0
  38. package/src/skf-create-skill/assets/skill-sections.md +2 -2
  39. package/src/skf-create-skill/references/report.md +2 -2
  40. package/src/skf-create-skill/references/validate.md +49 -29
  41. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  42. package/src/skf-create-stack-skill/references/report.md +3 -3
  43. package/src/skf-create-stack-skill/references/validate.md +9 -4
  44. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  45. package/src/skf-export-skill/references/summary.md +1 -1
  46. package/src/skf-quick-skill/references/batch-mode.md +28 -2
  47. package/src/skf-quick-skill/references/compile.md +2 -0
  48. package/src/skf-quick-skill/references/finalize.md +3 -2
  49. package/src/skf-quick-skill/references/halt-contract.md +3 -3
  50. package/src/skf-quick-skill/references/quick-extract.md +2 -1
  51. package/src/skf-quick-skill/references/write-and-validate.md +11 -5
  52. package/src/skf-refine-architecture/customize.toml +1 -1
  53. package/src/skf-refine-architecture/references/report.md +2 -2
  54. package/src/skf-rename-skill/references/report.md +1 -1
  55. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  56. package/src/skf-test-skill/references/coverage-check.md +2 -2
  57. package/src/skf-test-skill/references/invocation-contract.md +1 -1
  58. package/src/skf-test-skill/references/report.md +1 -1
  59. package/src/skf-test-skill/references/scoring-rules.md +1 -1
  60. package/src/skf-test-skill/references/signature-comparison-rules.md +4 -1
  61. package/src/skf-test-skill/scripts/gap-ledger.py +196 -46
  62. package/src/skf-test-skill/scripts/load-coverage-inputs.py +12 -6
  63. package/src/skf-update-skill/references/gap-driven.md +1 -1
  64. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  65. package/src/skf-update-skill/references/merge.md +1 -1
  66. package/src/skf-update-skill/references/report.md +18 -7
  67. package/src/skf-update-skill/references/write.md +24 -12
  68. package/src/skf-verify-stack/references/report.md +1 -1
@@ -15,6 +15,13 @@ atomicWriteProbeOrder:
15
15
  descriptionGuardProbeOrder:
16
16
  - '{project-root}/_bmad/skf/shared/scripts/skf-description-guard.py'
17
17
  - '{project-root}/src/shared/scripts/skf-description-guard.py'
18
+ # Resolve `{hashContentHelper}` by probing `{hashContentProbeOrder}` in order
19
+ # (installed SKF module path first, src/ dev-checkout fallback); first existing
20
+ # path wins. Advisory: if neither resolves, §2 hashes nothing and reports that
21
+ # SKILL.md may have been rewritten (not hashed) by skill-check --fix.
22
+ hashContentProbeOrder:
23
+ - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
24
+ - '{project-root}/src/shared/scripts/skf-hash-content.py'
18
25
  # Resolve `{frontmatterValidator}` by probing `{frontmatterValidatorProbeOrder}`
19
26
  # in order (installed SKF module path first, src/ dev-checkout fallback); first
20
27
  # existing path wins.
@@ -90,7 +97,7 @@ To validate the compiled SKILL.md content against the agentskills.io specificati
90
97
  - Validation and auto-fix modify files in the staging directory
91
98
  - `<staging-skill-dir>` resolves to `_bmad-output/.skf-stage/{skill-name}/` as created by step 5. Its last folder name must match the skill's frontmatter `name` field exactly — `skill-check`'s `frontmatter.name_matches_directory` rule rejects any suffix.
92
99
  - If skill-check unavailable: skip validation, add warning to evidence report
93
- - Ignore non-zero exit codes from skill-check if JSON output shows 0 errors
100
+ - For the validation verdict, ignore non-zero exit codes from skill-check if JSON output shows 0 errors; the security scan's row follows its exit code and stderr tail (§5)
94
101
  - Tessl Review (§6b) is optional and advisory: it never halts, never asks the user anything and never changes the staged files, and SKF applies none of its suggestions
95
102
  - A HARD HALT, once step 3 §2b has bound `{source_tree}`, first runs `uv run {sourceTreeHelper} close --tree "{source_tree}"` from `{project-root}` (resolved from `{sourceTreeProbeOrder}`) and goes on whatever it prints, and emits through `{emitEnvelopeHelper}`, resolved from `{emitEnvelopeProbeOrder}` when it is not bound. After its envelope, under `--batch` it ends only this brief: return to `references/batch-mode.md` §3, even when the halt reads as the end of the run.
96
103
 
@@ -104,9 +111,9 @@ To validate the compiled SKILL.md content against the agentskills.io specificati
104
111
 
105
112
  The deterministic phases are executed via `{descriptionGuardHelper}`: the calling sections (§2 and §4) invoke the helper at the capture and verify-restore points.
106
113
 
107
- **Guard outputs.** Bind `{guarded_description}` ← `description` from each `capture`, which reads the staged `<staging-skill-dir>/SKILL.md` just before the tool that may rewrite it. Bind `{guard_restored}` ← `restored` and `{guard_diff_kind}` ← `diff_kind` from each `verify-restore`. When `{guard_restored}` is true, set the in-context `description` to `{guarded_description}` so later sections do not work from the tool-mutated value, and record `description_guard_restored: true` with the tool name and `description_guard_diff_kind: {guard_diff_kind}` in workflow context for the evidence report (§8). A later `verify-restore` that exits 0 with `{guard_restored}` false leaves those records in place.
114
+ **Guard outputs.** Each `capture` writes the description it reads from the staged `<staging-skill-dir>/SKILL.md`, just before the tool that may rewrite it, to `{run_dir}/description-guard.json`, and each `verify-restore` reads it there with `--captured-file`, so the description never passes through a shell string. Bind `{guarded_description}` ← that file's `description`. Bind `{guard_restored}` ← `restored` and `{guard_diff_kind}` ← `diff_kind` from each `verify-restore`. When `{guard_restored}` is true, set the in-context `description` to `{guarded_description}` so later sections do not work from the tool-mutated value, and record `description_guard_restored: true` with the tool name and `description_guard_diff_kind: {guard_diff_kind}` in workflow context for the evidence report (§8). A later `verify-restore` that exits 0 with `{guard_restored}` false leaves those records in place.
108
115
 
109
- **Empty-snapshot rule.** `verify-restore` refuses an empty or whitespace-only `--captured-description` (exit 1, file untouched). Never re-run it with the empty value: writing it back would blank the field the guard protects. If the description compile §2a sanitized is still in context (an in-context copy of the description, not of the file), re-run `verify-restore` with that value. Otherwise record `description_guard_restored: false` and `description_guard_refused: empty-capture` with the tool name; the evidence report (§8) renders that as a fired guard, not as a clean run.
116
+ **Empty-snapshot rule.** `verify-restore` refuses an empty or whitespace-only description in `{run_dir}/description-guard.json`, and a file it cannot read (exit 1, file untouched). Never re-run it with the empty value: writing it back would blank the field the guard protects. If the description compile §2a sanitized is still in context (an in-context copy of the description, not of the file), re-run `verify-restore` with that value: write `{"description": <that value as a JSON string>}` to `{run_dir}/description-guard.json` with a file write, never `echo` (the value can hold quotes, `$` or backticks), and run the same `verify-restore` line again, once. Otherwise, or when that re-run exits 1 too, record `description_guard_restored: false` and `description_guard_refused: empty-capture` with the tool name; the evidence report (§8) renders that as a fired guard, not as a clean run.
110
117
 
111
118
  **This skill's post-restore re-validation hook:** after `{descriptionGuardHelper}` reports `restored: true`, resolve `{frontmatterValidator}` from `{frontmatterValidatorProbeOrder}` (first existing path wins), run `uv run {frontmatterValidator} <staging-skill-dir>/SKILL.md` and capture `schema_revalidation_result` in context. If the validator exits non-zero OR reports failure for the `description` field, flip the Schema result back to `FAIL` in the evidence report (overriding any prior PASS/WARN from §2), record `description_guard_revalidation: FAIL` with the validator's diagnostic message, and continue — do not halt (step 9 health-check and result contract still need to run so the failure is surfaced through the normal artifact path).
112
119
 
@@ -133,32 +140,39 @@ npx skill-check check --help 2>/dev/null | grep -- --no-security-scan
133
140
  - Else run a second probe — `npx skill-check check --help 2>/dev/null | grep -- --skip-security` — and if it matches, set `{security_scan_flag} = "--skip-security"`.
134
141
  - If neither flag exists: set `{security_scan_flag} = ""` (empty) AND set `{skill_check_flag_fallback} = true`. Skip §2 and §4 automated flows entirely — fall through to §3 manual frontmatter validation. Record in evidence-report: `skill_check_flag_probe: neither --no-security-scan nor --skip-security supported by installed skill-check; validation performed manually`.
135
142
 
136
- **If a security-scan-disable flag was resolved (probe succeeded):**
137
-
138
- ```bash
139
- npx skill-check check <staging-skill-dir> --fix --format json {security_scan_flag}
140
- ```
143
+ **If a security-scan-disable flag was resolved (probe succeeded):** run the `skill-check check --fix` call where the Description Guard block below holds it (its Phase 2), never before that block's first `hash` and its `capture`.
141
144
 
142
145
  This performs frontmatter validation, description quality checks, body limit enforcement, local link resolution, file formatting, auto-fix of deterministic issues, and quality scoring (0-100) across five weighted categories.
143
146
 
144
- **Parse the JSON output** for: `scores[].score` (0-100 — match the entry by `relativePath`/`skillId`; falls back to a top-level `qualityScore` on older skill-check builds), `diagnostics[]` (remaining issues), `fixed[]` (auto-corrected issues).
147
+ **Parse the JSON output** of the Phase 2 call below for: `scores[].score` (0-100; match the entry by `relativePath`/`skillId`; falls back to a top-level `qualityScore` on older skill-check builds) and `diagnostics[]` (remaining issues). skill-check's JSON lists no fix: the two `hash` calls below tell whether `--fix` changed SKILL.md.
145
148
 
146
- **Description Guard Protocol:** This invocation may modify SKILL.md (especially when `fixed[]` is non-empty). Wrap the `skill-check check --fix` call in the four-phase guard defined in §0 by invoking `{descriptionGuardHelper}` at the capture and verify-restore points:
149
+ **Description Guard Protocol:** This invocation may modify SKILL.md. Wrap the `skill-check check --fix` call in the four-phase guard defined in §0 by invoking `{descriptionGuardHelper}` at the capture and verify-restore points, and hash SKILL.md with `{hashContentHelper}` (resolve it ← first existing path in `{hashContentProbeOrder}`) before the capture and after the verify-restore:
147
150
 
148
151
  ```bash
149
- # Phase 1 — capture before the tool call
150
- uv run {descriptionGuardHelper} capture <staging-skill-dir>/SKILL.md
151
- # stash the returned `description` as `guarded_description` in workflow context
152
+ # Before the guard: hash SKILL.md
153
+ uv run {hashContentHelper} hash <staging-skill-dir>/SKILL.md
154
+ # stash the returned `content_hash` as `skill_md_hash_before`
155
+
156
+ # Phase 1: capture before the tool call
157
+ uv run {descriptionGuardHelper} capture <staging-skill-dir>/SKILL.md > "{run_dir}/description-guard.json"
158
+ # the snapshot lands in the run folder; read its `description` as `guarded_description`
152
159
 
153
- # Phase 2 — run skill-check (see command block above)
160
+ # Phase 2: run skill-check
161
+ npx skill-check check <staging-skill-dir> --fix --format json {security_scan_flag}
154
162
 
155
- # Phases 3+4 — verify and restore after the tool call
163
+ # Phases 3+4: verify and restore after the tool call
156
164
  uv run {descriptionGuardHelper} verify-restore <staging-skill-dir>/SKILL.md \
157
- --captured-description "{guarded_description}"
165
+ --captured-file "{run_dir}/description-guard.json"
166
+
167
+ # After the guard: hash SKILL.md again
168
+ uv run {hashContentHelper} hash <staging-skill-dir>/SKILL.md
169
+ # stash the returned `content_hash` as `skill_md_hash_after`
158
170
  ```
159
171
 
160
172
  If `restored: true` in the verify-restore output, apply §0's post-restore re-validation hook. Step 7 promotes the staged files as they are on disk, `--fix` changes included.
161
173
 
174
+ Run the second `hash` call after the last `verify-restore`, a §0 re-run included. When `skill_md_hash_before` and `skill_md_hash_after` differ, record `skill_check_fix_changed: true` for §8. When no `{hashContentProbeOrder}` path exists (skip both `hash` calls) or a `hash` call prints no `content_hash`, record `skill_check_fix_changed: unchecked` whatever the other call printed, so §8 says SKILL.md may have been rewritten.
175
+
162
176
  **Note:** `skill-check` may return non-zero exit code even when `summary.errorCount` is 0. Always rely on parsed JSON, not the shell exit code.
163
177
 
164
178
  - **Score ≥ 70:** Record "Schema: PASS (score: {score}/100)" in evidence-report
@@ -186,15 +200,15 @@ If fails: auto-fix (deterministic), re-validate once, record result. If passes:
186
200
 
187
201
  ```bash
188
202
  # Phase 1: capture before the split
189
- uv run {descriptionGuardHelper} capture <staging-skill-dir>/SKILL.md
190
- # stash returned `description` as `guarded_description`
203
+ uv run {descriptionGuardHelper} capture <staging-skill-dir>/SKILL.md > "{run_dir}/description-guard.json"
204
+ # the snapshot lands in the run folder; read its `description` as `guarded_description`
191
205
 
192
206
  # Phase 2: the split
193
207
  uv run {shardBodyHelper} <staging-skill-dir>/SKILL.md --budget 400
194
208
 
195
209
  # Phases 3 and 4: verify and restore after the split
196
210
  uv run {descriptionGuardHelper} verify-restore <staging-skill-dir>/SKILL.md \
197
- --captured-description "{guarded_description}"
211
+ --captured-file "{run_dir}/description-guard.json"
198
212
  ```
199
213
 
200
214
  If `restored: true` in the verify-restore output, apply §0's post-restore re-validation hook.
@@ -214,20 +228,26 @@ Then re-validate: `npx skill-check check <staging-skill-dir> --format json {secu
214
228
  **If skill-check available:**
215
229
 
216
230
  ```bash
217
- npx skill-check check <staging-skill-dir> --format json
231
+ npx skill-check check <staging-skill-dir> --format json > "{run_dir}/skill-check.json" 2> "{run_dir}/skill-check.stderr"; echo "skill-check exited $?"
218
232
  ```
219
233
 
220
- (Security scan enabled by default when `--no-security-scan` omitted. The scan uses [Snyk](https://docs.snyk.io/) to check for prompt injection risks, sensitive data exposure, and unsafe tool permissions.)
234
+ (Security scan enabled by default when `--no-security-scan` omitted. The scan uses [Snyk](https://docs.snyk.io/) to check for prompt injection risks, sensitive data exposure, and unsafe tool permissions.) The call keeps stdout, stderr and the exit code apart. Give it the longest timeout your shell tool takes. When no "skill-check exited" line came back, the shell tool stopped the call before skill-check finished (a cold `uvx mcp-scan` install can outlast its timeout).
221
235
 
222
- Record any security warnings in evidence-report. Security findings are advisory — they do not block artifact generation. If the full validation re-run produces a different quality score than section 2, update the evidence-report with the newer score.
236
+ **Score refresh.** stdout can go on after the JSON (a `Wrote <path>` line when the project config sets a report path), so read only the JSON object at the start of `{run_dir}/skill-check.json`. If its quality score differs from section 2's, update the evidence-report with the newer score. When the file does not start with a JSON object (exit 2 on a config, option or plugin error, or a stopped call), keep section 2's score.
223
237
 
224
- **If security scan fails due to missing SNYK_TOKEN** (skill-check exits non-zero and its stderr names `SNYK_TOKEN`):
238
+ **Read the JSON, not the exit code.** skill-check prints its JSON before the security scan runs, then exits 1 when `summary.errorCount` is above 0, when `--strict` or `--fail-on-warning` is passed and `summary.warningCount` is above 0 (this call passes neither), or when the scan exits non-zero, and 2 when no scan runner starts, so a non-zero exit is not a security finding. skill-check 1.2.0 puts no security key in the JSON. The scan exited non-zero, which without `--ci` means it did not finish, when skill-check exits 1 although `summary.errorCount` is 0: stderr is then the scan's last lines (its tail), even when blank. On exit 0, stderr is never a scan tail (npm and Node warnings can be there); when exit 1 has another cause, stderr is a scan tail only when it holds the scanner's own lines, such as its token message or a finding code. A finding code is `[`, a capital letter, three digits and `]` (such as `[E001]`), the codes skill-check's own scan summary counts. Bind `{security_result}` from `{run_dir}/skill-check.stderr` and the exit code, the first that applies:
225
239
 
226
- Display: "Security scan requires a Snyk Enterprise API token ([docs](https://docs.snyk.io/snyk-api/authentication-for-api)). Set `SNYK_TOKEN=your-token` in environment or `.env`, then re-run [SF] Setup Forge. Without Enterprise, use `--no-security-scan` to skip. Security scanning is optional and does not block skill compilation."
240
+ - `skipped`: §1 found no skill-check (**If skill-check unavailable** below), skill-check exits 2, the call stopped before it exited, or the scan exited non-zero and its tail carries no finding code. Log under the evidence report's Remaining Warnings "security scan skipped: SNYK_TOKEN not configured" when the tail says to set the `SNYK_TOKEN` environment variable (the scanner prints "To use Agent Scan, set the SNYK_TOKEN environment variable."), otherwise "security scan skipped: scanner could not run ({the stderr line that names the cause, or its last line})", or "security scan skipped: scanner exited non-zero with no output" when stderr is blank.
241
+ - `warn`: `security[]` holds findings, or the tail carries finding codes. Record each finding under the evidence report's Remaining Warnings as an advisory warning: security findings do not block artifact generation.
242
+ - `pass`: otherwise. The scan exited 0 and skill-check reported no finding. Without `--ci`, agent-scan also exits 0 when its analysis fails (a 401 from a bad token, a 429, a network error), so this does not show that the scan found nothing (#892).
227
243
 
228
- Record: "Security scan skipped — SNYK_TOKEN not configured"
244
+ **Security row.** §8 writes the `- Security:` row as `{security_result}`, then its note in parentheses: for `skipped` the note above less its "security scan skipped: " prefix, such as `skipped (SNYK_TOKEN not configured)`; for `warn` each finding; for `pass` the row reads `pass (scan exited 0; skill-check 1.2.0 does not pass the scan's findings on)`. Step 8 takes the row's first word as `security_result`.
245
+
246
+ **If the tail says to set `SNYK_TOKEN`** (the first `skipped` note):
247
+
248
+ Display: "Security scan requires a Snyk Enterprise API token ([docs](https://docs.snyk.io/snyk-api/authentication-for-api)). Set `SNYK_TOKEN=your-token` in environment or `.env`, then re-run [SF] Setup Forge. Without Enterprise, use `--no-security-scan` to skip. Security scanning is optional and does not block skill compilation."
229
249
 
230
- **If skill-check unavailable:** Skip with note: "Security scan skipped — skill-check tool unavailable"
250
+ **If skill-check unavailable:** bind `{security_result}` ← `skipped`, keep the Security row §1 marked, and log "security scan skipped: skill-check unavailable" under the evidence report's Remaining Warnings.
231
251
 
232
252
  ### 6. Description Angle-Bracket Check
233
253
 
@@ -346,14 +366,14 @@ Record the result on the evidence report's `Provenance lines` line (§8): `pass`
346
366
 
347
367
  ### 8. Update Evidence Report
348
368
 
349
- Write the validation results into the staged `<staging-skill-dir>/evidence-report.md`, in place of the `[PENDING: populated by step 6]` placeholders compile left, with `python3 {atomicWriteHelper} write --target <staging-skill-dir>/evidence-report.md`: step 7 promotes that file as it is. In the same write, add under `## Remaining Warnings` the notes steps 5a to 5c kept for the evidence report (the doc-sources note, `{temporal_feeder_notice}` when step 5c set it, because the Deep-tier temporal feeder held no files) and, once each, the skipped authoritative-files scan when the inventory's `authoritative_files_scan` records `not_scanned` (a remote source that was never cloned; the `skipped` count of candidates the user skipped is not a warning). When no split ran and the staged `references/` holds no file, the Body line says `Tier-2 kept inline (references/ empty)`, so an audit can tell inline by design from a split that failed.
369
+ Write the validation results into the staged `<staging-skill-dir>/evidence-report.md`, in place of the `[PENDING: populated by step 6]` placeholders compile left and of the line under `## Auto-Fixed Issues`, which compile wrote before §2 ran, with `python3 {atomicWriteHelper} write --target <staging-skill-dir>/evidence-report.md`: step 7 promotes that file as it is. In the same write, add under `## Remaining Warnings` the notes steps 5a to 5c kept for the evidence report (the doc-sources note, `{temporal_feeder_notice}` when step 5c set it, because the Deep-tier temporal feeder held no files) and, once each, the skipped authoritative-files scan when the inventory's `authoritative_files_scan` records `not_scanned` (a remote source that was never cloned; the `skipped` count of candidates the user skipped is not a warning). When no split ran and the staged `references/` holds no file, the Body line says `Tier-2 kept inline (references/ empty)`, so an audit can tell inline by design from a split that failed.
350
370
 
351
371
  ```markdown
352
372
  ## Validation Results
353
373
  - Schema: {pass/fail} (quality score: {score}/100)
354
374
  - Frontmatter: {pass/fail}
355
375
  - Body: {pass/fail} {body split applied if applicable}
356
- - Security: {pass/warn/skipped}
376
+ - Security: {pass/warn/skipped} ({the note §5's Security row gives})
357
377
  - Description angle brackets: {none | re-sanitized ({count} substitutions) | not checked — no description}
358
378
  - Tessl Review: {tessl_summary}
359
379
  - Metadata: {pass/fail}
@@ -370,7 +390,7 @@ Write the validation results into the staged `<staging-skill-dir>/evidence-repor
370
390
  - Notes: {one-sentence detail or —}
371
391
 
372
392
  ## Auto-Fixed Issues
373
- - {list of issues automatically corrected by --fix}
393
+ - {"skill-check --fix rewrote SKILL.md" when §2 recorded `skill_check_fix_changed: true`, "SKILL.md may have been rewritten (not hashed) by skill-check --fix" when it recorded `skill_check_fix_changed: unchecked`, otherwise "none"}
374
394
 
375
395
  ## Remaining Warnings
376
396
  - {warnings and security results, then each entry of {tessl_warnings} — or "none"}
@@ -31,4 +31,4 @@ The shared emitter, `skf-emit-result-envelope.py`, builds every `SKF_STACK_RESUL
31
31
  SKF_STACK_RESULT_JSON: {"status":"success|error","skill_package":"...|null","skill_name":"...","stack_libraries":["..."],"mode":"code|compose","quality_score":null,"exit_code":0,"halt_reason":null,"run_id":"...","result_path":"...|null","headless_decisions":[],"warnings":[]}
32
32
  ```
33
33
 
34
- A halt adds `error`: its `phase`, its message as `reason`, and `path` when it names a file or folder. When the customization resolver could not run, `warnings` holds `[activation/warn] customization_resolver_unavailable: <reason>`, which step 1 records before any other warning: the run used the bundled `customize.toml` alone, without the `{project-root}/_bmad/custom/` overrides.
34
+ A halt adds `error`: its `phase`, its message as `reason`, and `path` when it names a file or folder. A success line adds `security_result`, the outcome of step 8 §9's security scan (`pass`, `warn` or `skipped`); a halt leaves it out. When the customization resolver could not run, `warnings` holds `[activation/warn] customization_resolver_unavailable: <reason>`, which step 1 records before any other warning: the run used the bundled `customize.toml` alone, without the `{project-root}/_bmad/custom/` overrides.
@@ -53,17 +53,17 @@ The shared emitter writes the result contract (`shared/references/output-contrac
53
53
 
54
54
  ```json
55
55
  {"status": "success", "skill_package": "{skill_package}", "skill_name": "{stack_name}",
56
- "stack_libraries": ["<library>"], "mode": "code|compose", "quality_score": null, "halt_reason": null,
56
+ "stack_libraries": ["<library>"], "mode": "code|compose", "quality_score": null, "security_result": "pass|warn|skipped", "halt_reason": null,
57
57
  "result_contract": {"skill": "skf-create-stack-skill", "status": "success",
58
58
  "outputs": [{"type": "skill", "path": "{skill_package}/SKILL.md"},
59
59
  {"type": "skill", "path": "{skill_package}/context-snippet.md"},
60
60
  {"type": "skill", "path": "{skill_package}/metadata.json"},
61
61
  {"type": "report", "path": "{forge_version}/evidence-report.md"}],
62
62
  "summary": {"lib_count": 0, "integration_count": 0, "forge_tier": "<tier>", "confidence_tier": "<tier>",
63
- "confidence_distribution": {}, "quality_score": null}}}
63
+ "confidence_distribution": {}, "quality_score": null, "security_result": "pass|warn|skipped"}}}
64
64
  ```
65
65
 
66
- `skill_package` is the absolute path to the committed package and `stack_libraries` the committed `metadata.json`'s `libraries`. Both `quality_score` fields are `{quality_score}`, the skill-check score step 8 §3 bound (`null` when skill-check did not run), never a test-skill score. The summary's counts, `confidence_tier` and `confidence_distribution` are the committed `metadata.json`'s `library_count`, `integration_count`, `confidence_tier` and `confidence_distribution`, and `forge_tier` its run tier. Then run:
66
+ `skill_package` is the absolute path to the committed package and `stack_libraries` the committed `metadata.json`'s `libraries`. Both `quality_score` fields are `{quality_score}`, the skill-check score step 8 §3 bound (`null` when skill-check did not run), never a test-skill score. Both `security_result` fields are `{security_result}`, the scan outcome step 8 §9 bound. The summary's counts, `confidence_tier` and `confidence_distribution` are the committed `metadata.json`'s `library_count`, `integration_count`, `confidence_tier` and `confidence_distribution`, and `forge_tier` its run tier. Then run:
67
67
 
68
68
  ```bash
69
69
  uv run {emitEnvelopeHelper} emit --workflow skf-create-stack-skill --run-dir "{run_dir}" --result-dir "{forge_version}" < "{run_dir}/result-context.json"
@@ -3,6 +3,9 @@ nextStepFile: 'report.md'
3
3
  outputValidatorProbeOrder:
4
4
  - '{project-root}/_bmad/skf/shared/scripts/skf-validate-output.py'
5
5
  - '{project-root}/src/shared/scripts/skf-validate-output.py'
6
+ hashContentProbeOrder:
7
+ - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
8
+ - '{project-root}/src/shared/scripts/skf-hash-content.py'
6
9
  ---
7
10
 
8
11
  <!-- Config: communicate in {communication_language}. Artifact text in {document_output_language}. -->
@@ -70,17 +73,17 @@ timeout 10s npx --no-install skill-check -h
70
73
 
71
74
  ### 3. Validate SKILL.md via skill-check (if available)
72
75
 
73
- **If available**, run: `npx skill-check check <skill-dir> --fix --format json --no-security-scan`
76
+ **If available**, resolve `{hashContentHelper}` ← first existing path in `{hashContentProbeOrder}`, then run in order: `uv run {hashContentHelper} hash <skill-dir>/SKILL.md`, `npx skill-check check <skill-dir> --fix --format json --no-security-scan`, then `uv run {hashContentHelper} hash <skill-dir>/SKILL.md` again, keeping the `content_hash` each `hash` call prints.
74
77
 
75
- This validates frontmatter, description, body limits, links, formatting — and auto-fixes deterministic issues. Parse JSON for `scores[].score` (match the entry by `relativePath`/`skillId`; falls back to a top-level `qualityScore` on older skill-check builds), `diagnostics[]`, `fixed[]`.
78
+ This validates frontmatter, description, body limits, links and formatting, and auto-fixes deterministic issues. Parse JSON for `scores[].score` (match the entry by `relativePath`/`skillId`; falls back to a top-level `qualityScore` on older skill-check builds) and `diagnostics[]`. skill-check's JSON lists no fix: SKILL.md changed across the `--fix` call when the two `content_hash` values differ, and is taken as changed, not hashed, when no `{hashContentProbeOrder}` path exists (run the skill-check call alone) or a `hash` call prints no `content_hash`.
76
79
 
77
80
  Bind `{quality_score}` ← that score, from the latest skill-check run (§3 runs it again after a split); `{quality_score}` is null when skill-check is unavailable. It is the skill-check score, not a test-skill score, and step 9 puts it in the result envelope and the result contract.
78
81
 
79
- **Post-fix provenance drift guard (S15):** If `fixed[]` is non-empty, `skill-check --fix` has modified `SKILL.md` after step 7 wrote it — so the `metadata.json` hashes/provenance recorded against the pre-fix body may now be stale. Emit a **WARNING** finding listing each auto-fix (`"skill-check --fix modified SKILL.md: {fix_description} — metadata.json hashes/provenance may be out of date"`) rather than silently accepting the fixes, so the drift is surfaced. If the caller wants authoritative metadata, they should re-run the workflow.
82
+ **Post-fix provenance drift guard (S15):** When SKILL.md changed across the `--fix` call, `skill-check --fix` modified it after step 7 wrote it, so the `metadata.json` hashes and provenance recorded against the earlier body may now be stale. Emit one **WARNING** finding, `"skill-check --fix modified SKILL.md after step 7 wrote it: metadata.json hashes and provenance may be out of date"` (`"SKILL.md may have been rewritten (not hashed) by skill-check --fix after step 7 wrote it: metadata.json hashes and provenance may be out of date"` when SKILL.md was not hashed), rather than silently accepting the fix, so the drift is surfaced. If the caller wants authoritative metadata, they should re-run the workflow.
80
83
 
81
84
  **If `body.max_lines` reported**, prefer selective split: extract only the largest Tier 2 section(s) to `references/`, keeping Tier 1 content inline (inline passive context achieves 100% task accuracy vs 79% for on-demand retrieval). For a stack capstone the canonical split is the catalog (`Library Reference Index` + `Per-Library Summaries`) → `references/stack-catalog.md`, leaving an inline pointer (see `{stackSkillTemplatePath}` "Sizing Guidance"). The moved catalog's links resolve from `references/`, so rewrite each `[ref](references/{name}.md)` in it to `[ref]({name}.md)`. This is the **intended** large-stack layout, not a violation: §4 below accepts the pointer form, so clearing the skill-check body ERROR this way does not also trip the structure check. Fall back to `npx skill-check split-body <skill-dir> --write` if not feasible. After the split, verify that any in-SKILL.md anchor links (e.g. to the catalog/pointer or other moved sections) still resolve, and that every link inside a moved section resolves from its new file.
82
85
 
83
- **Re-validate a changed package.** When `fixed[]` is non-empty or a split ran, run the §1 `{outputValidator}` command again (after a split, skill-check too), so §4 to §8 record the package as it now stands.
86
+ **Re-validate a changed package.** When SKILL.md changed across the `--fix` call (not hashed included) or a split ran, run the §1 `{outputValidator}` command again (after a split, skill-check too), so §4 to §8 record the package as it now stands.
84
87
 
85
88
  **If unavailable**, do not hand-walk the frontmatter: use `validation.skill_md.frontmatter` from the §1 output-validator run, which checks delimiters, `name` format + directory match (`{stack_name}`), `description` presence/length, and unknown fields against the agentskills.io allow-set. Record each reported issue at its severity as a **WARNING** finding. (If the output validator was *also* unavailable in §1, fall back to the manual checklist: `---` delimiters; `name` lowercase-alphanumeric-plus-hyphens 1-64 chars matching `{stack_name}`; `description` present and 1-1024 chars; only `name`/`description`/`license`/`compatibility`/`metadata`/`allowed-tools` permitted.) Invalid frontmatter will fail `npx skills add` and `npx skill-check check`.
86
89
 
@@ -134,6 +137,8 @@ Record security findings as advisory **WARNING** findings: they do not block the
134
137
 
135
138
  **If unavailable:** Skip, with the note "security scan skipped: skill-check unavailable" in the §10 validation results.
136
139
 
140
+ Bind `{security_result}` ← `skipped` for **Could not run**, a run whose `{run_dir}/skill-check.json` does not start with a JSON object, or an unavailable skill-check; `warn` for **Findings**; `pass` for **Neither**. Step 9 puts it in the result envelope and the result contract.
141
+
137
142
  ### 10. Display Validation Results
138
143
 
139
144
  Report the validation outcome. If all checks passed, state so and name what was verified: file presence (`{count}/{count}`), SKILL.md structure, metadata.json fields, the `{lib_count}` library + `{pair_count}` integration reference files, and complete confidence-tier coverage. If there were findings, report the `{warning_count}` finding(s), each with severity, description, and file path, plus files present/expected and warning/error counts; when errors include missing files, note this may indicate a write failure in step 07. In both cases, list each §9 security scan note.
@@ -44,7 +44,7 @@ Field rules:
44
44
  - `skill`, `drop_mode`, `versions_affected`, `files_deleted`, `manifest_updated`: the values the emit site knows; a key not resolved yet takes the default shown in the template (`null`, `[]` or `false`). `versions_affected` is a list holding the one version dropped, or the string `"all"` for the whole skill.
45
45
  - `would_delete`: in a dry run of a purge, the folders the purge would delete (the step 1 purge check's `affected_directories`), so an automator can check the blast radius before it runs the drop; `[]` otherwise, and in a dry run of a deprecate.
46
46
  - `forge_left_in_place`: the forge folder a purge leaves in place (or, in a dry run, would leave) because SKF did not generate it; `null` otherwise.
47
- - `result_path`: the per-run result file step 3 wrote, `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC), stamped by the emitter; `null` for a dry run and a HALT, which write no result file.
47
+ - `result_path`: the per-run result file step 3 wrote, `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC), stamped by the emitter; `null` for a dry run and a HALT, which write no result file. A per-run record that could not be written leaves `result_path` null with a `result_file_write_failed` warning.
48
48
  - `error`: `null` on success and dry-run; on a HALT, `{"phase", "reason"}`, plus `path` when the halt names one: the step and gate that halted, and the halt message.
49
49
  - `warnings`: present only when the run recorded at least one. A run that reports `success` can still carry the degraded outcomes step 2 records, which the report also shows: `context_rebuild_failed: <context file>: <error>` (a context file it could not rebuild), `active_link_dangling: <skill folder>/active: <manual fix>` (an `active` link a version purge left pointing at no version), `delete_failed: <path>: <error>` (a folder a partial purge could not delete) and `verification_failed: <what failed>: <manual fix>` (a final-state check that failed). Any run can carry `result_file_write_failed: <path>: <reason>` when the emitter could not write a result file, and `customization_resolver_unavailable: <reason>` when On Activation records that the customization resolver could not run, so the overrides under `{project-root}/_bmad/custom/` were not applied.
50
50
 
@@ -183,7 +183,7 @@ Other workflow choices that diverge from the strict spec path may add their own
183
183
 
184
184
  ### 6b. Post-Export Hook (Optional)
185
185
 
186
- Skip this section on a dry run (a dry run fires no hook), when `{onCompleteCommand}` (resolved at SKILL.md On Activation §3 from `workflow.on_complete`) is empty, the bundled default, and when `{result_path}` is null (§6 wrote no per-run record). Otherwise invoke it now, after the emitter wrote both result files:
186
+ Skip this section on a dry run (a dry run fires no hook), when `{onCompleteCommand}` (resolved at SKILL.md On Activation §3 from `workflow.on_complete`) is empty, the bundled default, and when `{result_path}` is null (§6 wrote no per-run record). Otherwise invoke it now, after the emitter wrote the per-run record:
187
187
 
188
188
  ```bash
189
189
  {onCompleteCommand} --result-path="{result_path}"
@@ -75,6 +75,14 @@ uv run {quickBatchHelper} next --run-dir "{batch_dir}"
75
75
  - **`"status": "record"`**: target `batch` ended, its payload staged, before §3 recorded it (a compaction came between its envelope and §3): go to §3 with that `batch`, never running it again.
76
76
  - **`"status": "done"`**: no target is pending, or `--fail-fast` ended the batch after a failed target. Go to §4.
77
77
 
78
+ If it exits non-zero (the target's run folder cannot be created, or the batch file is gone or unreadable), the batch cannot go on: display "**The batch stopped:** {its `message`}" and HARD HALT with **exit code 4 (write-failure)**: no other target runs and the health check does not run. Emit with nothing to stage in, as §1 does:
79
+
80
+ ```bash
81
+ uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --target stderr <<'SKF_JSON'
82
+ {"phase": "batch-mode", "halt_reason": "write-failure", "reason": "The batch stopped: <its message, one line>", "skill_package": null}
83
+ SKF_JSON
84
+ ```
85
+
78
86
  ### 3. Record the Target
79
87
 
80
88
  Every target ends here: step 6 §4 returns here when the target finishes, and every HARD HALT of steps 1 to 6 returns here after its envelope (`references/halt-contract.md`, In `--batch`), the step 5 ownership halt included. Control returns here even when the step that ended the target reads as the end of the run. Record it:
@@ -83,7 +91,13 @@ Every target ends here: step 6 §4 returns here when the target finishes, and ev
83
91
  uv run {quickBatchHelper} record --run-dir "{batch_dir}" --batch {batch} --target stderr
84
92
  ```
85
93
 
86
- `{batch}` is the number §2's `next` printed. The helper reads how the target ended from what its run folder staged for the emitter (`halt.json`, or step 6's `result-context.json`), appends the outcome to the batch file, removes the run folder of a target that succeeded, and prints the target's `done` or `fail` event on stderr: display it verbatim. Then go back to §2. If it exits non-zero, its `message` names what is missing: stage the payload the target ended with, record again, then go back to §2.
94
+ `{batch}` is the number §2's `next` printed. The helper reads how the target ended from what its run folder staged for the emitter (`halt.json`, or step 6's `result-context.json`), appends the outcome to the batch file, removes the run folder of a target that succeeded, and prints the target's `done` or `fail` event on stderr: display it verbatim. Then go back to §2. If it exits non-zero, its `message` names what is missing: stage the payload the target ended with and record again, once. If it exits non-zero a second time (the batch file cannot be written, or the staged `halt.json` is not JSON or names a `halt_reason` that maps to no exit code), the outcome cannot be kept and the batch cannot go on: display "**The batch stopped:** {its `message`}" and HARD HALT with **exit code 4 (write-failure)**. This halt ends the batch, not only the target: do not go back to §2, which would hand the same target back, no other target runs and the health check does not run. Emit with nothing to stage in, as §1 does:
95
+
96
+ ```bash
97
+ uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --target stderr <<'SKF_JSON'
98
+ {"phase": "batch-mode", "halt_reason": "write-failure", "reason": "The batch stopped: <its message, one line>", "skill_package": null}
99
+ SKF_JSON
100
+ ```
87
101
 
88
102
  ### 4. Batch Summary
89
103
 
@@ -93,6 +107,14 @@ uv run {quickBatchHelper} summarize --run-dir "{batch_dir}" --output-dir "{batch
93
107
 
94
108
  The helper counts the targets, sets the batch status and exit code, writes the summary files below, appends the final status to the batch file (and removes the batch run folder when no target failed), and prints the `batch_summary` event on stderr: display it verbatim. Then print step 7's `start` event and load, read in full and execute `{healthCheckStepFile}`: the health check runs once per batch, here. The batch ends with the event's `exit_code`.
95
109
 
110
+ If `summarize` exits non-zero (a target is still pending, or the batch file is gone or unreadable), it wrote no summary file: display "**The batch stopped:** {its `message`}" and HARD HALT with **exit code 4 (write-failure)**: the health check does not run. When an earlier `summarize` already printed the `batch_summary` event (a compaction came before the health check), that event stays the batch's result (**Reading a batch.** below). Emit with nothing to stage in, as §1 does:
111
+
112
+ ```bash
113
+ uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --target stderr <<'SKF_JSON'
114
+ {"phase": "batch-mode", "halt_reason": "write-failure", "reason": "The batch stopped: <its message, one line>", "skill_package": null}
115
+ SKF_JSON
116
+ ```
117
+
96
118
  **Reading a batch.** Under `--batch`, a caller reads the batch's `batch_summary` event and the summary file its `summary_path` names, never `quick-skill-batch-latest.json` in its place: that copy holds the last batch summary written, which may be another batch's. The event comes before the health check, so a caller reads it from the session's output stream (`claude -p --output-format stream-json`) or transcript, not from the final message. A printed `batch_summary` event stays the batch's result even if a halt line follows it. A batch that printed no `batch_summary` event has no batch result, whatever the process exit code: a halt line of its own (phase `batch-mode`, or `on-activation`) says why it stopped, and with no halt line it was cut off or could not print its halt line. A target it printed a `done` or `fail` event for ended as that event says; a target with a `start` event and no end event has no result, and a target with no event did not run in this session. When the event's `summary_path` is null, the per-batch file was not written (its `summary_write_failed` warning says why): the event and `{batch_dir}/batch.jsonl`, which the helper then keeps, are the batch's result, never `quick-skill-batch-latest.json`. A `summary_write_failed` warning beside a set `summary_path` means only the `-latest` copy failed: the file it names is still the batch's result. After a cut-off batch, run the batch again: each run takes a new run folder (SKILL.md On Activation step 1), so it starts over from the first target.
97
119
 
98
120
  ## Batch summary contract
@@ -150,10 +172,14 @@ Batch mode emits per-target boundary events on stderr in addition to the per-ste
150
172
 
151
173
  ```
152
174
  {"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>,"exit_code":<code>,"summary_path":"<path>"}
175
+ {"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>,"exit_code":<code>,"summary_path":null,"warnings":["summary_write_failed: <dir>: <reason>"],"batch_file":"<path>"}
176
+ {"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>,"exit_code":<code>,"summary_path":"<path>","warnings":["<summary_write_failed or batch_file_write_failed>: <dir>: <reason>"]}
153
177
  ```
154
178
 
179
+ When the per-batch summary file cannot be written, `summary_path` is null and `batch_file` names `{batch_dir}/batch.jsonl`, which then holds the batch's result. When `summary_path` is set and the event still carries `warnings`, a later write failed: `summary_write_failed` means only the `-latest` copy failed, and `batch_file_write_failed` that only the summary line could not be added to `{batch_dir}/batch.jsonl`. The file `summary_path` names stays the batch's result.
180
+
155
181
  ## Exit code
156
182
 
157
183
  Without `--fail-fast`, the batch runs every target, whatever an earlier target's outcome, and exits with the batch summary's `exit_code`: `0` when `failed == 0`, otherwise the highest exit code among the failed targets. Automators that branch on the single-target exit-code map read each target's own code in `results[]`. With `--fail-fast`, the batch stops at the first failed target and exits with that target's code.
158
184
 
159
- A batch refused before its first target writes no batch summary. It exits with code `2` when `--description` or `--exports` was passed with `--batch` (or `--language-hint` or `--scope-hint`), §1 cannot read the batch file or `skf-quick-batch.py` is missing, and with code `4` when §1 cannot write the batch run folder.
185
+ A batch refused before its first target writes no batch summary. It exits with code `2` when `--description` or `--exports` was passed with `--batch` (or `--language-hint` or `--scope-hint`), §1 cannot read the batch file or `skf-quick-batch.py` is missing, and with code `4` when §1 cannot write the batch run folder. A batch that stops because `next`, a second `record` or `summarize` fails (§2 to §4) exits with code `4` and writes no batch summary then; a `batch_summary` event an earlier `summarize` already printed stays the batch's result.
@@ -45,6 +45,8 @@ Populate the SKILL.md section structure from `{skillTemplateData}` § "SKILL.md
45
45
 
46
46
  **Skills module** (`repo_shape: skills-module`): Unless `{overrides.exports}` is set, Key Exports lists the skills first, each with its description, then the menu codes, each with its description, display name and the skill it runs, in the inventory's order. Usage Patterns gives one entry per `module-help.csv` usage pattern in the inventory; with no `module-help.csv`, it comes from the README as for a library.
47
47
 
48
+ **Stars from other packages** (`extraction_inventory.external_stars`): after the Key Exports list, one prose sentence per record, not a list item or table row: "`<source statement>` (`{file}` line {line}) also exports every name of `{specifier}`: see that package's documentation." `<source statement>` is the statement as the source writes it at `{file}` line {line}, read from `{run_dir}/src/{file}` without its closing `;`, not the record's `statement`, which writes `export type *` as `export *`. When that line is an `export type *`, which passes on only the package's types, write "every type" for "every name". List none of that package's names.
49
+
48
50
  **Scripts & Assets Note** (add as an optional section when step 3's `{run_dir}/sniff.json` lists a `scripts/`, `bin/`, `assets/`, `templates/` or `schemas/` folder: its `asset_dir_count` is above 0): "This package may include scripts and assets. Run create-skill for full extraction with provenance tracking."
49
51
 
50
52
  **If confidence is low** — include a note: "This skill was generated with limited source data. Consider running create-skill for a more thorough compilation."
@@ -96,7 +96,8 @@ cat > "{run_dir}/result-context.json" <<'SKF_JSON'
96
96
  "detected_languages": {detected_languages},
97
97
  "zero_exports_rescue": {zero_exports_rescue},
98
98
  "active_pointer": "{active_pointer}",
99
- "validation_issues": {validation_issues}
99
+ "validation_issues": {validation_issues},
100
+ "security_result": "{security_result}"
100
101
  },
101
102
  "result_contract": {
102
103
  "skill": "skf-quick-skill",
@@ -111,7 +112,7 @@ SKF_JSON
111
112
  uv run {emitEnvelopeHelper} emit --workflow skf-quick-skill --run-dir "{run_dir}" --result-dir "{skill_package}" < "{run_dir}/result-context.json"
112
113
  ```
113
114
 
114
- This list is the summary, once: `skill_name`, `version`, `language`, `exports_documented` (the export count), `quality_score` and `validation_issues` (step 5, the object `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`), `confidence` and `repo_shape` (step 3, the shape null when it recorded none), `language_resolution` and `detected_languages` (step 1), `zero_exports_rescue` (step 3 §4.5, else null) and `active_pointer` (§1). Write each value as JSON: a string in double quotes with any `"` or `\` escaped, every path absolute with `/`, and `null` for a value the run did not record. Leave the snippet out of both `outputs` under `--skip-snippet`, and `outputs.active_pointer` out when §1 flipped no pointer.
115
+ This list is the summary, once: `skill_name`, `version`, `language`, `exports_documented` (the export count), `quality_score` and `validation_issues` (step 5, the object `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`), `security_result` (step 5 §4: `pass`, `warn` or `skipped`), `confidence` and `repo_shape` (step 3, the shape null when it recorded none), `language_resolution` and `detected_languages` (step 1), `zero_exports_rescue` (step 3 §4.5, else null) and `active_pointer` (§1). Write each value as JSON: a string in double quotes with any `"` or `\` escaped, every path absolute with `/`, and `null` for a value the run did not record. Leave the snippet out of both `outputs` under `--skip-snippet`, and `outputs.active_pointer` out when §1 flipped no pointer.
115
116
 
116
117
  The emitter writes the result contract from `result_contract`, with the payload's own `status` and `summary`, so neither is typed twice (the per-run record `{skill_package}/quick-skill-result-{YYYYMMDD-HHmmss}.json` and its copy `quick-skill-result-latest.json`, the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_QUICK_SKILL_RESULT_JSON` line) and prints one line on stdout, `SKF_QUICK_SKILL_RESULT_JSON: {...}`. In a headless single-target run, bind `{result_envelope_line}` ← that line and do not display it here: the shared health check displays it verbatim as the run's last line, so a `claude -p` caller reads the envelope as the final message. Otherwise (an interactive run, or a `--batch` target) display it verbatim as its own line. If it exits non-zero, fix `result-context.json` once (its `message` names the problem) and run it again; if it still fails, or no path resolved for `{emitEnvelopeHelper}`, leave `{result_envelope_line}` empty, say that the result contract was not written and why, and go on.
117
118
 
@@ -13,7 +13,7 @@ Every HARD HALT in this workflow exits with a stable, documented code so headles
13
13
  | 0 | success | step 7 (terminal) |
14
14
  | 2 | input-invalid | batch mode, before any target runs (`--description`, `--exports`, `--language-hint` or `--scope-hint` passed with `--batch`; §1: a batch file it cannot read, or `skf-quick-batch.py` missing); step 1 §1 (a headless run with no target) |
15
15
  | 3 | resolution-failure | SKILL.md On Activation step 1 (`python3` or `uv` missing); a shipped helper missing, named in the message and in `details.cause` (step 3: `skf-github-fetch.py`, `skf-skills-module.py` §1, `skf-extract-public-api.py` §2; step 5: `skf-atomic-write.py` §2, `skf-validate-frontmatter.py` §4 when skill-check is unavailable, `skf-validate-output.py` §5); step 1 (package resolver missing or failing §1b, a target that is no GitHub repository or package §2, registry chain §3, version tag missing or not checkable §3a, file listing unreadable, language detector missing or failing, language abort or no language found §4); step 3 (non-library shape §1.5, zero-exports §4.5) |
16
- | 4 | write-failure | SKILL.md On Activation step 1 (the run folder cannot be created); batch mode §1 (the batch run folder cannot be written); step 5 §2 (deliverable write failed) |
16
+ | 4 | write-failure | SKILL.md On Activation step 1 (the run folder cannot be created); batch mode §1 (the batch run folder cannot be written) and §2 to §4 (a failing `next`, a second failing `record`, a failing `summarize`); step 5 §2 (deliverable write failed) |
17
17
  | 5 | overwrite-cancelled | step 5 §1 (user selected [N], or a headless run reached a version another SKF workflow built; `details.existing_generator` names its generator) |
18
18
  | 6 | user-cancelled | step 1 §1 ([X] Cancel and exit, or cancel-line affordance) and §3 ([X] at the ambiguous-name gate); step 2 ([A] Abort at ecosystem-match gate); step 4 §6 (user selected [Q]) |
19
19
  | 7 | finalize-blocked | step 6 §1 (the flip helper refused or failed: something that is not a link is at `{skill_group}/active`, or another process holds its lock) |
@@ -31,7 +31,7 @@ SKF_JSON
31
31
  uv run {emitEnvelopeHelper} emit-halt --workflow skf-quick-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"
32
32
  ```
33
33
 
34
- - `phase`: the step's slug (`resolve-target`, `ecosystem-check`, `quick-extract`, `compile`, `write-and-validate`, `finalize`), `on-activation` for a halt before the run starts (SKILL.md On Activation, and batch mode's refusal of `--description` and `--exports` and of the hint flags), or `batch-mode` for batch mode §1.
34
+ - `phase`: the step's slug (`resolve-target`, `ecosystem-check`, `quick-extract`, `compile`, `write-and-validate`, `finalize`), `on-activation` for a halt before the run starts (SKILL.md On Activation, and batch mode's refusal of `--description` and `--exports` and of the hint flags), or `batch-mode` for batch mode §1 to §4.
35
35
  - `halt_reason`: the failure class the halt names: the Meaning of its exit code in the table above, or `not-skf-output` or `flat-layout` for exit 9. The emitter derives `exit_code` from it.
36
36
  - `reason`: the message the step displayed, as one line (its first sentence when it runs longer); it becomes `error.message`. Escape `"` and `\` in it as JSON requires.
37
37
  - `skill_package`: the absolute `{skill_package}` once step 5 §1 computed it, else `null`.
@@ -63,4 +63,4 @@ When `{headless_mode}` is true, each step prints one-line JSON progress events o
63
63
 
64
64
  ## In `--batch`
65
65
 
66
- A HARD HALT in steps 1 to 6 ends the current target, not the batch. After its envelope line and `halt` event, return to `references/batch-mode.md` §3, which records the target and prints its `fail` event: control returns there even when the halt message reads as the end of the run, so a target's halt never exits the process. The batch then takes its next target, or, under `--fail-fast`, writes its summary. A halt before the first target (SKILL.md On Activation, batch mode's refusal of `--description` and `--exports`, or batch mode §1) ends the run itself, with no batch summary.
66
+ A HARD HALT in steps 1 to 6 ends the current target, not the batch. After its envelope line and `halt` event, return to `references/batch-mode.md` §3, which records the target and prints its `fail` event: control returns there even when the halt message reads as the end of the run, so a target's halt never exits the process. The batch then takes its next target, or, under `--fail-fast`, writes its summary. A halt before the first target (SKILL.md On Activation, batch mode's refusal of `--description` and `--exports`, or batch mode §1) ends the run itself, with no batch summary. A batch mode §2 to §4 halt ends the run too and writes no batch summary, though a `batch_summary` event an earlier `summarize` already printed stays the batch's result; the targets recorded before it keep the `done` and `fail` events they printed.
@@ -155,7 +155,7 @@ The helper writes JSON to `{run_dir}/extract.json` with:
155
155
 
156
156
  Read `{run_dir}/extract.json` into the extraction context. The shape of the envelope is the same for every language; §4 builds the inventory from it without per-language branching.
157
157
 
158
- **Read the statements `unlisted[]` still names** before §4. `--follow` read every module it could, so read by eye only the `unlisted[]` records left, whose `module_file` is null or could not be read (a warning says why): list the names each adds, from the README's API section or the module's documentation, and stage them through a quoted heredoc as `{run_dir}/extract-added.json`, `{"exports": [{"name": "<name>", "type": "re-export", "source_file": "<the record's file>"}]}`, which step 4 §4 passes with the other extraction files. Leave the other warnings for §5.
158
+ **Read the statements `unlisted[]` still names** before §4. `--follow` read every module it could, so read by eye only the `unlisted[]` records left, whose `module_file` is null or could not be read (a warning says why): list the names each adds, from the README's API section or the module's documentation, and stage them through a quoted heredoc as `{run_dir}/extract-added.json`, `{"exports": [{"name": "<name>", "type": "re-export", "source_file": "<the record's file>"}]}`, which step 4 §4 passes with the other extraction files. A record whose `statement` is an `export * from` another package (its `specifier` is no relative path; the extractor writes `export type *` as `export *`) adds no name, from the README or anywhere: the recipe runner and Test Skill leave such a star unresolved, so a name listed for it reads as stale. Keep each such record for §4's `external_stars`, and list names only for the others. A named re-export from another package (`export { a, b } from 'pkg'`) is already in `exports[]`. Leave the other warnings for §5.
159
159
 
160
160
  **Multi-module loop:** when `modules[]` is non-empty, fetch every sub-module's manifest in one §2 call, each under its `module_folders[]` entry. Then, per module, run §2's entries call with `--scope "<its module_folders[] entry>"`, its list and output written to `{run_dir}/entries-module-<n>.txt` and `{run_dir}/entries-module-<n>.json`, fetch the files it lists, and run the helper (§3) on them, writing each to `{run_dir}/extract-module-<n>.json`, `<n>` counting from 1 in `modules[]` order (a module is a path such as `modules/core` or a Gradle name such as `core:api`, so it never names the file). Step 4 §4 passes `{run_dir}/extract.json` (the parent manifest, which names the `package_name`) first and then each module's file in that order, and the renderer aggregates `exports[]` and `dependencies[]` across them.
161
161
 
@@ -180,6 +180,7 @@ extraction_inventory:
180
180
  repo_shape: {the §1.5 shape, when one was recorded}
181
181
  skills_root: {skills-module only: the folder that holds the skill folders}
182
182
  exports: [{name, type, brief_description}]
183
+ external_stars: [{file, line, statement, specifier}: each export * from another package §3 kept]
183
184
  usage_patterns: [{pattern from README examples, or the skills-module helper's usage_patterns}]
184
185
  dependencies: [{key deps from manifest}]
185
186
  confidence: {high/medium/low based on data quality}
@@ -15,6 +15,9 @@ skillInventoryProbeOrder:
15
15
  atomicWriteProbeOrder:
16
16
  - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
17
17
  - '{project-root}/src/shared/scripts/skf-atomic-write.py'
18
+ hashContentProbeOrder:
19
+ - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
20
+ - '{project-root}/src/shared/scripts/skf-hash-content.py'
18
21
  ---
19
22
 
20
23
  <!-- Config: communicate in {communication_language}. Generated SKILL.md text in {document_output_language}. -->
@@ -116,18 +119,22 @@ Run: `npx skill-check -h`
116
119
 
117
120
  ### 4. Validate SKILL.md via skill-check (if available)
118
121
 
119
- **If `npx skill-check` is available**, run validation and its security scan in one call against the skill package written in section 2. `--fix` can also rewrite the frontmatter `description` the user approved in step 4, which would leave SKILL.md out of step with `metadata.json`, so the call runs inside the description guard. Resolve `{descriptionGuardHelper}` ← first existing path in `{descriptionGuardProbeOrder}`, then run the three commands in order. `verify-restore` runs whatever skill-check exits; note that exit code:
122
+ **If `npx skill-check` is available**, run validation and its security scan in one call against the skill package written in section 2. `--fix` can also rewrite the frontmatter `description` the user approved in step 4, which would leave SKILL.md out of step with `metadata.json`, so the call runs inside the description guard. Resolve `{descriptionGuardHelper}` ← first existing path in `{descriptionGuardProbeOrder}` and `{hashContentHelper}` ← first existing path in `{hashContentProbeOrder}`, then run the five commands in order. `verify-restore` runs whatever skill-check exits; note that exit code:
120
123
 
121
124
  ```bash
125
+ uv run {hashContentHelper} hash "{skill_package}/SKILL.md" > "{run_dir}/skill-md-hash-before.json"
122
126
  uv run {descriptionGuardHelper} capture "{skill_package}/SKILL.md" > "{run_dir}/description-guard.json"
123
127
  npx skill-check check "{skill_package}" --fix --format json > "{run_dir}/skill-check.json" 2> "{run_dir}/skill-check.stderr"; echo "skill-check exited $?"
124
128
  uv run {descriptionGuardHelper} verify-restore "{skill_package}/SKILL.md" --captured-file "{run_dir}/description-guard.json"
129
+ uv run {hashContentHelper} hash "{skill_package}/SKILL.md" > "{run_dir}/skill-md-hash-after.json"
125
130
  ```
126
131
 
127
- **If no "skill-check exited" line came back**, the shell tool stopped the block before skill-check finished (a cold `uvx mcp-scan` install can outlast its timeout): run the `verify-restore` command on its own before reading `{run_dir}/skill-check.json`, so a description `--fix` rewrote is restored.
132
+ **If no "skill-check exited" line came back**, the shell tool stopped the block before skill-check finished (a cold `uvx mcp-scan` install can outlast its timeout): run the `verify-restore` command on its own before reading `{run_dir}/skill-check.json`, so a description `--fix` rewrote is restored, then run the second `hash` command.
128
133
 
129
134
  **Guard outputs.** `capture` stages the approved description in `{run_dir}/description-guard.json` and `verify-restore` reads it there, so it never passes through a shell string. Bind `{guard_restored}` ← `restored` and `{guard_diff_kind}` ← `diff_kind` from `verify-restore`. When `{guard_restored}` is true, the approved description is back in SKILL.md, matching `metadata.json`: record the validation note "description restored after `skill-check --fix` ({guard_diff_kind})", which §6 lists with the auto-fixed issues. `verify-restore` refuses an empty or whitespace-only captured description, and a file it cannot read (exit 1, SKILL.md untouched): re-run it with `--captured-file "{skill_package}/metadata.json"`, whose `description` step 4 rendered, never with an empty value, and if that exits 1 too, record a high-severity issue: the guard could not check the description. When it exits 2 with JSON on stdout (the JSON carries `restore_error`), the restore could not be written, so record a high-severity issue: SKILL.md's description no longer matches `metadata.json`.
130
135
 
136
+ **Rewrite note.** skill-check's JSON lists no fix, so the two `hash` commands tell whether `--fix` changed SKILL.md. Bind `{skill_md_changed}` ← true when the `content_hash` in `{run_dir}/skill-md-hash-before.json` differs from the one in `{run_dir}/skill-md-hash-after.json`, taken after the last `verify-restore` (run the second `hash` command again after a `verify-restore` re-run). When no `{hashContentProbeOrder}` path exists (leave both `hash` commands out) or either file holds no `content_hash`, SKILL.md was not hashed: bind `{skill_md_changed}` ← true as well. When `{skill_md_changed}` is true, record the validation note "skill-check --fix rewrote SKILL.md", or "SKILL.md may have been rewritten (not hashed) by skill-check --fix" when it was not hashed, which §6 lists with the auto-fixed issues. When the block above did not run at all (§3 found no skill-check, or the guard below is missing), `--fix` did not run: hash nothing and bind `{skill_md_changed}` ← false.
137
+
131
138
  **If no `{descriptionGuardProbeOrder}` path exists**, never run `--fix` unguarded. Run `npx skill-check check "{skill_package}" --format json` instead, which validates and scans without fixing, with the same capture (`> "{run_dir}/skill-check.json" 2> "{run_dir}/skill-check.stderr"`, its exit code noted), and log the validation issue "description guard unavailable (`skf-description-guard.py` missing): skill-check ran without `--fix`".
132
139
 
133
140
  **If `{run_dir}/skill-check.json` does not start with a JSON object**, skill-check stopped before it validated (exit 2 on a config, option or plugin error): log "skill-check failed: {the stderr line that names the cause, or its last line}" and "security scan skipped: skill-check failed ({that line})", keep `{quality_score}` null, bind `{security_result}` ← `skipped`, and run the frontmatter validator below, as when skill-check is not available. Otherwise stdout can go on after the JSON (a `Wrote <path>` line when the project config sets a report path), so read only the JSON object at its start.
@@ -135,7 +142,6 @@ uv run {descriptionGuardHelper} verify-restore "{skill_package}/SKILL.md" --capt
135
142
  **Parse the JSON in `{run_dir}/skill-check.json`** to extract:
136
143
  - `scores[].score` — overall score (0-100); match the entry by `relativePath`/`skillId`
137
144
  - `diagnostics[]` — remaining issues after auto-fix
138
- - `fixed[]` — issues automatically corrected
139
145
  - `security[]` (when present) — security findings, recorded as advisory warnings (security issues do not block output)
140
146
 
141
147
  Bind `{quality_score}` ← that score, and record the remaining diagnostics and security findings as validation issues. `{quality_score}` is null when skill-check did not run.
@@ -172,7 +178,7 @@ The validator emits JSON with `result` (PASS/FAIL), `validation.skill_md.body[]`
172
178
 
173
179
  **SKILL.md:** {pass/issues found}{ (quality score: {quality_score}/100), only when `{quality_score}` is not null}
174
180
  {list any issues}
175
- {list any auto-fixed issues, and the §4 description-guard note when the guard restored the description}
181
+ {list the auto-fixed issues: the §4 rewrite note when `{skill_md_changed}` is true, and the §4 description-guard note when the guard restored the description}
176
182
 
177
183
  **context-snippet.md:** {pass/issues found}
178
184
  {list any issues}
@@ -190,7 +196,7 @@ These issues are advisory for community-tier skills: fix them in the written fil
190
196
 
191
197
  **Proceeding to finalize...**"
192
198
 
193
- Set `validation_result` with pass/fail status, quality score, and issues list, and `{validation_issues}` ← `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`, each `<n>` the number of entries the validators returned for that heading: `skill_md` counts skill-check's `diagnostics[]` (without skill-check, the frontmatter validator's `issues[]`) plus `validation.skill_md.body[]`; `context_snippet` counts `validation.context_snippet.issues[]`; `metadata` counts `validation.metadata.issues[]`; `security` counts skill-check's `security[]` (or, without one, the finding codes §4 read from the tail, each occurrence one finding, as skill-check's own scan summary counts them). A check that did not run counts 0, and `fixed[]` and the guard note count nothing. Step 6 §3's summary carries `{quality_score}` and `{validation_issues}`.
199
+ Set `validation_result` with pass/fail status, quality score, and issues list, and `{validation_issues}` ← `{"skill_md": <n>, "context_snippet": <n>, "metadata": <n>, "security": <n>}`, each `<n>` the number of entries the validators returned for that heading: `skill_md` counts skill-check's `diagnostics[]` (without skill-check, the frontmatter validator's `issues[]`) plus `validation.skill_md.body[]`; `context_snippet` counts `validation.context_snippet.issues[]`; `metadata` counts `validation.metadata.issues[]`; `security` counts skill-check's `security[]` (or, without one, the finding codes §4 read from the tail, each occurrence one finding, as skill-check's own scan summary counts them). A check that did not run counts 0, and the §4 rewrite note and the guard note count nothing. Step 6 §3's summary carries `{quality_score}`, `{validation_issues}` and `{security_result}`.
194
200
 
195
201
  ### 7. Auto-Proceed to Finalize
196
202
 
@@ -80,7 +80,7 @@ output_folder_path = ""
80
80
 
81
81
  # Optional post-completion hook: a shell command. When non-empty, step 6 runs
82
82
  # it with `--result-path=<path>` (the per-run result JSON) once the refined
83
- # architecture is promoted and both result files are written, before the run
83
+ # architecture is promoted and the per-run result file is written, before the run
84
84
  # finishes. Use to index the refined doc, notify, or chain the next workflow.
85
85
  # A failure shows as a warning line in step 6's report but never fails the run.
86
86
 
@@ -95,11 +95,11 @@ The shared emitter writes the result contract (per `shared/references/output-con
95
95
  uv run {emitEnvelopeHelper} emit --workflow skf-refine-architecture --run-dir "{run_dir}" --result-dir "{outputFolderPath}" < "{run_dir}/result-context.json"
96
96
  ```
97
97
 
98
- It writes `{outputFolderPath}/refine-architecture-result-{YYYYMMDD-HHmmss}.json` and its `refine-architecture-result-latest.json` copy (a copy, not a symlink: the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line), stamping the time, the run id, the run's warnings and the auto-decisions the gates recorded, and prints the `SKF_REFINE_ARCHITECTURE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. Bind `{result_path}` ← the line's `result_path`: a result file that could not be written leaves it null and a `result_file_write_failed` warning in the line, and the run still finishes. If the emitter exits non-zero, run §1's `context` command and the emitter once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`: the run's result contract could not be written.
98
+ It writes `{outputFolderPath}/refine-architecture-result-{YYYYMMDD-HHmmss}.json` and its `refine-architecture-result-latest.json` copy (a copy, not a symlink: the record of the last run whose emitter wrote this copy, at a stable path; a caller reads its own run's `SKF_REFINE_ARCHITECTURE_RESULT_JSON` line), stamping the time, the run id, the run's warnings and the auto-decisions the gates recorded, and prints the `SKF_REFINE_ARCHITECTURE_RESULT_JSON:` line on stdout. When `{headless_mode}` is true, display the line verbatim. Bind `{result_path}` ← the line's `result_path`: a per-run record that could not be written leaves it null, and a `-latest` copy that could not be written leaves it set; either adds a `result_file_write_failed` warning to the line, and the run still finishes. If the emitter exits non-zero, run §1's `context` command and the emitter once more; if it fails again, HALT (exit code 4, `halt_reason: "write-failed"`) at phase `report:result-contract`: the run's result contract could not be written.
99
99
 
100
100
  ### 5. Post-Completion Hook (optional)
101
101
 
102
- Skip this section when `{onCompleteCommand}` (resolved at SKILL.md On Activation §4 from `workflow.on_complete`) is empty, the bundled default. When `{result_path}` is null, skip it too, with one line saying no result file was written for the hook to read. Otherwise invoke it now, after the emitter wrote both result files, so a notifier, indexer or chained pipeline sees a complete record:
102
+ Skip this section when `{onCompleteCommand}` (resolved at SKILL.md On Activation §4 from `workflow.on_complete`) is empty, the bundled default. When `{result_path}` is null, skip it too, with one line saying no result file was written for the hook to read. Otherwise invoke it now, after the emitter wrote the per-run record, so a notifier, indexer or chained pipeline sees a complete record:
103
103
 
104
104
  ```bash
105
105
  {onCompleteCommand} --result-path="{result_path}"
@@ -78,7 +78,7 @@ If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 fr
78
78
  {onCompleteCommand} --result-path="{result_path}"
79
79
  ```
80
80
 
81
- Log success/failure but never fail the workflow on a hook error: the rename is already committed. The hook runs after the result files are written so an audit-log emit, registry re-index, or notifier sees the completed rename. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely; when `{result_path}` is null, skip it with one line saying no result file was written for it to read.
81
+ Log success/failure but never fail the workflow on a hook error: the rename is already committed. The hook runs after the emitter wrote the per-run record (`result_path` not null) so an audit-log emit, registry re-index, or notifier sees the completed rename. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely; when `{result_path}` is null, skip it with one line saying no result file was written for it to read.
82
82
 
83
83
  ### 4. Release the Run Lock and the Run Folder
84
84