bmad-module-skill-forge 1.0.0 → 1.1.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.
@@ -67,14 +67,23 @@ scope:
67
67
  # - "code/core/src/manager-api/**"
68
68
  # - "code/core/src/preview-api/**"
69
69
  notes: "Optional notes about scope decisions"
70
- # Optional: amendment log for scope decisions made during create-skill §2a
70
+ # Optional: amendment log for scope decisions made during create-skill §2a,
71
+ # update-skill §1b (auth-doc), and update-skill §1c (scope-expansion).
71
72
  # amendments:
72
73
  # - path: "apps/docs/public/llms.txt"
73
- # action: "promoted" # "promoted" | "skipped"
74
+ # action: "promoted" # "promoted" | "skipped" | "demoted-include" | "demoted-exclude"
75
+ # category: "auth-doc" # "auth-doc" (default for legacy entries) | "scope-expansion"
74
76
  # reason: "authoritative AI docs — only source for canonical install command"
75
- # heuristic: "llms.txt"
77
+ # heuristic: "llms.txt" # required for auth-doc; absent for scope-expansion
76
78
  # date: "2026-04-11"
77
79
  # workflow: "skf-create-skill"
80
+ # - path: "python/cocoindex/_internal/api.py"
81
+ # action: "promoted"
82
+ # category: "scope-expansion"
83
+ # reason: "out-of-scope new public API — drift report drift-report-20260424-212355.md"
84
+ # evidence: "~70 new exports flagged out-of-scope by audit"
85
+ # date: "2026-04-25"
86
+ # workflow: "skf-update-skill"
78
87
  # Additional fields when scope.type is "component-library":
79
88
  # registry_path: "path/to/registry.ts" # Optional — auto-detected if omitted
80
89
  # ui_variants: # Optional — design system variants
@@ -87,37 +96,46 @@ scope:
87
96
 
88
97
  ### Scope Amendments (Optional)
89
98
 
90
- `scope.amendments[]` is an additive, optional audit log of scope decisions made by workflows after the brief was first authored. Its primary writer is `skf-create-skill` §2a (Discovered Authoritative Files Protocol), which appends entries when extraction discovers authoritative AI documentation files (`llms.txt`, `AGENTS.md`, etc.) that the original scope patterns excluded.
99
+ `scope.amendments[]` is an additive, optional audit log of scope decisions made by workflows after the brief was first authored. Two writer paths exist today:
100
+
101
+ - **Auth-doc promotions** (`category: "auth-doc"`) — `skf-create-skill` §2a and its mirror `skf-update-skill` §1b append entries when extraction discovers authoritative AI documentation files (`llms.txt`, `AGENTS.md`, etc.) that the original scope patterns excluded.
102
+ - **Scope-expansion promotions** (`category: "scope-expansion"`) — `skf-update-skill` §1c appends entries when an audit drift report flags out-of-scope new public API paths (typically a major-version restructure where the brief's `scope.include` no longer reflects the real surface).
91
103
 
92
104
  **Entry fields:**
93
105
 
94
106
  | Field | Type | Required | Description |
95
107
  |---|---|---|---|
96
- | `path` | string | yes | Relative path from source root to the file being amended. Matches the literal path added to `scope.include` (for `promoted` actions). |
97
- | `action` | string | yes | One of: `promoted` (file added to scope with a literal `scope.include` entry), `skipped` (user declined promotion; file remains out of scope, decision recorded to prevent re-prompting). |
98
- | `reason` | string | yes | Human-readable sentence explaining the decision. Either user-provided at prompt time or auto-generated ("authoritative AI docs matched heuristic {basename}"). |
99
- | `heuristic` | string | yes | The basename heuristic that matched (`llms.txt`, `AGENTS.md`, etc.) so future audits can verify the file still matches its original classification. |
108
+ | `path` | string | yes | Relative path (or glob, for `category: "scope-expansion"`) from source root to the file or tree being amended. For `promoted` actions this matches the literal entry added to `scope.include`. |
109
+ | `action` | string | yes | One of: `promoted` (path added to `scope.include`), `skipped` (user declined promotion; decision recorded to prevent re-prompting), `demoted-include` (path removed from `scope.include` — only valid with `category: "scope-expansion"`), `demoted-exclude` (path removed from `scope.exclude` — only valid with `category: "scope-expansion"`). |
110
+ | `category` | string | no | One of: `auth-doc` (default for entries without this field — the historical sole use case), `scope-expansion`. Distinguishes which workflow path wrote the entry and which writer-rules apply on re-runs. |
111
+ | `reason` | string | yes | Human-readable sentence explaining the decision. Either user-provided at prompt time or auto-generated. |
112
+ | `heuristic` | string | conditional | Required for `category: "auth-doc"` — the basename that matched (`llms.txt`, `AGENTS.md`, etc.). Omit for `category: "scope-expansion"`. |
113
+ | `evidence` | string | conditional | Required for `category: "scope-expansion"` — short rationale from the source signal (e.g., a drift-report finding's evidence one-liner). Omit for `category: "auth-doc"`. |
100
114
  | `date` | string | yes | ISO date (`YYYY-MM-DD`) when the amendment was recorded. |
101
- | `workflow` | string | yes | Workflow name that wrote the amendment (`skf-create-skill`, `skf-update-skill`). Identifies which workflow's §2a-equivalent made the decision. |
115
+ | `workflow` | string | yes | Workflow name that wrote the amendment (`skf-create-skill`, `skf-update-skill`). Identifies which workflow made the decision. |
116
+
117
+ **Promotion write-through:** When `action: "promoted"`, the workflow also appends the literal path to `scope.include`. This is a belt-and-suspenders design: future runs read `scope.include` during scope filtering and include the file in the filtered list automatically, so the §2a/§1b/§1c discovery loop finds no candidate and does not re-prompt. The `amendments[]` entry is the human-readable audit trail of *why* the path was added.
102
118
 
103
- **Promotion write-through:** When `action: "promoted"`, the workflow also appends the literal path to `scope.include`. This is a belt-and-suspenders design: future `skf-create-skill` runs read `scope.include` during §2 and include the file in the filtered list automatically, so §2a finds no candidate and does not re-prompt. The `amendments[]` entry is the human-readable audit trail of *why* the path was added.
119
+ **Skip recording:** When `action: "skipped"`, the workflow does NOT modify `scope.include` or `scope.exclude`. The amendment entry alone is enough to prevent re-prompting, because the discovery loop checks `amendments[]` before prompting.
104
120
 
105
- **Skip recording:** When `action: "skipped"`, the workflow does NOT modify `scope.include` or `scope.exclude`. The amendment entry alone is enough to prevent re-prompting, because §2a checks `amendments[]` before prompting.
121
+ **Demotion (scope-expansion only):** `demoted-include` removes a previously-promoted path from `scope.include` used when a prior `[P]` decision is reversed. `demoted-exclude` removes a path from `scope.exclude` used when a previously excluded path needs to be re-evaluated. Both write the structural change and append the amendment so future runs see the rationale. Demotion is not valid for `category: "auth-doc"`: auth-doc skips already prevent re-prompting without scope mutation.
106
122
 
107
- **Backward compatibility:** `scope.amendments` is optional. Briefs without this field validate unchanged. Treat missing as an empty list.
123
+ **Backward compatibility:** `scope.amendments` is optional. Briefs without this field validate unchanged. Treat missing as an empty list. Existing entries without `category` are equivalent to `category: "auth-doc"` — readers must default the field when absent.
108
124
 
109
125
  **Who reads `amendments[]`:**
110
126
 
111
- - `skf-create-skill` §2a consults it to avoid re-prompting on decided files.
112
- - `skf-update-skill` §1b (mirror of §2a) consults it for the same reason.
113
- - `skf-audit-skill` may optionally report on stale promotions (promoted files that no longer exist in source) as a future enhancement — not currently implemented.
127
+ - `skf-create-skill` §2a consults it to avoid re-prompting on decided auth-doc files.
128
+ - `skf-update-skill` §1b (mirror of §2a) consults it for the same auth-doc reason.
129
+ - `skf-update-skill` §1c consults it to avoid re-prompting on decided scope-expansion candidates and to honor prior `demoted-*` decisions.
130
+ - `skf-audit-skill` may optionally report on stale promotions (promoted paths that no longer exist in source) as a future enhancement — not currently implemented.
114
131
  - Humans reading the brief see the audit trail of non-obvious scope decisions.
115
132
 
116
133
  **Who writes `amendments[]`:**
117
134
 
118
- - `skf-create-skill` §2a (Discovered Authoritative Files Protocol)
119
- - `skf-update-skill` §1b (mirror of §2a applied during change detection)
120
- - Manual edits by the brief author are permitted but should include all required fields above.
135
+ - `skf-create-skill` §2a (Discovered Authoritative Files Protocol) — `category: "auth-doc"`
136
+ - `skf-update-skill` §1b (mirror of §2a applied during change detection) — `category: "auth-doc"`
137
+ - `skf-update-skill` §1c (Major-Version Scope Reconciliation) `category: "scope-expansion"`
138
+ - Manual edits by the brief author are permitted but should include all required fields above (and `category` when the entry is not an auth-doc decision).
121
139
 
122
140
  ## YAML Template
123
141
 
@@ -95,7 +95,7 @@ For each entry in `target_context_files`:
95
95
 
96
96
  5. **Rewrite root paths for the current context file** using the generic rewrite algorithm from export-skill step-04 section 4d:
97
97
 
98
- For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the current entry's `skill_root` if different. **Override guard:** If `snippet_skill_root_override` is set in config.yaml AND the extracted prefix equals the override value, skip the rewrite leave the root path untouched. See `skf-export-skill/steps-c/step-04-update-context.md` §4d for full semantics.
98
+ For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the **effective target prefix** if different. The effective target prefix is `snippet_skill_root_override` when that key is set in config.yaml applied uniformly to every snippet so the managed section references the real on-disk location and never mixes override and per-IDE pathsotherwise the current entry's `skill_root`. See `skf-export-skill/steps-c/step-04-update-context.md` §4d for full semantics.
99
99
 
100
100
  6. **Sort skills alphabetically by name.** Count totals (skills, stack skills).
101
101
 
@@ -71,7 +71,7 @@ All other IDEs use AGENTS.md as the context file, each with its own skill direct
71
71
  **`snippet_skill_root_override` (optional):** Authoring repos where all skills live under one shared on-disk directory (e.g. `skills/`) that does not match any per-IDE skill root may set `snippet_skill_root_override: skills/` in `config.yaml`. When set:
72
72
 
73
73
  - `export-skill/step-03` §2.7 uses the override as `{skill_root}` for snippet generation instead of the IDE-mapped value
74
- - `export-skill/step-04` §4d (and its equivalents in `drop-skill/step-02` and `rename-skill/step-02`) skip the prefix rewrite for any snippet whose current prefix already equals the override — the managed section will reference the on-disk location instead of a per-IDE directory that does not exist
74
+ - `export-skill/step-04` §4d (and its equivalents in `drop-skill/step-02` and `rename-skill/step-02`) treat the override as the effective target prefix for the managed-section rebuild: snippets whose prefix already matches the override pass through unchanged, and any other prefix including per-IDE prefixes carried by sibling snippets that were exported before the override was adopted, and legacy `skills/` drafts — is rewritten to the override so the rebuilt managed section uniformly references the real on-disk location
75
75
 
76
76
  Consuming projects (the common case) omit the field and keep the default IDE-mapping behavior. The override is a narrow escape hatch for repos that author skills into `{skills_output_folder}` and never duplicate them into `.claude/skills/`-style directories.
77
77
 
@@ -166,11 +166,11 @@ For each snippet being included in the managed section:
166
166
 
167
167
  1. Read the `root:` value from the snippet's first line — it has the form `root: {prefix}{skill-name}/`
168
168
  2. Extract the current prefix by stripping the trailing `{skill-name}/` from the root value
169
- 3. **Override guard:** If `snippet_skill_root_override` is set in config.yaml AND the extracted prefix equals the override value, skip the rewrite for this snippetleave the root path untouched. This is the authoring-repo escape hatch: when the override is set, any snippet that already matches the override prefix is treated as ground truth and must not be rewritten to a per-IDE path that does not exist on disk.
170
- 4. Otherwise, compare the extracted prefix against the current target's `skill_root`
171
- 5. If they differ, replace the prefix with the target's `skill_root`, preserving the skill name
169
+ 3. **Resolve the effective target prefix:** If `snippet_skill_root_override` is set in config.yaml, use the override value as the effective target prefix for this rebuildby the override contract every skill in the repo lives under that single shared on-disk directory, so the managed section must reference the override for *every* snippet (not just the ones that already match it). Otherwise, use the current target's `skill_root` as the effective target prefix.
170
+ 4. Compare the extracted prefix against the effective target prefix
171
+ 5. If they differ, replace the prefix with the effective target prefix, preserving the skill name. When the override is set, this rewrites stale per-IDE prefixes carried by sibling snippets that were exported before the override was adopted, so the rebuilt managed section uniformly references the real on-disk location instead of mixing override and per-IDE paths.
172
172
  6. Example: if snippet has `root: .claude/skills/my-lib/` but target skill_root is `.windsurf/skills/`, rewrite to `root: .windsurf/skills/my-lib/`
173
- 7. Example: if snippet has legacy `root: skills/my-lib/` and target skill_root is `.github/skills/`, rewrite to `root: .github/skills/my-lib/` **unless** `snippet_skill_root_override: skills/` is set, in which case the `skills/` prefix is preserved
173
+ 7. Example: if `snippet_skill_root_override: skills/` is set, the effective target prefix is `skills/` regardless of the IDE mapping. A snippet with `root: skills/my-lib/` passes through unchanged; a sibling snippet with `root: .claude/skills/my-other/` (carried over from a pre-override export) is rewritten to `root: skills/my-other/`.
174
174
 
175
175
  This algorithm handles any IDE's skill root path — including future IDEs — without enumerating known prefixes. The legacy `skills/` prefix (no leading dot) may appear in draft snippets generated by create-skill/quick-skill before export.
176
176
 
@@ -199,7 +199,7 @@ For each entry in `target_context_files`:
199
199
 
200
200
  4. **Rewrite root paths for the current context file** using the generic rewrite algorithm from export-skill step-04 section 4d:
201
201
 
202
- For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the current entry's `skill_root` if different. **Override guard:** If `snippet_skill_root_override` is set in config.yaml AND the extracted prefix equals the override value, skip the rewrite leave the root path untouched. See `skf-export-skill/steps-c/step-04-update-context.md` §4d for full semantics.
202
+ For each snippet, parse the `root:` line (`root: {prefix}{skill-name}/`), strip the trailing `{skill-name}/` to extract the current prefix, and replace it with the **effective target prefix** if different. The effective target prefix is `snippet_skill_root_override` when that key is set in config.yaml applied uniformly to every snippet so the managed section references the real on-disk location and never mixes override and per-IDE pathsotherwise the current entry's `skill_root`. See `skf-export-skill/steps-c/step-04-update-context.md` §4d for full semantics.
203
203
 
204
204
  5. **Sort skills alphabetically by name.** Count totals (skills, stack skills).
205
205
 
@@ -79,7 +79,7 @@ test-skill is a quality gate — it MUST NOT trust subagent output blindly. Befo
79
79
 
80
80
  **Schema validation (required keys + types):**
81
81
 
82
- 1. Parse the subagent response as JSON. On parse failure → HALT "coverage-check: subagent response not valid JSON".
82
+ 1. Strip wrapping markdown fences before parsing. Subagents frequently return JSON wrapped in a code fence — a line of three backticks (optionally followed by a language tag like `json`) preceding the JSON and a closing line of three backticks after it — despite prompt instructions to return raw JSON. When the first non-empty line of the response is three backticks (optionally with a language tag) and the last non-empty line is three backticks, remove those two fence lines before parsing. Then parse the remaining content as JSON. On parse failure of the inner content → HALT "coverage-check: subagent response not valid JSON".
83
83
  2. Required keys present: `exports` (list), `cross_check_mismatches` (list — may be empty). Missing key or wrong type → HALT "coverage-check: subagent JSON schema invalid — missing/typo: {key}". Note: the parent already knows the skill name from workflow context (`{resolved_skill_package}` from step-01) — the subagent is not required to echo it back, and doing so introduces a contract-drift surface without improving verification.
84
84
  3. Each `exports[]` entry must be a dict with at minimum `name` (non-empty string) and `kind` (one of `function|class|type|constant|hook|interface|method`). Reject entries violating this; if >0 rejections, HALT "coverage-check: subagent returned malformed export entries — {count} entries do not match schema".
85
85
  4. `cross_check_mismatches[]` entries (when non-empty) must carry `export`, `skill_md_line`, `reference_file`, `reference_line`, `issue`. Missing fields → HALT.
@@ -131,6 +131,80 @@ Read the source directory at `{source_root}` and build a current file inventory:
131
131
 
132
132
  **Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, NOT in the change manifest. But §2 Category A ("files in source but not in provenance map → ADDED") would still find the promoted doc files on disk and classify them as ADDED if nothing prevents it. The coordination mechanism is an explicit pre-filter exclusion set built in §2.0 (below) that every Category A subprocess worker receives as an input before it starts scanning. See §2.0 for the exact contract. The exclusion set is the only mechanism guaranteeing that parallel subprocesses cannot double-count `promoted_docs_new[]` paths — prose-level "skip any path" instructions cannot cross subprocess boundaries.
133
133
 
134
+ ### 1c. Major-Version Scope Reconciliation (Pre-Detection)
135
+
136
+ **Purpose:** When upstream undergoes a paradigm shift (rebrand, package restructure, major-version rewrite), the brief's `scope.include` no longer reflects the real public API. §1b handles new authoritative-doc files; §1c handles new **code globs** that fall outside the original scope. Without it, update-skill silently misses the new surface and pays the gap cost on every future update — the cocoindex `0.3.37 → 1.0.0` and cognee `0.5.8 → 1.0.0` runs are existence proofs that this case is real and recurring.
137
+
138
+ **Skip this section entirely if:**
139
+
140
+ - `update_mode == "gap-driven"` (test-report mode — source hasn't drifted), OR
141
+ - `metadata.json.source_type == "docs-only"` (no source tree to scope), OR
142
+ - No audit drift report is available at the path computed in step 1 below.
143
+
144
+ **Procedure:**
145
+
146
+ 1. **Locate the most recent audit drift report.** Search `{forge_data_folder}/{skill_name}/{baseline_version}/drift-report-*.md`, sorted by timestamp descending. Pick the latest. If none found, **skip §1c entirely** — the post-detection deletion-ratio trigger in §2.2 still catches major restructures even without an audit pass.
147
+
148
+ 2. **Parse the report for an "Out-of-Scope Observations" section.** Look for either a top-level `## Out-of-Scope Observations` heading or a `### Out-of-Scope New Public API` subsection under `## Remediation Suggestions`. Each entry must expose:
149
+
150
+ - `path` — literal file path or directory glob (e.g., `python/cocoindex/_internal/api.py` or `python/cocoindex/resources/**`)
151
+ - `evidence` — short one-liner from the report (export count, rationale)
152
+
153
+ **Note:** This section is an optional audit-skill output. `skf-audit-skill` does not currently discover new files (per `src/skf-audit-skill/steps-c/step-02-re-index.md` — new-file detection is the responsibility of update-skill). The section is a forward-looking integration point: manual additions to the drift report or a future audit-skill enhancement populate it. If absent, no candidates from this trigger — proceed to step 6.
154
+
155
+ 3. **Reconcile against existing amendments.** For each candidate, consult `brief.scope.amendments[]`:
156
+
157
+ - `action: "promoted"` AND `path` matches → already in scope, skip silently.
158
+ - `action: "skipped"` AND `path` matches → user previously declined, honor silently.
159
+ - `action: "demoted-include"` or `action: "demoted-exclude"` AND `path` matches → user previously narrowed scope on this path, do not re-prompt; record as `pre_decided`.
160
+ - No matching amendment → continue to user prompt.
161
+
162
+ 4. **Prompt for each unresolved candidate.** Present the same menu shape as §1b:
163
+
164
+ ```
165
+ **Out-of-scope new public API discovered**
166
+
167
+ Path: {candidate.path}
168
+ Evidence: {evidence from drift report}
169
+ Drift report: {report relative path}
170
+
171
+ This path was not in the brief's `scope.include` when the skill was created. How should update-skill handle it?
172
+
173
+ [P] Promote — add to scope.include AND extract in this run
174
+ [S] Skip — leave out of scope AND record skip in amendments (no re-prompt)
175
+ [U] Update — halt this run and return to skf-brief-skill to refine scope
176
+ ```
177
+
178
+ 5. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate — record `action: "skipped"`, `category: "scope-expansion"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently expand scope.
179
+
180
+ 6. **Apply decision:**
181
+
182
+ - **[P] Promote:**
183
+ 1. Append `candidate.path` to `brief.scope.include` as a literal glob (preserve any wildcards from the drift report).
184
+ 2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "out-of-scope new public API — drift report {report basename}"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
185
+ 3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
186
+ 4. Display: `"Promoted {path} — brief amended; §2 Category A will pick up matching files as ADDED."`
187
+ 5. **No `promoted_docs_new[]` entry and no `change_detection_excludes` write** — promoted code globs flow through the standard §2 Category A → §3 extraction path, unlike §1b's promoted docs which bypass extraction.
188
+
189
+ - **[S] Skip:**
190
+ 1. Do NOT modify `scope.include` or `scope.exclude`.
191
+ 2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1c"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
192
+ 3. **Write the amended brief back to disk** so neither §1c nor a future run will re-prompt.
193
+ 4. Display: `"Skipped {path} — decision recorded in amendments."`
194
+
195
+ - **[U] Update:**
196
+ 1. Halt the workflow immediately.
197
+ 2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
198
+ 3. Exit with status `halted-for-brief-refinement`. Change manifest is not yet built — no partial writes to provenance.
199
+
200
+ 7. **Summary:** After all candidates are resolved (or none were found):
201
+
202
+ - `"Scope reconciliation: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments."`
203
+ - If N = 0 (section absent or empty): `"Scope reconciliation: no out-of-scope observations in drift report."`
204
+ - If §1c was skipped entirely (no drift report): omit this line; §2.2 will still run.
205
+
206
+ **Record for evidence report:** the update-skill evidence report appends `scope_reconciliation_pre: {drift_report: path, candidates: N, promoted: P, skipped: S, pre_decided: A, decisions: [{path, action, evidence}]}` (omit when §1c was skipped).
207
+
134
208
  ### 2. Compare Against Provenance Map
135
209
 
136
210
  **If normal mode (provenance map available):**
@@ -183,6 +257,55 @@ Aggregate all subprocess results into a unified change manifest.
183
257
  - All exports will be fully re-extracted in step 03
184
258
  - Skip export-level comparison
185
259
 
260
+ #### 2.2 — Major-Version Scope Reconciliation (Post-Detection)
261
+
262
+ **Purpose:** §1c catches the major-version case when an audit drift report supplies explicit candidates. §2.2 is the safety net that fires when no audit was run (or audit emitted no out-of-scope section): it inspects the just-built Category A/B results for the deletion-ratio signature of a major-version restructure and gives the user an off-ramp before §3 commits the change manifest.
263
+
264
+ **Skip this section entirely if:**
265
+
266
+ - `update_mode == "gap-driven"` (test-report mode), OR
267
+ - `degraded_mode == true` (no provenance baseline to compare against — every export looks "modified", deletion ratio is meaningless), OR
268
+ - Provenance map's tracked export count is zero.
269
+
270
+ **Trigger:** Compute
271
+
272
+ ```
273
+ deleted_export_count = (sum of exports across Category A DELETED files)
274
+ + (DELETED_EXPORT count from Category B)
275
+ total_provenance_exports = provenance_map.entries.length
276
+ deletion_ratio = deleted_export_count / total_provenance_exports
277
+ ```
278
+
279
+ If `deletion_ratio >= 0.50`, present the prompt below. Otherwise skip §2.2 silently and continue to §3.
280
+
281
+ **Prompt:**
282
+
283
+ ```
284
+ **Major-version scope shift detected**
285
+
286
+ Deleted exports: {deleted_export_count} of {total_provenance_exports} ({percent}%)
287
+ Deleted files: {deleted_file_count}
288
+ Added files (in scope): {added_in_scope_count}
289
+ Renamed/moved exports: {renamed_or_moved_count}
290
+
291
+ The upstream surface appears to have been substantially replaced. The brief's
292
+ `scope.include` patterns may no longer reflect the real public API.
293
+
294
+ [C] Continue — proceed with re-extraction; the deletion is intentional
295
+ [B] Brief — halt and re-run skf-brief-skill to refine scope first
296
+ [A] Audit — halt and run skf-audit-skill to map the new surface, then re-run update-skill
297
+ ```
298
+
299
+ **Headless mode (`{headless_mode}` is true):** auto-select `[C] Continue`, log a WARN-level entry to the evidence report (`scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "headless-continue"}`), and surface the warning in step-07's report. A non-interactive run must not silently halt, but the user must be able to see the signal post-hoc.
300
+
301
+ **Apply decision:**
302
+
303
+ - **[C] Continue:** record `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "continue"}` and proceed to §3.
304
+ - **[B] Brief:** halt with status `halted-for-brief-refinement`. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` Change manifest discarded — no partial writes.
305
+ - **[A] Audit:** halt with status `halted-for-audit`. Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface — its drift report will feed §1c on the next update-skill run."` Change manifest discarded.
306
+
307
+ **Why both §1c and §2.2:** §1c is precise (per-path P/S/U) but requires upstream signal from audit-skill. §2.2 is coarse (single halt/continue) but self-contained — it fires even when the user runs update-skill directly without audit. Together they cover the major-version case across the two real workflows.
308
+
186
309
  ### 3. Build Change Manifest
187
310
 
188
311
  Compile the change manifest with structured entries:
@@ -5,6 +5,9 @@
5
5
  * every version number, commit SHA, and library reference the docs cite.
6
6
  *
7
7
  * What it checks:
8
+ * - The anchor `skf_version` matches `package.json.version` (self-reference invariant
9
+ * declared in pinned.yaml's header; enforced here at CI/pre-commit time AND bumped
10
+ * by release.yaml on every release)
8
11
  * - Every skill listed in `docs/_data/pinned.yaml` actually exists at the
9
12
  * claimed version and commit in $OMS/skills/<name>/<version>/<name>/metadata.json
10
13
  * - The metadata.json `version` field matches the anchor `version`
@@ -32,6 +35,7 @@ const yaml = require('js-yaml');
32
35
  const SKF_ROOT = path.resolve(__dirname, '..');
33
36
  const DOCS_DIR = path.join(SKF_ROOT, 'docs');
34
37
  const ANCHORS_PATH = path.join(DOCS_DIR, '_data', 'pinned.yaml');
38
+ const PACKAGE_JSON_PATH = path.join(SKF_ROOT, 'package.json');
35
39
 
36
40
  function loadAnchors() {
37
41
  if (!fs.existsSync(ANCHORS_PATH)) {
@@ -46,6 +50,38 @@ function loadAnchors() {
46
50
  }
47
51
  }
48
52
 
53
+ function checkSkfVersionAgainstPackageJson(anchors) {
54
+ const errors = [];
55
+ if (!fs.existsSync(PACKAGE_JSON_PATH)) {
56
+ errors.push(`CRITICAL: package.json not found at ${PACKAGE_JSON_PATH}`);
57
+ return errors;
58
+ }
59
+ let pkg;
60
+ try {
61
+ pkg = JSON.parse(fs.readFileSync(PACKAGE_JSON_PATH, 'utf8'));
62
+ } catch (error) {
63
+ errors.push(`CRITICAL: could not parse ${PACKAGE_JSON_PATH}: ${error.message}`);
64
+ return errors;
65
+ }
66
+ const anchorVersion = anchors.skf_version;
67
+ const packageVersion = pkg.version;
68
+ if (!anchorVersion) {
69
+ errors.push(`MISSING_ANCHOR: pinned.yaml has no skf_version key`);
70
+ return errors;
71
+ }
72
+ if (!packageVersion) {
73
+ errors.push(`MISSING_PKG_VERSION: package.json has no version field`);
74
+ return errors;
75
+ }
76
+ if (anchorVersion !== packageVersion) {
77
+ errors.push(
78
+ `SKF_VERSION_DRIFT: pinned.yaml skf_version "${anchorVersion}" !== package.json version "${packageVersion}" ` +
79
+ `— bump pinned.yaml or check that release.yaml's "Update pinned.yaml skf_version" step fired`,
80
+ );
81
+ }
82
+ return errors;
83
+ }
84
+
49
85
  function resolveOmsPath(anchors) {
50
86
  if (process.env.OMS) {
51
87
  return path.resolve(process.env.OMS);
@@ -172,7 +208,11 @@ function main() {
172
208
  const anchors = loadAnchors();
173
209
  const omsPath = resolveOmsPath(anchors);
174
210
 
175
- const errors = [...checkCanonicalFiles(anchors, omsPath), ...checkDocsForStaleVersions(anchors)];
211
+ const errors = [
212
+ ...checkSkfVersionAgainstPackageJson(anchors),
213
+ ...checkCanonicalFiles(anchors, omsPath),
214
+ ...checkDocsForStaleVersions(anchors),
215
+ ];
176
216
 
177
217
  if (errors.length > 0) {
178
218
  console.error('DRIFT DETECTED:\n');
@@ -186,7 +226,7 @@ function main() {
186
226
  }
187
227
 
188
228
  const skillCount = Object.keys(anchors.skills).length;
189
- console.log(`OK: ${skillCount} skills checked against ${omsPath}, no drift.`);
229
+ console.log(`OK: skf_version ${anchors.skf_version} matches package.json; ${skillCount} skills checked against ${omsPath}, no drift.`);
190
230
  process.exit(0);
191
231
  }
192
232