bmad-module-skill-forge 1.8.0 → 2.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 (136) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +10 -5
  3. package/docs/_data/pinned.yaml +2 -2
  4. package/docs/agents.md +11 -2
  5. package/docs/architecture.md +5 -4
  6. package/docs/bmad-synergy.md +42 -3
  7. package/docs/campaign.md +172 -0
  8. package/docs/examples.md +7 -3
  9. package/docs/forge-auto.md +90 -0
  10. package/docs/getting-started.md +9 -0
  11. package/docs/how-it-works.md +6 -4
  12. package/docs/index.md +4 -3
  13. package/docs/skill-model.md +1 -1
  14. package/docs/troubleshooting.md +17 -1
  15. package/docs/verifying-a-skill.md +9 -3
  16. package/docs/why-skf.md +1 -1
  17. package/docs/workflows.md +73 -27
  18. package/package.json +2 -2
  19. package/src/module-help.csv +2 -1
  20. package/src/shared/_known-workarounds.yaml +155 -0
  21. package/src/shared/data/language-corpora.json +32 -0
  22. package/src/shared/references/pipeline-contracts.md +14 -2
  23. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -1
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  26. package/src/shared/scripts/skf-detect-docs.py +417 -0
  27. package/src/shared/scripts/skf-detect-language.py +5 -3
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +12 -1
  29. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  30. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  31. package/src/shared/scripts/skf-language-corpora.py +100 -0
  32. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  33. package/src/shared/scripts/skf-preapply.py +192 -0
  34. package/src/shared/scripts/skf-shape-detect.py +1114 -0
  35. package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
  36. package/src/shared/scripts/skf-validate-pins.py +368 -0
  37. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  38. package/src/skf-analyze-source/SKILL.md +26 -20
  39. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  40. package/src/skf-analyze-source/references/init.md +30 -0
  41. package/src/skf-analyze-source/references/step-auto-scope.md +677 -0
  42. package/src/skf-analyze-source/references/step-shape-detect.md +86 -0
  43. package/src/skf-audit-skill/SKILL.md +1 -0
  44. package/src/skf-audit-skill/assets/drift-report-template.md +6 -0
  45. package/src/skf-audit-skill/references/init.md +1 -1
  46. package/src/skf-audit-skill/references/report.md +4 -0
  47. package/src/skf-audit-skill/references/severity-classify.md +1 -1
  48. package/src/skf-audit-skill/references/step-doc-drift.md +147 -0
  49. package/src/skf-brief-skill/SKILL.md +7 -3
  50. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  51. package/src/skf-brief-skill/references/analyze-target.md +7 -4
  52. package/src/skf-brief-skill/references/confirm-brief.md +0 -1
  53. package/src/skf-brief-skill/references/gather-intent.md +22 -2
  54. package/src/skf-brief-skill/references/scope-definition.md +2 -1
  55. package/src/skf-brief-skill/references/step-auto-brief.md +188 -0
  56. package/src/skf-brief-skill/references/step-auto-validate.md +209 -0
  57. package/src/skf-brief-skill/references/write-brief.md +2 -3
  58. package/src/skf-campaign/SKILL.md +190 -0
  59. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  60. package/src/skf-campaign/customize.toml +73 -0
  61. package/src/skf-campaign/manifest.yaml +11 -0
  62. package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
  63. package/src/skf-campaign/references/health-check.md +35 -0
  64. package/src/skf-campaign/references/step-01-setup.md +122 -0
  65. package/src/skf-campaign/references/step-02-strategy.md +97 -0
  66. package/src/skf-campaign/references/step-03-pins.md +56 -0
  67. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  68. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  69. package/src/skf-campaign/references/step-06-batch.md +87 -0
  70. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  71. package/src/skf-campaign/references/step-08-verify.md +75 -0
  72. package/src/skf-campaign/references/step-09-refine.md +83 -0
  73. package/src/skf-campaign/references/step-10-export.md +106 -0
  74. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  75. package/src/skf-campaign/references/step-resume.md +114 -0
  76. package/src/skf-campaign/scripts/.gitkeep +0 -0
  77. package/src/skf-campaign/scripts/campaign-deps.py +235 -0
  78. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  79. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  80. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  81. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  82. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  83. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  85. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  86. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  87. package/src/skf-create-skill/SKILL.md +4 -1
  88. package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
  89. package/src/skf-create-skill/assets/skill-sections.md +2 -0
  90. package/src/skf-create-skill/references/compile.md +5 -2
  91. package/src/skf-create-skill/references/extract.md +9 -1
  92. package/src/skf-create-skill/references/extraction-patterns.md +3 -1
  93. package/src/skf-create-skill/references/generate-artifacts.md +11 -2
  94. package/src/skf-create-skill/references/health-check.md +2 -2
  95. package/src/skf-create-skill/references/report.md +17 -1
  96. package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
  97. package/src/skf-create-skill/references/step-auto-shard.md +110 -0
  98. package/src/skf-create-skill/references/step-doc-rot.md +109 -0
  99. package/src/skf-create-skill/references/step-doc-sources.md +123 -0
  100. package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
  101. package/src/skf-create-skill/references/validate.md +12 -3
  102. package/src/skf-drop-skill/SKILL.md +2 -2
  103. package/src/skf-drop-skill/references/execute.md +20 -9
  104. package/src/skf-drop-skill/references/report.md +2 -0
  105. package/src/skf-drop-skill/references/select.md +7 -2
  106. package/src/skf-forger/SKILL.md +15 -4
  107. package/src/skf-refine-architecture/SKILL.md +2 -1
  108. package/src/skf-refine-architecture/references/compile.md +1 -1
  109. package/src/skf-refine-architecture/references/report.md +1 -1
  110. package/src/skf-rename-skill/SKILL.md +2 -2
  111. package/src/skf-rename-skill/references/execute.md +5 -4
  112. package/src/skf-rename-skill/references/rebuild-context.md +2 -2
  113. package/src/skf-rename-skill/references/select.md +3 -3
  114. package/src/skf-setup/SKILL.md +1 -1
  115. package/src/skf-setup/references/auto-index.md +3 -1
  116. package/src/skf-setup/references/detect-and-tier.md +4 -0
  117. package/src/skf-setup/references/report.md +4 -1
  118. package/src/skf-setup/references/tier-rules.md +1 -1
  119. package/src/skf-test-skill/SKILL.md +8 -7
  120. package/src/skf-test-skill/customize.toml +2 -1
  121. package/src/skf-test-skill/references/coverage-check.md +10 -0
  122. package/src/skf-test-skill/references/external-validators.md +1 -1
  123. package/src/skf-test-skill/references/init.md +16 -1
  124. package/src/skf-test-skill/references/report.md +5 -1
  125. package/src/skf-test-skill/references/score.md +95 -6
  126. package/src/skf-test-skill/references/source-access-protocol.md +13 -3
  127. package/src/skf-test-skill/references/step-hard-gate.md +73 -0
  128. package/src/skf-test-skill/scripts/compute-score.py +4 -3
  129. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  130. package/src/skf-update-skill/customize.toml +0 -9
  131. package/src/skf-update-skill/references/detect-changes.md +33 -3
  132. package/src/skf-update-skill/references/health-check.md +2 -2
  133. package/src/skf-update-skill/references/init.md +1 -0
  134. package/src/skf-update-skill/references/re-extract.md +15 -1
  135. package/src/skf-verify-stack/references/report.md +1 -1
  136. package/tools/cli/lib/ui.js +3 -2
@@ -8,6 +8,13 @@ forgeTierConfig: '{sidecar_path}/forge-tier.yaml'
8
8
  atomicWriteProbeOrder:
9
9
  - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
10
10
  - '{project-root}/src/shared/scripts/skf-atomic-write.py'
11
+ # Resolve `{forgeTierRwHelper}` by probing `{forgeTierRwProbeOrder}` in order
12
+ # (installed SKF module path first, src/ dev-checkout fallback); first existing
13
+ # path wins. HALT if neither resolves — §6b's ccc-index registry round-trip is
14
+ # comment-preserving and has no prose fallback.
15
+ forgeTierRwProbeOrder:
16
+ - '{project-root}/_bmad/skf/shared/scripts/skf-forge-tier-rw.py'
17
+ - '{project-root}/src/shared/scripts/skf-forge-tier-rw.py'
11
18
  ---
12
19
 
13
20
  <!-- Config: communicate in {communication_language}. -->
@@ -195,11 +202,13 @@ Ensure the source path used for extraction is indexed by ccc and registered in t
195
202
 
196
203
  **Registry update:**
197
204
 
198
- Register the indexed source path via `register-ccc-index` (comment-preserving round-trip, same pattern as §6's `register-qmd-collection`). Acquire an exclusive `flock` on `{sidecar_path}/forge-tier.yaml.lock`, then:
205
+ **Resolve `{forgeTierRwHelper}`** from `{forgeTierRwProbeOrder}`; first existing path wins. HALT if no candidate exists.
206
+
207
+ Register the indexed source path via `register-ccc-index` — a comment-preserving round-trip on `forge-tier.yaml`. This differs from §6, which mutates the registry with an inline read→modify→atomic-write under `flock`; here the helper owns the read→modify→write internally so the `ccc_index_registry` deduplication logic stays in the script. Acquire an exclusive `flock` on `{sidecar_path}/forge-tier.yaml.lock`, then:
199
208
 
200
209
  ```bash
201
210
  echo '{"source_repo":"{brief.source_repo}","path":"{source_root}","skill_name":"{name}","indexed_at":"{current ISO date}","source_workflow":"create-skill"}' \
202
- | uv run {forgeTierRw} register-ccc-index --target {forgeTierConfig}
211
+ | uv run {forgeTierRwHelper} register-ccc-index --target {forgeTierConfig}
203
212
  ```
204
213
 
205
214
  Deduplicates by `source_repo` + `skill_name` (NOT local `path`, which may be ephemeral). Release the lock after the command completes. If `flock` is unavailable, fall back to read-CAS-by-mtime.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  # `shared/health-check.md` resolves relative to the SKF module root
3
- # (`_bmad/skf/` when installed, `src/` during development), NOT relative
4
- # to this step file.
3
+ # (`{project-root}/_bmad/skf/` when installed, `{project-root}/src/` during
4
+ # development), NOT relative to this step file.
5
5
  nextStepFile: 'shared/health-check.md'
6
6
  ---
7
7
 
@@ -125,10 +125,26 @@ End workflow. No further steps.
125
125
 
126
126
  **Resolve the schema reference:** before writing, verify that `{project-root}/src/shared/references/output-contract-schema.md` exists and is readable. Try in order: `{project-root}/src/shared/references/output-contract-schema.md`, then `{project-root}/_bmad/skf/shared/references/output-contract-schema.md` (installed-forge path).
127
127
 
128
- - **If resolved:** write the result contract per the schema — the per-run record at `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_version}/create-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include `SKILL.md`, `context-snippet.md`, and `metadata.json` paths in `outputs` and confidence distribution in `summary`. Use `python3 {atomicWriteHelper} write --target {forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` (stdin-piped JSON) for the per-run record, then the same helper for the `-latest.json` copy.
128
+ - **If resolved:** write the result contract per the schema — the per-run record at `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_version}/create-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include `SKILL.md`, `context-snippet.md`, `metadata.json`, **and `{forge_version}/evidence-report.md`** paths in `outputs` (the evidence report carries the `## Auto-Decisions` audit table where every silent auto-decision is recorded — pipeline consumers follow this path to audit the run) and confidence distribution in `summary`. Also set `summary.auto_decision_count` to `len(headless_decisions[])` (0 when the run was interactive) so a consumer can tell from the result JSON alone whether any gate auto-resolved. Use `python3 {atomicWriteHelper} write --target {forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` (stdin-piped JSON) for the per-run record, then the same helper for the `-latest.json` copy.
129
129
 
130
130
  - **If neither candidate path resolves:** skip the result-contract write entirely. Append a warning to `evidence-report.md`: "Result contract skipped — `shared/references/output-contract-schema.md` could not be resolved at either candidate path." Then set `validation_status: 'schema-unavailable'` in `metadata.json` (and re-write metadata.json via `skf-atomic-write.py write`). Pipeline consumers will observe the missing `-latest.json` and the metadata flag.
131
131
 
132
+ ### Result Contract on HARD HALT
133
+
134
+ The success-variant contract above is only reached at step 8. The ~10 HARD HALT conditions in steps 1–7 (forge-config missing, no brief, brief invalid, source not found, prerequisite failure in load-brief; atomic/detect/auth helper unresolved and the Tier-1 split-count drop in extract/validate; the non-symlink active-link refusal in generate-artifacts) otherwise print a human string and exit with **no machine-readable outcome** — a pipeline polling `create-skill-result-latest.json` cannot distinguish "halted at brief-invalid" from "still running" from "crashed". Mirror skf-quick-skill: **whenever `{headless_mode}` is true, every HARD HALT must surface an error-variant result before exiting.**
135
+
136
+ **Always (every HARD HALT under `{headless_mode}`, regardless of phase)** — emit a single line on **stderr** (one line, no pretty-print; matches the prefix-and-envelope convention used by `skf-emit-result-envelope.py`):
137
+
138
+ ```
139
+ SKF_CREATE_SKILL_RESULT_JSON: {"status":"failed","phase":"<step-slug>","outputs":{},"summary":{"halt_reason":"<short class>","evidence_report":"<path-or-null>"},"skill_package":"<path-or-null>"}
140
+ ```
141
+
142
+ Use `status: "partial"` instead of `"failed"` when artifacts were already staged or promoted before the HALT (i.e. the HALT fired at step 7 generate-artifacts after some files were written); use `"failed"` for HALTs before any artifact exists on disk (steps 1–6).
143
+
144
+ **Additionally, when `{forge_version}` is resolved** (HALT at step 7 onward, where the staging tree has been promoted) — write the same JSON object (without the `SKF_CREATE_SKILL_RESULT_JSON: ` prefix) to disk at `{forge_version}/create-skill-result-{YYYYMMDD-HHmmss}.json` and a copy at `{forge_version}/create-skill-result-latest.json` (copy, not symlink) via `python3 {atomicWriteHelper} write`, so consumers that hardcode the `-latest.json` path see a deterministic file even on failed runs. Set `summary.evidence_report` to `{forge_version}/evidence-report.md` whenever that file exists, so the consumer can still reach the `## Auto-Decisions` audit on a failed run. HALTs before step 7 cannot write to disk because `{forge_version}` is only created at step 7 §1; for those, the stderr envelope is the contract and `summary.evidence_report` is `null`.
145
+
146
+ When `{headless_mode}` is false, HARD HALTs display their human message only — no envelope is emitted.
147
+
132
148
  ### 6. Chain to Health Check
133
149
 
134
150
  **If not batch mode (or all batch briefs complete):**
@@ -154,7 +154,7 @@ If `source_repo` is a remote URL (GitHub URL or owner/repo format) AND tier is F
154
154
  - `remote_clone_path = {workspace_repo_path}`
155
155
  - `remote_clone_type = "workspace"`
156
156
 
157
- **Scope filtering:** Since the workspace uses a full checkout (no sparse-checkout), apply `include_patterns` and `exclude_patterns` from the brief as **file-level filters** when building the extraction file list. Always-included root files (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`, `setup.py`, `setup.cfg`, `VERSION`) are exempt from pattern filtering.
157
+ **Scope filtering:** Since the workspace uses a full checkout (no sparse-checkout), apply `include_patterns` and `exclude_patterns` from the brief as **file-level filters** when building the extraction file list. Always-included root files (`pyproject.toml`, `package.json`, `Cargo.toml`, `go.mod`, `pom.xml`, `build.gradle`, `build.gradle.kts`, `Package.swift`, `setup.py`, `setup.cfg`, `VERSION`) are exempt from pattern filtering.
158
158
 
159
159
  5. **Ephemeral fallback (on any workspace failure):**
160
160
 
@@ -0,0 +1,110 @@
1
+ ---
2
+ nextStepFile: 'step-doc-rot.md'
3
+ ---
4
+
5
+ <!-- Config: communicate in {communication_language}. -->
6
+
7
+ # Step 5b: Auto-Shard
8
+
9
+ ## STEP GOAL:
10
+
11
+ Proactively reduce oversized SKILL.md bodies to under 400 lines by extracting Tier 2 sections (`## Full` headings) to `references/`, providing 100 lines of headroom below the 500-line `body.max_lines` ceiling. Tier 1 sections always remain inline. If the body is already within budget, skip cleanly.
12
+
13
+ ## Rules
14
+
15
+ - Auto-proceed step — no user interaction required
16
+ - Graceful skip — if body is under threshold, proceed without modification
17
+ - Only extract Tier 2 sections (identified by `## Full` heading prefix)
18
+ - Tier 1 sections must NEVER be moved to references/
19
+ - Do not modify frontmatter — only body content and references/ directory
20
+ - Do not invoke `npx skill-check split-body` — this step uses direct extraction
21
+ - Do not invoke the Description Guard Protocol — frontmatter is untouched
22
+
23
+ ## MANDATORY SEQUENCE
24
+
25
+ ### §1. Count Body Lines
26
+
27
+ Count all lines in the staging SKILL.md between the frontmatter closing `---` and EOF. Exclude trailing blank lines from the count. Store as `body_line_count`.
28
+
29
+ ```
30
+ body_lines_before = body_line_count
31
+ ```
32
+
33
+ **IF `body_line_count` <= 400:**
34
+ - Log: `"auto-shard: skipped (body {body_line_count} lines)"`
35
+ - Set context: `auto_shard_triggered: false`, `sections_extracted: []`, `body_lines_before: {body_line_count}`, `body_lines_after: {body_line_count}`
36
+ - Skip to §6 (Auto-Proceed)
37
+
38
+ **ELSE:** Proceed to §2.
39
+
40
+ ### §2. Selective Shard — Tier 2 Extraction
41
+
42
+ Identify Tier 2 sections by their `## Full` heading prefix:
43
+ - `## Full API Reference` → `references/full-api-reference.md`
44
+ - `## Full Type Definitions` → `references/full-type-definitions.md`
45
+ - `## Full Integration Patterns` → `references/full-integration-patterns.md`
46
+
47
+ Sort identified sections by line count descending (extract largest first).
48
+
49
+ **FOR EACH Tier 2 section (largest first):**
50
+
51
+ 1. Extract the full section content (from `## Full` heading to the next `##` heading or EOF)
52
+ 2. Derive the reference filename from the heading: kebab-case (as shown above)
53
+ 3. Write the extracted content (preserving the `##` heading) to `<staging-skill-dir>/references/{filename}`
54
+ 4. Replace the extracted section in SKILL.md with a cross-reference blockquote:
55
+ ```markdown
56
+ > See [Full API Reference](references/full-api-reference.md)
57
+ ```
58
+ 5. Re-count body lines
59
+ 6. **IF `body_line_count` <= 400:** stop extracting, proceed to §3
60
+
61
+ Track: `sections_extracted: [{heading names}]`
62
+
63
+ ### §3. Tier 1 Preservation Check
64
+
65
+ Verify ALL Tier 1 sections remain inline in SKILL.md after extraction. Check for these headings:
66
+
67
+ - `## Overview`
68
+ - `## Quick Start`
69
+ - `## Common Workflows`
70
+ - `## Key API Summary` (or `## Component Catalog` for component-library scope)
71
+ - `## Migration & Deprecation Warnings` (conditional — only check if it was present before extraction)
72
+ - `## Key Types`
73
+ - `## Architecture at a Glance`
74
+ - `## CLI` (conditional — only check if present before extraction)
75
+ - `## Scripts & Assets` (conditional — only check if present before extraction)
76
+ - `## Manual Sections` (conditional — only check if present before extraction)
77
+
78
+ **IF any Tier 1 section is missing after extraction:**
79
+ HALT: `"Auto-shard removed Tier 1 section {name}. Aborting."`
80
+
81
+ ### §4. Post-Shard Validation
82
+
83
+ Re-count body lines after all Tier 2 sections have been extracted.
84
+
85
+ ```
86
+ body_lines_after = body_line_count
87
+ ```
88
+
89
+ **IF `body_line_count` > 400 after all Tier 2 sections extracted:**
90
+ - Trim oversized Tier 1 sections: reduce `## Key API Summary` and `## Architecture at a Glance` content to fit within the 400-line budget
91
+ - Do NOT move any Tier 1 section to references/
92
+ - Re-count and update `body_lines_after`
93
+
94
+ ### §5. Cross-Reference Integrity
95
+
96
+ **FOR EACH extracted reference file:**
97
+ 1. Verify the file exists at `<staging-skill-dir>/references/{filename}`
98
+ 2. Verify the cross-reference blockquote in SKILL.md contains a link resolving to the file
99
+
100
+ Log: `"auto-shard: {N} sections extracted, body reduced from {body_lines_before} to {body_lines_after} lines"`
101
+
102
+ Set context:
103
+ - `auto_shard_triggered: true`
104
+ - `sections_extracted: [{heading names}]`
105
+ - `body_lines_before: {before}`
106
+ - `body_lines_after: {after}`
107
+
108
+ ### §6. Auto-Proceed
109
+
110
+ Load, read the entire file, then execute `{nextStepFile}`.
@@ -0,0 +1,109 @@
1
+ ---
2
+ nextStepFile: 'validate.md'
3
+ ---
4
+
5
+ <!-- Config: communicate in {communication_language}. -->
6
+
7
+ # Step 5c: Doc-Rot
8
+
9
+ ## STEP GOAL:
10
+
11
+ Scan feeder artifacts for doc-rot correction indicators and annotate the compiled SKILL.md with `## CORRECTION` blocks. Matching is grep-based and deterministic — no AI judgment is used for detection.
12
+
13
+ ## Rules
14
+
15
+ - Auto-proceed step — no user interaction required
16
+ - Graceful skip — if no corrections are found in any feeder artifact, proceed without modification
17
+ - Only modify the compiled SKILL.md (correction block insertion) and references (if corrections target referenced content)
18
+ - Do not modify feeder artifacts (evidence-report.md, provenance-map.json, metadata.json) — this step READS them only
19
+ - Do not modify frontmatter — correction blocks are body content only
20
+ - Matching is case-insensitive substring grep — no semantic or AI-based assessment
21
+
22
+ ## MANDATORY SEQUENCE
23
+
24
+ ### §1. Locate Feeder Artifacts
25
+
26
+ Identify the feeder artifacts in the **staging directory** for the current skill. This step (5c) runs **before** step 7 promotes the staging tree to `{forge_data_folder}/{skill-name}/{version}/`, so the feeder artifacts only exist under the staging path compile (step 5 §1a) wrote — reading the not-yet-promoted `{forge_data_folder}` path would make every match a no-op:
27
+
28
+ 1. **Evidence report:** `_bmad-output/{skill-name}/evidence-report.md`
29
+ 2. **Provenance map:** `_bmad-output/{skill-name}/provenance-map.json` — focus on T2/T3 entries with temporal annotations
30
+ 3. **Temporal context:** changelogs, migration guides, and issue/PR data fetched by step 3b and enriched by step 4 (available in workflow context)
31
+ 4. **Compiled SKILL.md:** the staged `_bmad-output/{skill-name}/SKILL.md` itself — check for `[QMD:...]` or `[DOC:...]` annotations referencing corrections
32
+
33
+ For each artifact, attempt to load its content. If an artifact does not exist or is empty, skip it — this is not an error.
34
+
35
+ Store: `feeder_artifacts_scanned: [{list of artifacts that were loaded}]`
36
+
37
+ ### §2. Grep for Correction Indicators
38
+
39
+ Scan each loaded feeder artifact for the following correction patterns. All matches are **case-insensitive substring matches** — no regex interpretation, no semantic analysis.
40
+
41
+ | Pattern | Category |
42
+ |---------|----------|
43
+ | `deprecated` | Deprecation |
44
+ | `@deprecated` | Deprecation |
45
+ | `breaking change` | Breaking change |
46
+ | `BREAKING` | Breaking change |
47
+ | `removed in` | Removal |
48
+ | `was removed` | Removal |
49
+ | `renamed to` | Rename |
50
+ | `renamed from` | Rename |
51
+ | `superseded by` | Supersession |
52
+ | `replaced by` | Supersession |
53
+ | `no longer supported` | End of life |
54
+ | `migration required` | Migration |
55
+ | `signature changed` | Signature change |
56
+
57
+ For each match, record:
58
+ - `source`: the feeder artifact path or citation where the match was found
59
+ - `pattern`: the specific pattern string that matched
60
+ - `category`: the category from the table above
61
+ - `context_line`: the line or excerpt containing the match
62
+ - `affected`: the function name, API, or section the correction relates to (extract from surrounding context if identifiable; otherwise set to `"unknown"`)
63
+
64
+ Store all matches as `correction_matches: [{match records}]`
65
+
66
+ **IF `correction_matches` is empty:**
67
+ - Log: `"doc-rot: skipped (no correction indicators found in feeder artifacts)"`
68
+ - Set context: `doc_rot_triggered: false`, `corrections_added: 0`
69
+ - Skip to §5 (Auto-Proceed)
70
+
71
+ **ELSE:** Proceed to §3.
72
+
73
+ ### §3. Annotate SKILL.md with Correction Blocks
74
+
75
+ For each entry in `correction_matches`, insert a `## CORRECTION` block into the compiled SKILL.md.
76
+
77
+ **Block format:**
78
+
79
+ ```markdown
80
+ ## CORRECTION
81
+
82
+ **Source:** {source}
83
+ **Pattern:** {pattern}
84
+ **Affected:** {affected}
85
+ **Detail:** {context_line}
86
+ ```
87
+
88
+ **Insertion rules:**
89
+ - **After the relevant API section** in SKILL.md if the `affected` function or section can be identified and located in the document
90
+ - **At the end of SKILL.md body** (before any trailing sections like `## Manual Sections`) if the affected section cannot be determined
91
+ - **Never inside frontmatter** — body content only
92
+ - Each correction block is self-contained with its own source citation
93
+ - Multiple corrections produce multiple `## CORRECTION` blocks
94
+
95
+ Store: `corrections_added: {count of blocks inserted}`
96
+
97
+ ### §4. Log Results
98
+
99
+ Log: `"doc-rot: {corrections_added} correction blocks added from {feeder_artifacts_scanned_count} feeder artifacts"`
100
+
101
+ Set context:
102
+ - `doc_rot_triggered: true`
103
+ - `corrections_added: {count}`
104
+ - `feeder_artifacts_scanned: [{list}]`
105
+ - `correction_matches: [{match records}]`
106
+
107
+ ### §5. Auto-Proceed
108
+
109
+ Load, read the entire file, then execute `{nextStepFile}`.
@@ -0,0 +1,123 @@
1
+ ---
2
+ nextStepFile: 'step-auto-shard.md'
3
+ # Resolve `{detectDocsHelper}` by probing `{detectDocsProbeOrder}` in order
4
+ # (installed SKF module path first, src/ dev-checkout fallback); first existing
5
+ # path wins. HALT if neither resolves — §2 has no prose fallback for doc-source
6
+ # detection (Pages-API walk, docs/ folder scan, content hashing).
7
+ detectDocsProbeOrder:
8
+ - '{project-root}/_bmad/skf/shared/scripts/skf-detect-docs.py'
9
+ - '{project-root}/src/shared/scripts/skf-detect-docs.py'
10
+ ---
11
+
12
+ <!-- Config: communicate in {communication_language}. -->
13
+
14
+ # Step 5a: Doc Sources
15
+
16
+ ## STEP GOAL:
17
+
18
+ Record detected documentation pages and README with content hashes in metadata.json so that downstream audit (analyze-skill) can detect when upstream docs have changed since the skill was compiled.
19
+
20
+ ## Rules
21
+
22
+ - Auto-proceed step — no user interaction required
23
+ - Graceful failure — if doc detection fails, skip with a warning and proceed to validate
24
+ - Do not modify any compiled artifact other than `metadata.json`
25
+ - Do not block the pipeline on any doc detection error
26
+
27
+ ## MANDATORY SEQUENCE
28
+
29
+ ### 1. Check for Upstream Doc Detection Results
30
+
31
+ Check if `doc_detection_results` is already populated in the workflow context (set by BS auto-brief in the forge-auto pipeline).
32
+
33
+ - **If upstream results exist:** use them directly, skip to step 3.
34
+ - **If no upstream results:** continue to step 2.
35
+
36
+ ### 2. Run Doc Detection (if needed)
37
+
38
+ Check that `{source_repo}` is available from the skill brief.
39
+
40
+ **If `{source_repo}` is not available:**
41
+ - Set `doc_detection_results` to an empty array `[]`
42
+ - Add evidence note: `"Doc sources: detection skipped — no source_repo in brief"`
43
+ - Skip to step 3.
44
+
45
+ **If `{source_repo}` is available:**
46
+
47
+ **Resolve `{detectDocsHelper}`** from `{detectDocsProbeOrder}`; first existing path wins. HALT if no candidate exists.
48
+
49
+ Invoke the detect-docs script:
50
+
51
+ ```bash
52
+ uv run {detectDocsHelper} \
53
+ --repo-url {source_repo} \
54
+ [--local-path {source_path}] \
55
+ [--skip-pages-api]
56
+ ```
57
+
58
+ Pass `--local-path {source_path}` when a local clone exists from the extraction step to avoid redundant cloning for `docs/` folder detection.
59
+
60
+ **Handle exit codes:**
61
+ - **Exit 0** (found ≥1 doc): parse JSON stdout as `doc_detection_results`
62
+ - **Exit 1** (none found): set `doc_detection_results` to empty array `[]`
63
+ - **Exit 2** (error): set `doc_detection_results` to empty array `[]`, add evidence note: `"Doc sources: detection partial — skf-detect-docs.py error (exit 2)"`
64
+
65
+ ### 3. Ensure README Entry
66
+
67
+ After obtaining detection results, check if any entry has a URL matching `*/README.md` or `*/readme.md`.
68
+
69
+ **If a README entry already exists** from detection (e.g., `detected_via: "docs_folder"` found a README): keep it as-is.
70
+
71
+ **If no README entry exists,** add one:
72
+
73
+ - `url`: Construct from `{source_repo}/blob/main/README.md`
74
+ - `detected_via`: `"readme_always"`
75
+ - `content_hash`: Hash the README content using `sha256:{hexdigest}` convention:
76
+ - If `{source_path}` is available: read `{source_path}/README.md` locally with `encoding="utf-8"`, compute `sha256:{hexdigest}`
77
+ - Else: fetch via `gh api repos/{owner}/{repo}/readme --jq '.content'`, base64-decode, hash
78
+ - If README cannot be found or fetched: set `content_hash` to `null`
79
+ - `recorded_at`: current ISO-8601 timestamp with timezone
80
+
81
+ ### 4. Build doc_sources Array
82
+
83
+ Map each detection result to the `doc_sources` schema:
84
+
85
+ ```json
86
+ {
87
+ "url": "{url from detection result}",
88
+ "detected_via": "{detected_via from detection result}",
89
+ "content_hash": "{content_hash from detection result — sha256:{hexdigest} or null}",
90
+ "recorded_at": "{current ISO-8601 timestamp with timezone}"
91
+ }
92
+ ```
93
+
94
+ Field mapping from `skf-detect-docs.py` output:
95
+ - `url` ← `url` (direct copy)
96
+ - `detected_via` ← `detected_via` (direct copy; or `"readme_always"` for the mandatory README entry)
97
+ - `content_hash` ← `content_hash` (direct copy, already in `sha256:{hexdigest}` format from story 1.2)
98
+ - `recorded_at` ← generated at step execution time (ISO-8601 with timezone)
99
+
100
+ Note: `content_type` from detect-docs output is NOT carried into `doc_sources`.
101
+
102
+ ### 5. Update metadata.json
103
+
104
+ Read the staging `_bmad-output/{skill-name}/metadata.json` that compile (step 5) wrote.
105
+
106
+ **If the staging metadata.json is unreadable:** HALT — this indicates compile failed (critical, not doc-detection-related).
107
+
108
+ **Replace** the `doc_sources` field entirely (do not merge or append to stale data from prior compiles):
109
+
110
+ ```python
111
+ metadata["doc_sources"] = new_doc_sources # full replacement
112
+ ```
113
+
114
+ Write the updated metadata.json back to the staging directory.
115
+
116
+ Add evidence note summarizing the result:
117
+ - Success: `"Doc sources: {N} detected, README tracked"`
118
+ - Skip: `"Doc sources: detection skipped — {reason}"`
119
+ - Partial: `"Doc sources: detection partial — {N} found, {errors}"`
120
+
121
+ ### 6. Auto-Proceed
122
+
123
+ Load, read the entire file, then execute `{nextStepFile}`.
@@ -6,6 +6,12 @@ nextStepFile: '../enrich.md'
6
6
  atomicWriteProbeOrder:
7
7
  - '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
8
8
  - '{project-root}/src/shared/scripts/skf-atomic-write.py'
9
+ # Resolve `{deriveAssemblyShapeHelper}` the same way — used in §1 to decide
10
+ # whether this is a whole-language reference (registry-corpora prose retained
11
+ # as a Language Guide) or a standard skill (unchanged behaviour).
12
+ deriveAssemblyShapeProbeOrder:
13
+ - '{project-root}/_bmad/skf/shared/scripts/skf-derive-assembly-shape.py'
14
+ - '{project-root}/src/shared/scripts/skf-derive-assembly-shape.py'
9
15
  ---
10
16
 
11
17
  <!-- Config: communicate in {communication_language}. -->
@@ -33,6 +39,14 @@ Evaluate the following conditions. **If the condition fails, skip silently to se
33
39
 
34
40
  No tier gate — if `doc_urls` are present, this step runs at Quick, Forge, and Deep tiers alike.
35
41
 
42
+ **Determine assembly shape (whole-language gate).** Resolve `{deriveAssemblyShapeHelper}` from `{deriveAssemblyShapeProbeOrder}` (first existing path wins) and run it on the brief:
43
+
44
+ ```bash
45
+ uv run {deriveAssemblyShapeHelper} {brief_path}
46
+ ```
47
+
48
+ If the result's `assembly_shape` is `whole-language-reference` (the brief carries ≥1 `doc_urls` entry with `source: language-registry` — a compiler/interpreter repo enriched with the language's canonical prose), set the in-context flag `whole_language_reference: true`. This changes ONLY how the registry-sourced corpora are handled below (§4a): their prose is retained as a **Language Guide** rather than shredded into per-export items. For every other brief the flag is false and this step behaves exactly as before — no change to ordinary skills.
49
+
36
50
  ### 2. Security Notice
37
51
 
38
52
  Display an informational notice (not a gate — the user already approved these URLs in the brief):
@@ -122,6 +136,19 @@ Parse the successfully fetched markdown for:
122
136
 
123
137
  **No hallucination:** If information cannot be found in the fetched content, exclude it. Do not infer or fabricate API details.
124
138
 
139
+ **Whole-language references — retain prose, do NOT shred (`whole_language_reference: true`):** For a whole-language reference the registry-sourced corpora (the guide/Book, the standard/library docs) ARE the product, not the compiler's internal exports. Reducing that prose to per-export signature items and then discarding it under the §5 "T3 never overrides T1" rule (the compiler's AST already owns names like `Vec`, `Option`, `HashMap`) would gut exactly the content the skill exists to teach. So for these briefs, skip §4a below for the registry corpora.
140
+
141
+ ### 4a. Retain the Language Guide (whole-language references only)
142
+
143
+ **Skip this section entirely unless `whole_language_reference: true`.** When it is true, for each `doc_urls` entry whose `source` is `language-registry`:
144
+
145
+ - Do NOT reduce its fetched markdown to per-export items. Instead retain the cleaned prose as a Language-Guide entry `{url, label, prose}`, where `prose` is the substantive body (narrative, idioms, usage examples, conceptual reference) lightly trimmed of navigation/boilerplate, each block cited `[EXT:{url}]`.
146
+ - Collect these into a `language_guide[]` context artifact, in `doc_urls` order.
147
+
148
+ This artifact is a **distinct** carrier — it is NOT merged into the extraction inventory and is NOT subject to the §5 conflict rule, so the canonical prose survives intact into step 5 (compile), which foregrounds it as the skill's Language Guide. Non-registry docs (README-detected, homepage, Pages, docs-folder) still flow through §4's normal per-export extraction and the §5 merge unchanged.
149
+
150
+ **If a registry corpus could not be fetched** (network failure), record it in `language_guide[]` as `{url, label, prose: null}` and warn — step 5 surfaces the gap rather than emitting a thin guide silently.
151
+
125
152
  ### 5. Build Doc-Fetch Inventory
126
153
 
127
154
  **Mode determines merge behavior:**
@@ -131,6 +158,8 @@ Parse the successfully fetched markdown for:
131
158
 
132
159
  **Conflict rule:** T3 items NEVER override existing T1, T1-low, or T2 items for the same export. When an export already has a higher-confidence entry, the T3 item is discarded. T3 has the lowest priority.
133
160
 
161
+ **Language-Guide carve-out:** the `language_guide[]` artifact from §4a (whole-language references) is NOT part of the export inventory and is therefore NOT subject to this conflict rule — it carries no export key, so it cannot collide with a T1 compiler export and can never be pruned. It is passed separately into step 5, which renders it as the foregrounded Language Guide section. Only the per-export T3 items participate in the T1/T2/T3 merge.
162
+
134
163
  **Edge case — T1-zero supplemental mode:** If T1 extraction produced zero results and `doc_urls` are present in supplemental mode, T3 items should be used as the primary inventory since no T1 data exists to conflict with.
135
164
 
136
165
  **Aggregate totals for reporting:**
@@ -17,6 +17,13 @@ atomicWriteProbeOrder:
17
17
  descriptionGuardProbeOrder:
18
18
  - '{project-root}/_bmad/skf/shared/scripts/skf-description-guard.py'
19
19
  - '{project-root}/src/shared/scripts/skf-description-guard.py'
20
+ # Resolve `{frontmatterValidator}` by probing `{frontmatterValidatorProbeOrder}`
21
+ # in order (installed SKF module path first, src/ dev-checkout fallback); first
22
+ # existing path wins. §0's post-restore re-validation hook uses it; an installed
23
+ # module has no src/ tree, so a bare src/ path would silently fail the hook.
24
+ frontmatterValidatorProbeOrder:
25
+ - '{project-root}/_bmad/skf/shared/scripts/skf-validate-frontmatter.py'
26
+ - '{project-root}/src/shared/scripts/skf-validate-frontmatter.py'
20
27
  ---
21
28
 
22
29
  <!-- Config: communicate in {communication_language}. -->
@@ -43,7 +50,7 @@ To validate the compiled SKILL.md content against the agentskills.io specificati
43
50
 
44
51
  Load `{descriptionGuardProtocol}` for the full prose explanation of the four-phase guard (why it exists, what counts as divergence, why token-stream comparison is the right shape). The deterministic phases are executed via `{descriptionGuardHelper}` — the calling sections (§2 and §4) invoke the helper at the capture and verify-restore points.
45
52
 
46
- **This skill's post-restore re-validation hook:** after `{descriptionGuardHelper}` reports `restored: true`, run `uv run {project-root}/src/shared/scripts/skf-validate-frontmatter.py <staging-skill-dir>/SKILL.md` and capture `schema_revalidation_result` in context. If the validator exits non-zero OR reports failure for the `description` field, flip the Schema result back to `FAIL` in the evidence report (overriding any prior PASS/WARN from §2), record `description_guard_revalidation: FAIL` with the validator's diagnostic message, and continue — do not halt (step 9 health-check and result contract still need to run so the failure is surfaced through the normal artifact path).
53
+ **This skill's post-restore re-validation hook:** after `{descriptionGuardHelper}` reports `restored: true`, resolve `{frontmatterValidator}` from `{frontmatterValidatorProbeOrder}` (first existing path wins), run `uv run {frontmatterValidator} <staging-skill-dir>/SKILL.md` and capture `schema_revalidation_result` in context. If the validator exits non-zero OR reports failure for the `description` field, flip the Schema result back to `FAIL` in the evidence report (overriding any prior PASS/WARN from §2), record `description_guard_revalidation: FAIL` with the validator's diagnostic message, and continue — do not halt (step 9 health-check and result contract still need to run so the failure is surfaced through the normal artifact path).
47
54
 
48
55
  ### 1. Check Tool Availability
49
56
 
@@ -199,7 +206,9 @@ tessl installs automatically via `npx`. A missing tool is not an error — grace
199
206
 
200
207
  **If §6 produced no novel suggestions (all dismissed via `{tesslDismissalData}`) OR tessl was unavailable:** Skip this gate — auto-proceed.
201
208
 
202
- **If §6 produced novel suggestions** (ones not matched by any dismissal rule), present them to the user:
209
+ **GATE [default: S]** — If `{headless_mode}` is true AND §6 produced novel suggestions: auto-select [S] Skip (a headless run has no human to triage novel suggestions), record `"tessl suggestions: {N} novel suggestion(s) auto-skipped (headless)"` in the evidence report under "Dismissed tessl suggestions", log `"headless: auto-skip {N} novel tessl suggestion(s)"`, and append `{step: "validate", gate: "tessl-suggestions", decision: "S", value: "{N} novel auto-skipped", rationale: "headless mode — no human to triage novel tessl suggestions", timestamp: {ISO}}` to the in-context `headless_decisions[]` list (step 5 §7 reads it into the evidence-report `## Auto-Decisions` table). This is the one consequential auto-decision that drops human-relevant feedback, so it must leave an audit row rather than vanishing. Then auto-proceed to §7 — do NOT present the menu below.
210
+
211
+ **If §6 produced novel suggestions** (ones not matched by any dismissal rule) AND `{headless_mode}` is false, present them to the user:
203
212
 
204
213
  "**Content quality review: {score}%**
205
214
 
@@ -228,7 +237,7 @@ tessl suggestions (novel — not matched by `{tesslDismissalData}`):
228
237
  Cross-check metadata.json against extraction inventory:
229
238
  - `stats.exports_documented` / `stats.exports_public_api` / `stats.exports_internal` / `stats.exports_total` are accurate
230
239
  - `stats.public_api_coverage` and `stats.total_coverage` are correctly computed (null when denominator is 0)
231
- - `confidence_distribution.t1`, `confidence_distribution.t1_low`, `confidence_distribution.t2`, `confidence_distribution.t3` match actual counts
240
+ - `confidence_distribution.{t1, t1_low, t2, t3}` bin **per-export signature tiers** — each documented export lands in exactly one tier, so the four bins must **sum to `stats.exports_documented`**. Do NOT fold T2 annotation counts or T3 doc-item counts into the bins (a common error: binning ~8 annotations + ~80 doc items on top of 59 exports → sum 147 ≠ 59). `skf-test-skill` coverage-check §4b enforces this sum as an internal-consistency defect. Carve-outs mirror that consumer: for a **stack** skill the distribution bins constituents (sums to the constituent count, not `exports_documented`); for a **reference-app** it bins per-citation provenance entries (sums to the citation count, not `pattern_surfaces_documented`)
232
241
  - `spec_version` is "1.3"
233
242
  - If `scripts[]` or `assets[]` arrays present: verify `stats.scripts_count`/`stats.assets_count` match array lengths; verify `file_entries` count in provenance-map.json matches
234
243
 
@@ -62,9 +62,9 @@ Every HARD HALT in this workflow exits with a stable code so headless automators
62
62
  | Code | Meaning | Raised by |
63
63
  | ---- | -------------------- | -------------------------------------------------------------------------------------------- |
64
64
  | 0 | success | step 4 (terminal) |
65
- | 2 | input-missing / input-invalid | step 1 §4 (headless missing `skill_name` arg) → `input-missing`; non-existent skill / unparseable version → `input-invalid` |
65
+ | 2 | input-missing / input-invalid | step 1 §4 (headless missing `skill_name` arg) → `input-missing`; step 1 §4 (headless skill arg matches no listed skill) / step 1 §6 (headless version arg absent from the `versions` map) → `input-invalid` |
66
66
  | 3 | resolution-failure | step 1 §2 (manifest is malformed JSON); step 1 §3 (no skills found anywhere) |
67
- | 4 | write-failure | On-Activation §3 pre-flight write probe (skills_output_folder unwritable); step 2 §2 (manifest write); step 2 §3 (context-file rewrite); step 2 §4 (directory delete) |
67
+ | 4 | write-failure | On-Activation §3 pre-flight write probe (skills_output_folder unwritable); step 2 §2 (manifest write → `manifest-write-failed`); step 2 §3 (context-file rewrite → `context-rebuild-failed`); step 2 §4 (full purge failure — every target directory failed to delete → `delete-failed`) |
68
68
  | 5 | state-conflict | step 1 §7 (active-version-guard refuses to drop the active version while non-deprecated peers remain) |
69
69
  | 6 | user-cancelled | step 1 §10 confirmation gate `[N]`; any prompt that accepted `cancel`/`exit`/`:q`; On-Activation §4 (`headless-purge-forbidden`) |
70
70
 
@@ -152,13 +152,15 @@ Report: "**Rebuilt managed sections in:** {list of updated files}. {if any faile
152
152
 
153
153
  ### 4. Delete Files (Purge Mode Only)
154
154
 
155
- **If `drop_mode != "purge"`**, skip this section entirely. Set `files_deleted = []` and `disk_freed = "N/A (soft drop)"`, then jump to section 5.
155
+ **If `drop_mode != "purge"`**, skip this section entirely. Set `files_deleted = []`, `disk_freed = "N/A (soft drop)"`, `delete_failures = []`, and `purge_status = "success"`, then jump to section 5.
156
156
 
157
157
  **If `drop_mode == "purge"`:**
158
158
 
159
159
  1. Initialize `files_deleted = []` and `bytes_freed = 0`.
160
160
 
161
- 2. For each directory path in `affected_directories`:
161
+ 2. Also initialize `delete_failures = []` to track paths whose deletion was attempted but did not succeed.
162
+
163
+ 3. For each directory path in `affected_directories`:
162
164
  a. Verify the path is inside either `{skills_output_folder}` or `{forge_data_folder}` (defense in depth against accidental deletion of unrelated paths)
163
165
  b. If the directory does not exist, record it as "(already absent)" and continue
164
166
  c. Compute the directory size in bytes before deletion (recursive sum)
@@ -166,23 +168,29 @@ Report: "**Rebuilt managed sections in:** {list of updated files}. {if any faile
166
168
  e. Verify deletion succeeded (the path no longer exists)
167
169
  f. Append the path to `files_deleted` and add its byte size to `bytes_freed`
168
170
 
169
- 3. **Version-level purge, single version:**
171
+ 4. **Version-level purge, single version:**
170
172
  - `{skills_output_folder}/{target_skill}/{version}/` is deleted, but `{skills_output_folder}/{target_skill}/` remains (it still contains other versions or the `active` symlink)
171
173
  - If the `active` symlink pointed to the just-deleted version, update or remove it:
172
174
  - If other versions remain in the manifest for `{target_skill}`, repoint `active` to the manifest's current `active_version` (skipping deprecated)
173
175
  - If no non-deprecated versions remain, remove the `active` symlink (reachable only when dropping the sole surviving version, which in step 1 was permitted because no other non-deprecated versions existed)
174
176
 
175
- 4. **Skill-level purge:**
177
+ 5. **Skill-level purge:**
176
178
  - `{skills_output_folder}/{target_skill}/` and `{forge_data_folder}/{target_skill}/` are deleted in full — the `active` symlink disappears with the parent directory
177
179
 
178
- 5. Convert `bytes_freed` to a human-readable string for the final report (e.g. `"4.2 MB"`). Store as `disk_freed`.
180
+ 6. Convert `bytes_freed` to a human-readable string for the final report (e.g. `"4.2 MB"`). Store as `disk_freed`.
179
181
 
180
- **On deletion error:**
182
+ **On deletion error (per path):**
181
183
 
182
- - Record which path failed and the error message
183
- - Continue attempting the remaining paths — partial purge is still better than no purge
184
+ - Append the path and its error message to `delete_failures`
185
+ - Continue attempting the remaining paths — a partial purge is still better than no purge
184
186
  - Report all failures at the end of this section
185
187
 
188
+ **After the loop — classify the deletion outcome.** Let `attempted` be the number of paths in `affected_directories` that existed on disk (i.e. were not recorded as "(already absent)"):
189
+
190
+ - **Full purge failure** — `attempted > 0` AND every attempted path is in `delete_failures` (nothing was deleted): the purge accomplished none of its destructive intent, so it must NOT report success. HALT (exit code 4, `halt_reason: "delete-failed"`): "**Purge failed** — none of the {attempted} target director(ies) could be deleted: {list each path with its error}. The manifest and context files were already updated in sections 2–3; the on-disk files remain and can be removed manually (`rm -rf {path}`)." In headless mode, emit the error envelope per SKILL.md "Result Contract (Headless)" with the resolved `skill`, `drop_mode`, `versions_affected`, `files_deleted: []`, and `manifest_updated` from section 2. Do not proceed to section 5.
191
+ - **Partial purge failure** — `delete_failures` is non-empty but at least one path was deleted: keep record-and-continue. Set `purge_status = "partial"` so step 3's on-disk result record reflects it (the `output-contract-schema.md` `status` enum supports `"partial"`); proceed to section 5. The headless single-line envelope has no `"partial"` value in its enum, so it stays `"success"` while `context_files_failed`/`verification_errors`/the report surface the unfreed paths.
192
+ - **No failures** — `delete_failures` is empty: set `purge_status = "success"` and proceed to section 5.
193
+
186
194
  ### 5. Verify Final State
187
195
 
188
196
  Run these verification checks:
@@ -203,6 +211,8 @@ Store the following for step 3:
203
211
 
204
212
  - `files_deleted` — list of directory paths actually deleted (purge mode) or `[]` (soft drop)
205
213
  - `disk_freed` — human-readable size (purge mode) or `"N/A (soft drop)"`
214
+ - `delete_failures` — list of `{path, error}` for paths whose deletion was attempted but failed (empty if none; a *full* purge failure already HALTed in section 4 and never reaches this step)
215
+ - `purge_status` — `"success"`, `"partial"` (some paths failed to delete), or `"success"` for soft drops; step 3 maps this to the on-disk result record's `status` field
206
216
  - `manifest_updated` — boolean (true if section 2 succeeded)
207
217
  - `context_files_updated` — list of successfully rebuilt files
208
218
  - `context_files_failed` — list of files that failed to rebuild (empty if none)
@@ -220,7 +230,8 @@ If any stage fails, record which stage failed and provide recovery guidance in t
220
230
  |--------------|-------------------|
221
231
  | Manifest update | "Manifest is in pre-drop state. Re-run the workflow once the underlying I/O issue is resolved. No files were deleted." |
222
232
  | Context file rebuild | "Manifest is already updated. Re-run `[EX] Export Skill` against any still-valid skill to regenerate the affected managed sections, or rerun the drop workflow." |
223
- | File deletion (purge) | "Manifest and context files are consistent. Remaining directories listed in the report can be deleted manually: `rm -rf {path}`." |
233
+ | File deletion (purge — partial) | "Manifest and context files are consistent. Remaining directories listed in the report can be deleted manually: `rm -rf {path}`." (status `partial`) |
234
+ | File deletion (purge — full) | "No target directory could be deleted. Manifest and context files are already updated; the on-disk files remain. HALT with `delete-failed` (exit 4) — re-run once the I/O issue is resolved, or remove the listed directories manually." |
224
235
  | Verification | "Execution completed but post-write checks found drift. See the report for specific paths requiring manual review." |
225
236
 
226
237
  ## CRITICAL STEP COMPLETION NOTE
@@ -73,6 +73,8 @@ These require manual review — see the error-handling guidance in step 2.
73
73
 
74
74
  Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{skills_output_folder}/drop-skill-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{skills_output_folder}/drop-skill-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include all purged file paths in `outputs`; include `target_skill`, `drop_mode`, and `versions_affected` in `summary`.
75
75
 
76
+ Set the record's `status` from step 2's `purge_status`: `"partial"` when some (but not all) purge-mode directories failed to delete — surface the failing paths from `delete_failures` in `summary.delete_failures` — otherwise `"success"`. A *full* purge failure never reaches this step: step 2 §4 HALTs with `halt_reason: "delete-failed"` and the error-envelope path below handles it.
77
+
76
78
  When `{headless_mode}` is true, also emit the single-line envelope on **stdout** before chaining to step 4 (matches the SKILL.md "Result Contract (Headless)" shape):
77
79
 
78
80
  ```