bmad-module-skill-forge 4.0.0 → 6.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 (128) 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/agents.md +1 -1
  5. package/docs/architecture.md +5 -5
  6. package/docs/campaign.md +1 -1
  7. package/docs/concepts.md +4 -4
  8. package/docs/examples.md +32 -25
  9. package/docs/getting-started.md +3 -3
  10. package/docs/how-it-works.md +2 -2
  11. package/docs/index.md +1 -1
  12. package/docs/skill-model.md +41 -39
  13. package/docs/troubleshooting.md +9 -3
  14. package/docs/verifying-a-skill.md +42 -21
  15. package/docs/why-skf.md +1 -1
  16. package/docs/workflows.md +6 -6
  17. package/package.json +1 -1
  18. package/src/knowledge/tool-resolution.md +6 -6
  19. package/src/shared/references/headless-gate-convention.md +4 -1
  20. package/src/shared/references/output-contract-schema.md +3 -3
  21. package/src/shared/references/pipeline-contracts.md +2 -1
  22. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
  23. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +11 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  25. package/src/shared/scripts/skf-build-change-manifest.py +1180 -189
  26. package/src/shared/scripts/skf-classify-changed-files.py +15 -10
  27. package/src/shared/scripts/skf-count-tokens.py +54 -10
  28. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  29. package/src/shared/scripts/skf-emit-result-envelope.py +64 -20
  30. package/src/shared/scripts/skf-extract-public-api.py +472 -120
  31. package/src/shared/scripts/skf-extraction-inventory.py +10 -6
  32. package/src/shared/scripts/skf-extraction-snapshot.py +6 -6
  33. package/src/shared/scripts/skf-parse-gaps.py +136 -26
  34. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  35. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  36. package/src/shared/scripts/skf-shard-body.py +239 -41
  37. package/src/shared/scripts/skf-validate-output.py +12 -3
  38. package/src/shared/scripts/skf-verify-provenance-completeness.py +574 -45
  39. package/src/shared/tool-requirements.yaml +8 -2
  40. package/src/skf-analyze-source/SKILL.md +3 -2
  41. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  42. package/src/skf-analyze-source/references/generate-briefs.md +1 -1
  43. package/src/skf-analyze-source/references/headless-contract.md +1 -1
  44. package/src/skf-audit-skill/SKILL.md +4 -3
  45. package/src/skf-audit-skill/references/re-index.md +2 -2
  46. package/src/skf-audit-skill/references/report.md +1 -1
  47. package/src/skf-brief-skill/SKILL.md +3 -2
  48. package/src/skf-brief-skill/assets/skill-brief-schema.md +2 -2
  49. package/src/skf-brief-skill/references/invocation-contract.md +2 -2
  50. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  51. package/src/skf-campaign/SKILL.md +3 -2
  52. package/src/skf-campaign/references/campaign-contracts.md +2 -0
  53. package/src/skf-campaign/references/step-05-skill-loop.md +8 -3
  54. package/src/skf-campaign/references/step-10-export.md +3 -2
  55. package/src/skf-create-skill/SKILL.md +4 -3
  56. package/src/skf-create-skill/assets/skill-sections.md +1 -1
  57. package/src/skf-create-skill/references/ccc-index-check.md +1 -1
  58. package/src/skf-create-skill/references/component-extraction.md +1 -1
  59. package/src/skf-create-skill/references/extract.md +3 -2
  60. package/src/skf-create-skill/references/extraction-patterns.md +3 -3
  61. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  62. package/src/skf-create-skill/references/report.md +8 -3
  63. package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
  64. package/src/skf-create-skill/references/validate.md +1 -1
  65. package/src/skf-create-stack-skill/SKILL.md +3 -2
  66. package/src/skf-create-stack-skill/references/generate-output.md +1 -1
  67. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  68. package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
  69. package/src/skf-create-stack-skill/references/report.md +1 -1
  70. package/src/skf-create-stack-skill/references/validate.md +1 -1
  71. package/src/skf-drop-skill/SKILL.md +3 -2
  72. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  73. package/src/skf-drop-skill/references/report.md +1 -1
  74. package/src/skf-export-skill/SKILL.md +3 -2
  75. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  76. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  77. package/src/skf-export-skill/references/invocation-contract.md +1 -1
  78. package/src/skf-export-skill/references/load-skill.md +1 -1
  79. package/src/skf-export-skill/references/summary.md +1 -1
  80. package/src/skf-export-skill/references/token-report.md +29 -9
  81. package/src/skf-forger/SKILL.md +3 -2
  82. package/src/skf-forger/references/pipeline-mode.md +2 -2
  83. package/src/skf-quick-skill/SKILL.md +4 -3
  84. package/src/skf-quick-skill/references/finalize.md +1 -1
  85. package/src/skf-quick-skill/references/halt-contract.md +2 -2
  86. package/src/skf-quick-skill/references/write-and-validate.md +1 -1
  87. package/src/skf-refine-architecture/SKILL.md +8 -9
  88. package/src/skf-refine-architecture/customize.toml +1 -1
  89. package/src/skf-refine-architecture/references/report.md +1 -1
  90. package/src/skf-rename-skill/SKILL.md +3 -2
  91. package/src/skf-rename-skill/references/invocation-contract.md +2 -2
  92. package/src/skf-setup/SKILL.md +3 -2
  93. package/src/skf-setup/references/ccc-index.md +1 -1
  94. package/src/skf-setup/references/invocation-contract.md +1 -1
  95. package/src/skf-test-skill/SKILL.md +3 -2
  96. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  97. package/src/skf-test-skill/references/coherence-check.md +11 -8
  98. package/src/skf-test-skill/references/coverage-check-branches.md +81 -0
  99. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  100. package/src/skf-test-skill/references/coverage-check.md +60 -97
  101. package/src/skf-test-skill/references/init.md +2 -1
  102. package/src/skf-test-skill/references/inventory-contract.md +27 -0
  103. package/src/skf-test-skill/references/invocation-contract.md +3 -3
  104. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  105. package/src/skf-test-skill/references/report.md +5 -5
  106. package/src/skf-test-skill/references/scoring-rules.md +8 -7
  107. package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
  108. package/src/skf-test-skill/references/source-access-protocol.md +14 -10
  109. package/src/skf-test-skill/references/step-hard-gate.md +1 -0
  110. package/src/skf-test-skill/scripts/build-result-context.py +20 -3
  111. package/src/skf-test-skill/scripts/gap-ledger.py +456 -176
  112. package/src/skf-test-skill/scripts/load-coverage-inputs.py +516 -79
  113. package/src/skf-test-skill/scripts/locate-export-segments.py +685 -76
  114. package/src/skf-test-skill/scripts/reconcile-coverage.py +192 -39
  115. package/src/skf-test-skill/scripts/score-signatures.py +1205 -65
  116. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  117. package/src/skf-test-skill/scripts/validate-inventory.py +350 -35
  118. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  119. package/src/skf-update-skill/SKILL.md +3 -2
  120. package/src/skf-update-skill/references/detect-changes.md +1 -1
  121. package/src/skf-update-skill/references/gap-driven.md +54 -33
  122. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  123. package/src/skf-update-skill/references/merge.md +15 -10
  124. package/src/skf-update-skill/references/re-extract.md +3 -1
  125. package/src/skf-update-skill/references/report.md +6 -5
  126. package/src/skf-update-skill/references/write.md +42 -20
  127. package/src/skf-verify-stack/SKILL.md +4 -3
  128. package/tools/cli/lib/tool-check.js +158 -2
@@ -9,6 +9,7 @@ hardGate: ''
9
9
  testResult: ''
10
10
  score: ''
11
11
  threshold: ''
12
+ thresholdSource: ''
12
13
  analysisConfidence: ''
13
14
  toolingStatus: ''
14
15
  workspaceDrift: ''
@@ -31,6 +31,7 @@ These rules apply to every step in this workflow:
31
31
  - Never hallucinate — every statement must have AST provenance
32
32
  - [MANUAL] sections survive regeneration with zero content loss
33
33
  - Only load one step file at a time — never preload future steps
34
+ - Run every subagent in the foreground (parallel ones as several calls in one message, never with `run_in_background`) and wait for every long-running command to exit before the next instruction; in a headless run, never end a turn while either still runs, since under `claude -p` a run stops when its turn ends.
34
35
  - Always communicate in `{communication_language}`
35
36
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
36
37
  - Every HALT, ABORT or other exit before step 7 runs the **Halt procedure** of the step file it fires in: one `{runStateHelper}` `halt` call that undoes this run's writes, removes the private source tree, releases the run lock and, headless, prints the halt's line. A halt never falls through to step 6.
@@ -79,10 +80,10 @@ Headless callers: the inputs, flags, gate map, outputs and the `SKF_UPDATE_RESUL
79
80
  SKF_JSON
80
81
  ```
81
82
 
82
- 4. **Resolve workflow customization.** Run:
83
+ 4. **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:
83
84
 
84
85
  ```bash
85
- 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
86
87
  ```
87
88
 
88
89
  It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-update-skill.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Record the warning in the run log at once: write `customization_resolver_unavailable: <reason>` to `{run_dir}/resolver-warning.txt` with a file write (a resolver error can hold quotes or `$( )`, so never echo it or type it into an argument), then run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/resolver-warning.txt")"` from `{project-root}`.
@@ -313,7 +313,7 @@ Pass `--provenance-map` whenever init.md §4 found one; `--brief` (as §1c left
313
313
 
314
314
  Pass `--language` when metadata.json records it, and `--scope-type` when §1 bound `{scope_type}`. It takes no `--brief`: Category A applied the scope, and a tracked file the brief's scope leaves out (gap-driven.md §1's rule R1 excludes a rescoped export's file) keeps its exports. `--head-cap 0` keeps every match, since a dropped one would read as a deleted export. Step 3 reads this file and never runs the runner again. **Exit 1** (`incomplete`, its JSON written): run it once more; when still incomplete, step 2 below reads by eye each listed file with no export in `exports[]` or that an `errors[]` item lists in `unread`, and tells the user which files were read by eye. **Exit 2** (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too): HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, the stderr line that holds `error:` as `reason`). No by-eye read stands in for it: the skill is not written yet, and reading every file by eye would hide the error. **Exit 3 (`no-ast-grep`), no JSON on another exit, no candidate resolves, or Quick tier:** write `{"exports": []}` to `{run_dir}/extraction.json`, with `"scope": {"type": "{scope_type}"}` when §1 bound `{scope_type}`, so step 5's `apply` can warn that a `public-api` surface was not applied; step 2 reads every listed file by eye (at Quick tier, by text pattern). Tell the user which files were read by eye.
315
315
 
316
- Then, at every tier (in degraded mode there is no map: skip it), list the map's entries the runner left out that their file still declares: the runner never reports a module, an alias that renames, a dunder such as `__version__` or another underscore name, and step 3's diff would read each as a deleted export. From `{project-root}`, run:
316
+ Then, at every tier (in degraded mode there is no map: skip it), list the map's entries the runner left out that their file still declares: the runner never reports a module, an alias that renames, a dunder such as `__version__` or another Python or Rust underscore name, and step 3's diff would read each as a deleted export. From `{project-root}`, run:
317
317
 
318
318
  ```bash
319
319
  uv run {buildChangeManifestHelper} baseline-gaps \
@@ -82,20 +82,33 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
82
82
  - **Exit 0:** bullet 3 reads `{run_dir}/gaps.json` and passes each of its `warnings[]` on as `test-report: <entry>`.
83
83
  - **Exit 1** (`status: "error"` in `{run_dir}/gaps.json`, with `code` `REPORT_MISSING`, `LEDGER_MISSING`, `LEDGER_INVALID` or `INVALID_INPUT`), **any other exit, no JSON, or no candidate resolves:** HALT with status `blocked` and display "**The test report's gaps could not be read:** {code}: {error}" ("skf-parse-gaps.py is missing; re-install SKF" when no candidate resolved). The halt procedure takes `phase: "detect-changes:parse-gaps"`, `path: "{test_report_path}"`, `reason: "{code}: {error}"`.
84
84
 
85
- 2. **Translate each gap by its `category`** (the gap ledger's closed set of slugs), never by its severity: bullet 3's helper routes each one through the table below. A report older than the ledger gives no `category`: when bullet 3 asks, choose the row whose gap type its title and issue describe.
85
+ 2. **Translate each gap by its `category`** (the gap ledger's closed set of slugs), never by its severity: bullet 3's helper routes each one through the table below. A report older than the ledger gives no `category`: when bullet 3 asks, choose the row whose gap type its title and issue describe, or the Not routed slug that names it.
86
86
 
87
87
  | `category` | Gap type (a report older than the ledger) | Change Category |
88
88
  |------------|-------------------------------------------|-----------------|
89
89
  | `missing-export`, `missing-type` | Missing export, type or interface documentation | NEW_EXPORT (an undocumented public export or type), unless the remediation says the export is internal or out of scope: then DELETED_EXPORT (rescope), see rule R1 |
90
- | `split-body-mismatch` | Split-body inconsistency: the SKILL.md body and a `references/*.md` file document one export differently, with a `Source:` inside the skill package | STRUCTURAL_FIX, see rule R2 (for an older report, this row takes precedence over the Signature mismatch row below) |
91
- | `signature-mismatch`, `fabricated-signature`, `stale-documentation` | Signature mismatch; stale documentation (the skill documents an export the source's enumerated surface lacks and that is not a documented extra, an Info `observation` this table never routes) | MODIFIED_EXPORT (the documentation needs updating from source) |
90
+ | `split-body-mismatch` | Split-body inconsistency: two files of the skill (the SKILL.md body or a `references/*.md` file) document one export differently, with a `Source:` inside the skill package | STRUCTURAL_FIX, see rule R2 (for an older report, this row takes precedence over the Signature mismatch row below) |
91
+ | `signature-mismatch`, `fabricated-signature` | Signature mismatch | MODIFIED_EXPORT (the documentation needs updating from source) |
92
92
  | `structural`, `broken-reference`, `inaccurate-reference`, `reference-escape`, `integration-pattern`, `migration-section`, `scripts-assets` | Structural or coherence drift in the output files only, no source change | STRUCTURAL_FIX, see rule R2 |
93
93
  | `provenance-completeness` | Export documented in SKILL.md/references but **missing from the provenance-map** | NEW_EXPORT (provenance-completeness), see rule R3 |
94
- | `provenance-line` | Provenance line is not the export's definition | MOVED_EXPORT (provenance line), see rule R5 |
95
- | `metadata-drift`, `metadata` | Divergent export counts in the metadata; missing metadata or examples | metadata update, see rule R4 |
94
+ | `provenance-line` | Provenance line is not the export's definition, or its citation does not resolve | MOVED_EXPORT (provenance line), see rule R5 |
95
+ | `metadata` | Missing metadata or examples | metadata update, see rule R4 |
96
96
  | any other | any other gap | not routed |
97
97
 
98
- **Not routed:** update-skill has no repair for the other categories. `provenance-unverified` asks for a check by hand, `denominator-inflation`, `brief-scope-stale` and `multi-denominator` concern the brief's scope and the count design, and `numerator-inflation`, `scripts-assets-provenance`, `observation`, `discovery`, `description` and `external-validator` need a person or another workflow. Such a gap takes no manifest entry (bullet 3 lists it).
98
+ **Not routed:** update-skill has no repair for the other categories. Such a gap takes no manifest entry, so this run writes nothing for it: no provenance entry, no `exports[]` name and no documentation removed. Bullet 3's helper lists it with the reason below, which says who repairs it. A gap with no category, or one no table holds, takes "update-skill has no repair for this category, so a person follows the gap's remediation", and one whose export is answered null "it names no export for update-skill to repair, so a person follows the gap's remediation".
99
+
100
+ - `stale-documentation` (the skill documents a name the source's enumerated surface lacks and that is not a documented extra, an Info `observation`): the source has no line to repair the name from, so a person removes its documentation when the name is gone from the source, fixes the brief's scope when the surface missed it, or, when the source still declares the name, keeps the documentation and re-runs Test Skill at a tier that classifies it.
101
+ - `metadata-drift` (divergent export counts in the metadata): update-skill recounts `stats.exports_public_api`, `stats.exports_documented` and `stats.effective_denominator` each time it writes metadata.json, so a metadata patch to them does not hold (under `--allow-workspace-drift` and for a docs-only skill it keeps the recorded public API counts and `stats.effective_denominator` instead); Create Skill rebuilds diverging barrel counts from the source and recounts a `stats.effective_denominator` the brief's `scope.tier_a_include` set disagrees with (denominator drift), a person fixes denominator deflation on a brief with no tier A list by adding `scope.tier_a_include` to it, and documented-surface counts that differ by the `Type::method` entries the named-export count leaves out need no fix.
102
+ - `provenance-unverified`: the line-check rules could not verify the line, or the recorded line is not a number, so a person checks by hand which line defines the export and, when it is not the recorded one, sets its provenance `source_line` to that line.
103
+ - `denominator-inflation`: it concerns the brief's scope and the count design, which a person decides in the skill brief, as the gap's remediation says.
104
+ - `brief-scope-stale`: a person edits the brief's scope globs by hand so each matches the files it meant, since update-skill does not rewrite them.
105
+ - `multi-denominator`: no fix is needed, since the barrel and the documented surface measure different sets by design and the report records both counts.
106
+ - `numerator-inflation`: a person documents the absent exports in SKILL.md or references/, reading each from the source.
107
+ - `scripts-assets-provenance`: a person adds the file's row to the provenance map's `file_entries`, or removes the file from the skill.
108
+ - `observation` (`Annotation contradicted: {name}` included, which needs no skill edit: its remediation is an upstream report of the source's annotation): a person applies it only when they agree, since it is advisory, and a documented extra needs no change.
109
+ - `discovery`: a person improves the skill's description so realistic prompts route to it, or runs the discovery test Test Skill skipped.
110
+ - `description`: a person edits the description in the SKILL.md frontmatter as the gap suggests.
111
+ - `external-validator`: a person applies the skill-check diagnostic or Tessl Review suggestion to SKILL.md.
99
112
 
100
113
  **Translation rules (referenced by the table above):**
101
114
 
@@ -110,10 +123,10 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
110
123
  A rescope is honest only if the reduction is expressed in the brief's scope: on `true`, bullet 3's helper will give the manifest entry a `rescope` object holding the `scope.amendments[]` entry (`category: "scope-expansion"`, `action: "excluded"`, `path`, `reason` (the remediation), `date`, `workflow: "skf-update-skill"`) **and** the export's source path for `brief.scope.exclude`, then route the entry to merge Priority 1 (removal) so steps 4 and 5 remove it and recompute stats from the amended scope. This step writes neither to the brief: step 4 §6b writes both, the first write of the repair, and skips an amendment or an exclude path the brief already holds, so a re-run never adds them twice and a run that stops before step 4 (a halt, `--dry-run`, `--detect-only`) leaves the brief as it was. In those read-only modes its `--read-only` adds `proposed-amendment: excluded {path} (scope-expansion); not written: {--dry-run or --detect-only}` to its `warnings[]`. Never close a coverage gap by editing `metadata.stats` to equal the documented count: that is denominator deflation and `skf-test-skill` will reject it.
111
124
  - **R2: STRUCTURAL_FIX.** A finding whose fix edits the generated markdown only, with no source change and no provenance entry change. It carries `remediation` text describing the surgical markdown edit and routes to merge Priority 8 (generated-markdown edit only); it never adds, modifies, or removes a provenance `entries[]` row. Two kinds of finding qualify:
112
125
  - a coherence finding from `skf-scan-skill-md-structure.py` (e.g., `table_drift`, `unbalanced_fences`, a broken intra-skill anchor) that touches the generated output file only;
113
- - a split-body consistency finding (test-skill coverage-check §1b `cross_check_mismatches`): the SKILL.md body and a `references/*.md` file document one export differently. test-skill rates it High, as it rates a signature mismatch against the source, yet it needs nothing from the source: merge Priority 8 edits the `references/*.md` file to match the body. Its `category` is `split-body-mismatch`; in a report older than the ledger, recognize it by its `Source:`, which points inside the skill package (at the skill's own `SKILL.md` or at one of its `references/*.md` files, whether the path is written relative to the package or through `{skill_package}`), while its issue sets the SKILL.md body against a `references/*.md` file. Test for it before the High "Signature mismatch" row: a High signature gap whose `Source:` points into the source tree, or that has no `Source:`, stays `MODIFIED_EXPORT`.
114
- - **R3: provenance-completeness.** An export documented in SKILL.md/`references/` but absent from the provenance-map. These are documented-and-known, so `unknown` is never correct: route to §4a's targeted re-extraction **regardless of severity**, gated only on `source_root` being pinned and readable (§4).
115
- - **R4: metadata update.** A metadata-coherence patch that changes no export's source (e.g., reconcile a divergent `stats` count). Routes to merge Priority 8b and is applied by write.md §2 *before* the automatic stat recount (see merge.md §3 / write.md §2).
116
- - **R5: MOVED_EXPORT (provenance line).** A `provenance-line` gap (in a report older than the ledger, a Low gap titled `Provenance line is not the definition of {export_name}`, test-skill coverage-check §4c) says the provenance map's `source_line` for that export is not the line that defines it. Its `Source:` is the map's `{source_file}:{source_line}`, so `source_citation` is set, and its remediation lists the file's definition lines. This row takes precedence over the Low `metadata update` row (R4). Route the entry to §4's spot-check only, with no public-reachability gate and no §4a.
126
+ - a split-body consistency finding (test-skill coverage-check §1b `cross_check_mismatches`): two files of the skill (the SKILL.md body or a `references/*.md` file) document one export differently. test-skill rates it High, as it rates a signature mismatch against the source, yet it needs nothing from the source: its `Source:` names the side the pinned source contradicts (test-skill coverage-check §5b), which bullet 3's helper keeps as the entry's `package_citation` when the ledger recorded its category, and merge Priority 8 says which file it edits. Its `category` is `split-body-mismatch`; in a report older than the ledger, recognize it by its `Source:`, which points inside the skill package (at the skill's own `SKILL.md` or at one of its `references/*.md` files, whether the path is written relative to the package or through `{skill_package}`), while its issue sets the SKILL.md body against a `references/*.md` file. Test for it before the High "Signature mismatch" row: a High signature gap whose `Source:` points into the source tree, or that has no `Source:`, stays `MODIFIED_EXPORT`.
127
+ - **R3: provenance-completeness.** An export documented in SKILL.md/`references/` but absent from the provenance-map. These are documented-and-known, so `unknown` is never correct: route to §4a's targeted re-extraction **regardless of severity**, gated only on `source_root` being pinned and readable (§4). One exception: when the map holds the name at another file than the gap cites (`map_match` `not-found` with a candidate that has a `source_file`), its provenance is not absent, so §4 records it `unknown` (`cited-elsewhere`) and never re-extracts it (§4 bullet 1). The other: no file it cites or its Remediation names that the skill brief's scope leaves out is read for it, so when no file the scope takes holds it, §4 records it `unknown` (`outside-scope`, §4 bullet 1).
128
+ - **R4: metadata update.** A `metadata` gap's patch: it changes no export's source and sets a field write.md §2's `metadata` call keeps (e.g., missing optional metadata), never a count that call recounts or `exports[]`, which is why a `metadata-drift` gap is not routed. Add no `effective_denominator_*` stats key: the recount drops each one when the denominator changes. Routes to merge Priority 8b and is applied by write.md §2 *before* the automatic stat recount (see merge.md §3 / write.md §2).
129
+ - **R5: MOVED_EXPORT (provenance line).** A `provenance-line` gap (in a report older than the ledger, a Low gap titled `Provenance line is not the definition of {export_name}`, test-skill coverage-check §4c) says the provenance map's `source_line` for that export is not the line that defines it, or that its citation does not resolve: a file the pinned source lacks, or a line past its end or below 1. Its `Source:` is the map's `{source_file}:{source_line}`, so `source_citation` is set, and the remediation of a line that is not the definition lists the file's definition lines. This row takes precedence over the Low `metadata update` row (R4). Route the entry to §4's spot-check only, with no public-reachability gate and no §4a.
117
130
 
118
131
  3. **Build the change manifest through `{parseGapsHelper}`**, never by hand. From `{project-root}`, run:
119
132
 
@@ -123,22 +136,23 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
123
136
  --provenance-map "{provenance_map_path}" \
124
137
  [--judgments "{run_dir}/gap-judgments.json"] \
125
138
  [--read-only {dry-run|detect-only}] \
126
- -o "{run_dir}/change-manifest.json"
139
+ -o "{run_dir}/change-manifest.json" \
140
+ > "{run_dir}/translate.json"
127
141
  ```
128
142
 
129
- Pass `--judgments` once the file exists, and `--read-only` with the run's flag in a `--detect-only` or `--dry-run` run. It routes each gap by the table and rules R1 to R5 and writes `{run_dir}/change-manifest.json`, where §4, merge and step 5 read it, as `{"mode": "gap-driven", "entries": [...]}`, each entry with the fields the "Translate" section of its docstring lists: §4 partitions on its `change_category` and merge.md §3 dispatches on it. Act on its `status`:
143
+ Pass `--judgments` once the file exists, and `--read-only` with the run's flag in a `--detect-only` or `--dry-run` run. It routes each gap by the table and rules R1 to R5 and writes `{run_dir}/change-manifest.json`, where §4, merge and step 5 read it, as `{"mode": "gap-driven", "entries": [...]}`, each entry with the fields the "Translate" section of its docstring lists: §4 partitions on its `change_category` and merge.md §3 dispatches on it. Its JSON goes to `{run_dir}/translate.json` (a rescope re-run's to its own file, below): act on its `status` there:
130
144
 
131
145
  - **`needs-judgment`** (it wrote nothing): for each `needs_judgment[]` item, judge from its gap's `title`, `issue` and `remediation`, which the item carries, add your answer to each of its `needs` to `{run_dir}/gap-judgments.json` under the item's `gap_id` (`{"<gap_id>": {"<need>": <answer>}}`), then run it again:
132
- - `category` (a report older than the ledger): the slug of the row bullet 2 says the gap's title and issue describe (`missing-export` or `missing-type` for a missing export or type), or null for a gap no row routes;
146
+ - `category` (a report older than the ledger): the slug of the row bullet 2 says the gap's title and issue describe (`missing-export` or `missing-type` for a missing export or type), else the Not routed slug that names it (`stale-documentation` for documentation of a name the source surface lacks, `metadata-drift` for divergent export counts in the metadata), so the gap takes that category's reason, or null for a gap neither names;
133
147
  - `name`: the export the gap's title names, or null when it names none, which leaves the gap unrouted;
134
148
  - `rescope`: rule R1's answer, true or false;
135
149
  - `source_file`: the source file, relative to `{source_root}`, that defines the export (a rescope's exclude path) or that a rule R3 gap's documentation cites, or null.
136
- - **`written`:** set `gap_count` from its `gap_count`, keep its `not_routed[]` (the `id`, `title` and `category` of each gap the table does not route) for §2, and add each of its `warnings[]` to `warnings[]`: the parse's, as `test-report: <entry>`, `test-report: not routed: {id} ({category})` for each gap not routed, and a read-only run's proposed amendments. When you answer `rescope` for a gap it did not ask about (rule R1: an export you found upstream internal or `#[doc(hidden)]`), add the answer to `{run_dir}/gap-judgments.json` and run it again before §2, taking that run's output in place of this one.
137
- - **Exit 1** (`status: "error"`) **or no JSON:** HALT with status `blocked` and display "**The test report's gaps could not be translated:** {code}: {error}". The halt procedure takes `phase: "detect-changes:parse-gaps"`, `path: "{test_report_path}"`, `reason: "{code}: {error}"`.
150
+ - **`written`:** set `gap_count` from its `gap_count`, keep its `not_routed[]` (the `id`, `title`, `category` and `reason` of each gap the table does not route, the reason saying why update-skill does not repair it and who does) for §2, and add each of its `warnings[]` to `warnings[]`: the parse's, as `test-report: <entry>`, `test-report: not routed: {id} ({category}): {reason}` for each gap not routed, and a read-only run's proposed amendments. Record them right away, in one call in place of one call per warning, from `{project-root}`: `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warnings-file "{run_dir}/translate.json"`. When you answer `rescope` for a gap it did not ask about (rule R1: an export you found upstream internal or `#[doc(hidden)]`), add the answer to `{run_dir}/gap-judgments.json` and run it again before §2, its output redirected to `{run_dir}/translate-rerun.json` in place of `translate.json`, taking that run's output in place of this one: act on its `status` as above, and record a `written` re-run's warnings the same way from `{run_dir}/translate-rerun.json` (the emitter drops a warning recorded twice).
151
+ - **Exit 1** (`status: "error"` in the file it wrote) **or no JSON:** HALT with status `blocked` and display "**The test report's gaps could not be translated:** {code}: {error}". The halt procedure takes `phase: "detect-changes:parse-gaps"`, `path: "{test_report_path}"`, `reason: "{code}: {error}"`.
138
152
 
139
153
  ### 2. Display the Gap Summary and Route
140
154
 
141
- Display "**Gap-driven update mode.** Translated {gap_count} test report findings into the change manifest; source drift detection skipped.", then list each gap §1 did not route, as `{id}: {title} ({category})`, and say this run does not repair them. Then take the branch that applies:
155
+ Display "**Gap-driven update mode.** Translated {gap_count} test report findings into the change manifest; source drift detection skipped.", then list each gap §1 did not route, as `{id}: {title} ({category}): {reason}`, and say this run does not repair them. Then take the branch that applies:
142
156
 
143
157
  - **`detect_only_mode == true`** → display "**Detect-only mode: skipping re-extract, merge and write.** Loading report..." and load, read the full file, then execute `{reportFile}` (report.md), which emits status `detect-only`.
144
158
  - **No gap translated** (`gap_count` is 0, an empty manifest) → load, read the full file, then execute `{reportFile}` (report.md), which emits status `no-changes`.
@@ -178,12 +192,12 @@ Bind `{workspace_drift_status}` ← `status` and `{head_short_sha}` ← `head_sh
178
192
  - **`mismatch`** (helper exit 2): HALT immediately with status `halted-for-workspace-drift`. Display the helper's `halt_message` verbatim: it already substitutes `{pinned_commit}`, `{source_ref or "unset"}`, `{source_root}`, `{head_sha}`, and the suggested `git checkout` command. Do not proceed to §4. Step 4 (merge) has not run; no partial writes. The halt procedure takes `phase: "re-extract:workspace-drift"`, `path: "{source_root}"`, `reason: "workspace HEAD {head_short_sha} is not the pinned commit {pinned_short_sha}"`.
179
193
  - **Any other result** (the helper exits non-zero without a `mismatch` envelope, prints no JSON, or returns a status not listed above): HALT with status `blocked`, since the guard could not tell which commit the spot-checks would read. Show the helper's stderr. Do not proceed to §4. Step 4 (merge) has not run; no partial writes. The halt procedure takes `phase: "re-extract:workspace-drift"`, `path: "{source_root}"`, `reason: "drift-check-failed: {what the helper printed, or no JSON}"`.
180
194
 
181
- **Drift gate (only when `{workspace_drift_status}` is `overridden`).** Run §4 bullet 1 now: with `--drift-status overridden` its plan checks this gate before any spot-check, and lists in `drift_blocked[]` every manifest entry whose repair needs something read from the tree: under the override that tree is HEAD, not the pinned commit, and update-skill writes nothing read there, neither a provenance line nor a signature, parameter list, return type or node kind. Every `NEW_EXPORT` and every `MODIFIED_EXPORT` needs it, whatever its `severity` and whether the provenance map holds the export: merge Priority 4 replaces a modified export's content with a fresh extraction, merge Priority 5 appends a new export's content, and write.md §3 adds an entry for an export the map does not hold, and in gap-driven mode each of those could only be read from the tree. That includes a gap §4 bullet 1 would send to §4a with nothing to scan (a blocking severity as bullet 1 defines it, no `source_citation` and an empty `resolved_paths[]`): under the override this gate halts on it first. Every `DELETED_EXPORT` needs the tree as well: in gap-driven mode it is a rescope (rule R1), which narrows the brief's scope, and write.md §2 must then recount `exports_public_api` and `exports_internal` over the narrowed scope from the tree, while under the override it keeps the counts metadata.json records. A rule R5 `MOVED_EXPORT`, a `STRUCTURAL_FIX` (a split-body consistency finding among them) and a `metadata update` need nothing from the tree and pass. Each item gives its entry's `reason`, the first that fits, read from the entry's `map_match` (§1):
195
+ **Drift gate (only when `{workspace_drift_status}` is `overridden`).** Run §4 bullet 1 now: with `--drift-status overridden` its plan checks this gate before any spot-check, and lists in `drift_blocked[]` every manifest entry whose repair needs something read from the tree: under the override that tree is HEAD, not the pinned commit, and update-skill writes nothing read there, neither a provenance line nor a signature, parameter list, return type or node kind. Every `NEW_EXPORT` and every `MODIFIED_EXPORT` needs it, whatever its `severity` and whether the provenance map holds the export: merge Priority 4 replaces a modified export's content with a fresh extraction, merge Priority 5 appends a new export's content, and write.md §3 adds an entry for an export the map does not hold, and in gap-driven mode each of those could only be read from the tree. That includes a gap §4 bullet 1 would send to §4a with nothing to scan (a blocking severity as bullet 1 defines it, no `source_citation` and an empty `resolved_paths[]`): under the override this gate halts on it first. Every `DELETED_EXPORT` needs the tree as well: in gap-driven mode it is a rescope (rule R1), which narrows the brief's scope, and write.md §2 must then recount `exports_public_api` and `exports_internal` over the narrowed scope from the tree, while under the override it keeps the counts metadata.json records. A rule R5 `MOVED_EXPORT`, a `STRUCTURAL_FIX` (a split-body consistency finding among them) and a `metadata update` need nothing from the tree and pass. So does, with `--brief`, a gap the brief's scope keeps from a repair before anything is read for it (§4 bullet 1 records it `outside-scope`, or `cited-elsewhere` with `citation_check` `outside-scope`, with no check run and nothing to scan). Each item gives its entry's `reason`, the first that fits, read from the entry's `map_match` (§1):
182
196
 
183
197
  - `a public API recount from the tree (rule R1)`: a rescope (`DELETED_EXPORT`);
184
198
  - `a line from the tree (rule R3)`: a provenance-completeness gap (`provenance_completeness: true`), which §4 bullet 1 routes to §4a;
185
- - `a line and a signature from the tree`: an export whose `map_match` is `not-found`;
186
- - `a signature from the tree`: an export the map holds (`found` or `ambiguous`).
199
+ - `a line and a signature from the tree`: an export whose `map_match` is `not-found` with no candidate that has a `source_file`;
200
+ - `a signature from the tree`: an export the map holds (`found` or `ambiguous`, or `not-found` with a candidate that has a `source_file`: a citation of another file than the map's entries of its name).
187
201
 
188
202
  When its `status` is not `drift-blocked`, go on with its output in §4. Otherwise HALT with status `halted-for-workspace-drift` before merge runs, so merge writes nothing and §4a never runs. A rescope's amendment is not in the skill brief yet (rule R1): step 4 §6b writes it, so this halt leaves the brief as it was, and a re-run asks again. Display, with `{source_commit}` and `{source_ref}` read from metadata.json:
189
203
 
@@ -221,20 +235,23 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
221
235
  --manifest "{run_dir}/change-manifest.json" \
222
236
  --provenance-map "{provenance_map_path}" \
223
237
  [--source-root "{source_root}"] \
238
+ [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] \
224
239
  --drift-status "{workspace_drift_status}" \
225
240
  [--judgments "{run_dir}/gap-judgments.json"] \
226
241
  --files-out "{run_dir}/remediation-files.json"
227
242
  ```
228
243
 
229
- Pass `--source-root` unless the skill is docs-only, and `--judgments` once the file exists. Act on its `status`:
244
+ Pass `--source-root` unless the skill is docs-only, `--brief` when `{forge_data_folder}/{skill_name}/skill-brief.yaml` exists (a skill built without a brief, such as a quick skill, has no scope to keep to, so nothing is left out), and `--judgments` once the file exists. Act on its `status`:
230
245
 
231
246
  - **`drift-blocked`:** §3's drift gate halts.
232
247
  - **`blocked`** (`rescope_without_amendment[]`): a `DELETED_EXPORT` whose manifest entry carries no `rescope` object (its `scope.amendments[]` entry, `action: "excluded"`, and its `scope.exclude` path, rule R1). HALT with status `blocked` (halt procedure: `phase: "re-extract:rescope"`, `reason: "rescope-without-amendment: {name}"`): a rescope without a brief scope amendment is denominator deflation and must not be written.
233
248
  - **`needs-judgment`:** each item is a spot-check whose `file` the definition-line rules cover no language of. Read the file and add, under the item's `gap_id` in `{run_dir}/gap-judgments.json`, its `declaring_line`: the line that declares the export itself, never a decorator or comment above it, or null when no line does. Then run it again.
234
- - **`planned`:** when `reextract.entries` is not empty, run §4a for them: the plan wrote their files to `{run_dir}/remediation-files.json`. Add none of its `warnings[]`: bullet 2's call prints them again.
249
+ - **`planned`:** when `reextract.entries` or `reextract.signatures` is not empty, run §4a for them: the plan wrote their files to `{run_dir}/remediation-files.json`, each entry's `resolved_paths[]` and then the `file` of each export `reextract.signatures` lists (a `NEW_EXPORT` or `MODIFIED_EXPORT` its spot-check located, `verified` or `moved`, at the `line` it gives), whose signature bullet 2 takes from §4a's record of it there. A `NEW_EXPORT` whose `reachability` `{run_dir}/gap-judgments.json` does not answer yet is listed too, so its scan goes unused when bullet 2's gate finds it internal. Add none of its `warnings[]`: bullet 2's call prints them again.
235
250
  - **Exit 1 or no JSON:** HALT with status `blocked` (halt procedure: `phase: "re-extract:gap-records"`, its stderr as `reason`).
236
251
 
237
- A severity is blocking unless it is `Medium`, `Low` or `Info`, compared case-insensitively, so a missing or unrecognized one is blocking, like `Critical` and `High`. A blocking gap must produce provenance read from the source or halt in this file before merge: only a `Medium`, `Low` or `Info` gap may degrade to `unknown`.
252
+ A severity is blocking unless it is `Medium`, `Low` or `Info`, compared case-insensitively, so a missing or unrecognized one is blocking, like `Critical` and `High`. A blocking gap whose export the map does not hold must produce provenance read from the source or halt in this file before merge: for such an export, only a `Medium`, `Low` or `Info` gap may degrade to `unknown`. A `NEW_EXPORT` or `MODIFIED_EXPORT` whose export the map holds (its `map_match` is `found` or `ambiguous`, or `not-found` with a candidate that has a `source_file`: the gap cites another file than the map's entries of its name) never goes to §4a's targeted re-extraction (`reextract.entries`), whatever its severity: when the map holds several entries of its name (`ambiguous`, so nothing is checked), its spot-check at the entry ends `unknown` or `missing` (the entry's file is gone), or the check at a citation of another file pins no line (`unknown`, `cited-elsewhere`: that file's declaration may be another package's, so it is never re-extracted), its entry stays unchanged. Step 5's `apply` warns for it, naming the cause of an `unknown` (several map entries of its name, no definition line or several at the entry, a file no rule covers, a failed or skipped check, a citation of another file than the map's entries of its name). For a blocking gap the `provenance:` warning also names the gap and the one fix a re-run acts on: for several entries of its name, fix them by hand, or give the gap a `file:line` Source at the definition when that file holds only one of them, so the re-run's lookup takes that one; otherwise fix the entry by hand (set its `source_file` and `source_line` to the line that defines it), since the re-run checks the same entry again. Then re-run update-skill. A rule R5 `MOVED_EXPORT` whose spot-check ends `missing` (its map entry's file is gone) keeps its entry unchanged too, whatever its severity, since R5 moves a line, never a file: its `provenance:` warning names the gap, the gone file and the same hand fix. For a `cited-elsewhere` gap, whatever its severity, the warning names the gap, the cited file and its check's cause (such as `missing` or `no-definition`), and where the map holds the name, and the fix for each side that may be wrong: when the map is right, re-run Test Skill, since the test compared another file's declaration, and if the gap comes back, exclude the cited file in the brief's scope when the skill does not cover it (not when the cited file is gone); when an export moved, fix that map entry by hand, then re-run update-skill. When its check read nothing at the cited file (no source tree, the verifier missing, a failed check), the warning says instead to give update-skill the source tree, re-install SKF, or fix what the check failed on, then re-run update-skill.
253
+
254
+ With `--brief`, the brief's scope bounds the repair, by the in-scope test Category A applies (`scope.include` and `scope.exclude`, an exclude winning over an include): a cited file or a Remediation path the scope leaves out is never checked, scanned or repaired from, since the declaration there may be another package's, one the skill does not cover. This is the one exception to the rule above that a blocking gap whose export the map does not hold must produce provenance or halt: a gap the scope keeps from a repair ends `unknown`, whatever its severity, with no halt and nothing written for it, so a re-test raises it again. When the map holds the gap's name at another file and the scope leaves out the cited file, no check runs there: `unknown`, `cited-elsewhere`, with `citation_check` `outside-scope`. When the map does not hold the name, a gap whose files the scope all leaves out is never in the plan, and one whose files the scope takes hold no export of its name while it names a file the scope leaves out is not `unresolved`: either is `unknown`, `outside-scope`, its record's `outside_scope` listing the files left out. Step 5's `apply` adds no entry for an `outside-scope` gap, a placeholder included, and warns, naming the gap and the files left out, with the fix: when the skill covers that file, change the brief's scope so it takes it (its `scope.include` or `scope.exclude`), then re-run update-skill; otherwise re-run Test Skill, since the test compared a declaration the skill does not cover. When the gap cites a file the scope takes, the warning also names what its check there found (such as `missing` or `no-definition`), and when that check read nothing (no source tree, the verifier missing, a failed check) it gives that check's fix instead. For a `cited-elsewhere` gap whose cited file the scope leaves out, the warning gives no advice to exclude the cited file, since the scope already leaves it out, and when the export moved there it says to change the brief's scope so it takes that file (its `scope.include` or `scope.exclude`) and fix the map entry by hand. A file the scope takes is repaired from as before, so when the scope takes several packages that declare the name, check each gap's Source before the repair.
238
255
  2. **Write the gap-driven records** to `{run_dir}/reextract-records.json`, before §5 sends the run on: merge reads them, and step 5's `apply` writes the provenance map from them. Run the same call without `--plan` and `--files-out`:
239
256
 
240
257
  ```bash
@@ -242,16 +259,18 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
242
259
  --manifest "{run_dir}/change-manifest.json" \
243
260
  --provenance-map "{provenance_map_path}" \
244
261
  [--source-root "{source_root}"] \
262
+ [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] \
245
263
  --drift-status "{workspace_drift_status}" \
246
264
  [--judgments "{run_dir}/gap-judgments.json"] \
247
265
  [--remediation-records "{run_dir}/remediation-records.json"] \
248
266
  [--evidence "{run_dir}/evidence-records.jsonl" --tier "{forge_tier}"] \
249
- -o "{run_dir}/reextract-records.json"
267
+ -o "{run_dir}/reextract-records.json" \
268
+ > "{run_dir}/gap-records.json"
250
269
  ```
251
270
 
252
- Pass `--remediation-records`, `--evidence` and `--tier` when §4a ran. It writes one verification record per export-bearing entry, the confidence breakdown and counts, and in `files` each record of `{run_dir}/remediation-records.json` it matched by name to an entry §4a scanned for, within that entry's own `resolved_paths[]`, copied as `records` wrote it: never type a record or count one by hand. With `--evidence` it appends its `targeted_reextraction` record to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `targeted_reextraction`, which the evidence report (step 5 §4) shows beside the verified, moved and missing tally. Act on its `status`:
271
+ Pass `--brief` as bullet 1 does, and `--remediation-records`, `--evidence` and `--tier` when §4a ran. It writes one verification record per export-bearing entry, the confidence breakdown and counts, and in `files` each record of `{run_dir}/remediation-records.json` it matched by name, or by the public name the runner gives its local name in that file, to an entry §4a scanned for, within that entry's own `resolved_paths[]`, copied as `records` wrote it (one matched by that public name is copied under it, with its own name as `local`), and the record of each export `reextract.signatures` lists, whose `export_type`, `params`, `return_type`, `extraction_method` and `ast_node_type` it puts on that export's verification record, with a `provenance: ... signature not repaired` warning for one it finds no record of at its line: never type a record or count one by hand. The public-name match reads the runner's entry-point diff, so it holds at Forge tier and above: at Quick tier, or when §4a reads the files by eye after the runner exits 3, a gap that names a renamed re-export by its public name matches no record and is `unresolved` below. With `--evidence` it appends its `targeted_reextraction` record to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `targeted_reextraction`, which the evidence report (step 5 §4) shows beside the verified, moved and missing tally. Its JSON goes to `{run_dir}/gap-records.json`. Act on its `status` there:
253
272
 
254
- - **`unresolved`:** an entry routed to §4a has no record of its name in its path set, or had no path to scan (`files_scanned: 0`). A found export the public-reachability gate below finds internal is a resolution, not a failure. HALT with status `halted-for-remediation-path`, under `--dry-run` too (the run stops here instead of exiting `dry-run`), and display a consolidated report of every `unresolved[]` entry:
273
+ - **`unresolved`:** an entry routed to §4a has no record of its name in its path set (nor one the runner re-exports under that name there), or had no path to scan (`files_scanned: 0`). A found export the public-reachability gate below finds internal is a resolution, not a failure. HALT with status `halted-for-remediation-path`, under `--dry-run` too (the run stops here instead of exiting `dry-run`), and display a consolidated report of every `unresolved[]` entry:
255
274
 
256
275
  ```
257
276
  Targeted re-extraction failed for {N} gap(s).
@@ -280,25 +299,25 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
280
299
  - **`needs-judgment`:** for each item, add your answer to each of its `needs` under its `gap_id` in `{run_dir}/gap-judgments.json`, then run it again:
281
300
  - **Public-reachability gate (`NEW_EXPORT` only):** `reachability`, for each `NEW_EXPORT` it located (`verified`, `moved` or `re-extracted`, at the item's `file` and `line`). Confirm the symbol is reachable as **public API**, not merely that a `pub` / `export` / top-level definition exists at the citation: resolve the symbol's module path, and confirm at least one of: (a) it is re-exported from the package entry-point barrel (`lib.rs` `pub use`, `index.ts` / `index.js` export, `__init__.py` import or `__all__`), or (b) every ancestor module on the path from that barrel is public (Rust `pub mod`, an exported TS namespace, a non-underscore Python package). A `pub(crate)` / `pub(super)` item, or one under a private module (e.g. `mod authority;` declared without `pub`), is **not** reachable. Answer `public`, or `internal-unreachable`: the helper then drops it from the export-bearing set, so it is never written to the documented `exports[]` array or SKILL.md, and lists it in `reclassified[]`. Do **not** hand-set any `stats` count: write.md §2's automatic recount derives `exports_internal` / `exports_total` from the merged surface and owns those fields. This honors the workflow rule that documented API must be importable by users, since a `pub`-but-internal symbol would otherwise inflate the documented public surface with a type users cannot import.
282
301
  - `docstring`, for each `re-extracted` export: the docstring at its line, or null when it has none. Its record in `files` takes it, so step 4 Priority 5 merges the record as it merges a normal run's extraction.
283
- - **`written`:** keep its `counts` for §5, and add its `warnings[]` to `warnings[]` (among them `provenance: spot-checks not run: skf-verify-provenance-completeness.py is missing; re-install SKF`, when every check recorded `unknown` for want of the verifier). Its `forwarded[]` entries (`STRUCTURAL_FIX`, rule R2, and `metadata update`, rule R4) go to merge as the manifest holds them, with no spot-check, no provenance lookup and no `entries[]` change; each export `reclassified[]` names goes to merge as a `metadata update` (rule R4) recording `reclassified: internal-unreachable` for the evidence report.
302
+ - **`written`:** keep its `counts` for §5, and add its `warnings[]` to `warnings[]` (among them `provenance: spot-checks not run: skf-verify-provenance-completeness.py is missing; re-install SKF`, when every check recorded `unknown` for want of the verifier), recording them in one call, from `{project-root}`: `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warnings-file "{run_dir}/gap-records.json"`. Its `forwarded[]` entries (`STRUCTURAL_FIX`, rule R2, and `metadata update`, rule R4) go to merge as the manifest holds them, with no spot-check, no provenance lookup and no `entries[]` change; each export `reclassified[]` names goes to merge as a `metadata update` (rule R4) recording `reclassified: internal-unreachable` for the evidence report.
284
303
  - **Exit 1 or no JSON:** HALT with status `blocked` (halt procedure: `phase: "re-extract:gap-records"`, its stderr as `reason`).
285
304
 
286
- write.md §3 (step 5) accepts null `source_file` / `source_line` only for an `unknown` `Medium`, `Low` or `Info` gap the map does not hold, and leaves unchanged an entry whose definition lines the rules do not recognize, which may be a shape they do not cover: never call it gone.
305
+ write.md §3 (step 5) accepts null `source_file` / `source_line` only for an `unknown` `Medium`, `Low` or `Info` gap the map does not hold, and leaves unchanged an entry whose definition lines the rules do not recognize, which may be a shape they do not cover: never call it gone. It also leaves unchanged a rule R5 `MOVED_EXPORT` whose map entry's file is gone, with the warning bullet 1 names.
287
306
  3. Set `no_reextraction: true` in workflow context: step 5 will use this flag to skip stale `source_file`/`source_line`/`confidence` field updates for `verified` exports, except the label relabel write.md §2 applies when its stats helper flags an entry whose labels disagree with its `extraction_method`. `moved` exports get updated citations; a cited `NEW_EXPORT` whose spot-check pinned a line gets a new `source-read` (`T1-low`) entry at that line, since the verifier's text rules found that line, not an ast-grep recipe; `re-extracted` exports get full fresh provenance from §4a's extraction records (see step 5 §3). The flag is a global gap-driven marker, not a per-entry one: step 5 dispatches on each verification outcome independently.
288
307
  4. Go on to §5. §4a below runs only where bullet 1 calls it.
289
308
 
290
309
  ### 4a. Targeted Re-Extraction Branch (Helper, Called from §4 Bullet 1)
291
310
 
292
- **Do not execute this section sequentially.** It is a helper procedure §4 bullet 1 invokes for the entries its plan lists in `reextract.entries`. A run in which §4 bullet 1 routes no entry here skips this section entirely.
311
+ **Do not execute this section sequentially.** It is a helper procedure §4 bullet 1 invokes for the entries its plan lists in `reextract.entries` and `reextract.signatures`. A run in which §4 bullet 1 routes no entry here skips this section entirely.
293
312
 
294
- **Used by:** §4 bullet 1's plan, for the entries it lists in `reextract.entries`: each scans its `resolved_paths[]` (an entry with none has nothing to scan), and §4 bullet 2 matches what this section extracts to each entry by name.
313
+ **Used by:** §4 bullet 1's plan, for the entries it lists in `reextract.entries`: each scans its `resolved_paths[]` (an entry with none has nothing to scan), and §4 bullet 2 matches what this section extracts to each entry by name. For each export it lists in `reextract.signatures`, §4 bullet 2 takes the signature this section extracts at its `file` and `line`.
295
314
 
296
- **Purpose:** produce AST-backed provenance for a gap with a blocking `severity` (§4 bullet 1) and no citation that pins a line, so step 5 §3 never writes `source_file: null` for a blocking finding. Honors the workflow-level rule **Never hallucinate** (every statement must have AST provenance) against the most common gap-driven trigger: a failing test report whose Gap Report `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) rather than a `file:line` pair.
315
+ **Purpose:** produce AST-backed provenance for a gap with a blocking `severity` (§4 bullet 1) and no citation that pins a line, so step 5 §3 never writes `source_file: null` for a blocking finding. Honors the workflow-level rule **Never hallucinate** (every statement must have AST provenance) against the most common gap-driven trigger: a failing test report whose Gap Report `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) rather than a `file:line` pair. For an export a spot-check located, it reads the signature SKILL.md and the provenance map take, so neither keeps a stale one nor takes one from merge's prose.
297
316
 
298
317
  **Procedure:**
299
318
 
300
319
  1. **Source access:** read the files under `{source_root}`, the tree §3 confirmed holds the pinned commit, never through the gh contents API, zread or deepwiki, which may serve another commit.
301
- 2. **The file set** is `{run_dir}/remediation-files.json`, which §4 bullet 1's plan wrote: the `resolved_paths[]` of every entry routed here, as they are. §1's `{parseGapsHelper}` resolved them under `{source_root}` and refused, into `rejected_paths[]`, every path that leaves it (`outside-root`, or `symlink-outside-root` through a link) or names no file (`not-found`, `no-match`). Remediation text is user-editable: never scan a refused path, and never expand or check a path by hand.
320
+ 2. **The file set** is `{run_dir}/remediation-files.json`, which §4 bullet 1's plan wrote: the `resolved_paths[]` of every entry routed here, as they are, less each one the brief's scope leaves out. §1's `{parseGapsHelper}` resolved them under `{source_root}` and refused, into `rejected_paths[]`, every path that leaves it (`outside-root`, or `symlink-outside-root` through a link) or names no file (`not-found`, `no-match`). The plan adds the `file` of each export `reextract.signatures` lists, where its spot-check located it under `{source_root}`; of a file only those exports bring, read by eye (item 3) only the exports they name. Remediation text is user-editable: never scan a refused path, and never expand or check a path by hand.
302
321
  3. **Extract:** read the file set with the tier-appropriate extractor: `records` below labels each export by the tool that produced it, T1 (`extraction_method: ast-grep`) for an ast-grep match and T1-low (`extraction_method: source-read`) for an export read by eye. At Forge tier and above, run the recipe runner over the file set (resolve `{extractPublicApiHelper}` ← first existing path in `{extractPublicApiProbeOrder}`), from `{project-root}`:
303
322
 
304
323
  ```bash
@@ -310,7 +329,7 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
310
329
  -o "{run_dir}/remediation-exports.json"
311
330
  ```
312
331
 
313
- Pass `--scope-type` when step 1 read a `scope_type` from metadata.json, so a component library runs its recipe set. It takes no `--brief`: a test report can name a file the brief's scope leaves out as an export's home, and the runner would skip it. On exit 1 (`incomplete`, its JSON written) run it once more. When it is still incomplete, read by eye each file of the file set that has no export in `exports[]` or that an `errors[]` item lists in `unread`, and tell the user which files were read by eye. On exit 2 (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too), HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, the stderr line that holds `error:` as `reason`): no by-eye read stands in for it, since this step has written nothing to the skill and reading the files by eye would hide the error. On exit 3, no JSON on another exit, or no candidate resolves, or at Quick tier, read the files by eye (by text pattern). Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names, a name `entry_point_diff.extraction_gaps[]` lists and a file the rule above reads after a still-incomplete run. Write each export read by eye, and the `params` and `return_type` of each function export whose `params` the runner left null, in the shape detect-changes.md §2.1 Category B step 2 gives them, to `{run_dir}/remediation-details.json` as `{"exports": [...]}`. Launch subprocesses in parallel (Pattern 4) when available; sequential fallback otherwise. Follow the tier-degradation rules in `{tierDegradationRulesData}` when AST tools fail on individual files. Then, from `{project-root}`, build the file set's records:
332
+ Pass `--scope-type` when step 1 read a `scope_type` from metadata.json, so a component library runs its recipe set. It takes no `--brief`: §4 bullet 1's plan already left out each file a gap cites or its Remediation names that the brief's scope leaves out. On exit 1 (`incomplete`, its JSON written) run it once more. When it is still incomplete, read by eye each file of the file set that has no export in `exports[]` or that an `errors[]` item lists in `unread`, and tell the user which files were read by eye. On exit 2 (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too), HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, the stderr line that holds `error:` as `reason`): no by-eye read stands in for it, since this step has written nothing to the skill and reading the files by eye would hide the error. On exit 3, no JSON on another exit, or no candidate resolves, or at Quick tier, read the files by eye (by text pattern). Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names, a name `entry_point_diff.extraction_gaps[]` lists and a file the rule above reads after a still-incomplete run. Write each export read by eye, and the `params` and `return_type` of each function export whose `params` the runner left null, in the shape detect-changes.md §2.1 Category B step 2 gives them, to `{run_dir}/remediation-details.json` as `{"exports": [...]}`. Launch subprocesses in parallel (Pattern 4) when available; sequential fallback otherwise. Follow the tier-degradation rules in `{tierDegradationRulesData}` when AST tools fail on individual files. Then, from `{project-root}`, build the file set's records:
314
333
 
315
334
  ```bash
316
335
  uv run {buildChangeManifestHelper} records \
@@ -326,7 +345,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
326
345
 
327
346
  Display the summary below, with the `counts` §4 bullet 2 printed:
328
347
 
329
- "**Gap-driven re-extraction.** Verified {verified_count}/{gap_count} citations against live source. Moved: {moved_count}. Missing: {missing_count}. Re-extracted (via remediation paths or provenance-completeness, §4a): {re_extracted_count}. Rescoped (removed from surface): {rescoped_count}. Unknown (not in provenance map, a `Medium`, `Low` or `Info` gap §4a found nothing for, no single definition line, or a line the drift override kept from being moved): {unknown_count}."
348
+ "**Gap-driven re-extraction.** Verified {verified_count}/{gap_count} citations against live source. Moved: {moved_count}. Missing (entry left unchanged): {missing_count}. Re-extracted (via remediation paths or provenance-completeness, §4a): {re_extracted_count}. Rescoped (removed from surface): {rescoped_count}. Unknown (not in provenance map, a `Medium`, `Low` or `Info` gap §4a found nothing for, a file the brief's scope leaves out, several map entries of its name, a citation of another file than the map's entries of its name, no single definition line, or a line the drift override kept from being moved): {unknown_count}."
349
+
350
+ A `NEW_EXPORT` or `MODIFIED_EXPORT` the provenance map holds that ends `missing` or `unknown` keeps its entry unchanged, whatever its severity: step 5's `apply` warns for each, with the cause of an `unknown`, and for a blocking gap says what to fix (§4 bullet 1). One whose gap cites another file than the map's entries of its name (`cited-elsewhere`) is never re-extracted, and its warning says what to fix whatever its severity: re-run Test Skill when the map is right, or fix the map entry by hand when the export moved. One the brief's scope keeps from a repair (`outside-scope`, or `cited-elsewhere` with `citation_check` `outside-scope`) is never checked, scanned or repaired from a file the scope leaves out, whatever its severity and with no halt: the provenance map and the export's documentation stay as they were, and its warning says to change the brief's scope so it takes that file (its `scope.include` or `scope.exclude`) when the skill covers it, else to re-run Test Skill.
330
351
 
331
352
  When `{workspace_drift_status}` is `overridden`, add: "Every check read HEAD {head_short_sha}, not pinned {pinned_short_sha}: no line was moved or pinned."
332
353
 
@@ -9,4 +9,4 @@ The argument set, gate map, outputs and headless result line of `skf-update-skil
9
9
  | **Gates** | step 1: init.md §4's degraded-rebuild gate [D]/[X] when no provenance map is found (headless: halts, or takes [D] under `--allow-degraded`; a gap-driven run halts `blocked` there instead); init.md §4b's [G]/[S] test-report offer, interactive only (headless warns `unconsumed-test-report`); Confirm Gate [C] (init.md §8) | step 2, each only when triggered: detect-changes.md §1b's promote prompt and §1c's scope-expansion prompt [P]/[S]/[U] (headless: defers each candidate to a person), §2.2's deletion-ratio gate [C]/[B]/[A] (headless: [C]) | step 2g (gap-driven): gap-driven.md §1 rule R1's [D]/[R] for each gap whose remediation names removal (headless: [D], or [R] only when the export is internal or `#[doc(hidden)]`) | step 4: Confirm Gate [C] only after the user resolved [MANUAL] conflicts (merge.md §4); a clean merge auto-proceeds, and a headless run with conflicts halts `halted-for-manual-mismatch` before anything is written |
10
10
  | **Outputs** | A new version folder `{skill_group}/{new_version}/` and its forge folder, beside the unchanged previous version (a gap-driven repair writes the current version in place), and a finished run's `update-skill-result-{YYYYMMDD-HHmmss}.json` and `update-skill-result-latest.json` in that version's forge folder (a run that found no change: the current one's). `--detect-only` and `--dry-run` write none of these, nor the skill brief. Run state goes to a run folder under `{project-root}/_bmad-output/.skf-run/` that step 7 removes; a halt keeps it |
11
11
  | **Concurrency** | A second real update of the same skill halts `halted-for-concurrent-run` while the first holds its run lock (init.md §1b); `--detect-only` and `--dry-run` take no lock. write.md §8 halts with the same status when another flip of the `active` link holds its lock for 5 seconds. A remote skill is read from a private tree, and no mode writes to the shared workspace clone before write.md §9. |
12
- | **Headless** | No gate prompts: each resolves as the Gates row says, and the run records the decision. One `SKF_UPDATE_RESULT_JSON` line (report.md §5b), at step 6 or at a HALT, printed by the shared emitter and checked against `shared/scripts/schemas/skf-update-result-envelope.v1.json`. It is the run's last line: the shared health check displays it after everything else; a HALT displays its line and stops. Pipelines branch on `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are successful exits. |
12
+ | **Headless** | No gate prompts: each resolves as the Gates row says, and the run records the decision. One `SKF_UPDATE_RESULT_JSON` line (report.md §5b), at step 6 or at a HALT, printed by the shared emitter and checked against `shared/scripts/schemas/skf-update-result-envelope.v1.json`. It is the run's last line: the shared health check displays it after everything else; a HALT displays its line and stops. Pipelines branch on `skf_update.status`: `success`, `no-changes`, `detect-only` and `dry-run` are successful exits. A caller reads the run's own `SKF_UPDATE_RESULT_JSON` line. A run that printed no envelope line has no result to read, whatever the process exit code: it was cut off, or could not write its result, and `update-skill-result-latest.json` may then describe an earlier run, so it is never read in its place. |
@@ -28,7 +28,7 @@ hashContentProbeOrder:
28
28
 
29
29
  ## STEP GOAL:
30
30
 
31
- Merge freshly extracted export data into the existing SKILL.md content while preserving all [MANUAL] sections. Detect and resolve conflicts where regenerated content overlaps developer-authored content.
31
+ Merge freshly extracted export data into the existing SKILL.md and its references/*.md while preserving all [MANUAL] sections. Detect and resolve conflicts where regenerated content overlaps developer-authored content.
32
32
 
33
33
  ## Rules
34
34
 
@@ -67,9 +67,11 @@ From the [MANUAL] inventory captured in step 1:
67
67
 
68
68
  ### 3. Apply Merge by Priority Order
69
69
 
70
- Read this step's inputs from disk, never from memory: the change manifest from `{run_dir}/change-manifest.json` (step 2, or gap-driven.md §1 in gap-driven mode), the extraction or verification records from `{run_dir}/reextract-records.json` (step 3, or gap-driven.md §4), and the documents step 2 §1b promoted from `{run_dir}/promoted-docs.json` when it wrote that file. A compacted context still merges what those steps found.
70
+ Read this step's inputs from disk, never from memory: the change manifest from `{run_dir}/change-manifest.json` (step 2, or gap-driven.md §1 in gap-driven mode), the extraction or verification records from `{run_dir}/reextract-records.json` (step 3, or gap-driven.md §4), and the documents step 2 §1b promoted from `{run_dir}/promoted-docs.json` when it wrote that file. A compacted context still merges what those steps found. A gap-driven record in `files` that carries a `local` name was matched by its public name (a renamed re-export): document it under that public name, its `name`, saying it re-exports the declared `local` name.
71
71
 
72
- Apply merge in the following priority order:
72
+ Apply merge in the following priority order.
73
+
74
+ **A split-body skill** (one whose `references/*.md` files document exports, the `## Full` sections a body split moved among them): Priorities 1 to 5 apply their change to every file that documents the export, SKILL.md and each `references/*.md` alike. Priority 1 removes the export's content from each file, and deletes a reference file it leaves documenting nothing and holding no `[MANUAL]` block, with SKILL.md's `> See` pointer to that file; Priority 2 moves its citations (`[AST:]` and `[SRC:]` alike) in each, Priority 3 renames it in each, Priority 4 replaces its content in each, and Priority 5 adds a new export where the skill documents that kind of export (a `references/*.md` file when the skill keeps that kind there) and corrects any statement, in SKILL.md or a `references/*.md` file, that calls the new export not covered. [MANUAL] blocks are inventoried and verified in SKILL.md only: nothing re-checks one in a `references/*.md` file yet, so the merge leaves such a block untouched, as it never edits inside one in SKILL.md without the user's §4 decision. §6b writes each `references/*.md` file this changes or adds, and deletes each one it removes.
73
75
 
74
76
  **Priority 1 — Process DELETED exports:**
75
77
  - Remove generated content for deleted exports
@@ -91,6 +93,8 @@ Apply merge in the following priority order:
91
93
 
92
94
  **Priority 4 — Process MODIFIED exports:**
93
95
  - Replace generated content for the export with fresh extraction
96
+ - **Gap-driven `MODIFIED_EXPORT`:** its fresh extraction is its record in `files` of `{run_dir}/reextract-records.json`, which gap-driven.md §4 copied from §4a's extraction at the line its spot-check located (`verified` or `moved`): document it from that record, as a `re-extracted` one is, taking its signature, parameters and return type from the record, never from the old content or the gap's text, so SKILL.md names the signature step 5's `apply` writes to the provenance map. The record holds no docstring (§4 asks none for it), so keep the export's description. With no record there (its verification record's `extraction_method` is null), repair it from the source line its spot-check read.
97
+ - **Gap-driven `MODIFIED_EXPORT` recorded `unknown` or `missing`** (gap-driven.md §4 located no line for it, and step 5's `apply` warns with the cause, `cited-elsewhere` or `outside-scope` among them): keep its documented content as it stands. Never rewrite it from the gap's text (its title, issue or remediation), which may describe another file's declaration.
94
98
  - Preserve [MANUAL] blocks adjacent to the export
95
99
  - Check for position conflicts (new content shifts [MANUAL] block)
96
100
  - If position conflict: flag as POSITION conflict
@@ -100,7 +104,8 @@ Apply merge in the following priority order:
100
104
  - Place before any [MANUAL] blocks at section boundary
101
105
  - No conflicts expected (new content, no existing [MANUAL])
102
106
  - **A `public-api` skill's public surface (normal mode only):** document no new export, and no export of an ADDED or MOVED file, whose record in `{run_dir}/reextract-records.json` is marked `public: false`, unless the provenance map holds the export (by name at its `old_path` for a MOVED file) or the old name it renames, or this update removes an entry of its name (a `DELETED_EXPORT`, a DELETED file's entry): `records` (re-extract.md §4) found it off the public surface, and step 5's `apply` adds no entry for it, so SKILL.md, the provenance map and metadata.json's `exports[]` name the same exports. A record with no `public` mark is documented as before, and an export the map already holds keeps its content (Priority 4) whatever its mark. In gap-driven and degraded mode document every record whatever its mark: `apply` maps them all.
103
- - **Gap-driven cited `NEW_EXPORT` whose spot-check pinned a line** (gap-driven.md §4 recorded `verified` or `moved` for an export the provenance map does not hold): cite it as `[SRC:{source_file}:L{line}]`, where `{line}` is the citation's line for `verified` and the `new_location` line for `moved`: the line write.md §3 records in its new `source-read` entry. The spot-check found that line by the verifier's text rules, not by an ast-grep recipe, so the prefix is `SRC`, never `AST`.
107
+ - **Gap-driven cited `NEW_EXPORT` whose spot-check pinned a line** (gap-driven.md §4 recorded `verified` or `moved` for an export the provenance map does not hold): cite it as `[SRC:{source_file}:L{line}]`, where `{line}` is the citation's line for `verified` and the `new_location` line for `moved`: the line write.md §3 records in its new `source-read` entry. The spot-check found that line by the verifier's text rules, not by an ast-grep recipe, so the prefix is `SRC`, never `AST`. Document it from its record in `files` of `{run_dir}/reextract-records.json`, as a `re-extracted` one is, when §4 copied one there from §4a's extraction at that line (its verification record's `extraction_method` is set): its signature, parameters and return type come from that record, never from the gap's text, and the record holds no docstring (§4 asks none for it). It keeps its `[SRC:]` citation and its T1-low label whatever its record's tool.
108
+ - **Gap-driven `NEW_EXPORT` recorded `unknown` with `cited-elsewhere` or `outside-scope`** (gap-driven.md §4: the provenance map already holds its name at another file than the gap cites, or the skill brief's scope leaves out the file the gap names): add nothing for it, and never document it from the gap's text (its title, issue or remediation), which may describe another file's declaration.
104
109
 
105
110
  **Priority 6 — Process script/asset file changes (from Category D in change manifest):**
106
111
 
@@ -139,7 +144,7 @@ For each entry of `promoted_docs_new[]`, read from `{run_dir}/promoted-docs.json
139
144
  For each `STRUCTURAL_FIX` entry gap-driven.md §4 forwarded:
140
145
 
141
146
  - Apply the surgical edit described in the entry's `remediation` text to the **generated output file only** (e.g., escape an unescaped `|` inside a code span, balance a fence, repair a broken intra-skill anchor in SKILL.md or a `references/*.md`).
142
- - **A split-body consistency finding** (rule R2: the SKILL.md body and a `references/*.md` file document one export differently): edit the `references/*.md` file so it documents the export as the SKILL.md body does, whichever of the two files the gap's `Source:` names. The body is authoritative (test-skill coverage-check §1b), so never change the body to match the reference file.
147
+ - **A split-body consistency finding** (rule R2: two files of the skill, the SKILL.md body or a `references/*.md` file, document one export differently): edit the file the entry's `package_citation` names, the side the pinned source contradicts (test-skill coverage-check §5b), so it documents the export as the other file does: of two `references/*.md` files no source settled, that is the one whose path sorts last, matched to the other. Without a `package_citation`, edit the file the remediation names to change; when neither names one, edit the `references/*.md` file to match the SKILL.md body, or of two `references/*.md` files the one whose path sorts last to match the other. Several split-body entries that cite one file and line are one edit of it. Change only what the remediation names as contradicting (a parameter, a type, the return type), taking it from the other file, and nothing more when it says both forms agree with the source. Keep each file's level of detail: the body keeps its brief form, and a `|` the change puts inside a table cell is escaped (`\|`). Read no source. When the manifest also holds a `MODIFIED_EXPORT` for the same export, Priority 4's rewrite settles the body: skip this edit of SKILL.md for that export.
143
148
  - Do **not** add, modify, or remove any provenance `entries[]` row — STRUCTURAL_FIX never touches the provenance map.
144
149
  - Preserve any [MANUAL] blocks; if the fix location overlaps a [MANUAL] block, flag as a POSITION conflict instead of editing.
145
150
  - Record in the update report: `"Structural fix: {remediation summary} at {file}:{line}"`.
@@ -150,7 +155,7 @@ For each `STRUCTURAL_FIX` entry gap-driven.md §4 forwarded:
150
155
 
151
156
  For each `metadata update` entry gap-driven.md §4 forwarded:
152
157
 
153
- - Queue the surgical metadata patch described in the entry's `remediation` (e.g., reconcile a divergent `stats` count, add an explanatory stat) in workflow context as `metadata_patches[]` for write.md §2 to apply **before** its automatic stat recount.
158
+ - Queue the surgical metadata patch described in the entry's `remediation` in workflow context as `metadata_patches[]` for write.md §2 to apply **before** its automatic stat recount.
154
159
  - Touch no provenance `entries[]` row and no generated markdown — this priority only stages the patch; write.md §2 applies it.
155
160
  - Record in the update report: `"Metadata patch queued: {remediation summary}"`.
156
161
 
@@ -214,7 +219,7 @@ Merge Results:
214
219
  manual_orphans_removed: [count]
215
220
  ```
216
221
 
217
- In gap-driven mode, write what step 5's `apply` reads from merge to `{run_dir}/merge-records.json`: for each `NEW_EXPORT` or `MODIFIED_EXPORT` the provenance map does not hold and no `re-extracted` record covers (a cited export whose spot-check pinned a line, and a `Medium`, `Low` or `Info` one recorded `unknown`), the `export_type`, `params` and `return_type` its merged documentation gives, leaving out each one it does not:
222
+ In gap-driven mode, write what step 5's `apply` reads from merge to `{run_dir}/merge-records.json`: for each `NEW_EXPORT` or `MODIFIED_EXPORT` the provenance map does not hold and no `re-extracted` record covers (a cited export whose spot-check pinned a line, and a `Medium`, `Low` or `Info` one recorded `unknown`, but not one whose `unknown_reason` is `outside-scope`, for which `apply` adds no entry), the `export_type`, `params` and `return_type` its merged documentation gives, leaving out each one it does not. Leave out each part a record gives, and leave out an export whose record gives all three: when §4a found an export's record (its verification record in `{run_dir}/reextract-records.json` has an `extraction_method`), `apply` takes its whole signature from that record, a part the record gives as null included, and reads merge's only for an export with no record:
218
223
 
219
224
  ```bash
220
225
  cat > "{run_dir}/merge-records.json" <<'SKF_JSON'
@@ -279,7 +284,7 @@ uv run {runLockHelper} acquire \
279
284
 
280
285
  Bind `{version_staging}` ← `staging`: the empty folder `{skill_group}/{new_version}.skf-tmp` it created (it clears one an interrupted run left).
281
286
  3. **Copy the current package** `{skill_package}` to `{version_staging}/{skill_name}`, naming both without a trailing `/` — equivalent to `cp -a "{skill_package}" "{version_staging}/{skill_name}"`: SKILL.md, metadata.json, context-snippet.md, `references/`, `scripts/`, `assets/` and every other entry, each link copied as a link and never followed. The files this update does not rewrite carry over to the new version this way.
282
- 4. **Write and verify the merged SKILL.md in the staged package**, before anything is published. Use the `Edit` or `Write` tool to write the merged SKILL.md content (UTF-8) to `{version_staging}/{skill_name}/SKILL.md`, and any `references/*.md` the merge changed into `{version_staging}/{skill_name}/references/`. Then, from `{project-root}`, check the [MANUAL] blocks against the inventory §4 amended:
287
+ 4. **Write and verify the merged SKILL.md in the staged package**, before anything is published. Use the `Edit` or `Write` tool to write the merged SKILL.md content (UTF-8) to `{version_staging}/{skill_name}/SKILL.md`, and each `references/*.md` the merge changed or added, at any priority, into `{version_staging}/{skill_name}/references/`, and delete from there each one the merge removed. Then, from `{project-root}`, check the [MANUAL] blocks against the inventory §4 amended:
283
288
 
284
289
  ```bash
285
290
  uv run {hashContentHelper} manual-verify "{version_staging}/{skill_name}/SKILL.md" \
@@ -302,7 +307,7 @@ If no `{atomicWriteProbeOrder}` candidate resolves, or the stage, copy, write, p
302
307
  **Write the merged files in place** (gap-driven mode only; every other mode wrote them in step 4 above):
303
308
 
304
309
  1. **The rescopes' brief amendments.** For each manifest entry with a `rescope` (rule R1), append its `scope.amendments[]` entry and add its path to `scope.exclude` in `{forge_data_folder}/{skill_name}/skill-brief.yaml`, preserving every other field; skip an amendment the brief already holds (same `action`, `category` and `path`) and a path `scope.exclude` already lists, so a re-run never adds them twice.
305
- 2. **Stage and verify SKILL.md in the run folder.** Write the merged SKILL.md (UTF-8) to `{run_dir}/SKILL.md`, then, from `{project-root}`:
310
+ 2. **Stage and verify SKILL.md in the run folder.** Write the merged SKILL.md (UTF-8) to `{run_dir}/SKILL.md`, and each `references/*.md` the merge changed or added, at any priority, to `{run_dir}/references/`, and list each one it removed, one `references/<name>.md` path a line, in `{run_dir}/references-removed.txt`, then, from `{project-root}`:
306
311
 
307
312
  ```bash
308
313
  uv run {hashContentHelper} manual-verify "{run_dir}/SKILL.md" \
@@ -310,7 +315,7 @@ If no `{atomicWriteProbeOrder}` candidate resolves, or the stage, copy, write, p
310
315
  ```
311
316
 
312
317
  On `ok` false, or a failing command: HALT with status `halted-for-manual-mismatch` before the package changes (halt procedure: `phase: "merge:verify-manual-integrity"`, `path: "{run_dir}/SKILL.md"`, `reason: "[MANUAL] blocks differ from the approved inventory: modified {modified}; missing {missing}"`); its rollback restores the brief step 1 amended.
313
- 3. **Copy it over the package.** Copy `{run_dir}/SKILL.md` to `{skill_package}/SKILL.md`, and write any `references/*.md` the merge changed (merge Priority 8) into `{skill_package}/references/`.
318
+ 3. **Copy it over the package.** Copy `{run_dir}/SKILL.md` to `{skill_package}/SKILL.md`, and each file in `{run_dir}/references/`, every `references/*.md` the merge changed or added at any priority, into `{skill_package}/references/`, and delete from the package each path `{run_dir}/references-removed.txt` lists.
314
319
 
315
320
  **Do NOT write here:**
316
321
  - `metadata.json`, `provenance-map.json`, `evidence-report.md`: derived from the merge output, written by step 5 sections 2–4 (the version folder above only copies the previous version's files, which step 5 then rewrites)
@@ -66,7 +66,7 @@ The halt leaves `{run_dir}` in place.
66
66
  **Forge tier (AST structural extraction):**
67
67
 
68
68
  - Step 2's Category B ran the recipe runner (`{extractPublicApiHelper}` `--mode full`, the recipes of the AST Extraction Protocol in `{extractionPatternsData}`) over every file to extract, `{run_dir}/extract-files.json`, and wrote `{run_dir}/extraction.json`: never run it again here. §4's helper takes each file's exports from its `exports[]`, each at the line of its name, never a decorator or `export` line above it: T1 (AST-verified structural truth) with `extraction_method: ast-grep` and the recipe's labels.
69
- - Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names (not UTF-8, or code the parser could not read, where a recipe can miss an export), a name `entry_point_diff.extraction_gaps[]` lists, a map entry `{run_dir}/baseline-gaps.json` lists (a module, an alias that renames, `__version__` or another underscore name the runner never reports, which its file still declares), and each file step 2 told the user it read by eye. Step 2 already read those of the modified and added files into `{run_dir}/export-details.json` (its entries with an `export_type`), and §4's helper takes them from there. An export read by eye is T1-low with `extraction_method: source-read` and `ast_node_type: null`.
69
+ - Read by eye only what the recipes leave out: an export a file defines in a form Known Limitation #11 in `{extractionPatternsData}` lists, a file `file_issues[]` names (not UTF-8, or code the parser could not read, where a recipe can miss an export), a name `entry_point_diff.extraction_gaps[]` lists, a map entry `{run_dir}/baseline-gaps.json` lists (a module, an alias that renames, `__version__` or another Python or Rust underscore name the runner never reports, which its file still declares), and each file step 2 told the user it read by eye. Step 2 already read those of the modified and added files into `{run_dir}/export-details.json` (its entries with an `export_type`), and §4's helper takes them from there. An export read by eye is T1-low with `extraction_method: source-read` and `ast_node_type: null`.
70
70
 
71
71
  **Tier degradation handling (Forge/Forge+/Deep):** If step 2's runner could run no ast-grep, or left files unread (step 2 told the user which), follow `{tierDegradationRulesData}` for fallback strategy and user notification requirements. Silent degradation is forbidden: the user must always know which files were read by eye instead of matched, and why.
72
72
 
@@ -116,6 +116,8 @@ Launch a subprocess that loads qmd_bridge and for each changed export:
116
116
  2. Searches for usage patterns, documentation references, temporal history
117
117
  3. Returns T2 evidence per export (usage frequency, context snippets, related concepts), as patches §4 merges: write one `{"file_path", "exports": [{"name", "qmd_evidence"}]}` block per file to its own file in `{run_dir}/reextract-patches/`
118
118
 
119
+ Then bind `{qmd_version}` ← the version `qmd --version` prints (`qmd 2.0.1` gives `2.0.1`): write.md §2 records it as `tool_versions.qmd`.
120
+
119
121
  **If no matching collection found in registry:**
120
122
  Log: "No QMD extraction collection found for {skill_name}. T2 enrichment skipped. Re-run [CS] Create Skill to generate the collection."
121
123
  Continue without T2 enrichment: extraction still produces its structural results, labeled by tool.
@@ -46,7 +46,7 @@ When `{unconsumed_test_report}` is bound (step 1 §4b found a test report newer
46
46
  - `fail`: "**Recommendation:** The source has not changed, but test report `{its file name}` (fail) has not been applied to this skill. Run `@Ferris US {skill_name} --from-test-report` to repair the gaps it lists, then re-run test-skill."
47
47
  - `pass-with-drift`: "**Recommendation:** The source has not changed, but test report `{its file name}` passed only under `--allow-workspace-drift`: test-skill read a workspace HEAD other than the commit this skill is pinned to. Once the workspace holds the pinned commit, re-run test-skill without `--allow-workspace-drift` before exporting."
48
48
 
49
- In gap-driven mode (gap-driven.md §1 translated none of the report's gaps), replace the sentence that starts "Source code matches provenance map exactly" with "Test report `{its file name}` lists no gap update-skill repairs.", followed by each gap it did not route (`{id}: {title} ({category})`), and the recommendation with "Repair the listed gaps by hand, or re-run test-skill once the skill changes."
49
+ In gap-driven mode (gap-driven.md §1 translated none of the report's gaps), replace the sentence that starts "Source code matches provenance map exactly" with "Test report `{its file name}` lists no gap update-skill repairs.", followed by each gap it did not route (`{id}: {title} ({category}): {reason}`, with the `reason` gap-driven.md §1's `not_routed[]` gives it), and the recommendation with "Repair the listed gaps by hand, or re-run test-skill once the skill changes."
50
50
 
51
51
  When `{source_moved}` is true, add before the recommendation: "Upstream moved to `{target_commit}`, but no file this skill tracks changed, so nothing was written and the skill stays pinned at `{source_commit}`." When `{target_ref_override}` is set and `{target_ref}` differs from `{source_ref}`, also add "**The re-pin to `{target_ref}` was not recorded** — an update records a new ref only when it writes, and `{target_ref}` changes no file this skill tracks." and add `target-ref-not-recorded: {target_ref} changes no file {skill_name} tracks; the skill still records {source_ref}` to `warnings[]`.
52
52
 
@@ -155,7 +155,7 @@ In `{headless_mode}`, print the line as §5b says, with `status: "dry-run"` and
155
155
 
156
156
  {when step 2 wrote `{run_dir}/new-files.json`: **New scripts and assets:** {the length of its `new_files[]`} new; set aside: {the length of `tracked_code[]`} code files the map cites, {the length of `out_of_scope[]`} outside the brief's scope, {the length of `intent_none[]`} of a kind whose intent is `none` (counted from the lists, never from `stats`)}
157
157
 
158
- {in gap-driven mode, when gap-driven.md §1 left gaps unrouted: **Not repaired by this run:** each `{id}: {title} ({category})`}
158
+ {in gap-driven mode, when gap-driven.md §1 left gaps unrouted: **Not repaired by this run:** each `{id}: {title} ({category}): {reason}`, with the `reason` gap-driven.md §1's `not_routed[]` gives it}
159
159
 
160
160
  ### Export Changes
161
161
 
@@ -250,7 +250,7 @@ The shared emitter writes the result contract and prints the line; never type ei
250
250
 
251
251
  ```bash
252
252
  cat > "{run_dir}/result-context.json" <<'SKF_JSON'
253
- {"status": "success", "skill_name": "{skill_name}", "version": "{new_version}", "previous_version": "{baseline_version}", "update_mode": "{update_mode}", "files_written": [<each artifact write.md wrote and verified: SKILL.md, metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, active-symlink>], "error": null,
253
+ {"status": "success", "skill_name": "{skill_name}", "version": "{new_version}", "previous_version": "{baseline_version}", "update_mode": "{update_mode}", "files_written": [<each artifact write.md wrote and verified: SKILL.md, references (when this run wrote, added or removed a references/*.md file, as the files on disk show), metadata.json, provenance-map.json, evidence-report.md, context-snippet.md, active-symlink>], "error": null,
254
254
  "result_contract": {"skill": "skf-update-skill", "status": "success", "outputs": [{"type": "skill", "path": "<each modified file's path>"}], "summary": {"update_status": "success", "exports_affected": <total_export_changes, or the gap count in gap-driven mode>, "files_modified": <the files written>, "validation_status": "<passed, warnings or failures>"}}}
255
255
  SKF_JSON
256
256
  ```
@@ -261,7 +261,7 @@ Then, from `{project-root}`, run the emitter with the run folder and the new ver
261
261
  uv run {emitEnvelopeHelper} emit --workflow skf-update-skill --run-dir "{run_dir}" --result-dir "{forge_version}" < "{run_dir}/result-context.json"
262
262
  ```
263
263
 
264
- It writes the per-run record `{forge_version}/update-skill-result-{YYYYMMDD-HHmmss}.json` (UTC, the time from the clock; `-2`, `-3` appended when a run already took that second's name) and its copy `{forge_version}/update-skill-result-latest.json` (the stable path pipeline consumers read: a copy, not a symlink), each the `result_contract` with the run's `timestamp`, `run_id`, `headless_decisions` and `warnings` stamped in (see `shared/references/output-contract-schema.md`). It folds the run's decision log and warnings in `{run_dir}` into the line, checks it against `shared/scripts/schemas/skf-update-result-envelope.v1.json` and prints it:
264
+ It writes the per-run record `{forge_version}/update-skill-result-{YYYYMMDD-HHmmss}.json` (UTC, the time from the clock; `-2`, `-3` appended when a run already took that second's name) and its copy `{forge_version}/update-skill-result-latest.json` (the record of the last run whose emitter wrote this copy, at a stable path: a copy, not a symlink; a caller reads its own run's `SKF_UPDATE_RESULT_JSON` line), each the `result_contract` with the run's `timestamp`, `run_id`, `headless_decisions` and `warnings` stamped in (see `shared/references/output-contract-schema.md`). It folds the run's decision log and warnings in `{run_dir}` into the line, checks it against `shared/scripts/schemas/skf-update-result-envelope.v1.json` and prints it:
265
265
 
266
266
  ```
267
267
  SKF_UPDATE_RESULT_JSON: {"skf_update": {"status": "success", "skill_name": ..., "version": ..., "previous_version": ..., "update_mode": ..., "files_written": [...], "headless_decisions": [...], "warnings": [...], "error": null}}
@@ -270,9 +270,10 @@ SKF_UPDATE_RESULT_JSON: {"skf_update": {"status": "success", "skill_name": ...,
270
270
  In `{headless_mode}`, bind `{result_envelope_line}` to that line and do not display it here: the shared health check displays it verbatim as the run's last line. An interactive run runs the command for its result files and need not show the line. When the emitter exits non-zero and its `message` names the payload, fix the payload once and run it again; a write it could not make adds `result_file_write_failed` to the line's `warnings[]`.
271
271
 
272
272
  - `status`: `"success"` here; §1 stages `"no-changes"`, §1a `"detect-only"` and §1b `"dry-run"`; a halt prints its own `halted-for-*` or `blocked` line through its step's halt procedure and never reaches this step. The full enum lives in the schema.
273
+ - `files_written[]`: `references` stands for every `references/*.md` file of the package this run wrote, added or removed, one value however many: merge.md §6b's merge, at any priority, write.md §6's provenance fixes and write.md §7's body split. Read it from the files on disk, never from memory: a `references/` path in the `files_written[]` write.md §6's provenance `fix` printed; a file the `Body split` row lists in this run's `## Update Operation` section of `{forge_version}/evidence-report.md`, the one write.md §4 appended, never an earlier update's; outside gap-driven mode, a `references/*.md` file that `{skill_package}` and the previous version's package (`{skill_group}/{baseline_version}/{skill_name}/references/`) do not hold alike, one lacking it or the two differing; in gap-driven mode, which repairs the version in place, a file in `{run_dir}/references/`, where merge.md §6b staged each one it wrote, or a path `{run_dir}/references-removed.txt` lists.
273
274
  - `headless_decisions[]`: every gate's record in the run's decision log (init.md §4 degraded-rebuild and §8 confirmation, detect-changes.md §1b / §1c / §2.2, gap-driven.md §1 rule R1, merge.md §8). Each entry `{gate, default_action, taken_action, reason, evidence?}`. Empty when no gate auto-resolved.
274
275
  - `error`: null on every exit this step prints. A halt's line carries `{phase, path?, reason}`, and pipelines branch on `error !== null` for non-zero exit semantics.
275
- - `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `new-files-not-checked` (Category D: a skill with no brief, so no new script or asset was looked for), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category})` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished`, `public-api-counted-by-hand:` (write.md §2: the recipe runner exited 2, so the public names were found without it; it quotes the runner's error line) and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
276
+ - `warnings[]`: every entry the run recorded, among them `source-tree:`, `source-not-fetched`, `file-diff-unavailable`, `no-baseline-time`, `moved-check-skipped` and `unknown-language` (detect-changes.md §2.1 Category A), `new-files-not-checked` (Category D: a skill with no brief, so no new script or asset was looked for), `source-version-lower`, `unconsumed-test-report` (init.md §4b), `interrupted-run-cleaned` and `interrupted-run-not-cleaned` (init.md §1b), `test-report:` entries (what the test report's helpers could not read, and `test-report: not routed: {id} ({category}): {reason}` for each gap gap-driven.md §1 did not route), `proposed-amendment:` (a read-only run's brief decisions, not written), `doc-fetch-failed`, `doc-not-hashed` and `doc-drift-not-checked` (a docs-only skill's documents), `workspace-clone-not-updated`, `target-ref-not-recorded`, `workspace_drift_overridden` (gap-driven.md §3), `run-state-not-finished`, `public-api-counted-by-hand:` (write.md §2: the recipe runner exited 2, so the public names were found without it; it quotes the runner's error line) and `provenance:` entries (write.md §3 and §6: provenance findings left for a person, and spot-check entries §3 left for a person to decide).
276
277
 
277
278
  **Post-finalization hook.** A finished run, this one or §1's no-change exit, fires the hook; `--detect-only`, `--dry-run` and a halt never do (customize.toml says so). If `{onCompleteCommand}` (resolved in SKILL.md On Activation §4 from `workflow.on_complete`) is non-empty, invoke it after the emitter wrote both result files:
278
279