bmad-module-skill-forge 1.4.1 → 1.5.1

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 (231) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +0 -8
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/docs/_internal/STABILITY.md +1 -1
  5. package/docs/architecture.md +2 -2
  6. package/docs/examples.md +1 -1
  7. package/docs/workflows.md +5 -5
  8. package/package.json +2 -2
  9. package/src/README.md +1 -1
  10. package/src/knowledge/ccc-bridge.md +12 -12
  11. package/src/knowledge/qmd-registry.md +5 -5
  12. package/src/shared/health-check.md +5 -5
  13. package/src/shared/references/description-guard-protocol.md +100 -0
  14. package/src/shared/references/output-contract-schema.md +1 -1
  15. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
  16. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
  17. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
  18. package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
  19. package/src/shared/scripts/skf-build-change-manifest.py +420 -0
  20. package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
  21. package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
  22. package/src/shared/scripts/skf-description-guard.py +359 -0
  23. package/src/shared/scripts/skf-detect-language.py +2 -2
  24. package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
  25. package/src/shared/scripts/skf-detect-tools.py +147 -16
  26. package/src/shared/scripts/skf-detect-workspaces.py +1 -1
  27. package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
  29. package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
  30. package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
  31. package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
  32. package/src/shared/scripts/skf-hash-content.py +284 -0
  33. package/src/shared/scripts/skf-load-provenance.py +295 -0
  34. package/src/shared/scripts/skf-manifest-ops.py +22 -6
  35. package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
  36. package/src/shared/scripts/skf-pair-intersect.py +250 -0
  37. package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
  38. package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
  39. package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
  40. package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
  41. package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
  42. package/src/shared/scripts/skf-scan-manifests.py +738 -0
  43. package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
  44. package/src/shared/scripts/skf-update-active-symlink.py +315 -0
  45. package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
  46. package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
  47. package/src/shared/scripts/skf-write-skill-brief.py +61 -5
  48. package/src/skf-analyze-source/SKILL.md +69 -16
  49. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  50. package/src/skf-analyze-source/customize.toml +55 -0
  51. package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
  52. package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
  53. package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  54. package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +48 -14
  55. package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +32 -9
  56. package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
  57. package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
  58. package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +16 -10
  59. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
  60. package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
  61. package/src/skf-audit-skill/SKILL.md +66 -15
  62. package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
  63. package/src/skf-audit-skill/customize.toml +49 -0
  64. package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
  65. package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +63 -19
  66. package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
  67. package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
  68. package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
  69. package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
  70. package/src/skf-audit-skill/references/severity-rules.md +2 -0
  71. package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +35 -17
  72. package/src/skf-brief-skill/SKILL.md +49 -19
  73. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  74. package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
  75. package/src/skf-brief-skill/customize.toml +44 -0
  76. package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +27 -15
  77. package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
  78. package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
  79. package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +35 -29
  80. package/src/skf-brief-skill/references/headless-args.md +9 -9
  81. package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
  82. package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
  83. package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
  84. package/src/skf-brief-skill/references/qmd-collection-registration.md +2 -2
  85. package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +27 -20
  86. package/src/skf-brief-skill/references/version-resolution.md +2 -2
  87. package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +29 -16
  88. package/src/skf-create-skill/SKILL.md +25 -19
  89. package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
  90. package/src/skf-create-skill/assets/skill-sections.md +5 -5
  91. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
  92. package/src/skf-create-skill/customize.toml +35 -0
  93. package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
  94. package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
  95. package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
  96. package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
  97. package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
  98. package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
  99. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  100. package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
  101. package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
  102. package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
  103. package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
  104. package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
  105. package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
  106. package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
  107. package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
  108. package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +48 -28
  109. package/src/skf-create-stack-skill/SKILL.md +76 -17
  110. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
  111. package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
  112. package/src/skf-create-stack-skill/customize.toml +46 -0
  113. package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
  114. package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
  115. package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +37 -11
  116. package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +32 -28
  117. package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
  118. package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
  119. package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
  120. package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
  122. package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +44 -17
  123. package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
  124. package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
  125. package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
  126. package/src/skf-drop-skill/SKILL.md +84 -12
  127. package/src/skf-drop-skill/customize.toml +57 -0
  128. package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
  129. package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
  130. package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  131. package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
  132. package/src/skf-export-skill/SKILL.md +85 -16
  133. package/src/skf-export-skill/assets/managed-section-format.md +3 -3
  134. package/src/skf-export-skill/assets/snippet-format.md +2 -2
  135. package/src/skf-export-skill/customize.toml +49 -0
  136. package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
  137. package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  138. package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
  139. package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
  140. package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
  141. package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
  142. package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
  143. package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
  144. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +79 -0
  145. package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +23 -11
  146. package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
  147. package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
  148. package/src/skf-quick-skill/SKILL.md +63 -123
  149. package/src/skf-quick-skill/customize.toml +44 -0
  150. package/src/skf-quick-skill/references/batch-mode.md +102 -0
  151. package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
  152. package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
  153. package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
  154. package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
  155. package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
  156. package/src/skf-quick-skill/references/registry-resolution.md +2 -0
  157. package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
  158. package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
  159. package/src/skf-refine-architecture/SKILL.md +86 -16
  160. package/src/skf-refine-architecture/customize.toml +49 -0
  161. package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
  162. package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
  163. package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
  164. package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
  165. package/src/skf-refine-architecture/references/init.md +144 -0
  166. package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
  167. package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
  168. package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
  169. package/src/skf-rename-skill/SKILL.md +82 -12
  170. package/src/skf-rename-skill/customize.toml +52 -0
  171. package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
  172. package/src/skf-rename-skill/references/health-check.md +30 -0
  173. package/src/skf-rename-skill/references/rebuild-context.md +110 -0
  174. package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
  175. package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
  176. package/src/skf-setup/SKILL.md +35 -35
  177. package/src/skf-setup/customize.toml +33 -0
  178. package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
  179. package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
  180. package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
  181. package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
  182. package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
  183. package/src/skf-setup/references/tier-rules.md +1 -1
  184. package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
  185. package/src/skf-test-skill/SKILL.md +77 -15
  186. package/src/skf-test-skill/customize.toml +54 -0
  187. package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +37 -73
  188. package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
  189. package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
  190. package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
  191. package/src/skf-test-skill/references/health-check.md +14 -0
  192. package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
  193. package/src/skf-test-skill/references/migration-section-rules.md +4 -2
  194. package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
  195. package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
  196. package/src/skf-test-skill/references/scoring-rules.md +8 -6
  197. package/src/skf-test-skill/references/source-access-protocol.md +7 -5
  198. package/src/skf-test-skill/scripts/compute-score.py +88 -17
  199. package/src/skf-test-skill/templates/test-report-template.md +13 -13
  200. package/src/skf-update-skill/SKILL.md +23 -14
  201. package/src/skf-update-skill/customize.toml +44 -0
  202. package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
  203. package/src/skf-update-skill/references/health-check.md +32 -0
  204. package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
  205. package/src/skf-update-skill/references/manual-section-rules.md +4 -0
  206. package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
  207. package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
  208. package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
  209. package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
  210. package/src/skf-update-skill/references/report.md +214 -0
  211. package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
  212. package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
  213. package/src/skf-verify-stack/SKILL.md +89 -17
  214. package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
  215. package/src/skf-verify-stack/customize.toml +50 -0
  216. package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
  217. package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +13 -9
  218. package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
  219. package/src/skf-verify-stack/references/init.md +170 -0
  220. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  221. package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +18 -14
  222. package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +27 -42
  223. package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +13 -9
  224. package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +14 -10
  225. package/tools/validate-docs-drift.js +1 -1
  226. package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
  227. package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
  228. package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
  229. package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
  230. package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
  231. package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
@@ -0,0 +1,359 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """SKF Description Guard — defend SKILL.md frontmatter description against
6
+ well-meaning tool rewrites.
7
+
8
+ External validators (`skill-check check --fix`, `skill-check split-body`) may
9
+ rewrite the frontmatter `description` field — replace it with a generic
10
+ version, truncate it, or re-introduce angle-bracket tokens that earlier
11
+ sanitization removed. The compiled/merged description on disk is the
12
+ authoritative one; losing it breaks agent discovery quality.
13
+
14
+ This script implements the deterministic parts of the four-phase guard
15
+ protocol (capture before tool call; verify-and-restore after) so that
16
+ workflows do not have to re-implement frontmatter parsing or token-stream
17
+ comparison in prose.
18
+
19
+ Subcommands:
20
+ capture <skill-md>
21
+ Snapshot the current `description` field. Emits JSON
22
+ {"description": "...", "schema_hash": "sha256:..."}
23
+ where schema_hash covers the full frontmatter block (handy for
24
+ tamper detection if the caller wants belt-and-braces verification).
25
+
26
+ verify-restore <skill-md> --captured-description <STR>
27
+ Re-read the file, compare current description against the captured
28
+ value using token-stream equality (split on whitespace, compare
29
+ element-by-element). If diverged, atomically rewrite the frontmatter
30
+ with the captured description and emit
31
+ {"diverged": true, "restored": true|false, "diff_kind": "...",
32
+ "current_description": "..."}
33
+ If not diverged (including whitespace-only differences), emit
34
+ {"diverged": false, "restored": false, "diff_kind": "none"|"whitespace-only"}
35
+
36
+ Token-stream comparison is the documented sweet spot — catches replaced
37
+ words, truncation, and reintroduced angle-brackets while ignoring cosmetic
38
+ whitespace fixes (trailing newline, re-wrapped quoted strings). See
39
+ src/shared/references/description-guard-protocol.md for the full protocol.
40
+
41
+ CLI — invoke via `uv run` so the PEP 723 PyYAML dependency declared above
42
+ is auto-resolved:
43
+
44
+ uv run skf-description-guard.py capture <skill-md>
45
+ uv run skf-description-guard.py verify-restore <skill-md> \\
46
+ --captured-description "the snapshot string"
47
+
48
+ Exit codes:
49
+ 0 — operation succeeded (including no-divergence verify)
50
+ 1 — user error (bad args, file missing, frontmatter unparseable)
51
+ 2 — operation failure (restore write failed)
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ import argparse
57
+ import hashlib
58
+ import json
59
+ import os
60
+ import sys
61
+ import tempfile
62
+ from pathlib import Path
63
+
64
+ import yaml
65
+
66
+
67
+ # --------------------------------------------------------------------------
68
+ # Frontmatter parsing
69
+ # --------------------------------------------------------------------------
70
+
71
+
72
+ def _split_frontmatter(text: str) -> tuple[str, str, str]:
73
+ """Split a markdown file into (leading, frontmatter_yaml, body).
74
+
75
+ Returns ('', '', text) if there is no frontmatter. Frontmatter is
76
+ recognized when the file starts with '---\\n' and a matching '---\\n'
77
+ line closes it before the body.
78
+ """
79
+ if not text.startswith("---\n"):
80
+ return "", "", text
81
+ rest = text[4:]
82
+ close = rest.find("\n---\n")
83
+ if close == -1:
84
+ # also accept '\n---' as the closing line at EOF (no trailing newline)
85
+ if rest.endswith("\n---"):
86
+ return "---\n", rest[: -len("\n---")], ""
87
+ return "", "", text
88
+ fm = rest[:close]
89
+ body = rest[close + len("\n---\n") :]
90
+ return "---\n", fm, body
91
+
92
+
93
+ def read_description(skill_md: Path) -> tuple[str, str]:
94
+ """Read `description` field from SKILL.md frontmatter.
95
+
96
+ Returns (description, schema_hash). schema_hash is the sha256 of the
97
+ raw frontmatter YAML block as bytes, prefixed with "sha256:".
98
+
99
+ Raises ValueError if the file has no frontmatter, the frontmatter is
100
+ unparseable, or the description field is missing.
101
+ """
102
+ text = skill_md.read_text(encoding="utf-8")
103
+ _, fm_yaml, _ = _split_frontmatter(text)
104
+ if not fm_yaml:
105
+ raise ValueError(f"no frontmatter found in {skill_md}")
106
+ try:
107
+ fm = yaml.safe_load(fm_yaml)
108
+ except yaml.YAMLError as exc:
109
+ raise ValueError(f"frontmatter in {skill_md} is not valid YAML: {exc}") from exc
110
+ if not isinstance(fm, dict):
111
+ raise ValueError(f"frontmatter in {skill_md} is not a mapping")
112
+ if "description" not in fm:
113
+ raise ValueError(f"frontmatter in {skill_md} has no `description` field")
114
+ description = fm["description"]
115
+ if not isinstance(description, str):
116
+ raise ValueError(f"`description` in {skill_md} is not a string")
117
+ schema_hash = "sha256:" + hashlib.sha256(fm_yaml.encode("utf-8")).hexdigest()
118
+ return description, schema_hash
119
+
120
+
121
+ # --------------------------------------------------------------------------
122
+ # Divergence detection
123
+ # --------------------------------------------------------------------------
124
+
125
+
126
+ def classify_divergence(captured: str, current: str) -> str:
127
+ """Compare two descriptions and classify the difference.
128
+
129
+ Returns one of:
130
+ "none" — strings are byte-identical
131
+ "whitespace-only" — token streams match but raw strings differ
132
+ "replaced" — token streams differ (semantic content change)
133
+ "truncated" — current's tokens are a strict prefix of captured's
134
+ "deleted" — current is empty/whitespace-only and captured is not
135
+ """
136
+ if captured == current:
137
+ return "none"
138
+ if not current.strip() and captured.strip():
139
+ return "deleted"
140
+ cap_tokens = captured.split()
141
+ cur_tokens = current.split()
142
+ if cap_tokens == cur_tokens:
143
+ return "whitespace-only"
144
+ # truncation = cur_tokens is a strict prefix of cap_tokens
145
+ if len(cur_tokens) < len(cap_tokens) and cap_tokens[: len(cur_tokens)] == cur_tokens:
146
+ return "truncated"
147
+ return "replaced"
148
+
149
+
150
+ def is_diverged(diff_kind: str) -> bool:
151
+ """Whether a diff_kind indicates real (non-whitespace) divergence."""
152
+ return diff_kind not in ("none", "whitespace-only")
153
+
154
+
155
+ # --------------------------------------------------------------------------
156
+ # Restore (atomic frontmatter rewrite)
157
+ # --------------------------------------------------------------------------
158
+
159
+
160
+ def restore_description(skill_md: Path, captured: str) -> None:
161
+ """Atomically rewrite SKILL.md so its frontmatter `description` equals captured.
162
+
163
+ Preserves frontmatter key order and surrounding whitespace by doing a
164
+ line-level rewrite of just the `description:` value, not a full YAML
165
+ re-dump (which would re-order keys, change quoting style, and likely
166
+ fail roundtrip tests downstream).
167
+ """
168
+ text = skill_md.read_text(encoding="utf-8")
169
+ leading, fm_yaml, body = _split_frontmatter(text)
170
+ if not fm_yaml:
171
+ raise ValueError(f"cannot restore: no frontmatter in {skill_md}")
172
+
173
+ new_fm = _rewrite_description_line(fm_yaml, captured)
174
+ new_text = f"{leading}{new_fm}\n---\n{body}"
175
+
176
+ # atomic: write to a sibling temp file, fsync, rename
177
+ fd, tmp_path = tempfile.mkstemp(
178
+ prefix=skill_md.name + ".", suffix=".skf-guard.tmp", dir=skill_md.parent
179
+ )
180
+ try:
181
+ with os.fdopen(fd, "w", encoding="utf-8") as tmp:
182
+ tmp.write(new_text)
183
+ tmp.flush()
184
+ os.fsync(tmp.fileno())
185
+ os.replace(tmp_path, skill_md)
186
+ except Exception:
187
+ try:
188
+ os.unlink(tmp_path)
189
+ except FileNotFoundError:
190
+ pass
191
+ raise
192
+
193
+
194
+ def _rewrite_description_line(fm_yaml: str, new_description: str) -> str:
195
+ """Replace the `description:` value in a YAML frontmatter block.
196
+
197
+ Preserves other keys verbatim. Handles three common shapes:
198
+ description: single-line value
199
+ description: "double-quoted value"
200
+ description: | # block scalar (folded variant: >)
201
+ multi-line
202
+ value here
203
+ """
204
+ lines = fm_yaml.split("\n")
205
+ out: list[str] = []
206
+ i = 0
207
+ quoted = _yaml_quote_inline(new_description)
208
+ replaced = False
209
+ while i < len(lines):
210
+ line = lines[i]
211
+ if not replaced and _is_description_key_line(line):
212
+ stripped = line.lstrip()
213
+ indent = line[: len(line) - len(stripped)]
214
+ # detect block-scalar indicator (| or >)
215
+ after_key = stripped[len("description:") :].lstrip()
216
+ if after_key.startswith("|") or after_key.startswith(">"):
217
+ # skip block-scalar continuation lines (deeper indent than the key line)
218
+ key_indent = len(indent)
219
+ i += 1
220
+ while i < len(lines):
221
+ nxt = lines[i]
222
+ if nxt.strip() == "":
223
+ # blank lines belong to the block scalar
224
+ i += 1
225
+ continue
226
+ nxt_indent = len(nxt) - len(nxt.lstrip())
227
+ if nxt_indent <= key_indent:
228
+ break
229
+ i += 1
230
+ out.append(f"{indent}description: {quoted}")
231
+ replaced = True
232
+ continue
233
+ out.append(f"{indent}description: {quoted}")
234
+ replaced = True
235
+ i += 1
236
+ continue
237
+ out.append(line)
238
+ i += 1
239
+ if not replaced:
240
+ raise ValueError("description key not found while rewriting frontmatter")
241
+ return "\n".join(out)
242
+
243
+
244
+ def _is_description_key_line(line: str) -> bool:
245
+ stripped = line.lstrip()
246
+ return stripped.startswith("description:") and (
247
+ len(stripped) == len("description:") or stripped[len("description:")] in (" ", "\t", "")
248
+ )
249
+
250
+
251
+ def _yaml_quote_inline(value: str) -> str:
252
+ """Emit a YAML scalar suitable as the inline value of `description: `.
253
+
254
+ Uses double-quoted form so control characters and embedded quotes are
255
+ safe. Block scalars (| / >) are not used — the description field is a
256
+ single semantic string and downstream readers (skill-check, agentskills)
257
+ expect inline form.
258
+ """
259
+ escaped = (
260
+ value.replace("\\", "\\\\")
261
+ .replace("\"", "\\\"")
262
+ .replace("\n", "\\n")
263
+ .replace("\r", "\\r")
264
+ .replace("\t", "\\t")
265
+ )
266
+ return f'"{escaped}"'
267
+
268
+
269
+ # --------------------------------------------------------------------------
270
+ # CLI
271
+ # --------------------------------------------------------------------------
272
+
273
+
274
+ def _cmd_capture(args: argparse.Namespace) -> int:
275
+ skill_md = Path(args.skill_md)
276
+ if not skill_md.is_file():
277
+ _fail(f"file not found: {skill_md}")
278
+ try:
279
+ description, schema_hash = read_description(skill_md)
280
+ except ValueError as exc:
281
+ _fail(str(exc))
282
+ json.dump({"description": description, "schema_hash": schema_hash}, sys.stdout)
283
+ sys.stdout.write("\n")
284
+ return 0
285
+
286
+
287
+ def _cmd_verify_restore(args: argparse.Namespace) -> int:
288
+ skill_md = Path(args.skill_md)
289
+ if not skill_md.is_file():
290
+ _fail(f"file not found: {skill_md}")
291
+ try:
292
+ current, _ = read_description(skill_md)
293
+ except ValueError as exc:
294
+ _fail(str(exc))
295
+
296
+ captured = args.captured_description
297
+ diff_kind = classify_divergence(captured, current)
298
+ diverged = is_diverged(diff_kind)
299
+
300
+ result = {
301
+ "diverged": diverged,
302
+ "restored": False,
303
+ "diff_kind": diff_kind,
304
+ "current_description": current,
305
+ }
306
+
307
+ if diverged:
308
+ try:
309
+ restore_description(skill_md, captured)
310
+ result["restored"] = True
311
+ except (OSError, ValueError) as exc:
312
+ json.dump({**result, "restore_error": str(exc)}, sys.stdout)
313
+ sys.stdout.write("\n")
314
+ return 2
315
+
316
+ json.dump(result, sys.stdout)
317
+ sys.stdout.write("\n")
318
+ return 0
319
+
320
+
321
+ def _fail(msg: str) -> None:
322
+ print(f"error: {msg}", file=sys.stderr)
323
+ raise SystemExit(1)
324
+
325
+
326
+ def _build_parser() -> argparse.ArgumentParser:
327
+ parser = argparse.ArgumentParser(
328
+ prog="skf-description-guard",
329
+ description="Defend SKILL.md frontmatter description against tool rewrites.",
330
+ )
331
+ sub = parser.add_subparsers(dest="cmd", required=True)
332
+
333
+ p_cap = sub.add_parser("capture", help="snapshot the current description value")
334
+ p_cap.add_argument("skill_md", help="path to SKILL.md")
335
+ p_cap.set_defaults(func=_cmd_capture)
336
+
337
+ p_ver = sub.add_parser(
338
+ "verify-restore",
339
+ help="verify on-disk description against captured snapshot; restore if diverged",
340
+ )
341
+ p_ver.add_argument("skill_md", help="path to SKILL.md")
342
+ p_ver.add_argument(
343
+ "--captured-description",
344
+ required=True,
345
+ help="the description string captured before the tool call",
346
+ )
347
+ p_ver.set_defaults(func=_cmd_verify_restore)
348
+
349
+ return parser
350
+
351
+
352
+ def main(argv: list[str] | None = None) -> int:
353
+ parser = _build_parser()
354
+ args = parser.parse_args(argv)
355
+ return args.func(args)
356
+
357
+
358
+ if __name__ == "__main__":
359
+ raise SystemExit(main())
@@ -5,7 +5,7 @@
5
5
  """SKF Detect Language — deterministic primary-language detection from a flat file tree.
6
6
 
7
7
  Single source of truth for the language-detection rule table that
8
- skf-brief-skill step-02 §3 applies. The rule walk is purely deterministic:
8
+ skf-brief-skill step 2 §3 applies. The rule walk is purely deterministic:
9
9
  manifest-file presence first (Cargo.toml → rust, package.json → js/ts,
10
10
  etc.), then extension-frequency fallback. Moving it into a shared script
11
11
  saves ~150-250 tokens per workflow invocation and removes a quiet drift
@@ -167,7 +167,7 @@ def _frequency_fallback(tree: list[str]) -> dict[str, Any]:
167
167
  else:
168
168
  confidence = "low"
169
169
  source = f"no dominant extension: top is {top_ext} at {top_count}/{total} ({share:.0%}, threshold {int(_DOMINANCE_THRESHOLD * 100)}%)"
170
- # Still surface the best-guess language; caller / step-03 §4 lets
170
+ # Still surface the best-guess language; caller / step 3 §4 lets
171
171
  # the user override on low confidence.
172
172
  return {
173
173
  "language": language,