bmad-module-skill-forge 5.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 (91) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/agents.md +1 -1
  4. package/docs/architecture.md +3 -3
  5. package/docs/campaign.md +1 -1
  6. package/docs/troubleshooting.md +9 -3
  7. package/docs/verifying-a-skill.md +9 -9
  8. package/docs/workflows.md +4 -4
  9. package/package.json +1 -1
  10. package/src/knowledge/tool-resolution.md +6 -6
  11. package/src/shared/references/headless-gate-convention.md +4 -1
  12. package/src/shared/references/output-contract-schema.md +3 -3
  13. package/src/shared/references/pipeline-contracts.md +2 -1
  14. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
  15. package/src/shared/scripts/skf-build-change-manifest.py +332 -54
  16. package/src/shared/scripts/skf-classify-changed-files.py +15 -10
  17. package/src/shared/scripts/skf-emit-result-envelope.py +58 -15
  18. package/src/shared/scripts/skf-extract-public-api.py +33 -13
  19. package/src/shared/scripts/skf-parse-gaps.py +3 -2
  20. package/src/shared/scripts/skf-verify-provenance-completeness.py +108 -11
  21. package/src/skf-analyze-source/SKILL.md +1 -0
  22. package/src/skf-analyze-source/references/generate-briefs.md +1 -1
  23. package/src/skf-analyze-source/references/headless-contract.md +1 -1
  24. package/src/skf-audit-skill/SKILL.md +2 -1
  25. package/src/skf-audit-skill/references/report.md +1 -1
  26. package/src/skf-brief-skill/SKILL.md +1 -0
  27. package/src/skf-brief-skill/references/invocation-contract.md +2 -2
  28. package/src/skf-campaign/SKILL.md +1 -0
  29. package/src/skf-campaign/references/campaign-contracts.md +2 -0
  30. package/src/skf-campaign/references/step-05-skill-loop.md +2 -2
  31. package/src/skf-campaign/references/step-10-export.md +3 -2
  32. package/src/skf-create-skill/SKILL.md +2 -1
  33. package/src/skf-create-skill/references/ccc-index-check.md +1 -1
  34. package/src/skf-create-skill/references/component-extraction.md +1 -1
  35. package/src/skf-create-skill/references/extract.md +1 -1
  36. package/src/skf-create-skill/references/generate-artifacts.md +1 -1
  37. package/src/skf-create-skill/references/report.md +2 -2
  38. package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
  39. package/src/skf-create-skill/references/validate.md +1 -1
  40. package/src/skf-create-stack-skill/SKILL.md +1 -0
  41. package/src/skf-create-stack-skill/references/generate-output.md +1 -1
  42. package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
  43. package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
  44. package/src/skf-create-stack-skill/references/report.md +1 -1
  45. package/src/skf-drop-skill/SKILL.md +1 -0
  46. package/src/skf-drop-skill/references/invocation-contract.md +1 -1
  47. package/src/skf-drop-skill/references/report.md +1 -1
  48. package/src/skf-export-skill/SKILL.md +1 -0
  49. package/src/skf-export-skill/references/invocation-contract.md +1 -1
  50. package/src/skf-export-skill/references/summary.md +1 -1
  51. package/src/skf-forger/SKILL.md +1 -0
  52. package/src/skf-forger/references/pipeline-mode.md +2 -2
  53. package/src/skf-quick-skill/SKILL.md +2 -1
  54. package/src/skf-quick-skill/references/finalize.md +1 -1
  55. package/src/skf-quick-skill/references/halt-contract.md +2 -2
  56. package/src/skf-quick-skill/references/write-and-validate.md +1 -1
  57. package/src/skf-refine-architecture/SKILL.md +2 -1
  58. package/src/skf-refine-architecture/customize.toml +1 -1
  59. package/src/skf-refine-architecture/references/report.md +1 -1
  60. package/src/skf-rename-skill/SKILL.md +1 -0
  61. package/src/skf-rename-skill/references/invocation-contract.md +2 -2
  62. package/src/skf-setup/SKILL.md +1 -0
  63. package/src/skf-setup/references/ccc-index.md +1 -1
  64. package/src/skf-setup/references/invocation-contract.md +1 -1
  65. package/src/skf-test-skill/SKILL.md +1 -0
  66. package/src/skf-test-skill/assets/output-section-formats.md +4 -4
  67. package/src/skf-test-skill/references/coherence-check.md +8 -6
  68. package/src/skf-test-skill/references/coverage-check-branches.md +23 -3
  69. package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
  70. package/src/skf-test-skill/references/coverage-check.md +30 -43
  71. package/src/skf-test-skill/references/init.md +1 -1
  72. package/src/skf-test-skill/references/inventory-contract.md +27 -0
  73. package/src/skf-test-skill/references/invocation-contract.md +3 -3
  74. package/src/skf-test-skill/references/report.md +3 -3
  75. package/src/skf-test-skill/references/scoring-rules.md +6 -6
  76. package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
  77. package/src/skf-test-skill/references/source-access-protocol.md +12 -8
  78. package/src/skf-test-skill/references/step-hard-gate.md +1 -0
  79. package/src/skf-test-skill/scripts/build-result-context.py +20 -3
  80. package/src/skf-test-skill/scripts/gap-ledger.py +78 -13
  81. package/src/skf-test-skill/scripts/load-coverage-inputs.py +345 -9
  82. package/src/skf-test-skill/scripts/locate-export-segments.py +591 -63
  83. package/src/skf-test-skill/scripts/score-signatures.py +939 -74
  84. package/src/skf-test-skill/scripts/validate-inventory.py +245 -29
  85. package/src/skf-update-skill/SKILL.md +1 -0
  86. package/src/skf-update-skill/references/gap-driven.md +28 -22
  87. package/src/skf-update-skill/references/invocation-contract.md +1 -1
  88. package/src/skf-update-skill/references/merge.md +4 -4
  89. package/src/skf-update-skill/references/report.md +1 -1
  90. package/src/skf-update-skill/references/write.md +5 -4
  91. package/src/skf-verify-stack/SKILL.md +2 -1
@@ -87,11 +87,11 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
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) |
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
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 |
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
95
  | `metadata` | Missing metadata or examples | metadata update, see rule R4 |
96
96
  | any other | any other gap | not routed |
97
97
 
@@ -99,13 +99,13 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
99
99
 
100
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
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.
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
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
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
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
106
  - `numerator-inflation`: a person documents the absent exports in SKILL.md or references/, reading each from the source.
107
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.
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
109
  - `discovery`: a person improves the skill's description so realistic prompts route to it, or runs the discovery test Test Skill skipped.
110
110
  - `description`: a person edits the description in the SKILL.md frontmatter as the gap suggests.
111
111
  - `external-validator`: a person applies the skill-check diagnostic or Tessl Review suggestion to SKILL.md.
@@ -123,10 +123,10 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
123
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.
124
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:
125
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;
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).
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
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. 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.
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.
130
130
 
131
131
  3. **Build the change manifest through `{parseGapsHelper}`**, never by hand. From `{project-root}`, run:
132
132
 
@@ -136,18 +136,19 @@ Read the gaps of the test report at `{test_report_path}` and translate them into
136
136
  --provenance-map "{provenance_map_path}" \
137
137
  [--judgments "{run_dir}/gap-judgments.json"] \
138
138
  [--read-only {dry-run|detect-only}] \
139
- -o "{run_dir}/change-manifest.json"
139
+ -o "{run_dir}/change-manifest.json" \
140
+ > "{run_dir}/translate.json"
140
141
  ```
141
142
 
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`:
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:
143
144
 
144
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:
145
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;
146
147
  - `name`: the export the gap's title names, or null when it names none, which leaves the gap unrouted;
147
148
  - `rescope`: rule R1's answer, true or false;
148
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.
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.
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}"`.
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}"`.
151
152
 
152
153
  ### 2. Display the Gap Summary and Route
153
154
 
@@ -191,7 +192,7 @@ Bind `{workspace_drift_status}` ← `status` and `{head_short_sha}` ← `head_sh
191
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}"`.
192
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}"`.
193
194
 
194
- **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):
195
196
 
196
197
  - `a public API recount from the tree (rule R1)`: a rescope (`DELETED_EXPORT`);
197
198
  - `a line from the tree (rule R3)`: a provenance-completeness gap (`provenance_completeness: true`), which §4 bullet 1 routes to §4a;
@@ -234,12 +235,13 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
234
235
  --manifest "{run_dir}/change-manifest.json" \
235
236
  --provenance-map "{provenance_map_path}" \
236
237
  [--source-root "{source_root}"] \
238
+ [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] \
237
239
  --drift-status "{workspace_drift_status}" \
238
240
  [--judgments "{run_dir}/gap-judgments.json"] \
239
241
  --files-out "{run_dir}/remediation-files.json"
240
242
  ```
241
243
 
242
- 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`:
243
245
 
244
246
  - **`drift-blocked`:** §3's drift gate halts.
245
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.
@@ -247,7 +249,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
247
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.
248
250
  - **Exit 1 or no JSON:** HALT with status `blocked` (halt procedure: `phase: "re-extract:gap-records"`, its stderr as `reason`).
249
251
 
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.
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.
251
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`:
252
256
 
253
257
  ```bash
@@ -255,14 +259,16 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
255
259
  --manifest "{run_dir}/change-manifest.json" \
256
260
  --provenance-map "{provenance_map_path}" \
257
261
  [--source-root "{source_root}"] \
262
+ [--brief "{forge_data_folder}/{skill_name}/skill-brief.yaml"] \
258
263
  --drift-status "{workspace_drift_status}" \
259
264
  [--judgments "{run_dir}/gap-judgments.json"] \
260
265
  [--remediation-records "{run_dir}/remediation-records.json"] \
261
266
  [--evidence "{run_dir}/evidence-records.jsonl" --tier "{forge_tier}"] \
262
- -o "{run_dir}/reextract-records.json"
267
+ -o "{run_dir}/reextract-records.json" \
268
+ > "{run_dir}/gap-records.json"
263
269
  ```
264
270
 
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`:
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:
266
272
 
267
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:
268
274
 
@@ -293,10 +299,10 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
293
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:
294
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.
295
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.
296
- - **`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.
297
303
  - **Exit 1 or no JSON:** HALT with status `blocked` (halt procedure: `phase: "re-extract:gap-records"`, its stderr as `reason`).
298
304
 
299
- 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.
300
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.
301
307
  4. Go on to §5. §4a below runs only where bullet 1 calls it.
302
308
 
@@ -311,7 +317,7 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
311
317
  **Procedure:**
312
318
 
313
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.
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.
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.
315
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}`:
316
322
 
317
323
  ```bash
@@ -323,7 +329,7 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
323
329
  -o "{run_dir}/remediation-exports.json"
324
330
  ```
325
331
 
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:
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:
327
333
 
328
334
  ```bash
329
335
  uv run {buildChangeManifestHelper} records \
@@ -339,9 +345,9 @@ Spot-check each gap's citation instead of re-extracting the files: confirm each
339
345
 
340
346
  Display the summary below, with the `counts` §4 bullet 2 printed:
341
347
 
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}."
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}."
343
349
 
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.
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.
345
351
 
346
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."
347
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. |
@@ -94,7 +94,7 @@ Apply merge in the following priority order.
94
94
  **Priority 4 — Process MODIFIED exports:**
95
95
  - Replace generated content for the export with fresh extraction
96
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.
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.
98
98
  - Preserve [MANUAL] blocks adjacent to the export
99
99
  - Check for position conflicts (new content shifts [MANUAL] block)
100
100
  - If position conflict: flag as POSITION conflict
@@ -105,7 +105,7 @@ Apply merge in the following priority order.
105
105
  - No conflicts expected (new content, no existing [MANUAL])
106
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
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.
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.
109
109
 
110
110
  **Priority 6 — Process script/asset file changes (from Category D in change manifest):**
111
111
 
@@ -144,7 +144,7 @@ For each entry of `promoted_docs_new[]`, read from `{run_dir}/promoted-docs.json
144
144
  For each `STRUCTURAL_FIX` entry gap-driven.md §4 forwarded:
145
145
 
146
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`).
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.
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.
148
148
  - Do **not** add, modify, or remove any provenance `entries[]` row — STRUCTURAL_FIX never touches the provenance map.
149
149
  - Preserve any [MANUAL] blocks; if the fix location overlaps a [MANUAL] block, flag as a POSITION conflict instead of editing.
150
150
  - Record in the update report: `"Structural fix: {remediation summary} at {file}:{line}"`.
@@ -219,7 +219,7 @@ Merge Results:
219
219
  manual_orphans_removed: [count]
220
220
  ```
221
221
 
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:
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:
223
223
 
224
224
  ```bash
225
225
  cat > "{run_dir}/merge-records.json" <<'SKF_JSON'
@@ -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}}
@@ -282,15 +282,16 @@ uv run {buildChangeManifestHelper} apply \
282
282
  --not-public-out "{run_dir}/not-public.json" \
283
283
  --metadata-in "{skill_package}/metadata.json" \
284
284
  --lists-out "{run_dir}/metadata-lists.json" \
285
- -o "{forge_version}/provenance-map.json"
285
+ -o "{forge_version}/provenance-map.json" \
286
+ > "{run_dir}/apply.json"
286
287
  ```
287
288
 
288
289
  `{update_type}` follows the run's mode as report.md §2 names it (`{update_mode}`): `incremental` for `normal`, `gap-driven` for `gap-driven`, `full` for `degraded` (a degraded run has no map: leave out `--provenance-map`, and `apply` starts a new one). Pass each `{run_dir}` file the run wrote: the change manifest and the records in every mode, `category-c.json` and `ccc-pairs.json` (step 2's Category C and its CCC check), `extraction.json` and `export-details.json` when step 2 wrote them, `merge-records.json` when step 4 did, the Category D files (`category-d-compare.json`, and `new-files.json` when step 2 wrote it: a skill with no brief has none) with `--source-root` when step 2 ran Category D, and `promoted-docs.json` when step 2 §1b wrote it. `{generation_date}` is the value §2 bound, `--test-report-run-id` goes only with a gap-driven run (the `{test_report_run_id}` of the report it applied), `{manual_sections_preserved}` is `len(preserved) + len(moved)` from the §1 `manual-verify` verdict (never a marker count), and `{map_source_commit}` and `{map_source_ref}` are the `source_commit` and `source_ref` §2 writes to `metadata.json` (an empty string for a null one): audit-skill reads its baseline commit from the map's top-level `source_commit` and `source_ref`. Pass `--drift-head` and `--drift-pinned` only when `{workspace_drift_status}` is `overridden` (gap-driven.md §3). Pass `--metadata-in` and `--lists-out` in every mode: `apply` writes there the lists §2's `metadata` call reads. Every entry `apply` writes carries `signature_source`, which §2's stats helper bins (an entry the map held with none, or with a value other than `T1`, `T1-low`, `T2` or `T3`, as a map written before the key existed does, takes its `confidence` when that is `T1-low`, `T2` or `T3`, else `T1` when its `extraction_method` is `ast-grep` or `ast_bridge`, else `T1-low`), and `apply` sets the map's update operation block (`"last_update"`, `"update_type"` and its other keys), replacing an earlier update's.
289
290
 
290
291
  Dispatch on its exit code:
291
292
 
292
- - **0:** it printed `{status: "written", entries, not_public, relabeled, signature_source_backfilled, file_entries, warnings, map}`. Bind `{provenance_spot_check_warnings}` ← its `warnings`, and add each to `warnings[]`; the update report lists each as a WARN (report.md §2). In gap-driven mode a `NEW_EXPORT` or `MODIFIED_EXPORT` the map holds whose spot-check ended `missing` or `unknown` keeps its entry unchanged, whatever its severity, with one of these warnings, which names 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 (any severity but `Medium`, `Low` or `Info`) it also names the gap and the one fix a re-run acts on (gap-driven.md §4 bullet 1): fix the entry by hand, or, for several entries of its name, give the gap a `file:line` Source at the definition when that file holds only one of them, then re-run update-skill. A `cited-elsewhere` gap, whatever its severity, takes its own warning and fix in their place (gap-driven.md §4 bullet 1). `not_public` lists, `{name, file}`, each new export of a `public-api` skill it did not add in normal mode as off the public surface (the `public: false` mark of `reextract-records.json`, else the same rule over the `scope.type` and `entry_point_diff` of `extraction.json`), and one of its `warnings` counts them; it also wrote that list to `{run_dir}/not-public.json` (`[]` when it left none out). When `extraction.json` names `public-api` but gives no surface the rule can read (Quick tier, a runner failure, an incomplete run), it adds every name, with one warning that says so. `relabeled` lists, `{name, file, from, to}`, each entry whose `signature_source` a gap-driven repair changed when it wrote the signature §4a's record gave (a record read by eye moves an `ast-grep` entry from T1 to T1-low), each with one of its `warnings`: §2 adds each name to `{provenance_relabels}`. `signature_source_backfilled` counts, by value, the entries it gave a `signature_source` because they lacked or held an unrecognized one (`{"T1-low": 2}`, `{}` when none), with one of its `warnings`, which also lists each unrecognized value it replaced (`name: old -> new`); it does not list them in `entries.updated`. In a map naming several libraries, one of its `warnings` counts and names each new entry it gave the map's first `source_library` because the entry's file or folder did not settle which (`<name> in <file>`, or `<name> (no file)`): check that entry's `source_library` in `{forge_version}/provenance-map.json` and correct it by hand when it names the wrong library.
293
- - **3** (`status` `refused`): a blocking gap reached the provenance write with no source line (`blocking_unresolved[]`: a `NEW_EXPORT` the map does not hold, `unknown`, with a severity other than `Medium`, `Low` or `Info`, a missing one included). gap-driven.md §4 sends every such gap to its §4a and halts with `halted-for-remediation-path` before merge when §4a does not find it, so this happens only when that check was skipped or bypassed: `apply` wrote nothing, and no null citation is written. HALT with status `blocked` and write no `metadata.json`, `provenance-map.json` or other artifact. Tell the user: "**A blocking gap reached the provenance write with no source line:** {each export name, with its severity or "no severity"}. gap-driven.md §4 should have stopped on it with `halted-for-remediation-path`. The halt restored SKILL.md and references/ from the snapshot step 4 took; give each gap a `file:line` Source or name the file that defines its export in its Remediation (or downgrade it to Medium, Low or Info), then re-run update-skill." The halt procedure takes `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "blocking-gap-unresolved: {export names}: gap-driven.md §4 should have halted with halted-for-remediation-path"`.
293
+ - **0:** it printed `{status: "written", entries, not_public, relabeled, signature_source_backfilled, file_entries, warnings, map}` to `{run_dir}/apply.json`. Bind `{provenance_spot_check_warnings}` ← its `warnings`, and add each to `warnings[]`, recording them in one call, from `{project-root}`: `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warnings-file "{run_dir}/apply.json"`; the update report lists each as a WARN (report.md §2). In gap-driven mode a `NEW_EXPORT` or `MODIFIED_EXPORT` the map holds whose spot-check ended `missing` or `unknown` keeps its entry unchanged, whatever its severity, with one of these warnings, which names 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 (any severity but `Medium`, `Low` or `Info`) it also names the gap and the one fix a re-run acts on (gap-driven.md §4 bullet 1): fix the entry by hand, or, for several entries of its name, give the gap a `file:line` Source at the definition when that file holds only one of them, then re-run update-skill. A `cited-elsewhere` gap, whatever its severity, takes its own warning and fix in their place (gap-driven.md §4 bullet 1). A rule R5 `MOVED_EXPORT` whose map entry's file is gone keeps its entry unchanged too, whatever its severity, with a warning that names the file and the hand fix. An `outside-scope` gap (the skill brief's scope leaves out the file it names), whatever its severity, adds no entry, a placeholder included, and is never refused, with its own warning, which names the files left out and 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 (gap-driven.md §4 bullet 1). `not_public` lists, `{name, file}`, each new export of a `public-api` skill it did not add in normal mode as off the public surface (the `public: false` mark of `reextract-records.json`, else the same rule over the `scope.type` and `entry_point_diff` of `extraction.json`), and one of its `warnings` counts them; it also wrote that list to `{run_dir}/not-public.json` (`[]` when it left none out). When `extraction.json` names `public-api` but gives no surface the rule can read (Quick tier, a runner failure, an incomplete run), it adds every name, with one warning that says so. `relabeled` lists, `{name, file, from, to}`, each entry whose `signature_source` a gap-driven repair changed when it wrote the signature §4a's record gave (a record read by eye moves an `ast-grep` entry from T1 to T1-low), each with one of its `warnings`: §2 adds each name to `{provenance_relabels}`. `signature_source_backfilled` counts, by value, the entries it gave a `signature_source` because they lacked or held an unrecognized one (`{"T1-low": 2}`, `{}` when none), with one of its `warnings`, which also lists each unrecognized value it replaced (`name: old -> new`); it does not list them in `entries.updated`. In a map naming several libraries, one of its `warnings` counts and names each new entry it gave the map's first `source_library` because the entry's file or folder did not settle which (`<name> in <file>`, or `<name> (no file)`): check that entry's `source_library` in `{forge_version}/provenance-map.json` and correct it by hand when it names the wrong library.
294
+ - **3** (`status` `refused`): a blocking gap reached the provenance write with no source line (`blocking_unresolved[]`: a `NEW_EXPORT` the map does not hold, `unknown` for any cause but `outside-scope`, with a severity other than `Medium`, `Low` or `Info`, a missing one included). gap-driven.md §4 sends every such gap to its §4a and halts with `halted-for-remediation-path` before merge when §4a does not find it, so this happens only when that check was skipped or bypassed: `apply` wrote nothing, and no null citation is written. HALT with status `blocked` and write no `metadata.json`, `provenance-map.json` or other artifact. Tell the user: "**A blocking gap reached the provenance write with no source line:** {each export name, with its severity or "no severity"}. gap-driven.md §4 should have stopped on it with `halted-for-remediation-path`. The halt restored SKILL.md and references/ from the snapshot step 4 took; give each gap a `file:line` Source or name the file that defines its export in its Remediation (or downgrade it to Medium, Low or Info), then re-run update-skill." The halt procedure takes `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "blocking-gap-unresolved: {export names}: gap-driven.md §4 should have halted with halted-for-remediation-path"`.
294
295
  - **Exit 1, no JSON, or no candidate resolves:** HALT with status `halted-for-write-failure` (halt procedure: `phase: "write:provenance-map"`, `path: "{forge_version}/provenance-map.json"`, `reason: "<its stderr, or skf-build-change-manifest.py is missing; re-install SKF>"`).
295
296
 
296
297
  **For script/asset file changes:** `apply` updates the rows; copy or remove the files themselves in `{skill_package}`, each path the change manifest's `category_d` lists: a `scripts_*` path under `scripts/`, an `assets_*` path under `assets/`:
@@ -403,7 +404,7 @@ The helper reads its `--source-root` (falling back to the provenance map's own `
403
404
  - `missing[]` — documented exports (metadata `exports[]`) with no provenance entry: a coverage gap.
404
405
  - `orphaned[]` — provenance `entries[].export_name` whose export was removed but the entry remains.
405
406
  - `summary.set_diff`: `not-applicable` for a reference app (`scope_type: reference-app` in `{skill_package}/metadata.json`), whose `exports[]` is empty by design while its entries follow each citation: the helper then diffs no sets, `missing[]` and `orphaned[]` stay empty, and its `status` comes from `stale[]`, `citations[]` and `node_kinds[]` alone. `checked` for every other skill.
406
- - `stale[]`: entries whose `source_file:source_line` no longer resolves or is not the line that defines the export; each carries a `reason` of `file-missing`, `line-out-of-bounds`, `line-invalid` or `line-not-definition`. A `line-not-definition` item lists in `definition_lines` every line of the file that defines the export (the `def` or declaration line itself, not a decorator or blank line above it), except that a Python `@overload` group gives its first stub alone. An empty list means the rules found no definition line, which may be a shape they do not cover: the entry is unverified, not gone. Internal names are canonicalized through `reexport_map` before the diff, so a barrel-renamed export does not read as missing or orphaned.
407
+ - `stale[]`: entries whose `source_file:source_line` no longer resolves or is not the line that defines the export; each carries a `reason` of `file-missing`, `line-out-of-bounds`, `line-invalid` or `line-not-definition`. A `line-not-definition` item lists in `definition_lines` every line of the file that defines the export (the `def` or declaration line itself, not a decorator or blank line above it), except that a Python `@overload` group gives its first stub alone. An empty list means the rules found no definition line, which may be a shape they do not cover: the entry is unverified, not gone. A bookkeeping variant (`X_def`, `X_exact`, `a11y_X`) its file never names is checked against its base `X`, and its item names that base in `folded_to`. A bookkeeping variant (`X_def`, `X_exact`, `a11y_X`) its file never names is checked against its base `X`, and its item names that base in `folded_to`. Internal names are canonicalized through `reexport_map` before the diff, so a barrel-renamed export does not read as missing or orphaned.
407
408
  - `citations[]`: `[AST:]` / `[SRC:]` citations in SKILL.md and `references/` whose prefix disagrees with the entry they cite (`prefix-mismatch`: `ast-grep` gives `[AST:]`, `source-read` gives `[SRC:]`), and `[AST:]` citations in a skill whose map has no `ast-grep` entry (`ast-without-ast-grep`). Each gives the markdown `file` and `line`, the `citation` text and its `expected_prefix`.
408
409
  - `summary.stale_check` — `"checked"` when citations were resolved against the source tree, or `"skipped-no-source-root"` when no source root resolved on disk (the completeness + orphan diffs still ran; `stale` is empty by construction, not clean-by-verification).
409
410
  - `summary.skill_citations_scanned` and `summary.skill_citations_matched`: when citations were scanned and none matched, no citation names a path and line the map records (the citations use another root), so the prefix check compared nothing. List that as a WARN.
@@ -33,6 +33,7 @@ These rules apply to every step in this workflow:
33
33
  - Every verdict must cite evidence from the generated skills
34
34
  - Only load one step file at a time — never preload future steps
35
35
  - If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread
36
+ - 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.
36
37
  - Always communicate in `{communication_language}`
37
38
  - At any interactive prompt, the inputs `cancel`, `exit`, `[X]`, `q`, or `:q` exit cleanly with exit code 6 (`halt_reason: "user-cancelled"`)
38
39
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision; a gate that picks its default for the user (step 1's offer of the newest earlier report) also records that decision in the run sink the moment it decides
@@ -61,7 +62,7 @@ These rules apply to every step in this workflow:
61
62
  | **Flags** | `--headless` / `-H` (auto-resolve all gates); `--architecture-doc <path>` (skip step 1 prompt for the required input); `--prd <path>` (skip step 1 prompt for the optional PRD); `--previous-report <path>` (skip step 1 prompt for delta comparison) |
62
63
  | **Gates** | step 1 Input Gate (use args), including the offer to compare against the newest earlier report (headless default: use it). No later stage waits for input: a run with 0% coverage or only Blocked pairs prints one warning and continues, so its report still carries the recommendations. |
63
64
  | **Outputs** | `feasibility-report-{project_slug}-{timestamp}.md` and its `feasibility-report-{project_slug}-latest.md` copy (not a symlink), written by step 6 once the finished report passes the feasibility-report check, both per the SKF shared feasibility report schema (`{project-root}/_bmad/skf/shared/references/feasibility-report-schema.md`; `{project-root}/src/shared/references/feasibility-report-schema.md` in a dev checkout), with integration verdicts, coverage analysis, recommendations, and evidence sources; plus `verify-stack-result-{YYYYMMDD-HHmmss}.json` and `verify-stack-result-latest.json` in `{forge_data_folder}` |
64
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Per-flag args (`--architecture-doc`, `--prd`, `--previous-report`) consumed at the gates that would otherwise prompt. |
65
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Per-flag args (`--architecture-doc`, `--prd`, `--previous-report`) consumed at the gates that would otherwise prompt. A caller reads the run's own `SKF_VERIFY_STACK_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 `verify-stack-result-latest.json` may then describe an earlier run, so it is never read in its place. |
65
66
  | **Exit codes** | See `references/exit-codes.md` |
66
67
 
67
68
  ## Result Contract (Headless)