bmad-module-skill-forge 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +3 -3
  3. package/docs/_data/pinned.yaml +19 -13
  4. package/docs/architecture.md +2 -2
  5. package/docs/concepts.md +4 -4
  6. package/docs/examples.md +33 -26
  7. package/docs/getting-started.md +3 -3
  8. package/docs/how-it-works.md +2 -2
  9. package/docs/index.md +1 -1
  10. package/docs/skill-model.md +41 -39
  11. package/docs/troubleshooting.md +8 -0
  12. package/docs/verifying-a-skill.md +46 -21
  13. package/docs/why-skf.md +1 -1
  14. package/docs/workflows.md +5 -5
  15. package/package.json +1 -1
  16. package/src/knowledge/qmd-registry.md +1 -1
  17. package/src/knowledge/version-paths.md +1 -1
  18. package/src/shared/health-check.md +2 -50
  19. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
  20. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +12 -4
  21. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  22. package/src/shared/scripts/skf-atomic-write.py +212 -59
  23. package/src/shared/scripts/skf-build-change-manifest.py +1437 -178
  24. package/src/shared/scripts/skf-classify-changed-files.py +78 -0
  25. package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
  26. package/src/shared/scripts/skf-count-tokens.py +54 -10
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
  28. package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
  29. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  30. package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
  31. package/src/shared/scripts/skf-extract-public-api.py +1673 -189
  32. package/src/shared/scripts/skf-extraction-inventory.py +584 -46
  33. package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
  34. package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
  35. package/src/shared/scripts/skf-github-probe.py +47 -8
  36. package/src/shared/scripts/skf-hash-content.py +14 -8
  37. package/src/shared/scripts/skf-load-provenance.py +3 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
  39. package/src/shared/scripts/skf-parse-gaps.py +135 -26
  40. package/src/shared/scripts/skf-preflight.py +15 -1
  41. package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
  42. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  43. package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
  44. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  45. package/src/shared/scripts/skf-shard-body.py +239 -41
  46. package/src/shared/scripts/skf-structural-diff.py +133 -18
  47. package/src/shared/scripts/skf-update-active-symlink.py +115 -62
  48. package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
  49. package/src/shared/scripts/skf-validate-output.py +12 -3
  50. package/src/shared/scripts/skf-verify-provenance-completeness.py +1439 -79
  51. package/src/shared/scripts/skf-write-skill-brief.py +41 -8
  52. package/src/shared/tool-requirements.yaml +8 -2
  53. package/src/skf-analyze-source/SKILL.md +2 -2
  54. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  55. package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
  56. package/src/skf-audit-skill/SKILL.md +2 -2
  57. package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
  58. package/src/skf-audit-skill/references/headless-contract.md +1 -1
  59. package/src/skf-audit-skill/references/health-check.md +2 -0
  60. package/src/skf-audit-skill/references/init.md +2 -1
  61. package/src/skf-audit-skill/references/re-index.md +14 -9
  62. package/src/skf-audit-skill/references/report.md +2 -2
  63. package/src/skf-audit-skill/references/semantic-diff.md +15 -15
  64. package/src/skf-audit-skill/references/structural-diff.md +10 -5
  65. package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
  66. package/src/skf-brief-skill/SKILL.md +3 -3
  67. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  68. package/src/skf-brief-skill/assets/skill-brief-schema.md +4 -11
  69. package/src/skf-brief-skill/references/analyze-target.md +6 -6
  70. package/src/skf-brief-skill/references/confirm-brief.md +1 -1
  71. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  72. package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
  73. package/src/skf-brief-skill/references/version-resolution.md +5 -9
  74. package/src/skf-campaign/SKILL.md +2 -2
  75. package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
  76. package/src/skf-create-skill/SKILL.md +2 -2
  77. package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
  78. package/src/skf-create-skill/assets/skill-sections.md +7 -7
  79. package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
  80. package/src/skf-create-skill/references/compile.md +31 -60
  81. package/src/skf-create-skill/references/component-extraction.md +3 -3
  82. package/src/skf-create-skill/references/enrich.md +2 -2
  83. package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
  84. package/src/skf-create-skill/references/extract.md +23 -19
  85. package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
  86. package/src/skf-create-skill/references/extraction-patterns.md +10 -11
  87. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  88. package/src/skf-create-skill/references/report.md +21 -5
  89. package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
  90. package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
  91. package/src/skf-create-stack-skill/SKILL.md +2 -2
  92. package/src/skf-create-stack-skill/references/validate.md +1 -1
  93. package/src/skf-drop-skill/SKILL.md +2 -2
  94. package/src/skf-drop-skill/references/execute.md +5 -1
  95. package/src/skf-drop-skill/references/health-check.md +2 -0
  96. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  97. package/src/skf-drop-skill/references/report.md +2 -2
  98. package/src/skf-drop-skill/references/select.md +7 -3
  99. package/src/skf-export-skill/SKILL.md +2 -2
  100. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  101. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  102. package/src/skf-export-skill/references/load-skill.md +1 -1
  103. package/src/skf-export-skill/references/token-report.md +29 -9
  104. package/src/skf-forger/SKILL.md +2 -2
  105. package/src/skf-quick-skill/SKILL.md +2 -2
  106. package/src/skf-refine-architecture/SKILL.md +6 -8
  107. package/src/skf-rename-skill/SKILL.md +2 -2
  108. package/src/skf-setup/SKILL.md +2 -2
  109. package/src/skf-test-skill/SKILL.md +2 -2
  110. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  111. package/src/skf-test-skill/references/coherence-check.md +5 -4
  112. package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
  113. package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
  114. package/src/skf-test-skill/references/coverage-check.md +69 -111
  115. package/src/skf-test-skill/references/init.md +5 -6
  116. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  117. package/src/skf-test-skill/references/report.md +11 -9
  118. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  119. package/src/skf-test-skill/references/source-access-protocol.md +15 -18
  120. package/src/skf-test-skill/scripts/build-result-context.py +114 -21
  121. package/src/skf-test-skill/scripts/gap-ledger.py +580 -80
  122. package/src/skf-test-skill/scripts/load-coverage-inputs.py +646 -107
  123. package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
  124. package/src/skf-test-skill/scripts/reconcile-coverage.py +450 -23
  125. package/src/skf-test-skill/scripts/score-signatures.py +356 -61
  126. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  127. package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
  128. package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
  129. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  130. package/src/skf-update-skill/SKILL.md +2 -2
  131. package/src/skf-update-skill/references/detect-changes.md +32 -13
  132. package/src/skf-update-skill/references/gap-driven.md +37 -22
  133. package/src/skf-update-skill/references/init.md +6 -2
  134. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  135. package/src/skf-update-skill/references/merge.md +17 -12
  136. package/src/skf-update-skill/references/re-extract.md +4 -2
  137. package/src/skf-update-skill/references/report.md +7 -4
  138. package/src/skf-update-skill/references/write.md +77 -36
  139. package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
  140. package/src/skf-verify-stack/SKILL.md +2 -2
  141. package/tools/cli/lib/tool-check.js +158 -2
@@ -5,11 +5,10 @@
5
5
  Source reading via gh_bridge — infer exports from file structure and content.
6
6
 
7
7
  ### Strategy
8
- 1. `gh_bridge.list_tree(owner, repo, branch)` — map source structure
9
- 2. Identify entry points: index files, main exports, public modules
10
- 3. `gh_bridge.read_file(owner, repo, path)` — read each entry point
11
- 4. Extract: exported function names, parameter lists, return types (from signatures)
12
- 5. Infer types from JSDoc, docstrings, type annotations in source
8
+ 1. Files, entry points: step 3 §2's `files_in_scope_list`, `files_without_recipes_list`, `entry_points.files`
9
+ 2. Read each file that defines public API, locally or with `gh_bridge.read_file(owner, repo, path)`
10
+ 3. Extract: exported function names, parameter lists, return types (from signatures)
11
+ 4. Infer types from JSDoc, docstrings, type annotations in source
13
12
 
14
13
  ### Confidence
15
14
  - Every export is read by eye, so every entry is T1-low (source reading without structural verification): a `[SRC:...]` citation, `extraction_method: source-read` and `ast_node_type: null`
@@ -111,14 +110,14 @@ When AST tools are available (Forge, Forge+ and Deep tiers) and the step that ru
111
110
  - **Head cap:** each recipe keeps at most 200 matches, or at Forge+ and Deep 500 for `scope.type: "full-library"` and 300 for `scope.type: "component-library"`, in file and line order. `truncated` is true, and so is that recipe's `truncated` in `recipes[]`, when a recipe matched more: the matches past the cap are missing, so warn about them.
112
111
  - **Each export** is one name in one file, and gives `export_name`, `source_file`, `source_line` (`$NAME`'s line), `signature_line` (the source line holding it), `citation` (`[AST:{file}:L{line}]`), `ast_recipe`, `ast_node_type` (the `kind` its recipe declares), `export_type`, `from` (a re-export's module), `signature` (the declaration on one line), `params`, `return_type`, `confidence: T1` and `extraction_method: ast-grep`. Record them as they are.
113
112
  - **Signatures:** a function's `params` lists each parameter (its name, type, default and whether it is optional), and is null for any other export and for a declaration the runner does not parse; `return_type` is null when the declaration states none.
114
- - **Entry points:** `entry_point_diff` diffs the names the package's entry points export with the recipe matches (`public`, `internal`, `extraction_gaps`, `outside_scope`), `counts` gives `exports_public_api`, `exports_internal` and `effective_denominator`, and `arms` the monorepo, specific-modules and multi-subpath `exports` flags.
115
- - **Read by eye (T1-low)** only what it leaves: each file in `file_issues` (a syntax error where a recipe can miss an export, or a file that is not UTF-8, unreadable or missing), the files `files_without_recipes` counts that hold public API (by the Quick tier Strategy), the forms Known Limitation #11 below lists, and the `extraction_gaps` names.
113
+ - **Entry points:** `entry_point_diff` diffs the names the entry points export with the recipe matches (`public`, `other_definitions`, `internal`, `extraction_gaps`, `outside_scope`; a `namespace` item's `members` stand in for it), `counts` gives `exports_public_api`, `exports_internal` and `effective_denominator`, and `arms` the monorepo, specific-modules and multi-subpath `exports` flags.
114
+ - **Read by eye (T1-low)** only what it leaves: each file in `file_issues` (a syntax error where a recipe can miss an export, or a file that is not UTF-8, unreadable or missing), the files `files_without_recipes` counts that hold public API (by the Quick tier Strategy), the forms Known Limitation #11 below lists, the `unread` files of a rerun, and the `extraction_gaps` names.
116
115
 
117
- Act on its `status`, not its exit code: `ok`, every file in scope was read; `incomplete`, an ast-grep run failed or timed out: keep its exports and warn that the files of each `errors[]` item (how many, and the first) may be unread; `no-ast-grep`, no ast-grep it can run, so no recipe ran.
116
+ Act on its `status`, not its exit code: `ok`, every file in scope was read; `incomplete`, an ast-grep run failed or timed out: run the step's command once more, and when it is still incomplete keep its exports and read by eye each file an `errors[]` item lists in `unread`; `no-ast-grep`, no ast-grep it can run, so no recipe ran.
118
117
 
119
118
  ### When the Runner Cannot Run
120
119
 
121
- There is no runner result when the step gives no runner command, no runner path resolves, or no JSON is at the `-o` path after the call (`uv` missing or failing, an input error, which prints one line on stderr, a crash, or a shell timeout that stopped it). Then, and when its `status` is `no-ast-grep`, run the recipes as **Running the Recipes Without the Runner** below says, on the filtered file list: read this file on from that heading (a run the runner serves stops there), and first load `extraction-patterns-by-hand.md`, beside this file, completely: it holds each recipe's notes and the limitations of patterns run by hand. When no ast-grep can run them either (no `ast-grep` CLI and no ast-grep MCP tool), extraction degrades to source reading: see `tier-degradation-rules.md` "AST Tool Unavailable". When the runner's `scope.languages` is empty, whatever its `status`, no recipe reads the brief's language (the runner warns `no recipe reads {language} files` and reads no file): extract by the Quick tier Strategy (source reading, T1-low).
120
+ There is no runner result when the step gives no runner command, no runner path resolves, or no JSON is at the `-o` path after the call (`uv` missing or failing, an input error, which prints one line on stderr, a crash, or a shell timeout that stopped it). Then, and when its `status` is `no-ast-grep`, run the recipes as **Running the Recipes Without the Runner** below says, on the filtered file list: read this file on from that heading (a run the runner serves stops there), and first load `extraction-patterns-by-hand.md`, beside this file, completely: it holds each recipe's notes and the limitations of patterns run by hand. When no JSON is there and a stderr line holds `error:` (the runner's input error or crash, or `uv` failing; a shell timeout prints none), warn "**Recipe runner failed:** {that line}. Extraction ran without it." and keep the warning with the step's other warnings. When no ast-grep can run them either (no `ast-grep` CLI and no ast-grep MCP tool), extraction degrades to source reading: see `tier-degradation-rules.md` "AST Tool Unavailable". When the runner's `scope.languages` is empty, whatever its `status`, no recipe reads the brief's language (the runner warns `no recipe reads {language} files` and reads no file): extract by the Quick tier Strategy (source reading, T1-low).
122
121
 
123
122
  **"Files in scope"** are the files the brief's include and exclude globs keep, filtered by the language's extensions, never the repository's whole file count: use the filtered count from step 3 section 2 as the decision tree input.
124
123
 
@@ -136,9 +135,9 @@ Read the forms #11 lists by eye whether or not the runner ran:
136
135
  11. **Forms the recipes deliberately do not cover (ast-grep 0.45.3):** read these by eye at T1-low when the source uses them, or follow them with the Re-Export Tracing protocol in `extraction-patterns-tracing.md` where noted.
137
136
  - **Every JS/TS `export_statement` recipe:** `export` followed by a line break before the declaration (tree-sitter reads the bare `export` as a statement of its own, so there is no `export_statement` to match; formatters never write it); an export nested in a `namespace`, `declare module` or `declare global` block, a `.d.ts` file's `declare module "pkg" { ... }` included, since it is a member, not a module export.
138
137
  - **`js-exported-functions`, `react-component-functions`:** an anonymous `export default function () {}` (#3); a function exported through a list (`export { f }` and `export { f as g }`, which `js-local-exports` finds at the list's line), `export default f;` or `export = f`; a function bound to a `const` (the constants and arrow-function recipes find it); a wrapped default such as `export default memo(function Name() {})` or `forwardRef(...)`, whose value is a call, not a declaration; class, object-literal and interface methods; the implementation of an overloaded function (its signatures match instead). The React recipes also skip generator functions (an overload signature of one still matches, since a signature does not show it) and class components and keep a PascalCase function that is not a component (such as a Next.js `GET` handler).
139
- - **`js-exported-constants`, `js-exported-arrow-functions`, `react-component-arrow-functions`, `react-wrapped-components`:** every declarator after the first one a recipe accepts in `export const a = 1, b = () => 2` (one match per statement); destructuring (`export const { x } = obj`, which has no identifier name); `let`, `var` and `export declare const`. The arrow-function recipes also skip an arrow wrapped in parentheses, `as`, `satisfies` or a call, and a `function` expression; the constants recipe reports them, and `react-wrapped-components` reports a PascalCase `memo`, `forwardRef` or `lazy` call. `react-wrapped-components` skips any other wrapper (`observer(...)`, `styled.div`, `connect(...)(Component)`), a wrapped call cast with `as` or `satisfies`, and `export default memo(...)`, whose exposed name is `default`.
138
+ - **`js-exported-constants`, `js-exported-arrow-functions`, `react-component-arrow-functions`, `react-wrapped-components`:** every declarator after the first one a recipe accepts in `export const a = 1, b = () => 2` (one match per statement); destructuring (`export const { x } = obj`: the runner lists in `extraction_gaps` the public names of a pattern that opens the statement; read a later or unreadable pattern by eye, as quick mode asks); `let`, `var` and `export declare const`. The arrow-function recipes also skip an arrow wrapped in parentheses, `as`, `satisfies` or a call, and a `function` expression; the constants recipe reports them, and `react-wrapped-components` reports a PascalCase `memo`, `forwardRef` or `lazy` call. `react-wrapped-components` skips any other wrapper (`observer(...)`, `styled.div`, `connect(...)(Component)`), a wrapped call cast with `as` or `satisfies`, and `export default memo(...)`, whose exposed name is `default`.
140
139
  - **`js-exported-classes`, `react-props-interfaces`:** `export default class` and `export default interface` (their exposed name is `default`); `export declare class`, and `export declare interface` (`ts-exported-types` finds it); a class expression bound to a `const` (the constants recipe reports it); a props type written as a type alias (`export type XProps = {...}`, which `ts-exported-types` finds); a type exported through a list (`export type { T }`, which `js-local-exports` or `js-reexports` finds).
141
- - **`ts-exported-types`:** `export default interface` (its exposed name is `default`); an interface, type alias or enum inside a `namespace`, `declare module` or `declare global` block, a member rather than a module export; a type exported through a list (`export type { T }`, `export { type T }`), which `js-local-exports` finds, or `js-reexports` when the list has a `from`.
140
+ - **`ts-exported-types`:** `export default interface` (its exposed name is `default`); a type exported through a list (`export type { T }`, `export { type T }`), which `js-local-exports` finds, or `js-reexports` when the list has a `from`.
142
141
  - **`js-reexports`, `js-namespace-reexports`, `js-local-exports`:** a bare `export * from '...'`, which exposes no name of its own; an `export { a } from '...' with { type: 'json' }` statement, or the legacy `export { a } from '...' assert { type: 'json' }`, which parse as ERROR nodes; `export default x;` and `export = x`; a list inside a `namespace`, `declare module` or `declare global` block, a member rather than a module export; in a `.js` file, `default` as a list item's name or alias (`export { default } from '...'`, `export { x as default }`), a keyword token there that no `$NAME` captures (TypeScript parses it as a name). Follow these with the Re-Export Tracing protocol.
143
142
  - **`rust-public-functions`:** a `pub fn` in an `impl` block (#8); trait methods, which carry no `pub`; `pub fn` imports in an `extern "C" { }` block; functions a macro generates (`macro_rules!` and `cfg_if!` bodies are token trees, which ast-grep does not parse). One file cannot show reachability across files: a `pub fn` at the top of `foo.rs` matches whether or not `mod foo;` is `pub`, so check `pub use` re-exports with the Re-Export Tracing protocol. `#[cfg]`-gated and `#[doc(hidden)]` functions still match, and a raw identifier keeps its prefix (`r#match`).
144
143
  - **`go-exported-functions`:** methods; an exported function-typed variable (`var F = func() {}`). `Test*`, `Benchmark*` and `Example*` functions in `_test.go` files, and the exported functions of `package main` or an `internal/` package, match but are not importable API: exclude them through `scope.exclude`.
@@ -151,7 +151,7 @@ python3 {atomicWriteHelper} flip-link \
151
151
  --target {version}
152
152
  ```
153
153
 
154
- The helper returns non-zero (exit 2) if `{skill_group}/active` already exists as a real directory or file rather than a symlink. In that case **HARD HALT** (exit code 5, `active-link-blocked`, phase `generate-artifacts`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --result-dir "{forge_version}" --target stderr < "{run_dir}/halt.json"`), with `"skill_package"`, `"outputs"` and `{skill_group}/active` as `"path"`: "Refusing to flip `{skill_group}/active`: the existing path is not a symlink. Investigate manually; expected a symlink pointing at a version directory."
154
+ The helper exits 2, with a `{"status": "error", "message": …}` line on stderr, when `{skill_group}/active` already exists as a real directory or file rather than a symlink, or when it cannot take the flip lock on `{skill_group}/active.skf-lock`. In that case **HARD HALT** (exit code 5, `active-link-blocked`, phase `generate-artifacts`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --result-dir "{forge_version}" --target stderr < "{run_dir}/halt.json"`), with `"skill_package"`, `"outputs"` and `{skill_group}/active` as `"path"`: "Refusing to flip `{skill_group}/active`: {the `message` of the helper's stderr line, else its stderr}. Investigate manually; expected a symlink pointing at a version directory, with no other flip running."
155
155
 
156
156
  Do not `rm` + `ln -s` the active link by hand. The bare-rm pattern has two failure modes: (1) a concurrent reader sees a missing `active` mid-flip, and (2) a bug or typo that replaces `{skill_group}/active` with a plain directory turns the next manual `rm -rf {skill_group}/active` into data loss. The helper encapsulates both guards.
157
157
 
@@ -7,6 +7,14 @@ nextStepFile: 'health-check.md'
7
7
  atomicWriteProbeOrder:
8
8
  - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
9
9
  - '{project-root}/src/shared/scripts/skf-atomic-write.py'
10
+ # The result-contract emitter (Rules).
11
+ emitEnvelopeProbeOrder:
12
+ - '{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py'
13
+ - '{project-root}/src/shared/scripts/skf-emit-result-envelope.py'
14
+ # The customization helper §5's post-completion hook runs.
15
+ resolveCustomizationProbeOrder:
16
+ - '{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py'
17
+ - '{project-root}/src/shared/scripts/skf-resolve-customization.py'
10
18
  ---
11
19
 
12
20
  <!-- Config: communicate in {communication_language}. -->
@@ -19,7 +27,9 @@ To display the final compilation summary (skill name, version, source, export co
19
27
 
20
28
  ## Rules
21
29
 
22
- - Write only the brief's result files (through the emitter, §5) and, outside `--batch`, the removal of the brief's run folder. Change no skill artifact, except in §5's fallback when `{emitEnvelopeHelper}` resolved no path: `validation_status` in `{skill_package}/metadata.json` and one warning appended to `{forge_version}/evidence-report.md`
30
+ - Write only the brief's result files (through the emitter, §5) and, outside `--batch`, the removal of the brief's run folder. Change no skill artifact, except in §5's fallback when no path in `{emitEnvelopeProbeOrder}` exists: `validation_status` in `{skill_package}/metadata.json` and one warning appended to `{forge_version}/evidence-report.md`
31
+ - §5 emits through `{emitEnvelopeHelper}`, resolved from `{emitEnvelopeProbeOrder}` when it is not bound.
32
+ - §5's post-completion hook runs `{resolveCustomizationHelper}`, resolved from `{resolveCustomizationProbeOrder}` when it is not bound.
23
33
  - Deliver structured report with confidence breakdown
24
34
  - The user-facing report is not the end of the run: §5 and §6 follow it in order, and §6 chains to the health check via `{nextStepFile}`, or under `--batch` back to `references/batch-mode.md`, which runs it once the last brief is recorded
25
35
 
@@ -91,7 +101,7 @@ To use this skill immediately, add the context snippet to your CLAUDE.md:
91
101
 
92
102
  Every brief that reaches this step gets its own result contract and `on_complete` call, each brief of a `--batch` run included. A HARD HALT in steps 1 to 7 never reaches this step: it emits its envelope where it fires, as its HALT line says, and writes its result file there once step 7 has created `{forge_version}`.
93
103
 
94
- **Write the result contract through the emitter.** `{emitEnvelopeHelper}` (SKILL.md On Activation) writes `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` and its `create-skill-result-latest.json` copy (a stable path for pipeline consumers; a copy, not a symlink), stamps the timestamp, the run id and the run sink's decisions and warnings into them, checks the brief's `SKF_CREATE_SKILL_RESULT_JSON` line against `skf-create-skill-result-envelope.v1.json` and prints it. First read the run sink, from `{project-root}`:
104
+ **Write the result contract through the emitter.** `{emitEnvelopeHelper}` writes `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` and its `create-skill-result-latest.json` copy (a stable path for pipeline consumers; a copy, not a symlink), stamps the timestamp, the run id and the run sink's decisions and warnings into them, checks the brief's `SKF_CREATE_SKILL_RESULT_JSON` line against `skf-create-skill-result-envelope.v1.json` and prints it. First read the run sink, from `{project-root}`:
95
105
 
96
106
  ```bash
97
107
  grep -c . "{run_dir}/headless-decisions.jsonl" 2>/dev/null; grep -c '"gate":"zero-exports"' "{run_dir}/headless-decisions.jsonl" 2>/dev/null
@@ -108,15 +118,21 @@ uv run {emitEnvelopeHelper} emit --workflow skf-create-skill --run-dir "{run_dir
108
118
 
109
119
  The evidence report in `outputs` carries the `## Auto-Decisions` audit table: a pipeline consumer follows its path to audit the run. Display the line the emitter prints. If it exits non-zero, fix `result-context.json` once (its `message` names the problem) and run it again. When it fails again, display its `message`: this brief has no result contract.
110
120
 
111
- **If `{emitEnvelopeHelper}` resolved no path** (an incomplete install), write no result contract: append "Result contract skipped: skf-emit-result-envelope.py is missing." under `{forge_version}/evidence-report.md`'s `## Remaining Warnings`, and set `validation_status: 'schema-unavailable'` in `{skill_package}/metadata.json`, rewritten through `python3 {atomicWriteHelper} write --target {skill_package}/metadata.json` when `{atomicWriteHelper}` resolves. Pipeline consumers observe the missing `-latest.json` and the metadata flag.
121
+ **If no path in `{emitEnvelopeProbeOrder}` exists** (an incomplete install), write no result contract: append "Result contract skipped: skf-emit-result-envelope.py is missing." under `{forge_version}/evidence-report.md`'s `## Remaining Warnings`, and set `validation_status: 'schema-unavailable'` in `{skill_package}/metadata.json`, rewritten through `python3 {atomicWriteHelper} write --target {skill_package}/metadata.json` when `{atomicWriteHelper}` resolves. Pipeline consumers observe the missing `-latest.json` and the metadata flag.
112
122
 
113
- **Post-completion hook.** When `{onCompleteCommand}` is non-empty (SKILL.md On Activation §3 resolved it from `workflow.on_complete`) and the emitter wrote this brief's result contract (its `emit` exited 0), run it as a shell command from `{project-root}`, for this brief:
123
+ **Post-completion hook.** When `{onCompleteCommand}` is not bound, bind it to the `workflow.on_complete` value this prints, or to the one in `{skill-root}/customize.toml` when the resolver cannot run:
124
+
125
+ ```bash
126
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow.on_complete
127
+ ```
128
+
129
+ When `{onCompleteCommand}` is non-empty and the emitter wrote this brief's result contract (its `emit` exited 0), run it as a shell command from `{project-root}`, for this brief:
114
130
 
115
131
  ```bash
116
132
  {onCompleteCommand} --result-path={forge_version}/create-skill-result-latest.json
117
133
  ```
118
134
 
119
- The hook runs after the contract, so a git-add, registry registration or notifier sees a complete package. A hook failure never fails the brief, since the skill is written and its result contract is final: display "on_complete failed for `{name}`: {its exit code and first stderr line}" and go on. When the emitter wrote no result contract for this brief (no `{emitEnvelopeHelper}`, or `emit` failed twice), `--result-path` would name a missing file or an earlier compile's: skip the hook and display "on_complete skipped for `{name}`: no result contract was written ({the reason})". When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
135
+ The hook runs after the contract, so a git-add, registry registration or notifier sees a complete package. A hook failure never fails the brief, since the skill is written and its result contract is final: display "on_complete failed for `{name}`: {its exit code and first stderr line}" and go on. When the emitter wrote no result contract for this brief (no path in `{emitEnvelopeProbeOrder}` exists, or `emit` failed twice), `--result-path` would name a missing file or an earlier compile's: skip the hook and display "on_complete skipped for `{name}`: no result contract was written ({the reason})". When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
120
136
 
121
137
  **Remove the run folder.** The brief finished, so outside `--batch` delete `{run_dir}` (`rm -rf "{run_dir}"`); under `--batch`, batch-mode.md §3 reads the staged `result-context.json` there and removes the folder itself. A halted brief keeps its folder, with the decisions it recorded and its staged payloads.
122
138
 
@@ -36,7 +36,7 @@ Outcomes, from `tag_resolution.status`:
36
36
 
37
37
  ### Implicit Tag Resolution (when only brief.version is set)
38
38
 
39
- When `brief.target_version` is absent but `brief.version` is present, `brief.version` is an **implicit** target version. This matches `brief-skill`'s behavior, which auto-populates `brief.version` from the latest non-prerelease release tag, so a tag matching `brief.version` is the common case, and silently reading HEAD would produce a skill labeled with `brief.version` but built from an unrelated default-branch commit.
39
+ When `brief.target_version` is absent but `brief.version` is present, `brief.version` is an **implicit** target version. This matches `brief-skill`'s behavior, which fills `brief.version` from the source's manifest or version files or, when they hold none, its latest release tag, so a tag matching `brief.version` is the common case, and silently reading HEAD would produce a skill labeled with `brief.version` but built from an unrelated default-branch commit.
40
40
 
41
41
  With `--implicit`, resolve tries only two forms, in this order:
42
42
 
@@ -53,7 +53,7 @@ Outcomes, from `tag_resolution.status`:
53
53
 
54
54
  **Do not halt on zero matches.** Implicit resolution never blocks compilation: `brief.version` is an auto-populated hint, and some repositories simply do not tag releases. The warning is sufficient notice; the evidence report in step 8 will surface the HEAD fallback for reviewers.
55
55
 
56
- **Interaction with Version Reconciliation (below):** When implicit tag resolution matches a tag, the tree's source files should carry the same version as `brief.version`, so the Version Reconciliation section's source-vs-brief mismatch warning will not fire. When implicit resolution falls back to HEAD, Version Reconciliation runs normally against the default branch's version file and may produce its own mismatch warning.
56
+ **Interaction with Version Reconciliation (below):** When implicit tag resolution matches a tag, the tree's version file should carry `brief.version`, unless it holds a release-time placeholder, which the extractor reports as no version, so the Version Reconciliation section's source-vs-brief mismatch warning will not fire. When implicit resolution falls back to HEAD, Version Reconciliation runs normally against the default branch's version file and may produce its own mismatch warning.
57
57
 
58
58
  ### Several Matching Tags
59
59
 
@@ -114,23 +114,18 @@ Also store `source_ref` in context (from tag resolution above, or `HEAD` if no t
114
114
 
115
115
  **If `source_type: "docs-only"`:** skip this section — no source files exist to reconcile.
116
116
 
117
- After the source path is accessible (local path from step 1, or the private tree from above), check whether the source contains a version identifier and reconcile it with `brief.version`. Look for the first matching version file in the resolved source path:
117
+ After the source path is accessible (local path from step 1, or the private tree from above), read the source's version with `{extractPublicApiHelper}` (resolve it ← first existing path in `{extractPublicApiProbeOrder}`), never from a version file by eye: it reports a release-time placeholder such as `0.0.0-development`, and a value that is not a string, as no version. The version file is, in the source root, `pyproject.toml`, else `setup.py`, else `setup.cfg` for Python and `Cargo.toml` for Rust; for JavaScript or TypeScript, the `package.json` whose `name` matches `brief.name`, else one with a `bin` field, else the root one. Any other language, or a source that holds none of them, keeps `brief.version`: skip the rest of this section. From `{project-root}`, run:
118
118
 
119
- - Python: `pyproject.toml` (`[project] version`), `setup.py` (`version=`), `__version__` in `__init__.py`
120
- - JavaScript/TypeScript: `package.json` (`"version"`). **Monorepo resolution:** When multiple `package.json` files exist (workspace root + packages), resolve version using this priority:
121
- 1. Package whose `name` field matches `brief.name` (e.g., the skill's target library name)
122
- 2. Package with a `bin` field (CLI entry point — represents the published version)
123
- 3. Root workspace `package.json` version (if present)
124
- 4. Fall back to `brief.version` if no version found. For monorepos using workspace protocols (pnpm, yarn, npm workspaces), the root `package.json` often has no `version` field — this is expected, not an error.
125
- - Rust: `Cargo.toml` (`[package] version`)
126
- - Go: `go.mod` (module version if tagged)
119
+ ```bash
120
+ uv run {extractPublicApiHelper} --mode quick --language "{brief.language}" --source-root "{source_root}" --manifest-file "<version file>" --resolve-version
121
+ ```
127
122
 
128
- **If a source version is found AND it differs from `brief.version`:**
123
+ **A remote source with no local tree** (Quick tier, or one §2b could not read into a tree): first stage the version file with `{githubFetchHelper}` (resolve it ← the first existing path of `{project-root}/_bmad/skf/shared/scripts/skf-github-fetch.py` and `{project-root}/src/shared/scripts/skf-github-fetch.py`), then run the call above with `--source-root "{run_dir}/files"`:
129
124
 
130
- ⚠️ Warn the user: "Brief version ({brief.version}) differs from source version ({source_version}). Using source version ({source_version})."
125
+ ```bash
126
+ gh api "repos/{owner}/{repo}/git/trees/{source_ref}?recursive=1" --jq '.tree[] | select(.type == "blob") | .path' | uv run {githubFetchHelper} --repo "{owner}/{repo}" --ref "{source_ref}" --tree-file - --dest "{run_dir}/files" "<version file>"
127
+ ```
131
128
 
132
- Update the working version in context to the source version. Record the mismatch in context for the evidence report (step 8).
129
+ **The source version** is the JSON's `version` when its `version_source` is `manifest` or starts with `file:`. Keep `brief.version`, with no mismatch warning, when `version` is null or `version_source` starts with `tag:` (no file of the source states that version), and when a call exits non-zero, prints no JSON or resolves no path. Record the JSON's `warnings` in context for the evidence report (step 8).
133
130
 
134
- **If no version file is found or version cannot be extracted:** keep `brief.version` as-is. No warning needed.
135
-
136
- **If source is remote and accessed via Quick tier (gh_bridge, no local files):** attempt to read the version file via `gh_bridge.read_file(owner, repo, "{version_file}")` — resolved as `gh api repos/{owner}/{repo}/contents/{version_file}` or direct file read if local (see `knowledge/tool-resolution.md`) — for the primary version file of the detected language. If the read fails, keep `brief.version`.
131
+ **If the source version differs from `brief.version`:** ⚠️ Warn the user: "Brief version ({brief.version}) differs from source version ({source_version}). Using source version ({source_version})." Update the working version in context to the source version, and record the mismatch in context for the evidence report (step 8).
@@ -207,7 +207,7 @@ SKF_T3
207
207
  **Zero-content check.** Run `uv run {extractionInventoryHelper} summary --inventory "{extraction_inventory}"` from `{project-root}` and read its `counts`:
208
208
 
209
209
  - **`source_type: "docs-only"`, no URL or subpage fetched in this run, and `counts.items` is 0:** there is nothing to compile, and a docs-only skill has no other source. Stage and promote nothing: **HARD HALT** (exit code 3, `docs-unreachable`, phase `fetch-docs`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "**No documentation could be fetched for `{skill-name}`, so there is nothing to compile.** Failed: {each `doc_urls` URL with its reason}. Check the network and that this environment has a web fetch tool, or fix `doc_urls` in the brief, then re-run create-skill."
210
- - **`source_type: "source"`, `counts.exports` is 0 and every `doc_urls` fetch failed:** step 3 §6's zero-export check let this brief through because it declares `doc_urls`. Apply that check here: show its warning and offer **[C] Continue anyway** or **[R] Refine the brief**: stop here. **GATE [default: C]**: under `{headless_mode}`, log and record its `zero-exports` decision as step 3 §6 says, and continue. On [R], **HARD HALT** (exit code 6, `halted-for-brief-refinement`, phase `fetch-docs`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "**Halting create-skill:** no public export was extracted and no `doc_urls` page could be fetched. Fix `scope.include`, `target_version` or `doc_urls` in the brief (or re-run `skf-brief-skill`), then re-run `skf-create-skill`."
210
+ - **`source_type: "source"`, `counts.exports` is 0 (or equals `counts.not_public`) and every `doc_urls` fetch failed:** step 3 §6's zero-export check let this brief through because it declares `doc_urls`. Apply that check here: show its warning and offer **[C] Continue anyway** or **[R] Refine the brief**: stop here. **GATE [default: C]**: under `{headless_mode}`, log and record its `zero-exports` decision as step 3 §6 says, and continue. On [R], **HARD HALT** (exit code 6, `halted-for-brief-refinement`, phase `fetch-docs`; stage `{run_dir}/halt.json` per the Workflow Rules and run `uv run {emitEnvelopeHelper} emit-halt --workflow skf-create-skill --run-dir "{run_dir}" --target stderr < "{run_dir}/halt.json"`): "**Halting create-skill:** no public export was extracted (or, for a `public-api` brief, every export found is off the public surface: the `Off the public surface` warning names them) and no `doc_urls` page could be fetched. Fix `scope.include`, `target_version` or `doc_urls` in the brief (or re-run `skf-brief-skill`), then re-run `skf-create-skill`."
211
211
  - **Otherwise:** continue. When no page of this run was saved (every fetch failed, or no fetch tool), skip §5b and §5c, which have nothing to index or remove, and go on to §6: `{docs_staging}` stays as it was.
212
212
 
213
213
  ### 5b. Index into QMD (Deep Tier Only)
@@ -80,10 +80,10 @@ The sink, `{run_dir}/warnings.jsonl`, is the list: step 7 §8b lists it in `evid
80
80
  3. **Preferences fallback.** Otherwise, read `headless_mode` from `{sidecar_path}/preferences.yaml` (`true` or `false`).
81
81
  4. **Default:** `false`.
82
82
 
83
- 3. **Resolve workflow customization.** Run:
83
+ 3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
84
84
 
85
85
  ```bash
86
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
86
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
87
87
  ```
88
88
 
89
89
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-create-stack-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. In that case keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): `references/init.md` records it as the run's first warning once its pre-flight has created `{run_dir}`.
@@ -114,7 +114,7 @@ Take the first-line format, `|IMPORTANT:` second-line, and token-estimate checks
114
114
 
115
115
  Then verify the two stack-specific rows the generic validator does not cover:
116
116
  - [ ] Stack and integrations lines present
117
- - [ ] Token estimate lands near the ~80-120 design target from step 7 §5 (the validator flags only its wider <40 / >200 bounds; an ~80-150 snippet with an overflow-strategy `workflow_warning` is expected, not a defect)
117
+ - [ ] Token estimate lands near the ~80-120 design target from step 7 §5 (the validator flags only its wider <40 / >300 bounds; an ~80-150 snippet with an overflow-strategy `workflow_warning` is expected, not a defect)
118
118
 
119
119
  Record format violations as **WARNING** findings.
120
120
 
@@ -59,10 +59,10 @@ Headless callers and pipelines: the inputs, flags, gate map, exit codes and `SKF
59
59
 
60
60
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
61
61
 
62
- 3. **Resolve workflow customization.** Run:
62
+ 3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
63
63
 
64
64
  ```bash
65
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
65
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
66
66
  ```
67
67
 
68
68
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-drop-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): the run folder does not exist yet, so step 4 records it.
@@ -12,6 +12,10 @@ skillInventoryProbeOrder:
12
12
  dropRosterHelper: 'scripts/drop-roster.py'
13
13
  # {dirSizesHelper}: the §4 `disk_freed` label.
14
14
  dirSizesHelper: 'scripts/dir-sizes.py'
15
+ # {atomicWriteHelper}: the §4 manual fixes that point `active` at a version.
16
+ atomicWriteProbeOrder:
17
+ - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
18
+ - '{project-root}/src/shared/scripts/skf-atomic-write.py'
15
19
  ---
16
20
 
17
21
  <!-- Config: communicate in {communication_language}. -->
@@ -144,7 +148,7 @@ Report: "**Rebuilt managed sections in:** {list of updated files}. {if any faile
144
148
  --skill-group {skills_output_folder}/{target_skill} \
145
149
  --version {new_active_version}
146
150
  ```
147
- Record a `mismatch`/`missing-target` exit (code 2), or a missing helper (no probe candidate), as above with the manual fix `ln -sfn {new_active_version} {skills_output_folder}/{target_skill}/active`, and continue.
151
+ In each manual fix below, `{atomicWriteHelper}` is the first existing path in `{atomicWriteProbeOrder}`, or its first entry when neither exists (the fix then works once SKF is re-installed); the same command works in Git Bash, PowerShell and WSL2. Record a `mismatch`/`missing-target` exit (code 2), or a missing helper (no probe candidate), as above with the manual fix `uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target "{new_active_version}"`, and continue. Record a `busy` exit (code 2, another run was moving the link) the same way, with the manual fix "once the other run has finished, if `active` still names the dropped version, run `uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target "{new_active_version}"`". Record any other non-zero exit, or no JSON on stdout (exit 1, or an exit 2 that prints no JSON: the helper could not run, could not move the link, or failed after moving it, and the last line it prints on stderr says why), the same way, with the manual fix "if `active` still names the dropped version, run `uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target "{new_active_version}"`" followed by that line in parentheses, or by `(exit <code>)` when the helper prints nothing on stderr.
148
152
  - **No non-deprecated versions remain** (`resolve.counts.non_deprecated` is `0`, reachable only when dropping the sole surviving version, permitted in step 1 because no other non-deprecated versions existed): remove the now-dangling `active` symlink with a single atomic unlink of the link itself: `rm {skills_output_folder}/{target_skill}/active` (unlink removes only the symlink, never its target). When it fails, record it as above with the manual fix `rm {skills_output_folder}/{target_skill}/active`.
149
153
 
150
154
  3. Format the size of the deleted folders through the sizing helper; do not add or round in-prompt:
@@ -10,3 +10,5 @@ nextStepFile: 'shared/health-check.md'
10
10
  # Step 4: Workflow Health Check
11
11
 
12
12
  This is the terminal step of drop-skill. Load `{nextStepFile}`, read it fully, then execute it — do nothing else here (no user-facing reports, file writes, or result contracts; those were step 3). After the shared health check completes, the workflow is done.
13
+
14
+ In a headless run report.md bound `{result_envelope_line}` to the `SKF_DROP_SKILL_RESULT_JSON` line without displaying it: the shared health check displays that line verbatim as the run's last line wherever it stops, so a `claude -p` caller reads the envelope as the final message.
@@ -30,7 +30,7 @@ Every hard HALT exits with a stable code; the step files name the exact `halt_re
30
30
 
31
31
  The single-line JSON contract every headless run of skf-drop-skill prints. The shared emitter, `shared/scripts/skf-emit-result-envelope.py`, builds each line from the payload the emitting step hands it and checks it against `shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json`, so no stage types an envelope and a HALT prints the right shape whatever else is still in context.
32
32
 
33
- When `{headless_mode}` is true, step 3 prints this envelope on **stdout** before chaining to step 4, a `--dry-run` prints it on **stdout** at the step 1 confirmation gate, and every HARD HALT prints it on **stderr** with `status: "error"`:
33
+ When `{headless_mode}` is true, step 3 prints this envelope on **stdout** and the shared health check (step 4) displays it as the run's last line, a `--dry-run` prints it on **stdout** at the step 1 confirmation gate and displays it last there, and every HARD HALT prints it on **stderr** with `status: "error"`:
34
34
 
35
35
  ```
36
36
  SKF_DROP_SKILL_RESULT_JSON: {"status":"success|error|dry-run","skill":"…|null","drop_mode":"…|null","versions_affected":[],"files_deleted":[],"would_delete":[],"forge_left_in_place":null,"manifest_updated":false,"result_path":null,"exit_code":0,"halt_reason":null,"error":null}
@@ -121,7 +121,7 @@ Then run, in every mode:
121
121
  uv run {emitEnvelopeHelper} emit --workflow skf-drop-skill --run-dir "{run_dir}" --result-dir "{skills_output_folder}" < "{run_dir}/result-context.json"
122
122
  ```
123
123
 
124
- The emitter stamps the UTC time, the run id, the run's warnings and the auto-decisions select.md recorded (`headless_decisions`) into the record, writes it as `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json`, copies it to `{skills_output_folder}/drop-skill-result-latest.json` (the stable path pipelines read), and prints the `SKF_DROP_SKILL_RESULT_JSON:` line on stdout (field rules in `references/invocation-contract.md`). Bind `{result_json_path}` ← that line's `result_path`: null when the record could not be written, and the line's `warnings` then say why. When `{headless_mode}` is true, display the line verbatim before chaining to step 4. If the helper exits non-zero, correct `result-context.json` from the `message` of its stderr JSON and run it once more; if it fails again, display "The drop result record could not be written: {message}" and go on, since the drop itself is complete.
124
+ The emitter stamps the UTC time, the run id, the run's warnings and the auto-decisions select.md recorded (`headless_decisions`) into the record, writes it as `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json`, copies it to `{skills_output_folder}/drop-skill-result-latest.json` (the stable path pipelines read), and prints the `SKF_DROP_SKILL_RESULT_JSON:` line on stdout (field rules in `references/invocation-contract.md`). Bind `{result_json_path}` ← that line's `result_path`: null when the record could not be written, and the line's `warnings` then say why. When `{headless_mode}` is true, bind `{result_envelope_line}` ← the 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. If the helper exits non-zero, correct `result-context.json` from the `message` of its stderr JSON and run it once more; if it fails again, leave `{result_envelope_line}` empty, display "The drop result record could not be written: {message}" and go on, since the drop itself is complete.
125
125
 
126
126
  ### Post-drop hook (optional)
127
127
 
@@ -131,7 +131,7 @@ If `{onCompleteCommand}` is non-empty (resolved at SKILL.md On Activation §3 fr
131
131
  {onCompleteCommand} --result-path={result_json_path}
132
132
  ```
133
133
 
134
- When it exits non-zero, display "on_complete hook failed (exit {code}): {its first stderr line}" as a line of its own (after the envelope line when `{headless_mode}` is true), and never fail the workflow on a hook error: the drop has already completed and may be irreversible. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
134
+ When it exits non-zero, display "on_complete hook failed (exit {code}): {its first stderr line}" as a line of its own, and never fail the workflow on a hook error: the drop has already completed and may be irreversible. When `{onCompleteCommand}` is empty (bundled default), skip the invocation entirely.
135
135
 
136
136
  Whether or not a hook ran, then delete the run folder, whose payload the emitter has read: `rm -rf "{run_dir}"`.
137
137
 
@@ -8,6 +8,10 @@ skillInventoryProbeOrder:
8
8
  - '{project-root}/src/shared/scripts/skf-skill-inventory.py'
9
9
  # {dirSizesHelper}: the §9b size of the folders in scope.
10
10
  dirSizesHelper: 'scripts/dir-sizes.py'
11
+ # {atomicWriteHelper}: the §7 recovery that points `active` at the version to keep.
12
+ atomicWriteProbeOrder:
13
+ - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
14
+ - '{project-root}/src/shared/scripts/skf-atomic-write.py'
11
15
  ---
12
16
 
13
17
  <!-- Config: communicate in {communication_language}. -->
@@ -167,11 +171,11 @@ Set `target_versions = "all"` and `is_skill_level = true`.
167
171
  "**Cannot drop the active version `{version}`.**
168
172
  Other non-deprecated versions of `{target_skill}` still exist. To proceed, either:
169
173
 
170
- **(a)** Make the version you keep active first: point the `active` link at it (`ln -sfn <version to keep> {skills_output_folder}/{target_skill}/active`), run `[EX] Export Skill` for `{target_skill}`, which exports that version and archives `{version}`, then return here to drop `{version}`, OR
174
+ **(a)** Make the version you keep active first: point the `active` link at it (`uv run {atomicWriteHelper} flip-link --link "{skills_output_folder}/{target_skill}/active" --target <version to keep>`), run `[EX] Export Skill` for `{target_skill}`, which exports that version and archives `{version}`, then return here to drop `{version}`, OR
171
175
 
172
176
  **(b)** Use the `[A] All versions` option to drop every version of `{target_skill}` at once."
173
177
 
174
- In (a), `<version to keep>` is the first `{version_rows}` entry other than `{version}` whose `in_manifest` and `on_disk` are true and whose `status` is not `"deprecated"` (the rows are newest first); with no such entry, show `<version to keep>` as written.
178
+ In (a), `<version to keep>` is the first `{version_rows}` entry other than `{version}` whose `in_manifest` and `on_disk` are true and whose `status` is not `"deprecated"` (the rows are newest first); with no such entry, show `<version to keep>` as written. `{atomicWriteHelper}` is the first existing path in `{atomicWriteProbeOrder}`, or its first entry when neither exists.
175
179
 
176
180
  HALT (exit code 5, `halt_reason: "active-version-guard-refused"`, phase `select:active-version-guard`), with `skill: "{target_skill}"` and `versions_affected: ["{version}"]` in the §1 halt envelope. Do not proceed.
177
181
 
@@ -305,7 +309,7 @@ When `{headless_mode}` is true, stage the preview as `{run_dir}/result-context.j
305
309
  uv run {emitEnvelopeHelper} emit --workflow skf-drop-skill --run-dir "{run_dir}" < "{run_dir}/result-context.json"
306
310
  ```
307
311
 
308
- Display the line it prints verbatim (the `message` of its stderr JSON when it exits non-zero). A dry run passes no `--result-dir`, so it writes no result file. Then, in either mode, delete the run folder (`rm -rf "{run_dir}"`) and HALT (exit code 0). The manifest, filesystem, and context files are untouched.
312
+ A dry run passes no `--result-dir`, so it writes no result file. Then, in either mode, delete the run folder (`rm -rf "{run_dir}"`) and HALT (exit code 0). In a headless run, display the line the emitter printed verbatim (the `message` of its stderr JSON when it exits non-zero) after the delete and before the HALT, with nothing after it, so a `claude -p` caller reads it as the final message. The manifest, filesystem, and context files are untouched.
309
313
 
310
314
  **Otherwise (not a dry-run), GATE [default: Y]:** If `{headless_mode}`: auto-proceed with [Y], record `confirm_source = "headless-auto"`, stage `{run_dir}/decision.json` as `{"gate": "select.confirm", "default_action": "Y", "taken_action": "Y", "reason": "headless auto-confirm"}` and run the §8 `record` command, and log: "headless: auto-confirmed drop of {target_skill}"
311
315
 
@@ -58,10 +58,10 @@ Headless callers and pipelines: the inputs, flags, gates, outputs and exit codes
58
58
 
59
59
  2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `{sidecar_path}/preferences.yaml`. Default: false.
60
60
 
61
- 3. **Resolve workflow customization.** Run:
61
+ 3. **Resolve workflow customization.** Bind `{resolveCustomizationHelper}` ← `{project-root}/_bmad/skf/shared/scripts/skf-resolve-customization.py`, else `{project-root}/src/shared/scripts/skf-resolve-customization.py` (the first that exists), and run:
62
62
 
63
63
  ```bash
64
- uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
64
+ uv run {resolveCustomizationHelper} --skill {skill-root} --project-root {project-root} --key workflow
65
65
  ```
66
66
 
67
67
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-export-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Keep the reason as `{customization_resolver_unavailable}` (unset when the resolver ran): a headless run records it once step 4 creates its run folder, and an interactive run only prints it.
@@ -34,9 +34,9 @@ The root path in context-snippet.md on disk uses the **`skill_root` of the first
34
34
  - **Line 1:** Skill name + version + platform-aware root path — version signals training data staleness
35
35
  - **Line 2 (IMPORTANT):** Retrieval instruction — always present
36
36
  - **Line 3 (quick-start):** Anchor pointer to Quick Start section
37
- - **Line 4 (api):** Top exports from metadata.json `exports` array (up to 10, with `()` for functions)
37
+ - **Line 4 (api):** The ranked exports (see the api line rule below), with `()` for functions
38
38
  - **Line 5 (key-types):** Anchor pointer + inline summary of most important type/enum values
39
- - **Line 6 (gotchas):** 2-3 most critical pitfalls — derived from T2-future annotations, async requirements, breaking changes
39
+ - **Line 6 (gotchas):** At most 3 pitfalls, separated by `; `, derived from the current sources (see the gotchas line rule below)
40
40
 
41
41
  ## Stack Skill Snippet Template
42
42
 
@@ -50,12 +50,12 @@ The root path in context-snippet.md on disk uses the **`skill_root` of the first
50
50
 
51
51
  ## Rules
52
52
 
53
- - **api line**: Top exports from metadata.json `exports` array (up to 10 for Deep tier, up to 5 for all other tiers). Append `()` to function names. Comma-separated.
53
+ - **api line**: Ranked, so the line stays the same from export to export: the prior snippet's `|api:` names in order, then the names of SKILL.md's `## Key API Summary` rows in row order (a component library's `## Component Catalog` rows, a reference app's `## Pattern Surface` rows), then metadata.json `exports` in order. A row's name is matched without backticks, a module qualifier or generic parameters, and a cell naming several is split on `/`. Each name once, only when `exports` lists it, and never one the current SKILL.md marks deprecated or removed (a deprecation, migration or breaking-changes bullet gives it as the old name, or its Key API Summary row's purpose says deprecated or removed). The first 10 for Deep tier, 5 for all other tiers; above the 300-token ceiling, names drop from the end. Append `()` to a name whose provenance `export_type` is a function, or else when the prior `|api:` line wrote it with `()`. Comma-separated.
54
54
  - **key-types line**: Anchor to `#key-types` section + inline summary (~10 words) of the most important type values
55
- - **gotchas line**: Derived from: T2-future annotations (breaking changes), async requirements, version-specific behavior. If no gotchas available, omit the line.
55
+ - **gotchas line**: At most 3 pitfalls, separated by `; ` (the separator step 3 splits a prior line on). Derived from the current sources: a T2-future annotation in the evidence report (breaking changes, async requirements, version-specific behavior); the current SKILL.md, through a bullet of a section whose heading names gotchas, deprecations, migration, breaking changes or import corrections, a `## Key API Summary` row whose purpose says deprecated or removed, and an async requirement its Quick Start shows; and a prior pitfall either of them still states, kept in its prior wording. The prior pitfalls still stated come first, in their prior order, then the new ones in source order. `[CARRIED]` marks only a prior pitfall no current source backs, which takes a free slot for one export (one-cycle expiry). If no gotchas are available, omit the line.
56
56
  - **stack line**: Component versions from metadata.json `components` for stack skills
57
57
  - **integrations line**: Co-import patterns from metadata.json `integrations` for stack skills
58
58
  - If fewer exports than the limit, list all available
59
59
  - If no exports data available, omit the api line
60
- - Section anchors must be verified against actual SKILL.md headings during generation. For split-body skills (where `references/` exists and `## Full` headings are stubs), if a heading is missing from SKILL.md, rewrite the anchor to point to the reference file path (preferred). Omit the anchor line only if the heading cannot be found in either SKILL.md or `references/*.md`
60
+ - Section anchors must be verified against actual SKILL.md headings during generation. Keep `SKILL.md#{slug}` when SKILL.md has the heading. Only when SKILL.md lacks it and the skill is split-body (`references/` exists and the `## Full` headings are absent or stubs; not a stack skill's structural references), point the anchor at the `references/{file}.md#{slug}` that holds it. Omit the anchor line when neither SKILL.md nor `references/*.md` holds the heading
61
61
  - Skill path is relative to project root and uses the IDE's `skill_root` prefix (see **Skill Root Path** above, and the IDE → Context File Mapping table in `managed-section-format.md`)
@@ -18,7 +18,7 @@ To generate or update context-snippet.md for the skill in the Vercel-aligned ind
18
18
 
19
19
  - Focus only on generating the context-snippet.md content — T1-now only, no T2 annotations
20
20
  - If `passive_context: false` was detected in step 1, skip this step entirely
21
- - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), iterate sections 2 to 5 per skill. Each skill has its own prior-gotchas carry-forward state (§2.5): do not share state across skills. §2.7 resolves `{skill_root}`, the snippet's root only, and §2.8 creates the stage folder once for the run (neither depends on the skill). See step 1 §1c.
21
+ - **Multi-skill mode:** when step 1 loaded more than one skill (`len(skill_batch) > 1`), iterate sections 2 to 5 per skill. Each skill has its own prior snippet state (§2.5: its `prior_gotchas` carry-forward and its `prior_api`): do not share state across skills. §2.7 resolves `{skill_root}`, the snippet's root only, and §2.8 creates the stage folder once for the run (neither depends on the skill). See step 1 §1c.
22
22
  - Every HALT names its exit code, `halt_reason` and phase; in headless mode it first emits its envelope as `references/result-envelope.md` states
23
23
 
24
24
  ## MANDATORY SEQUENCE
@@ -43,11 +43,12 @@ Before generating new snippet content, check for a prior snippet:
43
43
 
44
44
  1. Read `{resolved_skill_package}/context-snippet.md` if it exists (resolved in step 1 — see `knowledge/version-paths.md`)
45
45
  2. If it exists, extract the `|gotchas:` line (if any). Trim leading whitespace and the `|gotchas:` prefix, then capture the remaining content as `prior_gotchas_content`.
46
- 3. **Detect the carry-forward marker:** If `prior_gotchas_content` starts with the token `[CARRIED]` (whitespace-insensitive), set `prior_gotchas_already_carried = true` and strip the marker before storing the remainder. Otherwise set `prior_gotchas_already_carried = false`.
46
+ 3. **Split it into pitfalls and detect the carry-forward marker:** split `prior_gotchas_content` on the pitfall separator {snippetFormatData} gives (`; `), and keep the pitfalls, in order, as `prior_gotchas`. A pitfall that starts with the token `[CARRIED]` (whitespace-insensitive) is **already carried**: strip the marker and record that. A line an older export marked once at its start (`[CARRIED] a, b`) reads as one pitfall, already carried.
47
47
  4. **Distinguish empty from absent:** If the `|gotchas:` line exists but has no non-whitespace content after the prefix, treat it as **absent** — set `prior_gotchas = null`. Only a non-empty value counts as a prior gotchas line worth carrying forward.
48
- 5. If no prior snippet exists at all, set `prior_gotchas = null` and `prior_gotchas_already_carried = false`.
48
+ 5. **Capture the prior api names:** if the prior snippet has an `|api:` line, split its value on commas, drop each name's trailing `()` and surrounding spaces, and capture the names, in order, as `prior_api` (no `()`, no spaces), noting each name the line wrote with `()`. With no `|api:` line, `prior_api` is empty.
49
+ 6. If no prior snippet exists at all, set `prior_gotchas = null` and `prior_api` to an empty list.
49
50
 
50
- These values will be used as a fallback in §3a if new gotchas cannot be derived. The `[CARRIED]` marker provides a **hard one-cycle expiry**: gotchas that were already carried once will be dropped on the next carry-forward attempt rather than preserved indefinitely.
51
+ §3a gives the gotchas line's free slots to the prior pitfalls no current source states, and §3b ranks the `prior_api` names first. The `[CARRIED]` marker provides a **hard one-cycle expiry**: a pitfall already carried once is dropped on the next export rather than preserved indefinitely.
51
52
 
52
53
  ### 2.7. Resolve Skill Root Path
53
54
 
@@ -71,28 +72,34 @@ python3 -c "import tempfile; print(tempfile.mkdtemp(prefix='skf-export-'))"
71
72
 
72
73
  #### 3a. Gotchas (Both Skill Types)
73
74
 
74
- Derive new gotchas from the T2-future annotations in the evidence report at `{forge_evidence_report}` (breaking changes), async requirements and version-specific behavior. Step 1 §2 bound that path; when it is null, the skill has no evidence report to read.
75
+ Derive the gotchas (breaking changes, deprecations, async requirements and version-specific behavior) from the current sources. A gotcha is **derived** when a current source states it:
76
+
77
+ 1. **The evidence report:** a T2-future annotation in the evidence report at `{forge_evidence_report}`, when step 1 §2 bound that path (when it is null, the skill has no evidence report to read).
78
+ 2. **The current SKILL.md** (`{resolved_skill_package}/SKILL.md`, the published, test-verified artifact): a bullet of a section whose heading names gotchas, deprecations, migration, breaking changes or import corrections (such as `## Migration & Deprecation Warnings`), a `## Key API Summary` row whose purpose says deprecated or removed, and an async requirement its Quick Start shows (an API it calls with `await`).
79
+ 3. **A prior pitfall still stated:** a pitfall of `prior_gotchas` (§2.5) that source 1 or 2 still states, kept in its prior wording without the `[CARRIED]` marker.
80
+
81
+ A gotcha the current SKILL.md states is therefore derived even when the evidence report records no T2-future annotation. `[CARRIED]` applies only to a prior pitfall no current source backs.
75
82
 
76
83
  **Detect first-export state before applying carry-forward logic.** The `[CARRIED]` one-cycle expiry is meaningful only on a *re-export*. On a first export, the prior `context-snippet.md` was written by the workflow that built or updated the skill (create-skill, create-stack-skill, quick-skill or update-skill) inside the same forge cycle: those gotchas are freshly derived, not "left over from a previous export." Treating them as carry-forward primes them for premature expiry on the second export.
77
84
 
78
85
  Read the skill's manifest entry: `python3 {manifestOpsHelper} {skills_output_folder} get {skill-name}`. When it returns `not_found`, or an `entry` none of whose `versions` records a `last_exported`, this is a first export: set `is_first_export = true`. Otherwise `is_first_export = false`. The helper returns the entry in the v2 shape whatever is on disk, so a v1 manifest reads the same here as in step 4.
79
86
 
80
- Resolve the gotchas line by this decision tree:
87
+ Resolve the gotchas line by this decision tree. The line holds at most 3 pitfalls, joined by the separator {snippetFormatData} gives (`|gotchas: {pitfall-1}; {pitfall-2}; {pitfall-3}`). A prior pitfall no current source states is **unbacked**.
81
88
 
82
- - **If new gotchas are derived:** Use them (they supersede any prior gotchas). Write as `|gotchas: {pitfall-1}, {pitfall-2}` with no marker.
83
- - **If NO new gotchas are derived AND `is_first_export == true` AND `prior_gotchas` exists:** Treat the prior gotchas as **freshly derived** by the workflow that wrote them, and write them **without** the `[CARRIED]` marker. (The marker only applies to re-exports.) No warning needed; this is the normal first-export shape.
84
- - **If NO new gotchas are derived BUT `prior_gotchas` exists AND `is_first_export == false` AND `prior_gotchas_already_carried == false`:** First carry-forward cycle on a re-export: preserve the prior gotchas line, prefixing the value with `[CARRIED]` so the next export can detect that expiry has been reached. Write as `|gotchas: [CARRIED] {prior gotchas content}`. Emit warning: "**Gotchas preserved from prior export (one-cycle carry-forward).** These gotchas will be DROPPED on the next export unless new gotchas are derived or you manually refresh them. Review now if they are still applicable."
85
- - **If NO new gotchas are derived AND `prior_gotchas` exists AND `prior_gotchas_already_carried == true`:** Expiry reached (re-export only: the first-export branch above takes precedence). Drop the gotchas line entirely. Emit warning: "**Stale gotchas dropped:** the prior gotchas were already carried forward once and cannot be derived from the current evidence report. The snippet now has no gotchas line. If the prior gotchas are still relevant, re-add them to the evidence report's T2-future section and re-run export."
86
- - **If NO new gotchas derived AND no `prior_gotchas`:** Omit the gotchas line.
89
+ - **If gotchas are derived:** Write them first, with no marker: the prior pitfalls a current source still states, in their prior order, then the new ones, in source order (source 1, then source 2 in SKILL.md order), the first 3 of them. Each slot left free goes to an unbacked prior pitfall, in prior order, by the next three branches; when no gotcha is derived, all 3 slots are free.
90
+ - **If an unbacked prior pitfall gets a free slot AND `is_first_export == true`:** Treat it as **freshly derived** by the workflow that wrote it, and write it **without** the `[CARRIED]` marker. (The marker only applies to re-exports.) No warning needed; this is the normal first-export shape.
91
+ - **If an unbacked prior pitfall gets a free slot AND `is_first_export == false` AND it is not already carried:** First carry-forward cycle on a re-export: write it as `[CARRIED] {pitfall}`, beside any derived ones, so the next export can detect that expiry has been reached. Emit warning: "**Gotchas preserved from prior export (one-cycle carry-forward):** {the pitfalls}. They will be DROPPED on the next export unless the current SKILL.md or evidence report states them. Review now if they are still applicable."
92
+ - **If an unbacked prior pitfall is already carried (expiry reached; re-export only, as the first-export branch above takes precedence) or gets no free slot:** Drop it. Emit warning: "**Stale gotchas dropped:** {the pitfalls} no longer appear in the current SKILL.md or evidence report (each was already carried forward once, or the line's 3 slots went to current gotchas). If one is still relevant, re-forge the skill with update-skill so its SKILL.md states it, or add it to the evidence report's T2-future section, then re-run export."
93
+ - **If no pitfall is written:** Omit the gotchas line.
87
94
 
88
95
  #### 3b. Single Skills (`skill_type: "single"`)
89
96
 
90
97
  1. Read metadata.json for `version`, `exports` array
91
- 2. Select top exports (up to 10 for Deep tier, 5 otherwise). Append `()` to function names.
98
+ 2. **Rank the api names**, so the `|api:` line stays the same from export to export while the surface does: first the `prior_api` names (the prior snippet's `|api:` line, §2.5), in order; then the names of the rows of SKILL.md's `## Key API Summary` table, in row order (for a component library, the Key Components of its `## Component Catalog` rows; for a reference app, the surfaces of its `## Pattern Surface` rows); then the metadata.json `exports`, in order. Match a row's name to `exports` without backticks, `()`, a module qualifier (`cognee.add` is `add`) or generic parameters (`Meta<T = Args>` is `Meta`), and split a cell that names several on `/` (a Key Components cell on commas). Take each name once, only when metadata.json `exports` lists it, and never one the current SKILL.md marks deprecated or removed: a bullet of a section whose heading names deprecations, migration or breaking changes gives it as the deprecated, removed or renamed name (not as its replacement), or its `## Key API Summary` row's purpose says deprecated or removed. Keep the first 10 for the Deep tier, 5 for the other tiers. Append `()` to a name whose `export_type` in the provenance map at `{forge_provenance_map}` is a function; when that map is null or gives no kind for the name, keep the `()` the prior `|api:` line gave it.
92
99
  3. Read SKILL.md to extract: heading slugs for `#quick-start` and `#key-types`, inline summary of key types (~10 words)
93
- 4. **Anchor verification (split-body awareness):** For each section anchor (`#quick-start`, `#key-types`), verify the heading exists in SKILL.md. If a `references/` directory exists and `## Full` headings in SKILL.md are absent or stubs (indicating split-body, not a stack skill's structural references), rewrite the anchor to point to the reference file path (e.g., `references/{file}.md#key-types`). If the heading cannot be resolved in either location, omit that anchor line from the snippet.
100
+ 4. **Anchor verification (split-body awareness):** For each section anchor (`#quick-start`, `#key-types`): when SKILL.md has the heading, keep `SKILL.md#{slug}`. Only when SKILL.md lacks the heading and the skill is split-body (a `references/` directory exists and the `## Full` headings in SKILL.md are absent or stubs; not a stack skill's structural references), point the anchor at the `references/{file}.md#{slug}` that holds the heading. When neither holds it, omit that anchor line from the snippet.
94
101
 
95
- Emit the single-skill template from {snippetFormatData} (loaded in §2), filling `api` from the exports selected above (list all if fewer than the limit; omit the line if there are none), `key-types` from the inline summary extracted above, and `gotchas` per the §3a decision tree (omit the line when it resolves to none).
102
+ Emit the single-skill template from {snippetFormatData} (loaded in §2), filling `api` from the names ranked above (all of them when fewer than the limit; omit the line when there are none), `key-types` from the inline summary extracted above, and `gotchas` per the §3a decision tree (omit the line when it resolves to none).
96
103
 
97
104
  #### 3c. Stack Skills (`skill_type: "stack"`)
98
105
 
@@ -112,7 +119,7 @@ Bind `{count}` ← the `tokens` value of its `context-snippet.md` row (`len(text
112
119
 
113
120
  - Target: ~80-120 tokens per skill (aspirational for Quick/Forge tiers)
114
121
  - Hard ceiling: 300 tokens (Deep tier may legitimately exceed 120 when gotchas carry load-bearing breaking-change notices)
115
- - If `{count}` is above 300, trim the description, the exports list or the refs, write the draft again and measure it again, until `{count}` is 300 or below. **Do NOT drop gotchas to fit**: gotchas exist precisely to deliver the "do not rely on training data" signal and are the last thing to cut
122
+ - If `{count}` is above 300, drop api names from the end of the §3b ranking, one at a time, writing the draft again and measuring it again after each, until `{count}` is 300 or below; only when the api line is gone and `{count}` is still above 300, shorten the key-types summary the same way. **Do NOT drop gotchas to fit**: gotchas exist precisely to deliver the "do not rely on training data" signal and are the last thing to cut
116
123
 
117
124
  When the helper exits non-zero, delete the `{export_stage_dir}` folder and HALT (exit code 4, `halt_reason: "context-rebuild-failed"`, phase `generate-snippet §4`): "SKF cannot measure the snippet: {the helper's error}. Re-install SKF and re-run the export."
118
125
 
@@ -99,7 +99,7 @@ The inventory helper chooses the version to export and binds its paths by the Re
99
99
  uv run {skillInventoryHelper} resolve "{skills_output_folder}" --skill {skill-name} --forge-data-folder "{forge_data_folder}"
100
100
  ```
101
101
 
102
- Bind from its `resolve` object `{resolved_version}` ← `chosen_version`, `{resolved_skill_package}` ← `skill_package`, `{forge_version}` ← `forge_version` (the version's forge folder) and `{forge_evidence_report}` ← `paths.evidence_report.path` (that folder's `evidence-report.md`, else the flat copy an older skill keeps, else null), which step 3 derives the gotchas from. Log each `resolve.errors` entry, and `resolve.manifest_error` when it is set, as an Info note, then act on `reason`:
102
+ Bind from its `resolve` object `{resolved_version}` ← `chosen_version`, `{resolved_skill_package}` ← `skill_package`, `{forge_version}` ← `forge_version` (the version's forge folder), `{forge_evidence_report}` ← `paths.evidence_report.path` (that folder's `evidence-report.md`, else the flat copy an older skill keeps, else null), which step 3 derives the gotchas from, and `{forge_provenance_map}` ← `paths.provenance_map.path` (that folder's `provenance-map.json`, else the flat copy, else null), whose `export_type` gives step 3 the api line's `()`. Log each `resolve.errors` entry, and `resolve.manifest_error` when it is set, as an Info note, then act on `reason`:
103
103
 
104
104
  1. `manifest-and-link`, `manifest` or `link`: the export manifest or the `active` link names the version, and its package is on disk. Continue.
105
105
  2. `manifest-lags-link` (the Manifest-lag guard): the `active` link names a version the manifest has not caught up with, as when a create or update run flipped `active` after the last export (the SS→TS→EX order). Continue with the link's version, and emit the helper's `detail` as an Info note, followed by "Exporting v{resolved_version}: the manifest's active_version advances to it on this export." Step 4 publishes the version in `{resolved_skill_package}/metadata.json`, so the manifest catches up here.