bmad-module-skill-forge 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +3 -3
  3. package/docs/_data/pinned.yaml +19 -13
  4. package/docs/architecture.md +2 -2
  5. package/docs/concepts.md +4 -4
  6. package/docs/examples.md +33 -26
  7. package/docs/getting-started.md +3 -3
  8. package/docs/how-it-works.md +2 -2
  9. package/docs/index.md +1 -1
  10. package/docs/skill-model.md +41 -39
  11. package/docs/troubleshooting.md +8 -0
  12. package/docs/verifying-a-skill.md +46 -21
  13. package/docs/why-skf.md +1 -1
  14. package/docs/workflows.md +5 -5
  15. package/package.json +1 -1
  16. package/src/knowledge/qmd-registry.md +1 -1
  17. package/src/knowledge/version-paths.md +1 -1
  18. package/src/shared/health-check.md +2 -50
  19. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +2 -2
  20. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +12 -4
  21. package/src/shared/scripts/schemas/skill-brief.v1.json +1 -1
  22. package/src/shared/scripts/skf-atomic-write.py +212 -59
  23. package/src/shared/scripts/skf-build-change-manifest.py +1437 -178
  24. package/src/shared/scripts/skf-classify-changed-files.py +78 -0
  25. package/src/shared/scripts/skf-compare-file-hashes.py +266 -104
  26. package/src/shared/scripts/skf-count-tokens.py +54 -10
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +18 -1
  28. package/src/shared/scripts/skf-detect-scripts-assets.py +196 -54
  29. package/src/shared/scripts/skf-detect-workspaces.py +67 -0
  30. package/src/shared/scripts/skf-emit-result-envelope.py +6 -5
  31. package/src/shared/scripts/skf-extract-public-api.py +1673 -189
  32. package/src/shared/scripts/skf-extraction-inventory.py +584 -46
  33. package/src/shared/scripts/skf-extraction-snapshot.py +158 -33
  34. package/src/shared/scripts/skf-forge-tier-rw.py +26 -3
  35. package/src/shared/scripts/skf-github-probe.py +47 -8
  36. package/src/shared/scripts/skf-hash-content.py +14 -8
  37. package/src/shared/scripts/skf-load-provenance.py +3 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +142 -19
  39. package/src/shared/scripts/skf-parse-gaps.py +135 -26
  40. package/src/shared/scripts/skf-preflight.py +15 -1
  41. package/src/shared/scripts/skf-provenance-gap-dispatch.py +25 -1
  42. package/src/shared/scripts/skf-render-metadata-stats.py +1 -1
  43. package/src/shared/scripts/skf-resolve-authoritative-files.py +29 -3
  44. package/src/shared/scripts/skf-resolve-customization.py +197 -0
  45. package/src/shared/scripts/skf-shard-body.py +239 -41
  46. package/src/shared/scripts/skf-structural-diff.py +133 -18
  47. package/src/shared/scripts/skf-update-active-symlink.py +115 -62
  48. package/src/shared/scripts/skf-validate-brief-schema.py +19 -1
  49. package/src/shared/scripts/skf-validate-output.py +12 -3
  50. package/src/shared/scripts/skf-verify-provenance-completeness.py +1439 -79
  51. package/src/shared/scripts/skf-write-skill-brief.py +41 -8
  52. package/src/shared/tool-requirements.yaml +8 -2
  53. package/src/skf-analyze-source/SKILL.md +2 -2
  54. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  55. package/src/skf-analyze-source/references/map-unit-exports.md +1 -1
  56. package/src/skf-audit-skill/SKILL.md +2 -2
  57. package/src/skf-audit-skill/assets/drift-report-template.md +1 -0
  58. package/src/skf-audit-skill/references/headless-contract.md +1 -1
  59. package/src/skf-audit-skill/references/health-check.md +2 -0
  60. package/src/skf-audit-skill/references/init.md +2 -1
  61. package/src/skf-audit-skill/references/re-index.md +14 -9
  62. package/src/skf-audit-skill/references/report.md +2 -2
  63. package/src/skf-audit-skill/references/semantic-diff.md +15 -15
  64. package/src/skf-audit-skill/references/structural-diff.md +10 -5
  65. package/src/skf-audit-skill/scripts/render-drift-tables.py +29 -2
  66. package/src/skf-brief-skill/SKILL.md +3 -3
  67. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  68. package/src/skf-brief-skill/assets/skill-brief-schema.md +4 -11
  69. package/src/skf-brief-skill/references/analyze-target.md +6 -6
  70. package/src/skf-brief-skill/references/confirm-brief.md +1 -1
  71. package/src/skf-brief-skill/references/scope-definition.md +4 -4
  72. package/src/skf-brief-skill/references/step-auto-brief.md +11 -29
  73. package/src/skf-brief-skill/references/version-resolution.md +5 -9
  74. package/src/skf-campaign/SKILL.md +2 -2
  75. package/src/skf-campaign/references/step-05-skill-loop.md +6 -1
  76. package/src/skf-create-skill/SKILL.md +2 -2
  77. package/src/skf-create-skill/assets/compile-assembly-rules.md +5 -3
  78. package/src/skf-create-skill/assets/skill-sections.md +7 -7
  79. package/src/skf-create-skill/references/authoritative-files-protocol.md +2 -23
  80. package/src/skf-create-skill/references/compile.md +31 -60
  81. package/src/skf-create-skill/references/component-extraction.md +3 -3
  82. package/src/skf-create-skill/references/enrich.md +2 -2
  83. package/src/skf-create-skill/references/entry-points-by-hand.md +24 -7
  84. package/src/skf-create-skill/references/extract.md +23 -19
  85. package/src/skf-create-skill/references/extraction-patterns-tracing.md +9 -9
  86. package/src/skf-create-skill/references/extraction-patterns.md +10 -11
  87. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  88. package/src/skf-create-skill/references/report.md +21 -5
  89. package/src/skf-create-skill/references/source-resolution-protocols.md +12 -17
  90. package/src/skf-create-skill/references/sub/fetch-docs.md +1 -1
  91. package/src/skf-create-stack-skill/SKILL.md +2 -2
  92. package/src/skf-create-stack-skill/references/validate.md +1 -1
  93. package/src/skf-drop-skill/SKILL.md +2 -2
  94. package/src/skf-drop-skill/references/execute.md +5 -1
  95. package/src/skf-drop-skill/references/health-check.md +2 -0
  96. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  97. package/src/skf-drop-skill/references/report.md +2 -2
  98. package/src/skf-drop-skill/references/select.md +7 -3
  99. package/src/skf-export-skill/SKILL.md +2 -2
  100. package/src/skf-export-skill/assets/snippet-format.md +5 -5
  101. package/src/skf-export-skill/references/generate-snippet.md +22 -15
  102. package/src/skf-export-skill/references/load-skill.md +1 -1
  103. package/src/skf-export-skill/references/token-report.md +29 -9
  104. package/src/skf-forger/SKILL.md +2 -2
  105. package/src/skf-quick-skill/SKILL.md +2 -2
  106. package/src/skf-refine-architecture/SKILL.md +6 -8
  107. package/src/skf-rename-skill/SKILL.md +2 -2
  108. package/src/skf-setup/SKILL.md +2 -2
  109. package/src/skf-test-skill/SKILL.md +2 -2
  110. package/src/skf-test-skill/assets/output-section-formats.md +6 -4
  111. package/src/skf-test-skill/references/coherence-check.md +5 -4
  112. package/src/skf-test-skill/references/coverage-check-branches.md +61 -0
  113. package/src/skf-test-skill/references/coverage-check-tiers.md +5 -5
  114. package/src/skf-test-skill/references/coverage-check.md +69 -111
  115. package/src/skf-test-skill/references/init.md +5 -6
  116. package/src/skf-test-skill/references/migration-section-rules.md +26 -14
  117. package/src/skf-test-skill/references/report.md +11 -9
  118. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  119. package/src/skf-test-skill/references/source-access-protocol.md +15 -18
  120. package/src/skf-test-skill/scripts/build-result-context.py +114 -21
  121. package/src/skf-test-skill/scripts/gap-ledger.py +580 -80
  122. package/src/skf-test-skill/scripts/load-coverage-inputs.py +646 -107
  123. package/src/skf-test-skill/scripts/locate-export-segments.py +101 -20
  124. package/src/skf-test-skill/scripts/reconcile-coverage.py +450 -23
  125. package/src/skf-test-skill/scripts/score-signatures.py +356 -61
  126. package/src/skf-test-skill/scripts/stage-helper-payload.py +64 -13
  127. package/src/skf-test-skill/scripts/validate-inventory.py +108 -9
  128. package/src/skf-test-skill/scripts/verify-declared-numerator.py +7 -5
  129. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  130. package/src/skf-update-skill/SKILL.md +2 -2
  131. package/src/skf-update-skill/references/detect-changes.md +32 -13
  132. package/src/skf-update-skill/references/gap-driven.md +37 -22
  133. package/src/skf-update-skill/references/init.md +6 -2
  134. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  135. package/src/skf-update-skill/references/merge.md +17 -12
  136. package/src/skf-update-skill/references/re-extract.md +4 -2
  137. package/src/skf-update-skill/references/report.md +7 -4
  138. package/src/skf-update-skill/references/write.md +77 -36
  139. package/src/skf-update-skill/scripts/skf-new-file-diff.py +157 -42
  140. package/src/skf-verify-stack/SKILL.md +2 -2
  141. package/tools/cli/lib/tool-check.js +158 -2
@@ -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
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 no longer has) | MODIFIED_EXPORT (the documentation needs updating from source) |
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
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 |
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` 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, so a person checks by hand that it defines the export and, when another line does, 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`: 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,9 +123,9 @@ 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).
126
+ - 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: 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). The 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).
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).
116
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. 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.
117
130
 
118
131
  3. **Build the change manifest through `{parseGapsHelper}`**, never by hand. From `{project-root}`, run:
@@ -129,16 +142,16 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
129
142
  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`:
130
143
 
131
144
  - **`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;
145
+ - `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
146
  - `name`: the export the gap's title names, or null when it names none, which leaves the gap unrouted;
134
147
  - `rescope`: rule R1's answer, true or false;
135
148
  - `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.
149
+ - **`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. 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
150
  - **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}"`.
138
151
 
139
152
  ### 2. Display the Gap Summary and Route
140
153
 
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:
154
+ 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
155
 
143
156
  - **`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
157
  - **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`.
@@ -182,8 +195,8 @@ Bind `{workspace_drift_status}` ← `status` and `{head_short_sha}` ← `head_sh
182
195
 
183
196
  - `a public API recount from the tree (rule R1)`: a rescope (`DELETED_EXPORT`);
184
197
  - `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`).
198
+ - `a line and a signature from the tree`: an export whose `map_match` is `not-found` with no candidate that has a `source_file`;
199
+ - `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
200
 
188
201
  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
202
 
@@ -231,10 +244,10 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
231
244
  - **`drift-blocked`:** §3's drift gate halts.
232
245
  - **`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
246
  - **`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.
247
+ - **`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
248
  - **Exit 1 or no JSON:** HALT with status `blocked` (halt procedure: `phase: "re-extract:gap-records"`, its stderr as `reason`).
236
249
 
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`.
250
+ 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. 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.
238
251
  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
252
 
240
253
  ```bash
@@ -249,9 +262,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
249
262
  -o "{run_dir}/reextract-records.json"
250
263
  ```
251
264
 
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`:
265
+ 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, 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. Act on its `status`:
253
266
 
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:
267
+ - **`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
268
 
256
269
  ```
257
270
  Targeted re-extraction failed for {N} gap(s).
@@ -289,16 +302,16 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
289
302
 
290
303
  ### 4a. Targeted Re-Extraction Branch (Helper, Called from §4 Bullet 1)
291
304
 
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.
305
+ **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
306
 
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.
307
+ **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
308
 
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.
309
+ **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
310
 
298
311
  **Procedure:**
299
312
 
300
313
  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.
314
+ 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`). 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
315
  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
316
 
304
317
  ```bash
@@ -310,7 +323,7 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
310
323
  -o "{run_dir}/remediation-exports.json"
311
324
  ```
312
325
 
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`) run it once more; on exit 2 or 3, no JSON, 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 and a name `entry_point_diff.extraction_gaps[]` lists. 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:
326
+ 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:
314
327
 
315
328
  ```bash
316
329
  uv run {buildChangeManifestHelper} records \
@@ -326,7 +339,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
326
339
 
327
340
  Display the summary below, with the `counts` §4 bullet 2 printed:
328
341
 
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}."
342
+ "**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, 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}."
343
+
344
+ 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.
330
345
 
331
346
  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
347
 
@@ -34,6 +34,10 @@ sourceTreeProbeOrder:
34
34
  findTestReportProbeOrder:
35
35
  - '{project-root}/_bmad/skf/shared/scripts/skf-find-test-report.py'
36
36
  - '{project-root}/src/shared/scripts/skf-find-test-report.py'
37
+ # `{extractPublicApiHelper}`: §6c's source version.
38
+ extractPublicApiProbeOrder:
39
+ - '{project-root}/_bmad/skf/shared/scripts/skf-extract-public-api.py'
40
+ - '{project-root}/src/shared/scripts/skf-extract-public-api.py'
37
41
  ---
38
42
 
39
43
  <!-- Config: communicate in {communication_language}. -->
@@ -343,7 +347,7 @@ Dispatch on `{source_tree_status}`:
343
347
 
344
348
  ### 6c. Detect the Source Version
345
349
 
346
- Run only when `{source_tree_status}` is `ready` or `offline`. Read the source's version from `{source_root}` with the Version Reconciliation rules of `skf-create-skill/references/source-resolution-protocols.md`: the version file for the detected language (`pyproject.toml`, `setup.py`, `__version__`, `package.json`, `Cargo.toml`, `go.mod`) and its monorepo `package.json` priority, matching package names against this skill's `name`. When no version file gives one, leave `{source_version_detected}` unset and skip the rest of this section. Otherwise compare it with the metadata.json `version` through `{skillInventoryHelper}` (resolve it ← first existing path in `{skillInventoryProbeOrder}`), from `{project-root}`:
350
+ Run only when `{source_tree_status}` is `ready` or `offline`. Read the source's version from `{source_root}` by the Version Reconciliation section of `skf-create-skill/references/source-resolution-protocols.md` (its `{extractPublicApiHelper}` call, from `{extractPublicApiProbeOrder}`), with this skill's `name` and metadata.json `language` for the brief's. When no version file gives one, leave `{source_version_detected}` unset and skip the rest of this section. Otherwise compare it with the metadata.json `version` through `{skillInventoryHelper}` (resolve it ← first existing path in `{skillInventoryProbeOrder}`), from `{project-root}`:
347
351
 
348
352
  ```bash
349
353
  uv run {skillInventoryHelper} version order "{the source's version}" "{version}"
@@ -353,7 +357,7 @@ It prints `order` and `major_minor` (the source's version against `{version}`) a
353
357
 
354
358
  - **`order` is `higher`** (a higher semantic version than the metadata.json `version`): bind `{source_version_detected}` ← `a.normalized`. Step 4 §6b and step 5 §2 name the version this update writes after it.
355
359
  - **Otherwise** leave `{source_version_detected}` unset. When `major_minor` is `lower`, add `source-version-lower: {target_ref} reads {a.normalized}, older than {version}; this update keeps the patch-version rule` to `warnings[]`. A source version lower only in its patch number is expected, not a regression: every update that finds no higher version increments the skill's patch number (step 4 §6b), so a skill forged at the source's 1.2.0 is 1.2.1 after one update while the source still reads 1.2.0.
356
- - **Exit 1** (`code` `NOT_A_VERSION`: a value that names no version, such as a dynamic version in `pyproject.toml`), **no candidate, or no JSON:** leave `{source_version_detected}` unset: step 4 §6b takes the next patch version.
360
+ - **Exit 1** (`code` `NOT_A_VERSION`: a value that names no version), **no candidate, or no JSON:** leave `{source_version_detected}` unset: step 4 §6b takes the next patch version.
357
361
 
358
362
  ### 7. Present Baseline Summary
359
363
 
@@ -8,5 +8,5 @@ The argument set, gate map, outputs and headless result line of `skf-update-skil
8
8
  | **Flags** | `--headless` / `-H`; `--from-test-report` (gap-driven mode); `--target-ref <ref>`; `--allow-workspace-drift` (gap-driven only: update-skill takes nothing from HEAD, and a gap that needs the pinned tree, a rescope included, halts `halted-for-workspace-drift` before merge); `--allow-degraded`; `--detect-only` (status `detect-only`); `--dry-run` (status `dry-run`, writes nothing in any mode; a gap-driven.md halt such as `halted-for-remediation-path` or `halted-for-workspace-drift` still stops it). init.md §1 describes each; `--detect-only` wins over `--dry-run`. |
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
- | **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. A remote skill is read from a private tree, and no mode writes to the shared workspace clone before write.md §9. |
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
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. |
@@ -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
@@ -79,7 +81,7 @@ Apply merge in the following priority order:
79
81
  - **Gap-driven rescopes** (`DELETED_EXPORT` from gap-driven.md §1 rule R1, verification `rescoped`) are processed here with the same removal. §6b writes the entry's `rescope` (its `scope.amendments[]` entry, `action: "excluded"`, and its `scope.exclude` path) to the skill brief before SKILL.md, and step 5 removes the provenance `entries[]` row and recomputes `stats` from the amended `brief.scope` (write.md §2/§3). gap-driven.md §4 HALTs on a rescope that carries no `rescope`, so no unscoped removal reaches here.
80
82
 
81
83
  **Priority 2 — Process MOVED exports:**
82
- - Update file:line citations in generated content
84
+ - Update file:line citations in generated content: a `MOVED_EXPORT`'s citation takes its `new_line`, and when the change manifest gives it an `old_file` (a move across files), a citation `[AST:{old_file}:L{old_line}]` (or `[SRC:...]`) becomes the file its `per_file` item names and its `new_line`, `[AST:{file_path}:L{new_line}]`, keeping its prefix. A `MODIFIED_EXPORT` with an `old_file` (it also changed) is rewritten whole by Priority 4.
83
85
  - **Gap-driven:** move citations only for an export whose gap-driven.md §4 spot-check recorded `moved`. A `MOVED_EXPORT` that recorded `unknown` (the drift override among the causes), `verified` or `missing` moves none: write.md §3 leaves its line as it is.
84
86
  - Update provenance map file references
85
87
  - [MANUAL] blocks unaffected (content unchanged)
@@ -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` 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
@@ -99,7 +103,9 @@ Apply merge in the following priority order:
99
103
  - Append new export content to appropriate section
100
104
  - Place before any [MANUAL] blocks at section boundary
101
105
  - No conflicts expected (new content, no existing [MANUAL])
102
- - **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`.
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.
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`** (gap-driven.md §4: the provenance map already holds its name at another file than the gap cites): 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.
103
109
 
104
110
  **Priority 6 — Process script/asset file changes (from Category D in change manifest):**
105
111
 
@@ -111,7 +117,6 @@ Category D operates on every `file_entries[]` row regardless of `file_type`. Han
111
117
  - NEW_FILE: queue file for copy from `{source_root}`, add to `file_entries`
112
118
  - Files in `scripts/[MANUAL]/` or `assets/[MANUAL]/` are never modified (user-authored)
113
119
  - Update Section 7b manifest table to reflect changes
114
- - Update `metadata.json` `scripts[]`/`assets[]` arrays and `stats.scripts_count`/`stats.assets_count`
115
120
 
116
121
  - **`file_type: "doc"`** (authoritative docs promoted by §2a/§1b):
117
122
  - MODIFIED_FILE: update `file_entries` content_hash only. **Do NOT copy the file** — doc-type entries are source-tracked but not bundled. Record the drift in the update report.
@@ -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: the SKILL.md body and 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. 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. 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`), 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, 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.
@@ -135,7 +137,7 @@ uv run {buildChangeManifestHelper} records \
135
137
  -o "{run_dir}/reextract-records.json"
136
138
  ```
137
139
 
138
- Pass `--extraction` and `--export-details` when step 2 wrote them, and `--patches` when §2 or §3 wrote a patch. It seeds each export's record from step 2's files (name, type, signature, location, labels, return type and `params` in the provenance map's typed form), merges the patches, writes `{run_dir}/reextract-records.json`, where merge and step 5's `apply` read it, and prints `files_extracted`, `exports_extracted` and `confidence_breakdown` (T1, T1-low, T2): never type a record or count one by hand. Add each of its `warnings[]` to `warnings[]`. On exit 1 whose error names a patch file, rewrite that file once in the shape of §2's return contract and run `records` again. On a second exit 1, any other exit 1, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, its stderr as `reason`).
140
+ Pass `--extraction` and `--export-details` when step 2 wrote them, and `--patches` when §2 or §3 wrote a patch. It seeds each export's record from step 2's files (name, type, signature, location, labels, return type and `params` in the provenance map's typed form), merges the patches, writes `{run_dir}/reextract-records.json`, where merge and step 5's `apply` read it, and prints `files_extracted`, `exports_extracted` and `confidence_breakdown` (T1, T1-low, T2): never type a record or count one by hand. Under a `public-api` scope (the `scope.type` detect-changes.md §1 bound, which `{run_dir}/extraction.json` records) it marks each export `public: true` or `public: false` by the runner's public surface (its `entry_point_diff`, by the rule the "Records" section of the helper's docstring gives), and prints `marked_not_public`, the count of `public: false`: in a normal-mode update merge documents, and step 5's `apply` adds, no export marked `public: false` that the provenance map does not already hold (merge.md Priority 5). In degraded mode the mark is not used. Add each of its `warnings[]` to `warnings[]`. On exit 1 whose error names a patch file, rewrite that file once in the shape of §2's return contract and run `records` again. On a second exit 1, any other exit 1, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "re-extract:records"`, its stderr as `reason`).
139
141
 
140
142
  ### 5. Display Extraction Summary and Auto-Proceed
141
143
 
@@ -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
 
@@ -153,7 +153,9 @@ In `{headless_mode}`, print the line as §5b says, with `status: "dry-run"` and
153
153
 
154
154
  {when the change manifest's `category_d` lists a document: **Tracked documents changed upstream:** each `docs_modified` path (modified) and `docs_deleted` path (deleted)}
155
155
 
156
- {in gap-driven mode, when gap-driven.md §1 left gaps unrouted: **Not repaired by this run:** each `{id}: {title} ({category})`}
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
+
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}
157
159
 
158
160
  ### Export Changes
159
161
 
@@ -248,7 +250,7 @@ The shared emitter writes the result contract and prints the line; never type ei
248
250
 
249
251
  ```bash
250
252
  cat > "{run_dir}/result-context.json" <<'SKF_JSON'
251
- {"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,
252
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>"}}}
253
255
  SKF_JSON
254
256
  ```
@@ -268,9 +270,10 @@ SKF_UPDATE_RESULT_JSON: {"skf_update": {"status": "success", "skill_name": ...,
268
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[]`.
269
271
 
270
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.
271
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.
272
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.
273
- - `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), `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` 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).
274
277
 
275
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:
276
279