bmad-module-skill-forge 7.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.
Files changed (87) 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 +8 -8
  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 +957 -183
  25. package/src/skf-analyze-source/SKILL.md +1 -1
  26. package/src/skf-audit-skill/SKILL.md +1 -1
  27. package/src/skf-audit-skill/customize.toml +4 -3
  28. package/src/skf-audit-skill/references/report.md +1 -1
  29. package/src/skf-brief-skill/SKILL.md +1 -1
  30. package/src/skf-campaign/SKILL.md +1 -1
  31. package/src/skf-campaign/assets/campaign-state-schema.json +58 -0
  32. package/src/skf-campaign/references/campaign-contracts.md +1 -1
  33. package/src/skf-campaign/references/step-06-batch.md +2 -2
  34. package/src/skf-campaign/references/step-07-capstone.md +1 -1
  35. package/src/skf-campaign/references/step-08-verify.md +1 -1
  36. package/src/skf-campaign/references/step-09-refine.md +1 -1
  37. package/src/skf-campaign/references/step-11-maintenance.md +1 -1
  38. package/src/skf-campaign/scripts/campaign-render-batch.py +43 -10
  39. package/src/skf-campaign/scripts/campaign-report.py +60 -4
  40. package/src/skf-campaign/scripts/campaign-state.py +67 -19
  41. package/src/skf-campaign/templates/campaign-report-template.md +4 -0
  42. package/src/skf-create-skill/SKILL.md +1 -1
  43. package/src/skf-create-skill/assets/skill-sections.md +2 -2
  44. package/src/skf-create-skill/references/report.md +2 -2
  45. package/src/skf-create-skill/references/validate.md +49 -29
  46. package/src/skf-create-stack-skill/SKILL.md +1 -1
  47. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  48. package/src/skf-create-stack-skill/references/report.md +3 -3
  49. package/src/skf-create-stack-skill/references/validate.md +9 -4
  50. package/src/skf-drop-skill/SKILL.md +1 -1
  51. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  52. package/src/skf-export-skill/SKILL.md +1 -1
  53. package/src/skf-export-skill/references/summary.md +1 -1
  54. package/src/skf-forger/SKILL.md +1 -1
  55. package/src/skf-quick-skill/SKILL.md +1 -1
  56. package/src/skf-quick-skill/references/batch-mode.md +28 -2
  57. package/src/skf-quick-skill/references/compile.md +2 -0
  58. package/src/skf-quick-skill/references/finalize.md +3 -2
  59. package/src/skf-quick-skill/references/halt-contract.md +3 -3
  60. package/src/skf-quick-skill/references/quick-extract.md +2 -1
  61. package/src/skf-quick-skill/references/write-and-validate.md +11 -5
  62. package/src/skf-refine-architecture/SKILL.md +1 -1
  63. package/src/skf-refine-architecture/customize.toml +1 -1
  64. package/src/skf-refine-architecture/references/report.md +2 -2
  65. package/src/skf-rename-skill/SKILL.md +1 -1
  66. package/src/skf-rename-skill/references/report.md +1 -1
  67. package/src/skf-setup/SKILL.md +1 -1
  68. package/src/skf-test-skill/SKILL.md +1 -1
  69. package/src/skf-test-skill/customize.toml +10 -0
  70. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  71. package/src/skf-test-skill/references/coverage-check.md +2 -2
  72. package/src/skf-test-skill/references/init.md +1 -1
  73. package/src/skf-test-skill/references/invocation-contract.md +1 -1
  74. package/src/skf-test-skill/references/report.md +3 -2
  75. package/src/skf-test-skill/references/scoring-rules.md +2 -2
  76. package/src/skf-test-skill/references/signature-comparison-rules.md +4 -1
  77. package/src/skf-test-skill/scripts/gap-ledger.py +218 -46
  78. package/src/skf-test-skill/scripts/load-coverage-inputs.py +12 -6
  79. package/src/skf-test-skill/templates/test-report-template.md +4 -3
  80. package/src/skf-update-skill/SKILL.md +1 -1
  81. package/src/skf-update-skill/references/gap-driven.md +1 -1
  82. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  83. package/src/skf-update-skill/references/merge.md +1 -1
  84. package/src/skf-update-skill/references/report.md +18 -7
  85. package/src/skf-update-skill/references/write.md +24 -12
  86. package/src/skf-verify-stack/SKILL.md +1 -1
  87. package/src/skf-verify-stack/references/report.md +1 -1
@@ -30,7 +30,7 @@
30
30
  "name": "skill-forge",
31
31
  "source": "./",
32
32
  "description": "Evidence-based agent skills compiler with progressive capability tiers (Quick/Forge/Forge+/Deep).",
33
- "version": "7.0.0",
33
+ "version": "8.1.0",
34
34
  "author": {
35
35
  "name": "Armel"
36
36
  },
package/README.md CHANGED
@@ -130,7 +130,7 @@ For docs-only skills, the audit shape is the same: `provenance-map.json` still l
130
130
 
131
131
  If it doesn't, that's a bug. Open an issue and SKF will republish the skill with a fresh provenance map (a new commit SHA for source skills, a fresh fetch for docs-only). Falsifiability isn't a feature; it's the whole deal.
132
132
 
133
- **Reference output: [oh-my-skills](https://github.com/armelhbobdad/oh-my-skills).** It holds four Deep-tier skills compiled by SKF (cocoindex, cognee, Storybook v10, uitripled), each shipping its full audit trail in-repo. Scores range from 99.0% to 99.49%. Every claim walks to an upstream line in under 60 seconds. Serves as both the worked example for this section and ongoing proof that the pipeline does what it says.
133
+ **Reference output: [oh-my-skills](https://github.com/armelhbobdad/oh-my-skills).** It holds four Deep-tier skills compiled by SKF (cocoindex, cognee, Storybook v10, uitripled), each shipping its full audit trail in-repo. On a strict Skill Forge 4.0.0 test of the version each one ships, two pass (cocoindex at 100%, cognee at 99%) and two are blocked (uitripled, Storybook); every report is published, failing ones included. Every claim walks to an upstream line in under 60 seconds. Serves as both the worked example for this section and a public record of what the pipeline gets right and where its test still falls short.
134
134
 
135
135
  ## Help SKF Improve
136
136
 
@@ -32,7 +32,7 @@
32
32
  # Enforced two ways: (1) release.yaml bumps this line in the same commit
33
33
  # as package.json + marketplace.json on every release; (2) validate-docs-drift.js
34
34
  # cross-checks this value against package.json.version and fails on mismatch.
35
- skf_version: "7.0.0"
35
+ skf_version: "8.1.0"
36
36
 
37
37
  # Path to the oh-my-skills repo, resolved relative to this repo's root.
38
38
  # Override at runtime with the OMS environment variable if oh-my-skills
@@ -51,8 +51,9 @@ skills:
51
51
  source_commit: "ba3631f2ed363a6ea50d649c34c56885af6b36fe"
52
52
  source_ref: "v1.6.2"
53
53
  upstream_commit_date: "2026-09-29"
54
- # strict SKF 4.0.0 re-test of the version that ships (20261103T134151Z-12e5ccdf)
55
- test_score: 99.0
54
+ # null: the strict SKF 8.0.0 re-test of the version that ships
55
+ # (20261107T112455Z-eec853d0) is blocked by the hard gate, so it records no score.
56
+ test_score: null
56
57
  test_mode: "naive"
57
58
  confidence_tier: "Deep"
58
59
  source_authority: "community"
@@ -65,7 +66,7 @@ skills:
65
66
  source_commit: "5fe4f37a526e51516655832ec9814e7ca24ade9a"
66
67
  source_ref: "v1.0.0"
67
68
  upstream_commit_date: "2026-04-22"
68
- # strict SKF 4.0.0 re-test of the version that ships (20261103T131524Z-5907fba9)
69
+ # strict SKF 8.0.0 re-test of the version that ships (20261107T115927Z-5db1615d)
69
70
  test_score: 100.0
70
71
  test_mode: "naive"
71
72
  confidence_tier: "Deep"
@@ -79,9 +80,8 @@ skills:
79
80
  source_commit: "e486d3826bcd40c52db1c766966d1c8ec16df6cb"
80
81
  source_ref: "v10.3.5"
81
82
  upstream_commit_date: "2026-04-07"
82
- # null: the strict SKF 4.0.0 re-test of the version that ships
83
- # (20261103T131516Z-6e2b972a) is blocked by the hard gate, so it records no score.
84
- test_score: null
83
+ # strict SKF 8.0.0 re-test of the version that ships (20261107T112449Z-0cc4693c)
84
+ test_score: 100.0
85
85
  test_mode: "naive"
86
86
  confidence_tier: "Deep"
87
87
  source_authority: "community"
@@ -94,9 +94,8 @@ skills:
94
94
  source_commit: "a5ffb45be05335d2c547436664cfbfb8c22d04df"
95
95
  source_ref: "HEAD"
96
96
  upstream_commit_date: "2026-03-22"
97
- # null: the strict SKF 4.0.0 report (20261103T100001Z-981f84e6) is
98
- # blocked by the hard gate, so it records no score.
99
- test_score: null
97
+ # strict SKF 8.0.0 re-test of the version that ships (20261107T112452Z-65994952)
98
+ test_score: 100.0
100
99
  test_mode: "naive"
101
100
  confidence_tier: "Deep"
102
101
  source_authority: "community"
package/docs/campaign.md CHANGED
@@ -184,9 +184,9 @@ A finished campaign leaves:
184
184
  - **Individual skills**: one for each library that completed. The Export stage exports the ones that clear the quality gate; the others stay on disk and in the report.
185
185
  - **One capstone stack skill**: composed from the completed skills that clear the quality gate. The Export stage exports only the individual skills, so run `skf-export-skill` on the stack skill yourself when you want it packaged.
186
186
  - **Verification report and refined architecture**: from Verification and Refinement, when an architecture document was found.
187
- - **Campaign report** (`campaign-report.md`): the quality gate; a table of every skill with its tier, status, score, pin and workarounds; an Export Gate section with each completed skill's verdict, whether Export exported it or why its export failed, and the skills the gate kept from export; the lowest, highest and average scores; time per skill; and a section on failed or skipped skills.
187
+ - **Campaign report** (`campaign-report.md`): the quality gate; a table of every skill with its tier, status, score, pin and workarounds; a Stages section with whether the capstone, verification and refinement stages completed, were skipped or failed, and why a stage failed; an Export Gate section with each completed skill's verdict, whether Export exported it or why its export failed, and the skills the gate kept from export; the lowest, highest and average scores; time per skill; and a section on failed or skipped skills.
188
188
  - **Decision log** (`_campaign-decision-log.md`): every decision made during the campaign.
189
- - **Headless result line** (`SKF_CAMPAIGN_RESULT_JSON`): printed in headless mode for scripts to read, as the run's last line. Besides the counts and the score of each skill, it carries `export_verdicts` (each completed skill's verdict at Export), `skills_excluded` (the completed skills the gate kept from export), `skills_exported` (the skills Export exported) and `export_failures` (each skill whose export failed, with its `halt_reason` and `exit_code`). The last two are null when the Export stage did not run. An `export_failures` entry's `exit_code` is null when the export printed no line (its `halt_reason` is then `no-result`) or one Campaign could not read (`result-unreadable`). A failed export does not fail the campaign, which still ends with `status` `success`, so a CI job that needs every skill exported checks `export_failures`.
189
+ - **Headless result line** (`SKF_CAMPAIGN_RESULT_JSON`): printed in headless mode for scripts to read, as the run's last line. Besides the counts and the score of each skill, it carries `export_verdicts` (each completed skill's verdict at Export), `skills_excluded` (the completed skills the gate kept from export), `skills_exported` (the skills Export exported) and `export_failures` (each skill whose export failed, with its `halt_reason` and `exit_code`). The last two are null when the Export stage did not run. An `export_failures` entry's `exit_code` is null when the export printed no line (its `halt_reason` is then `no-result`) or one Campaign could not read (`result-unreadable`). A failed export does not fail the campaign, which still ends with `status` `success`, so a CI job that needs every skill exported checks `export_failures`. It also carries `stage_outcomes`: for the capstone, verification and refinement stages, `completed`, `skipped` (a stage skipped on purpose, such as verification without an architecture document) or `failed`, with the failure's `halt_reason` (`no-result` when the sub-skill printed no line, `result-unreadable` when Campaign could not read it, otherwise the sub-skill's own) and `exit_code`. It is null when the capstone stage did not run, and a failed stage does not fail the campaign either, so a pipeline can tell a failed verification from a skipped one.
190
190
 
191
191
  The Export stage (stage 9) asks before it writes anything. It lists each completed skill with its verdict, names the skills it leaves out because they fall below the soft fallback or have no score, and waits for `[E]xport all` or `[C]ancel`. `[E]xport all` exports only the `pass` and `fallback` skills, each under the skill folder its build wrote: a Tier B skill under the name Quick Skill gave it, the library's name, even when you named the target differently. In headless mode it exports them without asking.
192
192
 
@@ -232,8 +232,8 @@ This is a trimmed excerpt from the real [`oms-cognee/1.6.2/metadata.json`](https
232
232
  "language": "python",
233
233
  "ast_node_count": 42,
234
234
  "confidence_distribution": {
235
- "t1": 37,
236
- "t1_low": 49,
235
+ "t1": 36,
236
+ "t1_low": 50,
237
237
  "t2": 0,
238
238
  "t3": 0
239
239
  },
@@ -254,7 +254,7 @@ Fields omitted from this excerpt for brevity: `source_root`, `description`, `exp
254
254
 
255
255
  Two fields are easy to mix up. `confidence_tier` holds the forge tier a single skill like this one was compiled at (Quick, Forge, Forge+ or Deep). A stack skill records there the dominant confidence tier of its libraries (T1, T1-low, T2 or T3) and keeps its forge tier in `forge_tier`; Export Skill still accepts a forge tier from a stack made by an earlier SKF version, with a warning that running Stack Skill again records the dominant tier. `confidence_distribution` counts each claim by the tier of its signature (`signature_source`: T1, T1-low, T2 or T3), which can be lower than the entry's `confidence` label. In a stack it counts each library once, by the library's tier, so its bins sum to `library_count`.
256
256
 
257
- Its counts follow the tool that read each export's signature: 37 exports whose signature an ast-grep rule matched count as `t1`, and 49 count as `t1_low`. Of those 49, 44 were read by eye (`source-read`), such as aliases, module namespaces and the names gap-driven repairs added. The other 5 were located by an ast-grep rule, but their signatures were read by eye, such as the methods of the `config` class. `ast_node_count` (42) counts every export an ast-grep rule matched. The skill's 1.0.0 metadata, written by an SKF version that labeled exports by the forge tier, counted all 34 of its exports as `t1`. `exports_documented` (86) is below `exports_public_api` (102): the brief leaves some top-level names out of scope, and the skill's test report lists them and logs the remaining difference as a Medium `metadata-drift` gap.
257
+ Its counts follow the tool that read each export's signature: 36 exports whose signature an ast-grep rule matched count as `t1`, and 50 count as `t1_low`. Of those 50, 45 were read by eye (`source-read`), such as aliases, module namespaces, the names gap-driven repairs added and `run_migrations`, whose file the brief's `scope.exclude` leaves out of the extraction. The other 5 were located by an ast-grep rule, but their signatures were read by eye, such as the methods of the `config` class. `ast_node_count` (42) counts every export an ast-grep rule matched when the skill was compiled, `run_migrations` included. The skill's 1.0.0 metadata, written by an SKF version that labeled exports by the forge tier, counted all 34 of its exports as `t1`. `exports_documented` (86) is below `exports_public_api` (102): the brief leaves some top-level names out of scope, and the skill's test report lists them and logs the remaining difference as a Medium `metadata-drift` gap.
258
258
 
259
259
  `scripts` and `assets` arrays are optional: SKF leaves them out entirely, rather than writing empty arrays, when the source has no scripts or assets.
260
260
 
@@ -44,7 +44,7 @@ Provenance maps live in `forge-data/{skill}/{version}/provenance-map.json` in th
44
44
 
45
45
  This is the published entry for cognee's `search` in oh-my-skills' [`forge-data/oms-cognee/1.6.2/provenance-map.json`](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cognee/1.6.2/provenance-map.json), shortened to its first two parameters (`search` takes 27). It points at line 41, where `async def search(` is defined at cognee v1.6.2's commit (`ba3631f2`), and the skill's `SKILL.md` cites `[AST:cognee/api/v1/search/search.py:L41]`. Its labels follow the tool that read it: an ast-grep match gives `T1`, `ast-grep`, the node kind its rule matched and an `[AST:]` citation. An export read by eye instead, because ast-grep could not parse its file, an ast-grep call that failed or timed out left its file unread, or no rule matched it, gets `T1-low`, `source-read`, no node kind and an `[SRC:]` citation, at every forge tier.
46
46
 
47
- The 1.6.2 map passes the [line, label and citation checks](#workflow-time-enforcement) that SKF 3.0.0 added. SKF 4.0.0's Test Skill first blocked the skill and, after one Update Skill repair, passed it at 99.00%. Strict re-tests then blocked it again. After fixes by hand, one strict test was still blocked on three borderline calls, and a later one on the same documentation, which judged those calls as matches, passed it at 99% (see [the scores](#the-scores-including-the-ones-we-lose)). Test Skill's signature rules now settle such calls instead of leaving them to the judge. The older map next to it, [`forge-data/oms-cognee/1.0.0/provenance-map.json`](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cognee/1.0.0/provenance-map.json), was compiled before labels followed the tool and still shows what those checks catch. It records `search` as `source-read` but labels it `T1`, and it points at line 26, the blank line above the definition, while that version's `SKILL.md` cites `[AST:cognee/api/v1/search/search.py:L27]`. A source read is `T1-low` with an `[SRC:]` citation, and its line is 27, the `def` line at v1.0.0.
47
+ The 1.6.2 map passes the [line, label and citation checks](#workflow-time-enforcement) that SKF 3.0.0 added. Under SKF 4.0.0 a strict test passed the skill at 99%. SKF 7.0.0's Test Skill found real errors in it, all since fixed by hand against the pinned source, and SKF 8.0.0 still blocks it on two false positives: it compares the skill's forms of the lightweight `cognee.pipelines.run_pipeline` with the dataset orchestrator of the same name ([#918](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/918)). So the skill has no score (see [the scores](#the-scores-including-the-ones-we-lose)). The older map next to it, [`forge-data/oms-cognee/1.0.0/provenance-map.json`](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cognee/1.0.0/provenance-map.json), was compiled before labels followed the tool and still shows what those checks catch. It records `search` as `source-read` but labels it `T1`, and it points at line 26, the blank line above the definition, while that version's `SKILL.md` cites `[AST:cognee/api/v1/search/search.py:L27]`. A source read is `T1-low` with an `[SRC:]` citation, and its line is 27, the `def` line at v1.0.0.
48
48
 
49
49
  ### 3. Visit the upstream repo at the pinned commit
50
50
 
@@ -85,27 +85,24 @@ Everything a reader needs to reconstruct the compilation is in the two sibling d
85
85
 
86
86
  The [scoring formula](#how-the-score-is-computed) is deterministic and the pass threshold is **80%**. Every test report also logs the specific edges where a skill falls short, so the numbers aren't marketing.
87
87
 
88
- Take oh-my-skills' four reference skills as an example. Each first failed the [hard gate](#hard-gate) on SKF 4.0.0's stricter checks and was repaired by Update Skill with `--from-test-report`. Three then passed at 99.00% to 100%, and oms-storybook-react-vite at 95.17% (after an earlier 99.93% that was reverted).
88
+ Take oh-my-skills' four reference skills as an example. Under SKF 4.0.0, strict tests that applied the signature rule as then written passed oms-cocoindex at 100% and oms-cognee at 99%, and blocked the other two. Earlier passes are kept in each skill's history; the oms-cocoindex, oms-cognee and oms-storybook-react-vite ones read the rule leniently.
89
89
 
90
- The oms-cocoindex, oms-cognee and oms-storybook-react-vite passes read the signature rules leniently. A short `SKILL.md` signature that left out parameters its reference lists counted as a match, and so did a prop list missing optional props or `dict` against `dict[str, Any]`. Strict re-tests applied the rule as it was then written, wrote down how they read the cases it did not settle ([#843](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/843), [#845](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/845)), and said which calls they counted. They blocked all four at first. Test Skill's signature rules now settle how a component's props compare, and that a type, a return type or type arguments a signature leaves out (`dict` against `dict[str, Any]`) are not compared. oms-uitripled's strict re-test was blocked only because it compared `cn` with the declaration the plan named, not the one the skill cites; its other 265 signatures matched.
90
+ SKF 5.0.0 to 7.0.0 put every signature rule in one file that every judge reads, compare each form a skill documents, and pin the names and signatures it writes in a fixed form. On the same skills, the first 7.0.0 tests passed oms-cocoindex and oms-uitripled at 100% (uitripled's `cn` false positive was gone) and blocked oms-cognee, on 8 Critical and 1 High gaps, and oms-storybook-react-vite, on 1 Critical gap with Export Coverage at 63.78% of the names its brief's `scope.tier_a_include` lists. Two rounds of fixes per skill (three for oms-storybook-react-vite), each followed by a strict re-test, then found real errors in all four skills, the two that passed included:
91
+ - `@coco.fn(batching=True)` documented as a working decorator, although at the pinned commit it raises `ValueError` for any function.
92
+ - Defaults written on props that a component's props type requires.
93
+ - Prose calls that leave out a required argument.
94
+ - 134 tier A names the storybook skill did not document.
91
95
 
92
- Most of the real errors were fixed against the pinned source. Update Skill repaired 22 storybook signatures. The rest were fixed by hand, each in a labeled commit:
93
- - Two storybook fixes were needed because Update Skill halted on a gap that cited Angular's declaration; repairing from that citation would have copied Angular's signature into a React skill ([#824](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/824), [#830](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/830)).
94
- - For oms-cocoindex's and oms-cognee's split-body gaps, Update Skill would have copied `SKILL.md`'s errors into reference files that were correct ([#779](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/779)), so their strict-test gaps were all fixed by hand.
95
- - Other storybook fixes corrected errors no test gap covered, found while reading the source or in a review.
96
+ Every fix was made by hand against the pinned source. Each round's fixes were reviewed before its test, except the last one-row storybook correction.
96
97
 
97
- Two storybook errors that block the gate, and the errors the reports record without blocking, are still open ([oh-my-skills#17](https://github.com/armelhbobdad/oh-my-skills/issues/17)).
98
-
99
- Today, on a strict test of the version each one ships, oms-cocoindex passes at **100%** and oms-cognee at **99%**; both passes turn on borderline calls their reports write down, which Test Skill's signature rules now settle. oms-uitripled is blocked by one gap oh-my-skills judges to be a false positive. oms-storybook-react-vite is blocked by two false positives and two older real errors that are not fixed yet.
100
-
101
- Every report, the failing ones included, is kept: in the skill's `forge-data/` folder, or, for that reverted repair, in oh-my-skills' git history. Even a passing report names the drift it found:
98
+ Today, on a strict SKF 8.0.0 test of the version each one ships, three skills pass, all at 100%, and one is blocked. On unchanged text, 8.0.0 raised oms-storybook-react-vite from 98.23% to 100%, because a star re-export from another package now adds no name ([#926](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/926)), and a written rule ([#932](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/932)) now settles the three oms-uitripled forms its 7.0.0 pass rested on without recording them; uitripled stays at 100%. The first 8.0.0 run of oms-cocoindex counted `ExceptionContext`, named only inside another export's type, as undocumented (99.37%), a reader's call the inventory contract does not settle ([#967](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/967)); oh-my-skills then documented it. Every pass still rests on calls the reader or the judge makes where the rules leave the case open, and each report now writes them down, with how it counted them: storybook's turns on how the split-body check reads two different exports that share a name, uitripled's on an em-dash placeholder in a Default cell read as empty, and cocoindex's on a call to a decorator instance compared with its `__call__`. The judges also still read some cases differently from run to run: three oms-uitripled lines were matches in one 7.0.0 run and Critical gaps in the next, with no change to the lines. Every report, the failing ones included, is kept: in the skill's `forge-data/` folder, or, for the reverted oms-storybook-react-vite repair, in oh-my-skills' git history. Even a passing report names the drift it found:
102
99
 
103
100
  | Skill | Score | What the report discloses |
104
101
  |---|---|---|
105
- | [oms-cocoindex](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cocoindex/1.0.0/test-report-oms-cocoindex-20261103T131524Z-5907fba9.md) | **100%** | 79/79 exports on the brief's `scope.include` union (95 files), 27/27 signatures, 36/36 types. Nine borderline calls are recorded and not counted: bare `*args` and `**kwargs` against `P.args` and `P.kwargs`, unannotated parameters and unstated returns. Test Skill's signature rules now read each of those as a match; one other call, `mount_each` documented with a `process_fn` parameter the source names `fn`, is a renamed Python parameter, so under those rules it is a Critical gap until the reference is corrected. The first strict re-test of the text behind its earlier 100% found seven `SKILL.md` signatures that misnamed parameters or left out `**kwargs` or an optional parameter. Left open: method signatures in the reference files that the report records as warnings without a gap ([oh-my-skills#17](https://github.com/armelhbobdad/oh-my-skills/issues/17)). |
106
- | [oms-cognee](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cognee/1.6.2/test-report-oms-cognee-20261103T134151Z-12e5ccdf.md) | **99%** | 92/92 exports of the `cognee/__init__.py` barrel at pinned commit `ba3631f2` (v1.6.2), 72/72 signatures, 35/35 types. External Validation is 90%: skill-check finds the `SKILL.md` body over its 5,000-token budget. The pass turns on borderline calls the report writes down: an earlier strict run counted three of them as Critical gaps ([#845](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/845), [#835](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/835)) and was blocked. Test Skill's signature rules now settle them: an overload set is read through its implementation, a bare `*args`, an unannotated parameter, an unstated return and omitted type arguments match, and a documented return type that matches what the function's body returns, against a declared annotation that says otherwise, is an Info observation that blocks nothing, recorded only when Test Skill's scoring script checks the return line the judge names. |
107
- | [oms-storybook-react-vite](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-storybook-react-vite/10.3.5/test-report-oms-storybook-react-vite-20261103T131516Z-6e2b972a.md) | **blocked** | 4 Critical signature gaps, so no score. Two compare React wrappers with core's functions, false positives ([#781](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/781)). Two are real errors written in April and not fixed yet: the react-vite preset's `viteFinal` typed `PresetProperty<'viteFinal'>`, which resolves to `never` because `StorybookConfigRaw` has no `viteFinal` key, and `useParameter`'s parameters ([oh-my-skills#17](https://github.com/armelhbobdad/oh-my-skills/issues/17)). Signature Accuracy is 82/86 and Type Coverage 98/120. Export Coverage reads 100% only because the stored count then credited every documented name ([#792](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/792)). In the last run that counted them, 204 of the 280 names on the brief's `scope.tier_a_include` were documented, and that list misses names another package also declares ([#830](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/830)). Test Skill now counts only the names on that list, so the next run reads lower and lists the ones the skill does not document. |
108
- | [oms-uitripled](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-uitripled/0.1.0/test-report-oms-uitripled-20261103T100001Z-981f84e6.md) | **blocked** | One Critical false positive: two exports share the name `cn`, and the test compared the one the skill does not document ([#781](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/781)). Under the strict rule, 265 of 266 signatures and every destructured component props list match. One props type, `NativeHoverCardProps`, writes `size` as required where the source has `size?` (an Info gap, still open). Export Coverage 100% against the stored count of 327, Type Coverage 42/42. |
102
+ | [oms-cocoindex](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cocoindex/1.0.0/test-report-oms-cocoindex-20261107T115927Z-5db1615d.md) | **100%** | 72/72 exports of the root barrel (since 7.0.0 the brief's `scope.exclude` applies before the source is read as one Python package, not a Cargo workspace), 48/48 signatures. Type Coverage is 1/1: the root exports Test Skill restores when the brief's doc globs match no file carry no kind, so the category counts a single type ([#913](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/913)). The report records how it read `@coco.fn(...)`, a call to a decorator instance, compared with the instance's `__call__`. The first 8.0.0 run counted `ExceptionContext`, named only inside another export's type, as undocumented (99.37%; [#967](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/967)); oh-my-skills documented it. Left open: 11 Medium stale-documentation gaps on connector exports that the reconcile counts outside the surface ([#912](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/912)), a brief glob that matches no file and a barrel count. |
103
+ | [oms-cognee](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-cognee/1.6.2/test-report-oms-cognee-20261107T112455Z-eec853d0.md) | **blocked** | 2 Critical false positives: the test compares the skill's two forms of the lightweight `cognee.pipelines.run_pipeline` with the orchestrator of the same name, the only `run_pipeline` its surface holds ([#918](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/918); the fix, [#956](https://github.com/armelhbobdad/bmad-module-skill-forge/pull/956), is not in 8.0.0). Every other check passes: 92/92 exports of the `cognee/__init__.py` barrel at pinned commit `ba3631f2` (v1.6.2), 74 of 75 signature names and 35/35 types. External Validation is 90%: skill-check finds the `SKILL.md` body over its 5,000-token budget. |
104
+ | [oms-storybook-react-vite](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-storybook-react-vite/10.3.5/test-report-oms-storybook-react-vite-20261107T112449Z-0cc4693c.md) | **100%** | Counted on the brief's `scope.tier_a_include` set: 370/370 exports, 180/180 signatures (66 of them Testing Library bindings whose parameters are typed in another package, counted as matching without comparison under 8.0.0's rule, [#925](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/925)), 128/128 types. Under 7.0.0 the run read `export type * from '@vitest/spy'` as 19 names, 13 of them undocumented (98.23%); 8.0.0 counts none ([#926](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/926)). The pass turns on how the split-body check reads two different exports that share a name, core's and React's `composeStories`: read as one export, they would be a High gap ([#927](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/927)). |
105
+ | [oms-uitripled](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-uitripled/0.1.0/test-report-oms-uitripled-20261107T112452Z-65994952.md) | **100%** | 327/327 exports against the stored count for the brief's `scope.include` union, 269/269 signatures and 42/42 types. The report records the call the pass rests on, an em-dash placeholder in two props tables' Default column read as an empty cell as Create Skill's own format writes it (read as a value, two Critical gaps), and one the 100% rests on: seven primitives named only on import lines, counted as documented (without them, Export Coverage would be 97.86%). |
109
106
 
110
107
  Perfection is suspicious. Visible fallibility is trustworthy. SKF writes down the edges it can't score cleanly, so you can read them and decide for yourself whether the remaining coverage is enough for your use case.
111
108
 
@@ -119,7 +116,9 @@ After the tiered brief, two re-tests were blocked and repaired by Update Skill b
119
116
 
120
117
  Removing the stored count made the next run name every undocumented export. It measured 206 of the 280 authored names documented, and it found real errors, such as `definePreview` listed under `storybook/preview-api`. Update Skill repaired 22 signatures from a later report. Two fixes were made by hand because its repair halted: the test had cited Angular's `definePreview`, and Update Skill would have copied that signature into a React skill ([#824](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/824), [#830](https://github.com/armelhbobdad/bmad-module-skill-forge/issues/830)). The other hand fixes, also checked against the pinned source, corrected errors no test gap covered and a later re-test's `DocsContainer` and Canvas gaps.
121
118
 
122
- A strict test of the version that ships ([report](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-storybook-react-vite/10.3.5/test-report-oms-storybook-react-vite-20261103T131516Z-6e2b972a.md)) covers all of those fixes. It is blocked on 4 Critical gaps: two compare the React portable-stories wrappers with core's functions, and two are older real errors no earlier run had flagged, now tracked for a fix. So the skill has no score, and oh-my-skills says so instead of quoting the earlier number. Nothing was hidden.
119
+ A strict SKF 4.0.0 test of the version that shipped then ([report](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-storybook-react-vite/10.3.5/test-report-oms-storybook-react-vite-20261103T131516Z-6e2b972a.md)) covered all of those fixes. It was blocked on 4 Critical gaps: two compared the React portable-stories wrappers with core's functions, and two were older real errors no earlier run had flagged. So oh-my-skills published no score instead of quoting the earlier number.
120
+
121
+ SKF 7.0.0 measures the skill against the names its brief lists. Its first test found 63.78% of them documented. The next rounds documented the 134 missing names and fixed the errors the tests and reviews found, by hand against the pinned source. A strict SKF 7.0.0 test of the version that ships passed at 98.23%, and the same text passes a strict SKF 8.0.0 test at 100% ([report](https://github.com/armelhbobdad/oh-my-skills/blob/main/forge-data/oms-storybook-react-vite/10.3.5/test-report-oms-storybook-react-vite-20261107T112449Z-0cc4693c.md)). Nothing was hidden: every report on the way is kept.
123
122
 
124
123
  That's the pattern SKF asks you to trust: when scoring can't reach 100%, the report says so, cites the line, and leaves a fingerprint for the next audit.
125
124
 
@@ -232,7 +231,7 @@ Each step rates every gap it records against the Gap Severity table in Test Skil
232
231
 
233
232
  Only some runs can find a documented extra: the source must be Python or TypeScript/JavaScript, and the run needs the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden`, a provenance map and a coverage surface that a complete extraction built. On any other run, every documented export outside the coverage surface is stale documentation.
234
233
 
235
- A declaration the run finds only in an `example`, `demo`, `demos`, `sample`, `samples`, `scripts`, `playground`, `bench`, `benchmark` or `migrations` folder right below a package's root, or in an `e2e` or `__fixtures__` folder anywhere, does not count, so a name declared only in `scripts/foo.py` stays stale documentation unless the provenance map cites a file in that folder. Nor does a module named after the name count (such as `client.py` for a documented `client()`) when the skill documents the name as a function, hook, class, interface, enum or type, unless the provenance map records the name as a module or package, or the module is a TypeScript or JavaScript file that is itself the export (`export default`, `module.exports =`).
234
+ A declaration the run finds only in an `example`, `examples`, `demo`, `demos`, `sample`, `samples`, `scripts`, `playground`, `bench`, `benchmark`, `benchmarks`, `migrations` or `docs` folder right below a package's root, or in an `e2e` or `__fixtures__` folder anywhere, does not count, so a name declared only in `scripts/foo.py` stays stale documentation unless the provenance map cites a file in that folder. Nor does a module named after the name count (such as `client.py` for a documented `client()`) when the skill documents the name as a function, hook, class, interface, enum or type, unless the provenance map records the name as a module or package, or the module is a TypeScript or JavaScript file that is itself the export (`export default`, `module.exports =`).
236
235
 
237
236
  Discovery testing runs after the hard gate, so a discovery gap (Info, Medium or High) is counted in the Gap Report and blocks nothing.
238
237
 
package/docs/workflows.md CHANGED
@@ -99,7 +99,7 @@ Each workflow is also a skill you can run directly, without Ferris: `/skf-setup`
99
99
 
100
100
  **Preview:** `--detect-only` lists the changes and stops. `--dry-run` also re-extracts and shows what would change. Neither writes anything.
101
101
 
102
- **Headless:** A headless run needs the skill's name or folder path as its argument: without one it stops `blocked` before it reads anything (`error.phase` `init:skill-name`, with an `error.reason` that starts `input-missing`). A skill with no `provenance-map.json`, such as one made by Quick Skill, needs a full rebuild. A headless run stops with `blocked` there unless you pass `--allow-degraded`. `--from-test-report` needs the map, interactive or headless: without one it stops `blocked` before any gate, so run a normal update first, which offers the rebuild and writes the map, then re-run Test Skill. Files outside the skill's scope that a headless run finds, such as a new AI documentation file, are left for a person: the run records each one as `deferred-headless` in the skill brief, and the next interactive run asks about it. A normal or gap-driven run that finds no change still writes its result files and runs `on_complete`, with the status `no-changes`. In the `SKF_UPDATE_RESULT_JSON` line, `skf_update.status` values `success`, `no-changes`, `detect-only` and `dry-run` all mean the run went fine.
102
+ **Headless:** A headless run needs the skill's name or folder path as its argument: without one it stops `blocked` before it reads anything (`error.phase` `init:skill-name`, with an `error.reason` that starts `input-missing`). A skill with no `provenance-map.json`, such as one made by Quick Skill, needs a full rebuild. A headless run stops with `blocked` there unless you pass `--allow-degraded`. `--from-test-report` needs the map, interactive or headless: without one it stops `blocked` before any gate, so run a normal update first, which offers the rebuild and writes the map, then re-run Test Skill. Files outside the skill's scope that a headless run finds, such as a new AI documentation file, are left for a person: the run records each one as `deferred-headless` in the skill brief, and the next interactive run asks about it. A normal or gap-driven run that finds no change still writes its result files and runs `on_complete`, with the status `no-changes`. In the `SKF_UPDATE_RESULT_JSON` line, `skf_update.status` values `success`, `no-changes`, `detect-only` and `dry-run` all mean the run went fine. A headless run whose emitter refused its result line for good ends on a `halted-for-write-failure` line instead, with `error.phase` `report:result-contract` and an `error.reason` that starts `result-contract-not-written`, and nothing it wrote is undone.
103
103
 
104
104
  **Agent:** Ferris (Surgeon mode)
105
105
 
@@ -547,9 +547,9 @@ Setup is not the only workflow whose result line is the run's final message: a h
547
547
 
548
548
  3. **Error-variant result contract on every HARD HALT.** A `SKF_QUICK_SKILL_RESULT_JSON: {…}` envelope is emitted on `stderr` (always) and copied to `{skill_package}/quick-skill-result-latest.json` once the skill package holds `metadata.json` (any HALT from the write step onward, except the exit `9` ownership halt, which writes nothing on disk). The full population rules live in [`src/skf-quick-skill/references/halt-contract.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-quick-skill/references/halt-contract.md) § "Result Contract on HARD HALT", and the envelope's schema is [`src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json).
549
549
 
550
- 4. **Success result contract.** A finished run prints one `SKF_QUICK_SKILL_RESULT_JSON: {…}` line on `stdout`. Its `summary` carries `quality_score` (the skill-check score of the written package, not a Test Skill score), `validation_issues`, `repo_shape`, `language_resolution` and `active_pointer`, beside the top-level `headless_decisions` and `warnings`, such as `also_found_in` when a headless run kept the first registry's pick of a package name another registry also holds. The run also writes `quick-skill-result-latest.json` in the skill package.
550
+ 4. **Success result contract.** A finished run prints one `SKF_QUICK_SKILL_RESULT_JSON: {…}` line on `stdout`. Its `summary` carries `quality_score` (the skill-check score of the written package, not a Test Skill score), `validation_issues`, `security_result` (the security scan's outcome: `pass` when the scan exited 0 and skill-check reported no finding, which does not show that the scan found nothing, `warn` when it reported findings, `skipped` when it did not run, such as without `SNYK_TOKEN`), `repo_shape`, `language_resolution` and `active_pointer`, beside the top-level `headless_decisions` and `warnings`, such as `also_found_in` when a headless run kept the first registry's pick of a package name another registry also holds. The run also writes `quick-skill-result-latest.json` in the skill package.
551
551
 
552
- **Batch mode (`--batch <file>`).** Drives N targets in sequence: each target runs steps 1 to 6, and the health check runs once, after the batch summary. Input format: one target per line, `#` comments and blank lines ignored, optional per-line modifiers `language=<lang>` and `scope=<path>`. Each target's output lands in its own `{skill_package}/`; an aggregated summary writes to `{skills_output_folder}/_batch/quick-skill-batch-{ts}.json` (with `quick-skill-batch-latest.json` copy). Per-target boundary events (`{"batch":N,"target":"…","status":"start|done|fail",…}`) and a final `{"batch_summary":true,…}` event extend the per-step event stream above. The batch's result is the file that event's `summary_path` names: the `-latest` copy holds the last batch summary written, which may be another batch's. The batch exits with the highest exit code among its failed targets (`0` when none failed); `--fail-fast` stops it at the first failed target and exits with that target's code. A batch refused before its first target writes no summary and exits `2`, or `4` when its run folder cannot be written. Full input grammar, summary schema, and exit-code rules in [`src/skf-quick-skill/references/batch-mode.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-quick-skill/references/batch-mode.md).
552
+ **Batch mode (`--batch <file>`).** Drives N targets in sequence: each target runs steps 1 to 6, and the health check runs once, after the batch summary. Input format: one target per line, `#` comments and blank lines ignored, optional per-line modifiers `language=<lang>` and `scope=<path>`. Each target's output lands in its own `{skill_package}/`; an aggregated summary writes to `{skills_output_folder}/_batch/quick-skill-batch-{ts}.json` (with `quick-skill-batch-latest.json` copy). Per-target boundary events (`{"batch":N,"target":"…","status":"start|done|fail",…}`) and a final `{"batch_summary":true,…}` event extend the per-step event stream above. The batch's result is the file that event's `summary_path` names: the `-latest` copy holds the last batch summary written, which may be another batch's. The batch exits with the highest exit code among its failed targets (`0` when none failed); `--fail-fast` stops it at the first failed target and exits with that target's code. A batch refused before its first target writes no summary and exits `2`, or `4` when its run folder cannot be written. A batch whose helper cannot hand out or record a target, or cannot summarize the batch, stops with exit `4` and a `batch-mode` halt line and skips the health check; a `batch_summary` event it already printed stays its result. Full input grammar, summary schema, and exit-code rules in [`src/skf-quick-skill/references/batch-mode.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-quick-skill/references/batch-mode.md).
553
553
 
554
554
  **Exception: `/skf-brief-skill` headless emits a final result envelope and supports presets.** Headless `/skf-brief-skill` is a first-class building block for scripted brief generation (for example, seeding briefs for N SaaS SDKs that share scope and authority defaults). Three operational contracts go beyond per-gate auto-proceed:
555
555
 
@@ -572,7 +572,7 @@ Setup is not the only workflow whose result line is the run's final message: a h
572
572
 
573
573
  **Other workflows emit result envelopes too.** In headless mode, AN, US, SS, AS, TS, VS, EX and campaign each print a single-line result envelope when they finish or halt: `SKF_ANALYZE_RESULT_JSON`, `SKF_UPDATE_RESULT_JSON`, `SKF_STACK_RESULT_JSON`, `SKF_AUDIT_RESULT_JSON`, `SKF_TEST_RESULT_JSON`, `SKF_VERIFY_STACK_RESULT_JSON`, `SKF_EXPORT_RESULT_JSON` and `SKF_CAMPAIGN_RESULT_JSON`. CS prints one `SKF_CREATE_SKILL_RESULT_JSON` line per finished brief on stdout (each `--batch` brief included) and one on stderr at every HARD HALT, in every mode, and once the skill's version folder under `forge_data_folder` exists (every finished brief, and a halt after step 7 creates it) writes `create-skill-result-<timestamp>.json` and its `-latest.json` copy there. SS checks its line against [`src/shared/scripts/schemas/skf-stack-result-envelope.v1.json`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json). Drop Skill, Rename Skill and Refine Architecture, described below, each emit a single-line `SKF_*_RESULT_JSON: {…}` envelope on every terminal exit (`status: "success"` on the happy path, `status: "error"` with a typed `halt_reason` on any HARD HALT) and exit with a stable code, so automators branch on the failure class without grepping message text. All three honour the universal `cancel`/`exit`/`:q` affordance at any prompt (exit `6`, `halt_reason: "user-cancelled"`).
574
574
 
575
- - **`/skf-drop-skill` (DS)**: exit `2` `input-missing`/`input-invalid` (including `--mode deprecate` on a skill with no manifest entry), `3` resolution-failure (`manifest-corrupt`, `nothing-to-drop`), `4` write-failure (`manifest-write-failed`, `context-rebuild-failed`, `delete-failed` for a failed full purge, `write-failed`), `5` state-conflict (`active-version-guard-refused`; `not-skf-output` for a folder SKF did not generate, a purge it cannot confirm, or a skill whose folder in `forge_data_folder` also holds other files), `6` `user-cancelled` or `headless-purge-forbidden`. Besides `status`, `exit_code` and `halt_reason`, the envelope carries `would_delete` (in a dry run of a purge, the folders the purge would delete), `result_path` (the per-run result file, `null` for a dry run or a halt) and `error` (the `phase` and `reason` of a halt). A `success` drop can still carry `warnings` that name what needs a manual fix (`context_rebuild_failed`, `active_link_dangling`, `delete_failed`, `verification_failed`), and any run can carry `customization_resolver_unavailable` when the customization resolver could not run. Exit codes in [`src/skf-drop-skill/references/invocation-contract.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-drop-skill/references/invocation-contract.md) § "Exit Codes"; the envelope and full `halt_reason` list in the same file's § "Result Contract (Headless)".
575
+ - **`/skf-drop-skill` (DS)**: exit `2` `input-missing`/`input-invalid` (including `--mode deprecate` on a skill with no manifest entry), `3` resolution-failure (`manifest-corrupt`, `nothing-to-drop`), `4` write-failure (`manifest-write-failed`, `context-rebuild-failed`, `delete-failed` for a failed full purge, `write-failed`), `5` state-conflict (`active-version-guard-refused`; `not-skf-output` for a folder SKF did not generate, a purge it cannot confirm, or a skill whose folder in `forge_data_folder` also holds other files), `6` `user-cancelled` or `headless-purge-forbidden`. Besides `status`, `exit_code` and `halt_reason`, the envelope carries `would_delete` (in a dry run of a purge, the folders the purge would delete), `result_path` (the per-run result file, `null` for a dry run or a halt; a per-run record that could not be written leaves `result_path` null with a `result_file_write_failed` warning) and `error` (the `phase` and `reason` of a halt). A `success` drop can still carry `warnings` that name what needs a manual fix (`context_rebuild_failed`, `active_link_dangling`, `delete_failed`, `verification_failed`), and any run can carry `customization_resolver_unavailable` when the customization resolver could not run. Exit codes in [`src/skf-drop-skill/references/invocation-contract.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-drop-skill/references/invocation-contract.md) § "Exit Codes"; the envelope and full `halt_reason` list in the same file's § "Result Contract (Headless)".
576
576
  - **`/skf-rename-skill` (RS)**: exit `2` `input-missing`/`input-invalid`, `3` resolution-failure (`manifest-corrupt`, `nothing-to-rename`), `4` write-failure (`copy-failed`, `write-failed`, `manifest-write-failed`; the context-file rebuild is best-effort and never halts), `5` state-conflict (`name-collision`, `source-authority-blocked`, `halted-for-concurrent-run` while another rename of the same skill holds its run lock or when this run's own lock went stale while it waited at a prompt, `not-skf-output` for a folder SKF did not generate, or a forge folder that also holds other files or that is a link, is not a folder, or cannot be listed; `flat-layout` for a skill not yet migrated; `verify-failed` when the old name is still in the renamed files, which rolls the rename back with the old skill intact, see [Troubleshooting](/docs/troubleshooting.md)). The run lock is `.skf-rename-<name>.lock` in `forge_data_folder`, and it goes stale 60 minutes after it was taken or renewed: when no other rename of that skill is running, delete the lock file the message names, or wait until the time it gives, then run the rename again. When this run's own lock went stale at a prompt, nothing was changed: run the rename again. Exit codes in [`src/skf-rename-skill/references/exit-codes.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-rename-skill/references/exit-codes.md); the envelope in [`src/skf-rename-skill/references/invocation-contract.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-rename-skill/references/invocation-contract.md) § "Result Contract (Headless)".
577
577
  - **`/skf-refine-architecture` (RA)**: exit `2` `input-missing`/`input-invalid` (`input-invalid` also covers a Verify Stack report with a `schemaVersion` other than `1.0`, a missing or doubled verdict table, or an unknown verdict token), `3` resolution-failure (`output-folder-unconfigured`, `forge-folder-unconfigured`, and `resolution-failure` when the result emitter or `scripts/skf-check-preservation.py` cannot start, when the refinement rules cannot be read, lack one of their six tables or break a tier rule (phase `init:rules`), or when the skill inventory helper (phase `init:inventory`) or the co-mention helper (phase `gap-analysis:comention`) is missing or its call fails, or the Verify Stack report validator (phase `init:feasibility-validator`) is missing), `4` write-failure (`write-failed`, and `preservation-failed` when the draft would lose or change a line of the original: at once in a headless run, or when the promotion still finds it after one rebuild), `5` state-conflict (`insufficient-skills`: no skill SKF generated was found), `7` `inventory-unreliable`, `8` `recovery-failed` (at step 3, the Verify Stack report cannot be read again, Verify Stack rewrote it during the run, or the refinement rules no longer pass step 1's check; the saved state is not enough to rebuild the findings; or a record of the draft's build or promotion is missing at step 6). Exit codes and the envelope in [`src/skf-refine-architecture/references/exit-codes.md`](https://github.com/armelhbobdad/bmad-module-skill-forge/blob/main/src/skf-refine-architecture/references/exit-codes.md).
578
578
 
@@ -581,9 +581,9 @@ Setup is not the only workflow whose result line is the run's final message: a h
581
581
  | Workflow | Result line | Fields worth reading |
582
582
  | --- | --- | --- |
583
583
  | AN | `SKF_ANALYZE_RESULT_JSON` | `brief_paths`, `unit_counts` |
584
- | CS | `SKF_CREATE_SKILL_RESULT_JSON` | `status`, `exit_code`, `phase`, `summary.halt_reason` |
585
- | SS | `SKF_STACK_RESULT_JSON` | `mode` (`code` or `compose`), `stack_libraries`, `skill_package`, `quality_score` (the skill-check score SS records for the committed stack, `null` when skill-check did not run; not a Test Skill score), `run_id`, `result_path`, `headless_decisions` and `warnings`, plus `exit_code`, `halt_reason` and `error` on a halt |
586
- | US | `SKF_UPDATE_RESULT_JSON` | `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are all good outcomes |
584
+ | CS | `SKF_CREATE_SKILL_RESULT_JSON` | `status`, `exit_code`, `phase`, `summary.halt_reason`, `summary.security_result` (`pass`, `warn` or `skipped`, the security scan's outcome, on a success line; `pass` when the scan exited 0 and skill-check reported no finding, which does not show that the scan found nothing) |
585
+ | SS | `SKF_STACK_RESULT_JSON` | `mode` (`code` or `compose`), `stack_libraries`, `skill_package`, `quality_score` (the skill-check score SS records for the committed stack, `null` when skill-check did not run; not a Test Skill score), `security_result` (`pass`, `warn` or `skipped`, the security scan's outcome, on a success line; `pass` when the scan exited 0 and skill-check reported no finding, which does not show that the scan found nothing), `run_id`, `result_path`, `headless_decisions` and `warnings`, plus `exit_code`, `halt_reason` and `error` on a halt |
586
+ | US | `SKF_UPDATE_RESULT_JSON` | `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are all good outcomes; `skf_update.security_result` (`pass`, `warn` or `skipped`, the security scan's outcome; `pass` when the scan exited 0 and skill-check reported no finding, which does not show that the scan found nothing) on a `success` line |
587
587
  | AS | `SKF_AUDIT_RESULT_JSON` | `drift_score`, `next_workflow` (`update-skill` whenever upstream moved), `upstream_moved`, `upstream_ref` (a `maintain` pipeline passes it to US as `--target-ref`) |
588
588
  | TS | `SKF_TEST_RESULT_JSON` | `verdict`, `score`, `threshold` |
589
589
  | EX | `SKF_EXPORT_RESULT_JSON` | `context_files_updated` |
@@ -597,7 +597,7 @@ To run these without questions, pass their inputs as flags. Analyze Source takes
597
597
 
598
598
  Every workflow ships a `customize.toml` with settings you can change. Do not edit that file: updates overwrite it. Put your changes in `_bmad/custom/<skill-name>.toml` (shared with your team) or `_bmad/custom/<skill-name>.user.toml` (just for you), for example `_bmad/custom/skf-test-skill.toml`. Start the file with a `[workflow]` line, as the bundled file does: a setting above it is ignored without a warning. A single value replaces the default; a list adds to it.
599
599
 
600
- These overrides are read by BMAD Method's customization script, which the BMAD Method installer adds. Every workflow, and Ferris, runs that script through `uv` when it starts. When the script cannot run, the run prints one warning, `[activation/warn] customization_resolver_unavailable: <reason>`, and uses the bundled defaults, ignoring `_bmad/custom/`. A workflow also adds a warning naming `customization_resolver_unavailable` and its reason to the run's warnings, in the result envelope and result file of a run that writes them (Campaign logs it in its decision log, a `--quiet` or headless Setup Forge run shows it only in its envelope, and an interactive Export Skill run only prints it). In a project with SKF alone, that script is missing, so every run warns `customization_resolver_unavailable: not found` and uses the bundled defaults. On a BMAD core older than 6.11.0, whose script reads the overrides of the nearest folder holding `_bmad` or `.git`, from the workflow's own folder up, SKF still applies them when that folder is this project, and otherwise warns `customization_resolver_unavailable: BMAD core before 6.11.0: ...`, naming the folder that script would read and the upgrade to 6.11.0 or later.
600
+ These overrides are read by BMAD Method's customization script, which the BMAD Method installer adds. Every workflow, and Ferris, runs that script through `uv` when it starts. When the script cannot run, the run prints one warning, `[activation/warn] customization_resolver_unavailable: <reason>`, and uses the bundled defaults, ignoring `_bmad/custom/`. A workflow also adds a warning naming `customization_resolver_unavailable` and its reason to the run's warnings, in the result envelope and result file of a run that writes them (Campaign logs it in its decision log, a `--quiet` or headless Setup Forge run shows it only in its envelope, and an interactive Export Skill run only prints it). In a project with SKF alone, that script is missing, so every run warns `customization_resolver_unavailable: not found` and uses the bundled defaults. When SKF's own helper is missing, as in an incomplete SKF install, the run warns `customization_resolver_unavailable: SKF helper skf-resolve-customization.py not found` instead: reinstall SKF. On a BMAD core older than 6.11.0, whose script reads the overrides of the nearest folder holding `_bmad` or `.git`, from the workflow's own folder up, SKF still applies them when that folder is this project, and otherwise warns `customization_resolver_unavailable: BMAD core before 6.11.0: ...`, naming the folder that script would read and the upgrade to 6.11.0 or later.
601
601
 
602
602
  Ferris reads `_bmad/custom/skf-forger.toml` and `_bmad/custom/skf-forger.user.toml` the same way when he starts, under an `[agent]` line, as his bundled file does (a `[workflow]` table there is ignored). An override there can add `activation_steps_prepend`, which runs once his config is loaded, `persistent_facts` (a `!` entry drops each earlier entry it names) and `activation_steps_append`, which runs after his greeting. His name, title and the other agent roster values stay as shipped, and he has no `on_complete`. When the script cannot run, the warning line opens his greeting.
603
603
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-module-skill-forge",
4
- "version": "7.0.0",
4
+ "version": "8.1.0",
5
5
  "description": "BMAD module — Turn code and docs into instructions AI agents can actually follow. Progressive capability tiers (Quick/Forge/Forge+/Deep).",
6
6
  "keywords": [
7
7
  "bmad",
@@ -12,14 +12,14 @@ Any tool invocation that may touch SKILL.md must run inside the four-phase guard
12
12
 
13
13
  ### 1. Capture
14
14
 
15
- Before invoking the tool, snapshot the on-disk `description` value:
15
+ Before invoking the tool, snapshot the on-disk `description` value into the run folder:
16
16
 
17
17
  ```bash
18
18
  uv run {descriptionGuardHelper} \
19
- capture <skill-md-path>
19
+ capture <skill-md-path> > <run-folder>/description-guard.json
20
20
  ```
21
21
 
22
- Output JSON:
22
+ The file holds `capture`'s output:
23
23
 
24
24
  ```json
25
25
  {
@@ -28,7 +28,7 @@ Output JSON:
28
28
  }
29
29
  ```
30
30
 
31
- Stash the `description` value in workflow context as `guarded_description`. Also keep the in-context copy synced with the on-disk pre-call state.
31
+ A caller may read the `description` value from the file into workflow context as `guarded_description`, never into a shell string. Also keep the in-context copy synced with the on-disk pre-call state.
32
32
 
33
33
  ### 2. Execute
34
34
 
@@ -41,16 +41,18 @@ After the tool completes, run:
41
41
  ```bash
42
42
  uv run {descriptionGuardHelper} \
43
43
  verify-restore <skill-md-path> \
44
- --captured-description "{guarded_description}"
44
+ --captured-file <run-folder>/description-guard.json
45
45
  ```
46
46
 
47
- In place of `--captured-description`, `--captured-file <path>` reads the snapshot from a JSON file's `description`: redirect `capture`'s output into the run folder (`capture <skill-md-path> > <run-folder>/description-guard.json`) and pass that file, so the value never passes through context or a shell string. quick-skill calls it this way. An empty or whitespace-only `description` in the file is refused like an empty `--captured-description` (below), and so is a file the helper cannot read or that holds no string `description`.
47
+ `--captured-file <path>` reads the snapshot from a JSON file's `description`: `capture`'s output saved in the run folder, or another JSON file whose `description` holds the value (quick-skill re-runs with its `metadata.json`). Every calling step passes the file, so the description never passes through a shell string. An empty or whitespace-only `description` in the file is refused like an empty `--captured-description` (below), and so is a file the helper cannot read or that holds no string `description`.
48
+
49
+ The alternative, `--captured-description <value>`, is for a caller that holds no file and runs the helper with an argument list, without a shell. A shell string must never carry a description: inside double quotes the shell runs backticks and `$(...)`, expands `$name`, and a `"` ends the argument or drops out of it, so the call breaks, or the guard writes a changed description over one the tool never touched.
48
50
 
49
51
  The script:
50
52
 
51
53
  1. Re-reads on-disk `description`
52
- 2. Compares against `guarded_description` using **token-stream equality** — split each string on whitespace, compare resulting token lists element-by-element
53
- 3. If diverged: atomically rewrites the frontmatter `description` field with `guarded_description`
54
+ 2. Compares it against the snapshot (the `description` in the `--captured-file` file, or `--captured-description`) using **token-stream equality**: split each string on whitespace, compare resulting token lists element-by-element
55
+ 3. If diverged: atomically rewrites the frontmatter `description` field with the snapshot
54
56
 
55
57
  Output JSON:
56
58
 
@@ -63,9 +65,9 @@ Output JSON:
63
65
  }
64
66
  ```
65
67
 
66
- If `restored == true`, also update the in-context copy of `description` to match `guarded_description` so subsequent stages do not work from a stale tool-mutated version. Record `description_guard_restored: true` (with the tool name and the `diff_kind`) in workflow context for the evidence report.
68
+ If `restored == true`, also set the in-context copy of `description` to the snapshot's `description`, read from the file as `guarded_description`, so subsequent stages do not work from a stale tool-mutated version. Record `description_guard_restored: true` (with the tool name and the `diff_kind`) in workflow context for the evidence report.
67
69
 
68
- **Empty snapshot refusal.** The helper exits 1 and leaves the file untouched when `--captured-description` is empty or whitespace-only. There is nothing to restore from: either `guarded_description` was lost from context between capture and verify, or the field was already empty before the tool ran. Never retry with the empty value — writing it back would blank the field the guard exists to protect. If the compiled description is still available in context (the in-context SKILL.md copy), re-run `verify-restore` with that value. Otherwise record `description_guard_restored: false` and `description_guard_refused: empty-capture` (with the tool name) in workflow context; the calling stage's evidence-report populator renders that as a fired guard (`Restored: false`, the triggering tool, `Original description preserved: false`, `Notes: guard refused — empty captured snapshot (empty-capture)`) rather than as the all-`—` clean-run state, and the frontmatter validator surfaces the empty field through the normal artifact path.
70
+ **Empty snapshot refusal.** The helper exits 1 and leaves the file untouched when the snapshot is empty or whitespace-only: the `description` in `<run-folder>/description-guard.json`, or an empty `--captured-description`. It does the same when it cannot read that file, or the file holds no JSON with a string `description`. There is nothing to restore from: either the snapshot was lost between capture and verify, or the field was already empty before the tool ran. Never retry with the empty value: writing it back would blank the field the guard exists to protect. If the compiled description is still available in context (the in-context SKILL.md copy), write it as `{"description": <that value as a JSON string>}` to the same file with a file write, never `echo`, and re-run `verify-restore` on it 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) in workflow context; the calling stage's evidence-report populator renders that as a fired guard (`Restored: false`, the triggering tool, `Original description preserved: false`, and a `Notes` line that names the `empty-capture` refusal) rather than as the clean-run state, whose follow-up fields all hold the template's no-fire dash, and the frontmatter validator surfaces the empty field through the normal artifact path.
69
71
 
70
72
  ## Why Token-Stream Comparison
71
73
 
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "result_path": {
84
84
  "type": ["string", "null"],
85
- "description": "The per-run result file the emitter wrote, {forge_data_folder}/analyze-source-result-<YYYYMMDD-HHmmss>.json (UTC), beside its analyze-source-result-latest.json copy. Stamped by the emitter, never typed. Null for a HALT, which writes no result file, and when the write failed (warnings say why)."
85
+ "description": "The per-run result file the emitter wrote, {forge_data_folder}/analyze-source-result-<YYYYMMDD-HHmmss>.json (UTC), beside its analyze-source-result-latest.json copy. Stamped by the emitter, never typed. Null for a HALT, which writes no result file, when --result-dir names no folder (nothing is written, and no warning is added), and when the per-run record could not be written (a result_file_write_failed warning says why); a -latest copy that could not be written leaves it set, and a result_file_write_failed warning names the copy."
86
86
  },
87
87
  "error": {
88
88
  "oneOf": [
@@ -123,7 +123,7 @@
123
123
  "result_path": {
124
124
  "type": ["string", "null"],
125
125
  "minLength": 1,
126
- "description": "The per-run result file the emitter wrote in the audited version's forge folder, audit-skill-result-<YYYYMMDD-HHmmss>.json (UTC), beside its audit-skill-result-latest.json copy. Stamped by the emitter, never typed. Null on every halt, which writes no result file, and when the write failed (warnings say why)."
126
+ "description": "The per-run result file the emitter wrote in the audited version's forge folder, audit-skill-result-<YYYYMMDD-HHmmss>.json (UTC), beside its audit-skill-result-latest.json copy. Stamped by the emitter, never typed. Null on every halt, which writes no result file, when --result-dir names no folder (nothing is written, and no warning is added), and when the per-run record could not be written (a result_file_write_failed warning says why); a -latest copy that could not be written leaves it set, and a result_file_write_failed warning names the copy."
127
127
  },
128
128
  "headless_decisions": {
129
129
  "type": "array",
@@ -147,6 +147,44 @@
147
147
  },
148
148
  "description": "Optional. Each skill whose skf-export-skill call failed, with that call's halt_reason and exit code (null when it gave none). The campaign still ends with status success, so a CI job gates on this list. Null when the Export stage did not run; absent on a HARD HALT."
149
149
  },
150
+ "stage_outcomes": {
151
+ "type": ["object", "null"],
152
+ "additionalProperties": false,
153
+ "required": ["capstone", "verification", "refinement"],
154
+ "properties": {
155
+ "capstone": {
156
+ "type": ["object", "null"],
157
+ "additionalProperties": false,
158
+ "required": ["outcome", "halt_reason", "exit_code"],
159
+ "properties": {
160
+ "outcome": { "type": "string", "enum": ["completed", "skipped", "failed"] },
161
+ "halt_reason": { "type": ["string", "null"] },
162
+ "exit_code": { "type": ["integer", "null"] }
163
+ }
164
+ },
165
+ "verification": {
166
+ "type": ["object", "null"],
167
+ "additionalProperties": false,
168
+ "required": ["outcome", "halt_reason", "exit_code"],
169
+ "properties": {
170
+ "outcome": { "type": "string", "enum": ["completed", "skipped", "failed"] },
171
+ "halt_reason": { "type": ["string", "null"] },
172
+ "exit_code": { "type": ["integer", "null"] }
173
+ }
174
+ },
175
+ "refinement": {
176
+ "type": ["object", "null"],
177
+ "additionalProperties": false,
178
+ "required": ["outcome", "halt_reason", "exit_code"],
179
+ "properties": {
180
+ "outcome": { "type": "string", "enum": ["completed", "skipped", "failed"] },
181
+ "halt_reason": { "type": ["string", "null"] },
182
+ "exit_code": { "type": ["integer", "null"] }
183
+ }
184
+ }
185
+ },
186
+ "description": "Optional. What the capstone, verification and refinement stages did, from the outcome campaign-state.py recorded for each one: completed, skipped (a stage skipped on purpose, such as verification with no architecture document) or failed, with the failure's halt_reason (no-result when the sub-skill printed no result line, result-unreadable when Campaign could not read it, otherwise the sub-skill's own) and exit code (null when it gave none). A stage with no recorded outcome is null. The campaign still ends with status success, so a pipeline tells a failed verification from a skipped one here. Null before the capstone stage ran; absent on a HARD HALT."
187
+ },
150
188
  "campaign_report_path": {
151
189
  "type": ["string", "null"],
152
190
  "minLength": 1,
@@ -195,6 +195,11 @@
195
195
  "minimum": 0,
196
196
  "description": "The decisions the run took without asking, as recorded in its run folder: every gate a headless run resolved by its default, and step 1's tier override in any mode. The rows of the evidence report's Auto-Decisions table; 0 when there were none."
197
197
  },
198
+ "security_result": {
199
+ "type": "string",
200
+ "enum": ["pass", "warn", "skipped"],
201
+ "description": "The outcome of step 6's security scan, the first word of the evidence report's Security row under Validation Results, and the value follows that row. 'pass': the scan exited 0 and skill-check reported no finding, which does not show that the scan found nothing. 'warn': it reported findings. 'skipped': it did not run (skill-check unavailable, or the scan could not run, such as without SNYK_TOKEN). Absent at a HARD HALT, and when that row is missing or starts with another word."
202
+ },
198
203
  "warning": {
199
204
  "type": "string",
200
205
  "enum": ["zero-exports"],
@@ -102,7 +102,7 @@
102
102
  },
103
103
  "result_path": {
104
104
  "type": ["string", "null"],
105
- "description": "The per-run result file the emitter wrote for a finished drop, {skills_output_folder}/drop-skill-result-<YYYYMMDD-HHmmss>.json (UTC), beside its drop-skill-result-latest.json copy. Stamped by the emitter, never typed. Null for a dry run and a HALT, which write no result file, and when the write failed (warnings say why)."
105
+ "description": "The per-run result file the emitter wrote for a finished drop, {skills_output_folder}/drop-skill-result-<YYYYMMDD-HHmmss>.json (UTC), beside its drop-skill-result-latest.json copy. Stamped by the emitter, never typed. Null for a dry run and a HALT, which write no result file, when --result-dir names no folder (nothing is written, and no warning is added), and when the per-run record could not be written (a result_file_write_failed warning says why); a -latest copy that could not be written leaves it set, and a result_file_write_failed warning names the copy."
106
106
  },
107
107
  "exit_code": {
108
108
  "type": "integer",
@@ -127,7 +127,7 @@
127
127
  "result_path": {
128
128
  "type": ["string", "null"],
129
129
  "minLength": 1,
130
- "description": "The per-run result file the emitter wrote in the skills output folder, export-skill-result-<YYYYMMDD-HHmmss>.json (its copy is export-skill-result-latest.json beside it). Null on a dry run, on every halt, and when the write failed (a result_file_write_failed warning says why)."
130
+ "description": "The per-run result file the emitter wrote in the skills output folder, export-skill-result-<YYYYMMDD-HHmmss>.json (its copy is export-skill-result-latest.json beside it). Null on a dry run, on every halt, when --result-dir names no folder (nothing is written, and no warning is added), and when the per-run record could not be written (a result_file_write_failed warning says why); a -latest copy that could not be written leaves it set, and a result_file_write_failed warning names the copy."
131
131
  },
132
132
  "warnings": {
133
133
  "type": "array",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/armelhbobdad/bmad-module-skill-forge/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json",
4
4
  "title": "SKF_QUICK_SKILL_RESULT_JSON envelope (v1)",
5
- "description": "Single-line JSON document skf-quick-skill prints, prefixed with the literal 'SKF_QUICK_SKILL_RESULT_JSON: ': on stdout when a run finishes (step 6, finalize), and on stderr at every HARD HALT. The shared emitter (skf-emit-result-envelope.py) builds both from a staged payload, so pipelines branch on status, exit_code and halt_reason without parsing the human-readable output. A --batch run prints one per target.",
5
+ "description": "Single-line JSON document skf-quick-skill prints, prefixed with the literal 'SKF_QUICK_SKILL_RESULT_JSON: ': on stdout when a run finishes (step 6, finalize), and on stderr at every HARD HALT. The shared emitter (skf-emit-result-envelope.py) builds both from a staged payload, so pipelines branch on status, exit_code and halt_reason without parsing the human-readable output. A --batch run prints one per target, and a batch-level halt line of its own when the batch stops outside a target: phase on-activation when a single-target flag came with --batch, phase batch-mode when the batch cannot start or stops because next, a second record or summarize failed.",
6
6
  "$defs": {
7
7
  "skf-envelope": {
8
8
  "description": "The shared emitter's settings for this workflow, read by skf-emit-result-envelope.py: the workflow, the line's prefix, the wrapper, the status a halt carries, the exit code of each halt_reason and the result file stem. Nothing in the schema references this entry, so it constrains no envelope.",
@@ -58,7 +58,7 @@
58
58
  "phase": {
59
59
  "type": ["string", "null"],
60
60
  "minLength": 1,
61
- "description": "The step slug where the HARD HALT fired (resolve-target, ecosystem-check, quick-extract, compile, write-and-validate or finalize), on-activation for a halt before the run starts (SKILL.md On Activation, or batch mode's refusal of --description and --exports), or batch-mode for a batch that cannot start (batch mode section 1). Null on success."
61
+ "description": "The step slug where the HARD HALT fired (resolve-target, ecosystem-check, quick-extract, compile, write-and-validate or finalize), on-activation for a halt before the run starts (SKILL.md On Activation, or batch mode's refusal of --description and --exports), or batch-mode for a batch that cannot start or go on (batch mode sections 1 to 4). Null on success."
62
62
  },
63
63
  "halt_reason": {
64
64
  "type": ["string", "null"],
@@ -187,6 +187,11 @@
187
187
  "security": { "type": "integer", "minimum": 0 }
188
188
  },
189
189
  "description": "The advisory issues step 5's validators returned for each heading of its validation report: skill_md (skill-check's diagnostics, or the frontmatter validator's issues without it, plus the body checks), context_snippet, metadata and security (skill-check's security findings). Auto-fixed issues count nothing, and a check that did not run counts 0."
190
+ },
191
+ "security_result": {
192
+ "type": "string",
193
+ "enum": ["pass", "warn", "skipped"],
194
+ "description": "The outcome of step 5's security scan. 'pass': the scan exited 0 and skill-check reported no finding, which does not show that the scan found nothing (without --ci, agent-scan also exits 0 when its analysis fails). 'warn': it reported findings. 'skipped': it did not run (skill-check unavailable or stopped before it validated, or the scan could not run, such as without SNYK_TOKEN). validation_issues.security reads 0 for a skipped scan as for a passed one, so read this field to tell them apart."
190
195
  }
191
196
  },
192
197
  "description": "Empty at a HARD HALT. On success, the run's facts a pipeline branches on."
@@ -202,7 +207,7 @@
202
207
  },
203
208
  "run_id": {
204
209
  "type": ["string", "null"],
205
- "description": "The run folder's id, stamped by the emitter; a --batch target's is the batch run's id and the target's number."
210
+ "description": "The run folder's id, stamped by the emitter; a --batch target's is the batch run's id and the target's number. Null on a line emitted without --run-dir (On Activation's run-folder halt, batch mode's write-failure halts)."
206
211
  },
207
212
  "result_path": {
208
213
  "type": ["string", "null"],
@@ -123,7 +123,7 @@
123
123
  "result_path": {
124
124
  "type": ["string", "null"],
125
125
  "minLength": 1,
126
- "description": "The per-run result file the emitter wrote in the output folder, refine-architecture-result-<YYYYMMDD-HHmmss>.json (UTC), beside its refine-architecture-result-latest.json copy. Stamped by the emitter, never typed; step 6 passes it to the on_complete hook. Null on every halt, which writes no result file, and when the write failed (warnings say why)."
126
+ "description": "The per-run result file the emitter wrote in the output folder, refine-architecture-result-<YYYYMMDD-HHmmss>.json (UTC), beside its refine-architecture-result-latest.json copy. Stamped by the emitter, never typed; step 6 passes it to the on_complete hook. Null on every halt, which writes no result file, when --result-dir names no folder (nothing is written, and no warning is added), and when the per-run record could not be written (a result_file_write_failed warning says why); a -latest copy that could not be written leaves it set, and a result_file_write_failed warning names the copy."
127
127
  },
128
128
  "headless_decisions": {
129
129
  "type": "array",