bmad-module-skill-forge 3.0.0 → 4.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 (93) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/examples.md +1 -1
  4. package/docs/troubleshooting.md +8 -0
  5. package/docs/verifying-a-skill.md +7 -3
  6. package/docs/workflows.md +3 -3
  7. package/package.json +1 -1
  8. package/src/knowledge/qmd-registry.md +1 -1
  9. package/src/knowledge/version-paths.md +1 -1
  10. package/src/shared/health-check.md +2 -50
  11. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
  12. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +2 -2
  13. package/src/shared/scripts/skf-atomic-write.py +212 -59
  14. package/src/shared/scripts/skf-build-change-manifest.py +593 -47
  15. package/src/shared/scripts/skf-classify-changed-files.py +78 -0
  16. package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
  17. package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
  18. package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
  19. package/src/shared/scripts/skf-extract-public-api.py +1309 -157
  20. package/src/shared/scripts/skf-extraction-inventory.py +580 -46
  21. package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
  22. package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
  23. package/src/shared/scripts/skf-github-probe.py +47 -8
  24. package/src/shared/scripts/skf-hash-content.py +14 -8
  25. package/src/shared/scripts/skf-load-provenance.py +3 -1
  26. package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
  27. package/src/shared/scripts/skf-preflight.py +15 -1
  28. package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
  29. package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
  30. package/src/shared/scripts/skf-structural-diff.py +133 -18
  31. package/src/shared/scripts/skf-update-active-symlink.py +115 -62
  32. package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
  33. package/src/shared/scripts/skf-verify-provenance-completeness.py +977 -49
  34. package/src/shared/scripts/skf-write-skill-brief.py +41 -8
  35. package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
  36. package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
  37. package/src/skf-audit-skill/references/headless-contract.md +1 -1
  38. package/src/skf-audit-skill/references/health-check.md +2 -0
  39. package/src/skf-audit-skill/references/init.md +2 -1
  40. package/src/skf-audit-skill/references/re-index.md +14 -9
  41. package/src/skf-audit-skill/references/report.md +2 -2
  42. package/src/skf-audit-skill/references/semantic-diff.md +15 -15
  43. package/src/skf-audit-skill/references/structural-diff.md +10 -5
  44. package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
  45. package/src/skf-brief-skill/SKILL.md +1 -1
  46. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  47. package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -9
  48. package/src/skf-brief-skill/references/analyze-target.md +6 -6
  49. package/src/skf-brief-skill/references/confirm-brief.md +1 -1
  50. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  51. package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
  52. package/src/skf-brief-skill/references/version-resolution.md +5 -9
  53. package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
  54. package/src/skf-create-skill/assets/skill-sections.md +6 -6
  55. package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
  56. package/src/skf-create-skill/references/compile.md +31 -60
  57. package/src/skf-create-skill/references/component-extraction.md +3 -3
  58. package/src/skf-create-skill/references/enrich.md +2 -2
  59. package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
  60. package/src/skf-create-skill/references/extract.md +22 -19
  61. package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
  62. package/src/skf-create-skill/references/extraction-patterns.md +8 -9
  63. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  64. package/src/skf-create-skill/references/report.md +16 -5
  65. package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
  66. package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
  67. package/src/skf-drop-skill/references/execute.md +5 -1
  68. package/src/skf-drop-skill/references/health-check.md +2 -0
  69. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  70. package/src/skf-drop-skill/references/report.md +2 -2
  71. package/src/skf-drop-skill/references/select.md +7 -3
  72. package/src/skf-test-skill/references/coherence-check.md +1 -1
  73. package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
  74. package/src/skf-test-skill/references/coverage-check.md +39 -57
  75. package/src/skf-test-skill/references/init.md +3 -5
  76. package/src/skf-test-skill/references/report.md +10 -8
  77. package/src/skf-test-skill/references/scoring-rules.md +6 -5
  78. package/src/skf-test-skill/references/source-access-protocol.md +14 -17
  79. package/src/skf-test-skill/scripts/build-result-context.py +114 -21
  80. package/src/skf-test-skill/scripts/gap-ledger.py +315 -30
  81. package/src/skf-test-skill/scripts/load-coverage-inputs.py +534 -96
  82. package/src/skf-test-skill/scripts/reconcile-coverage.py +276 -2
  83. package/src/skf-test-skill/scripts/score-signatures.py +36 -16
  84. package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
  85. package/src/skf-update-skill/references/detect-changes.md +32 -13
  86. package/src/skf-update-skill/references/gap-driven.md +3 -3
  87. package/src/skf-update-skill/references/init.md +6 -2
  88. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  89. package/src/skf-update-skill/references/merge.md +2 -2
  90. package/src/skf-update-skill/references/re-extract.md +2 -2
  91. package/src/skf-update-skill/references/report.md +3 -1
  92. package/src/skf-update-skill/references/write.md +43 -23
  93. package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
@@ -158,9 +158,9 @@ rm -f "{extraction_json}"
158
158
  uv run {extractPublicApiHelper} --mode full --source-root "{source_root}" --files-from "{scan_list}" --recipe-set component-library --scope-type component-library --tier {tier} -o "{extraction_json}"
159
159
  ```
160
160
 
161
- Act on the JSON's `status`, never on the exit code. **On `ok` or `incomplete`**, each of the `exports[]` in `{extraction_json}` is T1: it carries its `citation` (`[AST:{file}:L{line}]`), `ast_recipe`, `ast_node_type`, `export_type`, `confidence` and `extraction_method`, and the steps below sort the exports by `ast_recipe`. Read by eye, as T1-low, each file its `file_issues` lists as `syntax-errors` or `not-utf8`, where a recipe can miss an export. A `no-recipes` issue is a file no recipe reads, such as a stylesheet: there is nothing to extract from it. A `missing` or `unreadable` issue is a listed file the runner could not read: record it for the evidence report as not extracted. Step 3 §5's `init` records the runner's warnings, and Gate 2 shows them: the head-cap warning when `truncated` is true (a recipe, which `recipes[]` marks, found more matches than the head cap keeps), and each `errors[]` item of an `incomplete` run (an ast-grep run failed or timed out, so its files may be unread).
161
+ Act on the JSON's `status`, never on the exit code. **On `incomplete`**, remove the JSON and run the command once more, then act on the second run's JSON. **On `ok` or `incomplete`**, each of the `exports[]` in `{extraction_json}` is T1: it carries its `citation` (`[AST:{file}:L{line}]`), `ast_recipe`, `ast_node_type`, `export_type`, `confidence` and `extraction_method`, and the steps below sort the exports by `ast_recipe`, and an export read by eye, which has none, by its shape, as at Quick tier (a `*Props` interface read by eye is a Props contract for Steps 1 and 3). Read by eye, as T1-low, each file its `file_issues` lists as `syntax-errors` or `not-utf8`, where a recipe can miss an export, and, when the run is still incomplete, each file an `errors[]` item lists in `unread` (a failed call can keep some of a file's exports and lose the rest). A `no-recipes` issue is a file no recipe reads, such as a stylesheet: there is nothing to extract from it. A `missing` or `unreadable` issue is a listed file the runner could not read: record it for the evidence report as not extracted. Step 3 §5's `init` records the runner's warnings, and Gate 2 shows them: the head-cap warning when `truncated` is true (a recipe, which `recipes[]` marks, found more matches than the head cap keeps), and each `errors[]` item of an `incomplete` run (an ast-grep run failed or timed out, so its files were read by eye).
162
162
 
163
- **On `no-ast-grep`, or with no JSON at `{extraction_json}`** (an input error, or no candidate resolves): load `{extractionPatternsData}`, which holds every recipe the steps below name and the forms each does not cover (Known Limitation #11), and run each of them as its AST Extraction Protocol says: its **When the Runner Cannot Run** loads the recipes' notes, the languages each runs in among them.
163
+ **On `no-ast-grep`, or with no JSON at `{extraction_json}`** (an input error, or no candidate resolves): load `{extractionPatternsData}`, which holds every recipe the steps below name and the forms each does not cover (Known Limitation #11), and run each of them as its AST Extraction Protocol says: its **When the Runner Cannot Run** loads the recipes' notes, the languages each runs in among them. When that section warns that the runner failed (no JSON, and a stderr line that holds `error:`), send the warning with step 3 §5's `add --field warnings`, so Gate 2 and the evidence report show it.
164
164
 
165
165
  **Step 1 (extract Props interfaces):**
166
166
 
@@ -217,7 +217,7 @@ Display: "**Variant consolidation: {unique} unique components across {variant_co
217
217
 
218
218
  ### Phase 6: Build Component Extraction Results
219
219
 
220
- Step 3 §5 takes the runner's exports from `{extraction_json}` as it wrote them. Prepare what it sends on top: an entry below for each export read by eye (Quick tier, a `file_issues` file, or the runner's fallback), each Props interface's fields as its `params`, and `component_catalog[]`; keep the aggregate counts below in context for step 5's `metadata.json` stats:
220
+ Step 3 §5 takes the runner's exports from `{extraction_json}` as it wrote them. Prepare what it sends on top: an entry below for each export read by eye (Quick tier, a `file_issues` file, a file an `errors[]` item lists in `unread`, or the runner's fallback), each Props interface's fields as its `params`, and `component_catalog[]`; keep the aggregate counts below in context for step 5's `metadata.json` stats:
221
221
 
222
222
  **Per-export entry (for Props interfaces — primary API):**
223
223
 
@@ -61,11 +61,11 @@ Then immediately load, read entire file, then execute `{nextStepFile}`.
61
61
 
62
62
  ### 3. QMD Enrichment Searches (Deep Tier Only)
63
63
 
64
- For each major exported function (the **top-level public API surface**, typically the 10-20 functions that will appear in the context-snippet, not all extracted exports), search `{enrichment_collections}` for context. Every query below passes them as its collections, so no other skill's issues or docs can annotate this one:
64
+ For each major exported function (the **top-level public API surface**, typically the 10-20 functions that will appear in the context-snippet, not all extracted exports, and never one the inventory marks `public: false`, off a `public-api` skill's public surface), search `{enrichment_collections}` for context. Every query below passes them as its collections, so no other skill's issues or docs can annotate this one:
65
65
 
66
66
  **Search query construction:**
67
67
 
68
- Read the functions and their source files from `{extraction_inventory}`, the extraction inventory step 3 §5 wrote. For each function, derive the **module context** from its source file path (e.g., `src/graph/neo4j/index.ts` → module context `graph neo4j`). This context improves search relevance by scoping results to the function's subsystem without adding extra queries.
68
+ Read the functions and their source files from `{extraction_inventory}`, the extraction inventory step 3 §5 wrote, leaving out each export marked `public: false`. For each function, derive the **module context** from its source file path (e.g., `src/graph/neo4j/index.ts` → module context `graph neo4j`). This context improves search relevance by scoping results to the function's subsystem without adding extra queries.
69
69
 
70
70
  **Primary searches (BM25 — always runs, no GPU/VRAM dependency):**
71
71
 
@@ -2,18 +2,35 @@
2
2
 
3
3
  # Entry Points by Hand
4
4
 
5
- Step 3 §4b loads this file only when the recipe runner did not diff the entry points: at Quick tier, for extraction by source reading, for a brief whose language no recipe reads, and in the AST Extraction Protocol's fallback. Read the entry points yourself and compare them with the extracted exports, then count `exports_public_api` (the entry points' public names) and `exports_internal` (every other non-underscore export) for step 5 §4:
5
+ Where the recipe runner gives no file list or did not diff the entry points, `{extractPublicApiHelper}`'s `--mode denominator` lists the files in scope, counts the public names you find, diffs them with the exports extraction found and decides whether to store `effective_denominator`, by the runner's own rules: no list, count or diff is made by hand. Create-skill's step 3 runs step 1 below at §2 and steps 2 to 4 at §4b; update-skill's write.md §2 runs them for the same cases. Run each call from `{project-root}`. `<the brief>` and `<the JSON>` are create-skill's `{brief_path}` and `{scope_json}` (`{project-root}/_bmad-output/.skf-stage/{skill-name}.scope.json`), and update-skill's `{forge_data_folder}/{skill_name}/skill-brief.yaml` (leave `--brief` out when that file does not exist) and `{run_dir}/denominator.json`.
6
6
 
7
- - **Python:** Read `{source_root}/__init__.py`: extract imports to build the public export list. Compare against AST results:
8
- - In AST but not entry point → mark as internal (exclude from `metadata.json` exports)
9
- - In entry point but not AST → flag as extraction gap (trace via re-export protocol)
10
- - **TypeScript/JS:** Read `index.ts`/`index.js`: same comparison logic.
11
- - **Rust:** Read `lib.rs`: extract `pub use` items. Same logic. **Go:** Scan for exported (capitalized) identifiers.
7
+ 1. **The files in scope and the entry points**, unless a runner JSON gives them (its `no-ast-grep` result too):
8
+
9
+ ```bash
10
+ rm -f "<the JSON>"
11
+ echo '{}' | uv run {extractPublicApiHelper} --mode denominator --source-root "{source_root}" [--brief "<the brief>"] -o "<the JSON>"
12
+ ```
13
+
14
+ For a remote source with no local tree, pipe it `gh api "repos/{owner}/{repo}/git/trees/{source_ref}?recursive=1" --jq '.tree[] | select(.type == "blob") | .path'` in place of the `echo`, with `--tree-file -` in place of `--source-root`. The filtered file list is its `files_in_scope_list` (the files a recipe reads) and `files_without_recipes_list` (those in a language no recipe reads): record "**Filtered file count: {`files_in_scope`} files in scope**". The entry points are its `entry_points.files`; a remote source has none: find them with the reads below.
15
+ 2. **Their public names, each at the file that defines it:**
16
+ - **Python, JavaScript or TypeScript, from a local source:** run `uv run {extractPublicApiHelper} --mode quick --language {language} --source-root "{source_root}" --entry-file <entry-file>`, one `--entry-file` for each entry file of one package, and save its JSON beside `<the JSON>`: each export carries the file that defines it (`defined_in`). Read by eye only the statements its `warnings` name.
17
+ - **Otherwise,** read each entry point: **Python:** `__init__.py`, its imports. **TypeScript/JS:** `index.ts`/`index.js`. **Rust:** `lib.rs`, its `pub use` items. **Go:** the exported (capitalized) identifiers.
18
+ - **A submodule the entry point exports by name** (`from . import sub`, `export * as ns`, a Rust `pub mod`) stands for the names its module passes on: give it its `members`, each at its own file.
19
+ 3. **The counts and the diff.** Send each name you read by eye in `public` and each export extraction read by eye in `exports`, as `{"name", "file", "language"}` with `file` relative to `{source_root}` and written with `/`:
20
+
21
+ ```bash
22
+ uv run {extractPublicApiHelper} --mode denominator --source-root "{source_root}" [--brief "<the brief>"] [--quick "<a quick JSON>"]... [--extraction "<the runner's JSON>"] -o "<the JSON>" <<'SKF_NAMES'
23
+ {"public": [...], "exports": [...]}
24
+ SKF_NAMES
25
+ ```
26
+
27
+ Pass each quick JSON of step 2, and `--extraction` when the runner wrote a JSON with status `ok` or `incomplete` (create-skill's `{extraction_json}`, update-skill's `{run_dir}/public-api.json`): its names count too, each once with yours. Leave out `--source-root` for a remote source with no local tree. Its `counts` and `arms` stand in for the runner's. Its `entry_point_diff` replaces a compare by eye: trace each `extraction_gaps` name (re-export protocol), and send each `internal` export with `"internal": true` (create-skill's §5 `add --field exports`), which keeps it out of `metadata.json`'s `exports[]`.
28
+ 4. **When a call fails:** on exit 2, show the stderr line that holds `error:` and run the call once more, with the payload fixed when that line names it, or with the brief's scope as `--include`, `--exclude`, `--tier-a-include`, `--scope-type` and `--language` in place of `--brief` when it names the brief. When it fails again or writes no JSON, or no `{extractPublicApiHelper}` path resolves, no count is made by hand: the step that loaded this file halts.
12
29
 
13
30
  **Multi-entry packages (`exports` map / declaration-file entry points).** A single per-language entry-point read misses public surface that a package ships through its `package.json` `exports` map, especially committed `.d.ts` / `.d.mts` declaration files that resolve **outside** the conventional source dir (e.g. a monorepo package whose `./macro` subpath maps to `macro/index.d.mts`, listed in `files[]` but not under `src/`). When the in-scope package declares an `exports` map:
14
31
 
15
32
  - Resolve each `exports` subpath to its target file and treat that file, and any committed `.d.ts` / `.d.mts` declaration it resolves to, as an authoritative public entry point, reading it the same way as the primary barrel above even when it lives outside `src/`.
16
- - If a resolved `exports` subpath target falls **outside** the brief's `scope.include` globs, surface a note: `"warn: public entry point {path} (exports subpath '{subpath}') resolves outside scope.include: widen scope.include before extraction, or this surface stays undocumented while exports_public_api still counts it, so public_api_coverage drops (only effective_denominator, for the curated-subset shapes, leaves it out)."` Widening `scope.include` here keeps the documented surface aligned with the `effective_denominator` that compile.md §4 derives from those same globs, without mid-run scope surgery.
33
+ - If a resolved `exports` subpath target falls **outside** the brief's `scope.include` globs, surface a note: `"warn: public entry point {path} (exports subpath '{subpath}') resolves outside scope.include: widen scope.include before extraction, or this surface stays undocumented while exports_public_api still counts it, so public_api_coverage drops (only effective_denominator, for the curated-subset shapes, leaves it out)."` Widening `scope.include` here keeps the documented surface aligned with the `effective_denominator` the runner counts from those same globs, without mid-run scope surgery.
17
34
 
18
35
  Use the entry point as the authoritative source for `metadata.json`'s `exports[]` array.
19
36
 
@@ -36,8 +36,8 @@ sourceTreeProbeOrder:
36
36
  mergeCccExclusionsProbeOrder:
37
37
  - '{project-root}/_bmad/skf/shared/scripts/skf-merge-ccc-exclusions.py'
38
38
  - '{project-root}/src/shared/scripts/skf-merge-ccc-exclusions.py'
39
- # §4's recipe runner (--mode full); without it §4 follows the protocol's
40
- # fallback.
39
+ # §2b's Version Reconciliation (--mode quick) and §4's recipe runner (--mode
40
+ # full); without it §4 follows the protocol's fallback.
41
41
  extractPublicApiProbeOrder:
42
42
  - '{project-root}/_bmad/skf/shared/scripts/skf-extract-public-api.py'
43
43
  - '{project-root}/src/shared/scripts/skf-extract-public-api.py'
@@ -89,7 +89,7 @@ From the brief, apply scope and pattern filters:
89
89
  - `scope.include` — file globs to include
90
90
  - `scope.exclude` — file globs to exclude
91
91
 
92
- At Forge tier and above the recipe runner (§4) applies these globs to the source tree itself, and its `files_in_scope` is the filtered file count. Build the filtered file list by hand only where a step reads it: at Quick tier, for a `component-library` brief (§2c), and in a §4 branch that extracts without the runner. Build it from the source tree resolved in step 1 (for a remote source at Forge tier or above, §2b builds it again from the tree it reads), matching the globs by the **Files in scope** rule of the Recipe Runner section in `{extractionPatternsData}`, and record the result: "**Filtered file count: {N} files in scope**".
92
+ At Forge tier and above the recipe runner (§4) applies these globs to the source tree itself: its `files_in_scope_list` and `files_without_recipes_list` are the filtered file list, and `files_in_scope` its count. Where a step reads the list without the runner (at Quick tier, for a `component-library` brief (§2c), and in a §4 branch that extracts without the runner), resolve `{extractPublicApiHelper}` as §4 does and build it from the source tree resolved in step 1 by step 1 of `{entryPointsByHandData}`. When a call of that file fails as its step 4 says, **HARD HALT** (exit code 4, `write-failed`, phase `extract`, or exit code 3, `helper-missing`, when no path resolves; 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"`): "Cannot list or count the public API: {its `error:` line}. Fix the brief, or re-install SKF, then re-run create-skill."
93
93
 
94
94
  Sections 2b and 2a follow in that order: §2b resolves the source to a local tree, and §2a scans that tree for authoritative files.
95
95
 
@@ -128,7 +128,7 @@ Dispatch on `{source_resolve_status}`:
128
128
  - **`unavailable`** (exit 3): no commit could be read, and no tree was left behind (`{source_resolve_reason}` is `invalid-ref`, `git-unavailable`, `upstream-unreachable`, `ref-not-found`, `fetch-failed`, `checkout-failed`, `tree-folder-failed` or `timed-out`). ⚠️ Warn the user explicitly: "Could not read `{source_repo}`: {source_resolve_message}. Degrading to source reading (T1-low) for this run. For T1 (AST-verified) confidence, clone the repository locally and update `source_repo` in your brief to the local path." Keep `{source_root}` the remote URL, bind `source_ref` ← `source_ref` when it is not null and `HEAD` otherwise, and extract with the Quick tier strategy in §4. Note the degradation reason in context for the evidence report.
129
129
  - **No candidate resolves, or the command exits 1 or 2, or prints no JSON:** continue as for `unavailable`, with `{source_resolve_message}` = "skf-source-tree.py did not finish: {the first stderr line; 'the shell stopped it before it printed a result' when your shell tool's timeout ended it; or 'it is missing; re-install SKF'}". A later run removes a tree a stopped run left behind once it is seven days old.
130
130
 
131
- Then run Source Commit Capture and Version Reconciliation from `{sourceResolutionData}` on `{source_root}`. This ensures source code is accessible regardless of which extraction path is taken below (standard, component-library, or docs-only).
131
+ Then run Source Commit Capture and Version Reconciliation from `{sourceResolutionData}` on `{source_root}`.
132
132
 
133
133
  **Deferred CCC Discovery (Forge+ and Deep, remote sources only):**
134
134
 
@@ -213,7 +213,7 @@ uv run {extractPublicApiHelper} --mode full \
213
213
  -o "{extraction_json}"
214
214
  ```
215
215
 
216
- `{brief_path}` is the `skill-brief.yaml` step 1 loaded: the runner takes its `scope.include`, `scope.exclude`, `scope.tier_a_include`, `scope.type` and `language` from it, and the head cap from `--tier`. A JSON at `{extraction_json}` after the call is this call's: read it, in parts when it is large, and act on its `status` and fields as the protocol's **Recipe Runner** section says, never on the exit code. Each export it returns is T1 as it stands, its `source_line`, `citation`, `ast_recipe`, `ast_node_type`, `signature`, `params` and `return_type` included: read a signature from the source, at its `source_line`, only for a function whose `params` is null, and read by eye (T1-low) only what the runner leaves. When no path resolves, or a case the protocol's **When the Runner Cannot Run** lists applies, such as no JSON at `{extraction_json}` after the call or an empty `scope.languages` (no recipe reads the brief's language), follow that section.
216
+ `{brief_path}` is the `skill-brief.yaml` step 1 loaded: the runner takes its `scope.include`, `scope.exclude`, `scope.tier_a_include`, `scope.type` and `language` from it, and the head cap from `--tier`. A JSON at `{extraction_json}` after the call is this call's: read it, in parts when it is large, and act on its `status` and fields as the protocol's **Recipe Runner** section says, never on the exit code. Each export it returns is T1 as it stands, its `source_line`, `citation`, `ast_recipe`, `ast_node_type`, `signature`, `params` and `return_type` included: read a signature from the source, at its `source_line`, only for a function whose `params` is null, and read by eye (T1-low) only what the runner leaves. When no path resolves, or a case the protocol's **When the Runner Cannot Run** lists applies, such as no JSON at `{extraction_json}` after the call or an empty `scope.languages` (no recipe reads the brief's language), follow that section. When that section warns that the runner failed (no JSON, and a stderr line that holds `error:`), send the warning with §5's `add --field warnings` for §6 and the evidence report.
217
217
 
218
218
  **When `truncated` is true,** a recipe matched more exports than its head cap keeps, and the matches past the cap are missing: a public name among them comes back as an extraction gap (§4b), read by eye, and the others are left out. Warn, and keep the warning for §6 and the evidence report: "**Extraction hit the head cap:** {each `recipes[]` id whose `truncated` is true} matched more than {head_cap} exports, so some exports were read by eye or left out. Narrow `scope.include` in the brief until no recipe reaches the cap."
219
219
 
@@ -229,6 +229,7 @@ Degrade to Quick tier extraction. Note the degradation reason in context for the
229
229
 
230
230
  - If a file cannot be read: log warning, skip file, continue with remaining files
231
231
  - If AST parsing fails on a file (the runner lists each one in `file_issues`): fall back to source reading for that file, continue
232
+ - If an ast-grep call fails or times out: when the protocol's second run is still incomplete, read by eye each file an `errors[]` item lists in `unread`, continue
232
233
 
233
234
  **Re-export tracing (Forge tier and above):** the runner follows each entry point's re-exports to the file that defines each name. Follow the **Re-Export Tracing** protocol in `{extractionPatternsTracingData}` for what it could not follow: each `entry_points.unresolved` chain, and each `entry_point_diff.extraction_gaps` name with no `file`. Without the runner, check for unresolved public exports from entry points (`__init__.py`, `index.ts`, `lib.rs`) and follow the same protocol to resolve them to their definition files.
234
235
 
@@ -243,26 +244,28 @@ After extraction, validate the collected exports against the package's actual pu
243
244
  - `extraction_gaps`: names an entry point exports that no recipe found: read each by eye at its `file` and `line` (T1-low), or trace it (§4) when it has no `file`.
244
245
  - `outside_scope`: names an entry point exports that are defined in files outside the brief's scope. Display each of the runner's `warnings` (one names each `package.json` `exports` subpath whose entry point is outside the files in scope) and list these names with them: widen `scope.include` before extraction, or this surface stays undocumented while `exports_public_api` still counts it, so `public_api_coverage` drops (only `effective_denominator`, for the curated-subset shapes, leaves it out).
245
246
 
246
- §5's `init` records its `counts` (`exports_public_api`, `exports_internal`, `effective_denominator` and `effective_denominator_basis`) and `arms` for step 5 §4. Files you read by eye because no recipe reads their language (`files_without_recipes`) are outside this diff and these counts: read their entry points as **Otherwise** says, add their names to the counts, and send the counts you changed with §5's `set`.
247
+ **A `public-api` scope documents only the public surface.** Under the brief's `scope.type: public-api`, §5's `init` also marks each export of a language family whose `entry_points.by_language` status is `barrel` `public: true` or `public: false` by this diff, as update-skill's `records` marks an update's exports (`skf-extraction-inventory.py --help` gives the rule): an export is public when `public` or `extraction_gaps` names it at its file (by its own name or the `local` one), or when a `public` name with no `file` is its name. A `namespace` item (a submodule the package exports, or `export * as ns`) with a `members` list stands for its members, the names its module passes on, which users call as `pkg.sub.name`: each member is public at its file (by its name or its `local` one), and the module itself is not. Step 5 leaves each export marked `public: false` out of SKILL.md, `references/`, `metadata.json`'s `exports[]` and the provenance map, and one warning of the inventory names them, each with its file (the first ten, then how many more). An export §5's `add` sends later is marked by the same rule. When the diff cannot be read (no `entry_point_diff`, an incomplete run, one with `errors`, or a non-empty `entry_points.unresolved`), nothing is marked and every export is documented, with a warning that says why. Any other scope type, and a family with no barrel, marks nothing.
247
248
 
248
- **Otherwise** (Quick tier, extraction by source reading, a brief whose language no recipe reads included, or the protocol's fallback), read the entry points yourself (when the runner's JSON has `status: no-ast-grep`, its `entry_points.files` lists them, and §5's `init` records its `arms` for step 5 §4): load `{entryPointsByHandData}`, which only this branch reads, compare and count as it says, and send the `counts` (and, without the runner's JSON, the `arms`) with §5's `set`.
249
+ §5's `init` records its `counts` (`exports_public_api`, `exports_internal`, `effective_denominator` and `effective_denominator_basis`) and `arms` for step 5 §4. Files you read by eye because no recipe reads their language (`files_without_recipes_list`) are outside this diff and these counts: count their entry points' names by steps 2 to 4 of `{entryPointsByHandData}`, with `--extraction "{extraction_json}"` (its names join them, each once), and send the `counts` its call writes with §5's `set`.
250
+
251
+ **Otherwise** (Quick tier, extraction by source reading, a brief whose language no recipe reads included, or the protocol's fallback), find the entry points' public names and count them by steps 2 to 4 of `{entryPointsByHandData}`, and send the `counts` and `arms` its call writes with §5's `set`.
249
252
 
250
253
  ### 4c. Detect and Inventory Scripts/Assets
251
254
 
252
- The brief's `scripts_intent` and `assets_intent` are each `"detect"` (also when absent), `"none"` or free text describing the files wanted. The detector takes only `detect` or `none`: pass `none` for `"none"`, else `detect`. Resolve `{detectScriptsAssetsHelper}` ← first existing path in `{detectScriptsAssetsProbeOrder}`; if neither exists, **HARD HALT** (exit code 3, `helper-missing`, phase `extract`; 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"`): "Cannot detect scripts and assets: skf-detect-scripts-assets.py is missing. Re-install SKF, then re-run create-skill." The detector implements the heuristics of `{extractionPatternsTracingData}`. From `{project-root}`, write its JSON to `{detected_json}` (bound in §2b), which §5's `init` reads:
255
+ The brief's `scripts_intent` and `assets_intent` are each `"detect"` (also when absent), `"none"` or free text describing the files wanted. The detector takes only `detect` or `none`: pass `none` for `"none"`, else `detect`. Resolve `{detectScriptsAssetsHelper}` ← first existing path in `{detectScriptsAssetsProbeOrder}`; if neither exists, **HARD HALT** (exit code 3, `helper-missing`, phase `extract`; 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"`): "Cannot detect scripts and assets: skf-detect-scripts-assets.py is missing. Re-install SKF, then re-run create-skill." The detector implements the heuristics of `{extractionPatternsTracingData}`, and `--brief` keeps it to the files the brief's `scope.include` and `scope.exclude` take, by the scope test update-skill uses for the same files. From `{project-root}`, write its JSON to `{detected_json}` (bound in §2b), which §5's `init` reads:
253
256
 
254
257
  ```bash
255
258
  mkdir -p "{project-root}/_bmad-output/.skf-stage"
256
259
  uv run {detectScriptsAssetsHelper} detect "{source_root}" \
257
260
  --scripts-intent {detect|none} \
258
261
  --assets-intent {detect|none} \
259
- [--scope-include "<glob1>,<glob2>,..."] \
262
+ --brief "{brief_path}" \
260
263
  [--max-lines 500] > "{detected_json}"
261
264
  ```
262
265
 
263
266
  It writes `scripts_inventory[]` and `assets_inventory[]` (each entry with `name`, `source_file`, `purpose`, `content_hash`, `confidence`, `lines` and `size_flag`), `scripts_skipped`, `assets_skipped` and `stats`.
264
267
 
265
- **If it exits non-zero** (a `{source_root}` that is no local folder, as for a remote source read by eye): warn "Scripts and assets were not detected: {the first stderr line}", send the warning with §5's `add --field warnings` for §6 and the evidence report, and run §5's `init` without `--detected`: the inventories stay empty.
268
+ **If it exits non-zero** (a `{source_root}` that is no local folder, as for a remote source read by eye, or a brief it cannot read): warn "Scripts and assets were not detected: {the first stderr line}", send the warning with §5's `add --field warnings` for §6 and the evidence report, and run §5's `init` without `--detected`: the inventories stay empty.
266
269
 
267
270
  **Otherwise** §5's `init` takes every entry, unchanged and already `T1-low` with its `content_hash`, which is the inventory for a `"detect"` intent. For a free-text intent, choose the entries that match it and, after `init`, send them with §5's `set` as `{"intent_mapping": {"scripts": {"intent": "<the text>", "kept": [source_file, ...]}}}`, `"assets"` the same way: the helper keeps only those entries and records the others as the mapping's `left_out`, which §6 lists and step 5 writes into the evidence report. §6 surfaces each `size_flag: "oversized"` entry before bundling.
268
271
 
@@ -276,14 +279,14 @@ It writes `scripts_inventory[]` and `assets_inventory[]` (each entry with `name`
276
279
  uv run {extractionInventoryHelper} init --inventory "{extraction_inventory}" --skill "{name}" --mode {source|component-library} --tier "{tier}" [--extraction "{extraction_json}"] [--detected "{detected_json}"]
277
280
  ```
278
281
 
279
- Pass `--mode component-library` for a component library (§2c), else `source`. Pass `--extraction` when §4's runner, or step 3d's Phase 4 for a component library, left a JSON at `{extraction_json}`, and `--detected` when §4c's detector exited 0. From the runner's JSON, `init` writes each export as the runner recorded it (T1, `ast-grep`, with its `source_file`, `source_line`, `citation`, `ast_recipe`, `ast_node_type` and `export_type`), marked `internal: true` when §4b's `internal` lists it; `files_scanned` (the runner's `files_in_scope`); its `aggregates`, `counts` and `arms`; `extraction_rules` (its `recipe_set`, the `recipes` ids, its `scope` and `ast_grep.version`), from which step 7 writes `extraction-rules.yaml`; and the runner warnings in `warnings`, one line each: the head-cap warning (§4), each `errors[]` item, each `file_issues[]` file, each extension `files_without_recipes` counts, and each of its `warnings`. From the detector's JSON it writes `scripts_inventory` and `assets_inventory`.
282
+ Pass `--mode component-library` for a component library (§2c), else `source`. Pass `--extraction` when §4's runner, or step 3d's Phase 4 for a component library, left a JSON at `{extraction_json}`, and `--detected` when §4c's detector exited 0. From the runner's JSON, `init` writes each export as the runner recorded it (T1, `ast-grep`, with its `source_file`, `source_line`, `citation`, `ast_recipe`, `ast_node_type` and `export_type`), marked `internal: true` when §4b's `internal` lists it and, under a `public-api` scope, `public: true` or `public: false` by §4b's public surface; `files_scanned` (the runner's `files_in_scope`); its `aggregates`, `counts` and `arms`; `extraction_rules` (its `recipe_set`, the `recipes` ids, its `scope` and `ast_grep.version`), from which step 7 writes `extraction-rules.yaml`; and the runner warnings in `warnings`, one line each: the head-cap warning (§4), each `errors[]` item, each `file_issues[]` file, each extension `files_without_recipes` counts, and each of its `warnings`. From the detector's JSON it writes `scripts_inventory` and `assets_inventory`.
280
283
 
281
284
  **Then send what you produced**, each call with its JSON payload on stdin, leaving out a call with nothing to send:
282
285
 
283
- - **Signatures** (`patch`): for each function export whose `params` the runner left null (and each Props interface of a component library), the full signature, the parameters (each as `{"name", "type", "default", "optional"}`, as the runner records them) and the return type you read from the source at its `source_line`. It sets only those three fields, matches each entry on `export_name` and `source_file`, and lists in `unmatched` and `ambiguous` the entries it could not place: correct those and send them again.
284
- - **Exports read by eye** (`add --field exports`): each export §4 or §4b read by eye (an extraction gap, a `file_issues` file, a language no recipe reads, or every export when the runner did not run), with its `export_name`, `export_type`, `signature`, `params`, `return_type`, `source_file`, `source_line` and `[SRC:...]` citation. The helper labels it T1-low (`source-read`, with `ast_node_type` and `ast_recipe` null) unless the entry carries its own labels, as a `find_code` match of the protocol's fallback does (T1, `ast-grep`, the `kind` its recipe or pattern declares, and the pattern as its `ast_recipe`). A component library (§2c) sends here only the exports step 3d read by eye (Quick tier, a `file_issues` file, or the runner's fallback); it sends each Props interface's fields with `patch` as `params`, and `component_catalog` with `set`.
285
- - **Warnings you raise** (`add --field warnings`): a list of strings, such as a degradation reason (§2b, §4), the detector's failure (§4c) or a file you could not read.
286
- - **The rest** (`set`), in one JSON object: `top_exports`, the 10 to 20 public API function names by prominence (import frequency or documentation position), which step 3b's targeted temporal fetch reads; `co_imports` (Forge tier and above), the libraries commonly imported alongside the exports, with integration point suggestions; `promoted_docs` and `authoritative_files_scan`, §2a's records; `intent_mapping` for a free-text intent (§4c); a component library's `component_catalog`; and, when the runner did not run, `files_scanned` (§2's filtered file count) and the `counts` and `arms` §4b recorded. `counts` and `arms` merge into what is there, so after the runner send only a count you changed.
286
+ - **Signatures** (`patch`): for each function export whose `params` the runner left null (and each Props interface of a component library), the full signature, the parameters (each as `{"name", "type", "default", "optional"}`, as the runner records them) and the return type you read from the source at its `source_line`. It sets only those three fields, matches each entry on `export_name` and `source_file`, and lists in `unmatched` and `ambiguous` the entries it could not place: correct those and send them again. An export marked `public: false` needs no signature: it is not documented.
287
+ - **Exports read by eye** (`add --field exports`): each export §4 or §4b read by eye (an extraction gap, a `file_issues` file, a file an `errors[]` item lists in `unread`, a language no recipe reads, or every export when the runner did not run), with its `export_name`, `export_type`, `signature`, `params`, `return_type`, `source_file` (relative to `{source_root}`, with `/` separators, as the runner writes its files, so §4b's public surface can match it), `source_line` and `[SRC:...]` citation. The helper marks it `public` by §4b's public surface, as `init` marks the runner's, and labels it T1-low (`source-read`, with `ast_node_type` and `ast_recipe` null) unless the entry carries its own labels, as a `find_code` match of the protocol's fallback does (T1, `ast-grep`, the `kind` its recipe or pattern declares, and the pattern as its `ast_recipe`). A component library (§2c) sends here only the exports step 3d read by eye (Quick tier, a `file_issues` file, a file an `errors[]` item lists in `unread`, or the runner's fallback); it sends each Props interface's fields with `patch` as `params`, and `component_catalog` with `set`.
288
+ - **Warnings you raise** (`add --field warnings`): a list of strings, such as a degradation reason (§2b, §4), the runner's failure (§4), the detector's failure (§4c) or a file you could not read.
289
+ - **The rest** (`set`), in one JSON object: `top_exports`, the 10 to 20 public API function names by prominence (import frequency or documentation position), none marked `public: false`, which step 3b's targeted temporal fetch reads; `co_imports` (Forge tier and above), the libraries commonly imported alongside the exports, with integration point suggestions; `promoted_docs` and `authoritative_files_scan`, §2a's records; `intent_mapping` for a free-text intent (§4c); a component library's `component_catalog`; and, when the runner did not run, `files_scanned` (§2's filtered file count) and the `counts` and `arms` §4b recorded. `counts` and `arms` merge into what is there, so after the runner send only a count you changed.
287
290
 
288
291
  ```bash
289
292
  uv run {extractionInventoryHelper} patch --inventory "{extraction_inventory}" <<'SKF_INVENTORY'
@@ -303,15 +306,15 @@ uv run {extractionInventoryHelper} summary --inventory "{extraction_inventory}"
303
306
 
304
307
  A call that exits non-zero changes nothing: fix what its `message` names and run it again. An entry the inventory already holds is not added twice (`duplicates`), so a call run again is safe. When a call fails twice, **HARD HALT** (exit code 4, `write-failed`, phase `extract`; 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"`), with `"path": "{extraction_inventory}"`: "Cannot write the extraction inventory `{extraction_inventory}`: {the helper's message}. Check permissions and disk space."
305
308
 
306
- The summary's `counts` give §6 its numbers: `files_scanned`, `exports`, `by_type`, `t1`, `t1_low`, `co_imports`, `scripts` and `assets`.
309
+ The summary's `counts` give §6 its numbers: `files_scanned`, `exports`, `not_public` (the exports marked `public: false`), `by_type`, `t1`, `t1_low`, `co_imports`, `scripts` and `assets`.
307
310
 
308
311
  ### 6. Present Extraction Summary (Gate 2)
309
312
 
310
313
  **Docs-only note:** for `extraction_mode: "docs-only"`, display "Docs-only mode: extraction inventory is empty. Documentation content will be fetched from `doc_urls` in step 3c. Auto-proceeding." and auto-proceed past this gate.
311
314
 
312
- **Zero-export sanity check (source mode):** If `extraction_mode != "docs-only"` AND the §5 summary counts no export (its `exports` is 0) AND the brief declares no `doc_urls` (step 3c applies this check to a brief whose every `doc_urls` fetch failed), the empty extraction is almost always an error, not a valid empty surface. Surface a distinct warning at the gate:
315
+ **Zero-export sanity check (source mode):** If `extraction_mode != "docs-only"` AND the §5 summary counts no export the provenance map will hold (its `exports` is 0, or equals its `not_public`) AND the brief declares no `doc_urls` (step 3c applies this check to a brief whose every `doc_urls` fetch failed), the empty extraction is almost always an error, not a valid empty surface. Surface a distinct warning at the gate:
313
316
 
314
- "**⚠️ Zero public exports extracted.** A source-type brief produced no documented surface and declares no `doc_urls`. This usually means a scope/branch/tag mismatch (wrong `target_version`, over-narrow `scope.include`) or a failed AST run — the compiled skill would document nothing. Verify the brief's source ref and scope before continuing.
317
+ "**⚠️ Zero public exports extracted.** A source-type brief produced no documented surface and declares no `doc_urls`. This usually means a scope/branch/tag mismatch (wrong `target_version`, over-narrow `scope.include`) or a failed AST run: the compiled skill would document nothing. Verify the brief's source ref and scope before continuing. For a `public-api` brief, every export found may instead be off the public surface: the `Off the public surface` warning names them.
315
318
 
316
319
  **[C] Continue anyway**: compile an empty surface (default)
317
320
  **[R] Refine the brief**: stop here"
@@ -323,7 +326,7 @@ Display the extraction findings for user confirmation, the numbers from the §5
323
326
  "**Extraction complete.**
324
327
 
325
328
  **Files scanned:** {files_scanned}
326
- **Exports found:** {exports} ({by_type, each as its count and `export_type`})
329
+ **Exports found:** {exports} ({by_type, each as its count and `export_type`}){if not_public > 0: , {not_public} of them off the public surface and left out}
327
330
  **Confidence:** {t1} T1 (AST-verified), {t1_low} T1-low (source reading)
328
331
  **Tier used:** {tier}
329
332
  **Co-import patterns:** {co_imports} detected
@@ -52,18 +52,18 @@ Scripts and assets are file-level artifacts, not code exports. They follow the *
52
52
 
53
53
  ### Detection Heuristics
54
54
 
55
- **Script directories:** `scripts/`, `bin/`, `tools/`, `cli/`
55
+ **Script directories:** `scripts/`, `bin/`, `tools/`, `cli/`, at any depth. A Python package's folder that has one of these four names (it holds `__init__.py`) is code, not a script folder: a `.py` module in it is a script only with a shebang, a top-level `if __name__ == "__main__":` block (either comparison order, parentheses allowed), or as the package's `__main__.py`.
56
56
  **Asset directories:** `assets/`, `templates/`, `schemas/`, `configs/`, `examples/`
57
57
 
58
58
  **Script file signals:**
59
59
 
60
- | Signal | Strength | Pattern |
61
- |-------------------------|----------|--------------------------------------------------------------------------------------|
62
- | Entry point declaration | Strong | `package.json` `bin` field, Cargo.toml `[[bin]]`, pyproject.toml `[project.scripts]` |
63
- | Shebang + executable | Strong | `#!/bin/bash`, `#!/usr/bin/env python`, `#!/usr/bin/env node` |
64
- | CLI argument parser | Moderate | `argparse`, `yargs`, `commander`, `cobra`, `clap` imports in file |
65
- | Directory convention | Moderate | File in `scripts/`, `bin/`, `tools/` directory |
66
- | CI/CD reference | Moderate | Script referenced in `.github/workflows/*.yml` |
60
+ | Signal | Strength | Pattern |
61
+ |-------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------|
62
+ | Entry point declaration | Strong | `package.json` `bin` field, Cargo.toml `[[bin]]`, pyproject.toml `[project.scripts]` |
63
+ | Shebang + executable | Strong | `#!/bin/bash`, `#!/usr/bin/env python`, `#!/usr/bin/env node` |
64
+ | CLI argument parser | Moderate | `argparse`, `yargs`, `commander`, `cobra`, `clap` imports in file |
65
+ | Directory convention | Moderate | File in `scripts/`, `bin/`, `tools/`, `cli/`; in a Python package's folder, only `__main__.py` or a module with a shebang or `__main__` block |
66
+ | CI/CD reference | Moderate | Script referenced in `.github/workflows/*.yml` |
67
67
 
68
68
  **Asset file signals:**
69
69
 
@@ -77,7 +77,7 @@ Scripts and assets are file-level artifacts, not code exports. They follow the *
77
77
 
78
78
  ### Inclusion Rules
79
79
 
80
- - Only include files within brief's `scope.include` patterns (or auto-detected directories)
80
+ - Only include files the brief's scope takes: a `scope.include` pattern matches them and no `scope.exclude` pattern does (an empty `scope.include` takes the files of the brief's `language` that no exclude matches)
81
81
  - Exclude binary files (check extension: `.so`, `.dll`, `.jar`, `.wasm`, `.exe`)
82
82
  - Exclude generated files (`dist/`, `build/`, `.webpack/` output)
83
83
  - Exclude vendored/third-party files
@@ -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 package's entry points export with the recipe matches (`public`, `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
 
@@ -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,10 @@ 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'
10
14
  ---
11
15
 
12
16
  <!-- Config: communicate in {communication_language}. -->
@@ -19,7 +23,8 @@ To display the final compilation summary (skill name, version, source, export co
19
23
 
20
24
  ## Rules
21
25
 
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`
26
+ - 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`
27
+ - §5 emits through `{emitEnvelopeHelper}`, resolved from `{emitEnvelopeProbeOrder}` when it is not bound.
23
28
  - Deliver structured report with confidence breakdown
24
29
  - 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
30
 
@@ -91,7 +96,7 @@ To use this skill immediately, add the context snippet to your CLAUDE.md:
91
96
 
92
97
  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
98
 
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}`:
99
+ **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
100
 
96
101
  ```bash
97
102
  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 +113,21 @@ uv run {emitEnvelopeHelper} emit --workflow skf-create-skill --run-dir "{run_dir
108
113
 
109
114
  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
115
 
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.
116
+ **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
117
 
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:
118
+ **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:
119
+
120
+ ```bash
121
+ uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow.on_complete
122
+ ```
123
+
124
+ 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
125
 
115
126
  ```bash
116
127
  {onCompleteCommand} --result-path={forge_version}/create-skill-result-latest.json
117
128
  ```
118
129
 
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.
130
+ 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
131
 
121
132
  **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
133
 
@@ -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)
@@ -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