bmad-module-skill-forge 2.0.0 → 2.0.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 (235) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/skill-lifecycle.md +1 -1
  6. package/src/module-help.csv +2 -2
  7. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  8. package/src/shared/scripts/skf-count-tokens.py +222 -0
  9. package/src/shared/scripts/skf-detect-docs.py +232 -1
  10. package/src/shared/scripts/skf-detect-language.py +77 -0
  11. package/src/shared/scripts/skf-detect-tools.py +105 -2
  12. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  13. package/src/shared/scripts/skf-find-cycles.py +246 -0
  14. package/src/shared/scripts/skf-hash-content.py +487 -2
  15. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  16. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  17. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  18. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  19. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  20. package/src/shared/scripts/skf-shard-body.py +461 -0
  21. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  22. package/src/shared/scripts/skf-structural-diff.py +307 -67
  23. package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
  24. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  25. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  26. package/src/shared/scripts/skf-validate-output.py +443 -10
  27. package/src/shared/scripts/skf-validate-pins.py +1 -1
  28. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  30. package/src/skf-analyze-source/SKILL.md +17 -32
  31. package/src/skf-analyze-source/customize.toml +2 -0
  32. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  33. package/src/skf-analyze-source/references/continue.md +8 -13
  34. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  35. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  36. package/src/skf-analyze-source/references/health-check.md +1 -2
  37. package/src/skf-analyze-source/references/identify-units.md +25 -36
  38. package/src/skf-analyze-source/references/init.md +12 -25
  39. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  40. package/src/skf-analyze-source/references/recommend.md +4 -14
  41. package/src/skf-analyze-source/references/scan-project.md +3 -11
  42. package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
  43. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  44. package/src/skf-audit-skill/SKILL.md +9 -6
  45. package/src/skf-audit-skill/customize.toml +2 -0
  46. package/src/skf-audit-skill/references/init.md +58 -32
  47. package/src/skf-audit-skill/references/re-index.md +15 -34
  48. package/src/skf-audit-skill/references/report.md +11 -58
  49. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  50. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  51. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  52. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  53. package/src/skf-brief-skill/SKILL.md +7 -36
  54. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  55. package/src/skf-brief-skill/customize.toml +14 -0
  56. package/src/skf-brief-skill/references/analyze-target.md +6 -14
  57. package/src/skf-brief-skill/references/confirm-brief.md +2 -14
  58. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  59. package/src/skf-brief-skill/references/gather-intent.md +19 -22
  60. package/src/skf-brief-skill/references/headless-args.md +1 -1
  61. package/src/skf-brief-skill/references/health-check.md +1 -22
  62. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  63. package/src/skf-brief-skill/references/scope-definition.md +4 -14
  64. package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
  65. package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
  66. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  67. package/src/skf-brief-skill/references/write-brief.md +17 -10
  68. package/src/skf-campaign/SKILL.md +15 -84
  69. package/src/skf-campaign/customize.toml +2 -0
  70. package/src/skf-campaign/manifest.yaml +4 -0
  71. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  72. package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
  73. package/src/skf-campaign/references/step-01-setup.md +4 -5
  74. package/src/skf-campaign/references/step-02-strategy.md +8 -3
  75. package/src/skf-campaign/references/step-03-pins.md +1 -1
  76. package/src/skf-campaign/references/step-04-provenance.md +1 -1
  77. package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
  78. package/src/skf-campaign/references/step-06-batch.md +7 -6
  79. package/src/skf-campaign/references/step-11-maintenance.md +2 -2
  80. package/src/skf-campaign/references/step-resume.md +9 -8
  81. package/src/skf-campaign/scripts/campaign-deps.py +10 -1
  82. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  83. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
  85. package/src/skf-create-skill/SKILL.md +14 -3
  86. package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
  87. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  88. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  89. package/src/skf-create-skill/customize.toml +13 -0
  90. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  91. package/src/skf-create-skill/references/compile.md +34 -47
  92. package/src/skf-create-skill/references/component-extraction.md +8 -35
  93. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  94. package/src/skf-create-skill/references/enrich.md +4 -17
  95. package/src/skf-create-skill/references/extract.md +14 -29
  96. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  97. package/src/skf-create-skill/references/extraction-patterns.md +7 -7
  98. package/src/skf-create-skill/references/generate-artifacts.md +9 -23
  99. package/src/skf-create-skill/references/health-check.md +1 -1
  100. package/src/skf-create-skill/references/load-brief.md +16 -18
  101. package/src/skf-create-skill/references/report.md +12 -15
  102. package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
  103. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  104. package/src/skf-create-skill/references/step-doc-rot.md +30 -12
  105. package/src/skf-create-skill/references/step-doc-sources.md +2 -2
  106. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  107. package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
  108. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  109. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  110. package/src/skf-create-skill/references/validate.md +59 -28
  111. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  112. package/src/skf-create-stack-skill/SKILL.md +15 -9
  113. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  114. package/src/skf-create-stack-skill/customize.toml +10 -0
  115. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  116. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  117. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  118. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  119. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  120. package/src/skf-create-stack-skill/references/init.md +9 -81
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  122. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  123. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  124. package/src/skf-create-stack-skill/references/report.md +27 -62
  125. package/src/skf-create-stack-skill/references/validate.md +35 -31
  126. package/src/skf-drop-skill/SKILL.md +20 -23
  127. package/src/skf-drop-skill/customize.toml +10 -0
  128. package/src/skf-drop-skill/references/execute.md +51 -31
  129. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  130. package/src/skf-drop-skill/references/health-check.md +1 -13
  131. package/src/skf-drop-skill/references/report.md +42 -10
  132. package/src/skf-drop-skill/references/select.md +88 -48
  133. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  134. package/src/skf-export-skill/SKILL.md +17 -14
  135. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  136. package/src/skf-export-skill/customize.toml +10 -6
  137. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  138. package/src/skf-export-skill/references/load-skill.md +44 -42
  139. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  140. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  141. package/src/skf-export-skill/references/package.md +27 -33
  142. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  143. package/src/skf-export-skill/references/result-envelope.md +28 -0
  144. package/src/skf-export-skill/references/summary.md +11 -5
  145. package/src/skf-export-skill/references/token-report.md +42 -24
  146. package/src/skf-export-skill/references/update-context.md +32 -33
  147. package/src/skf-forger/SKILL.md +27 -39
  148. package/src/skf-forger/references/pipeline-mode.md +34 -0
  149. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  150. package/src/skf-quick-skill/SKILL.md +10 -60
  151. package/src/skf-quick-skill/customize.toml +9 -0
  152. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  153. package/src/skf-quick-skill/references/compile.md +15 -52
  154. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  155. package/src/skf-quick-skill/references/finalize.md +12 -6
  156. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  157. package/src/skf-quick-skill/references/health-check.md +3 -4
  158. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  159. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  160. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  161. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  162. package/src/skf-refine-architecture/SKILL.md +14 -7
  163. package/src/skf-refine-architecture/customize.toml +9 -0
  164. package/src/skf-refine-architecture/references/compile.md +9 -7
  165. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  166. package/src/skf-refine-architecture/references/health-check.md +1 -1
  167. package/src/skf-refine-architecture/references/improvements.md +9 -25
  168. package/src/skf-refine-architecture/references/init.md +9 -16
  169. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  170. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  171. package/src/skf-refine-architecture/references/report.md +13 -10
  172. package/src/skf-rename-skill/SKILL.md +11 -21
  173. package/src/skf-rename-skill/customize.toml +10 -0
  174. package/src/skf-rename-skill/references/execute.md +107 -78
  175. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  176. package/src/skf-rename-skill/references/report.md +16 -8
  177. package/src/skf-rename-skill/references/select.md +69 -42
  178. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  179. package/src/skf-setup/SKILL.md +20 -12
  180. package/src/skf-setup/customize.toml +10 -0
  181. package/src/skf-setup/references/auto-index.md +9 -10
  182. package/src/skf-setup/references/ccc-index.md +14 -15
  183. package/src/skf-setup/references/detect-and-tier.md +6 -8
  184. package/src/skf-setup/references/report.md +22 -31
  185. package/src/skf-setup/references/tier-rules.md +2 -32
  186. package/src/skf-setup/references/write-config.md +9 -10
  187. package/src/skf-test-skill/SKILL.md +10 -8
  188. package/src/skf-test-skill/customize.toml +2 -0
  189. package/src/skf-test-skill/references/coherence-check.md +19 -31
  190. package/src/skf-test-skill/references/coverage-check.md +109 -75
  191. package/src/skf-test-skill/references/detect-mode.md +10 -41
  192. package/src/skf-test-skill/references/external-validators.md +11 -19
  193. package/src/skf-test-skill/references/init.md +38 -20
  194. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  195. package/src/skf-test-skill/references/report.md +60 -40
  196. package/src/skf-test-skill/references/score.md +32 -64
  197. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  198. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  199. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  200. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  201. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  202. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  203. package/src/skf-test-skill/scripts/compute-score.py +131 -1
  204. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  205. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  206. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  207. package/src/skf-update-skill/SKILL.md +17 -5
  208. package/src/skf-update-skill/customize.toml +14 -0
  209. package/src/skf-update-skill/references/detect-changes.md +19 -37
  210. package/src/skf-update-skill/references/health-check.md +3 -9
  211. package/src/skf-update-skill/references/init.md +39 -32
  212. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  213. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  214. package/src/skf-update-skill/references/merge.md +13 -57
  215. package/src/skf-update-skill/references/re-extract.md +12 -24
  216. package/src/skf-update-skill/references/report.md +14 -7
  217. package/src/skf-update-skill/references/validate.md +32 -30
  218. package/src/skf-update-skill/references/write.md +86 -51
  219. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  220. package/src/skf-verify-stack/SKILL.md +11 -21
  221. package/src/skf-verify-stack/customize.toml +12 -3
  222. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  223. package/src/skf-verify-stack/references/coverage.md +21 -10
  224. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  225. package/src/skf-verify-stack/references/health-check.md +1 -1
  226. package/src/skf-verify-stack/references/init.md +20 -25
  227. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  228. package/src/skf-verify-stack/references/integrations.md +44 -49
  229. package/src/skf-verify-stack/references/report.md +28 -34
  230. package/src/skf-verify-stack/references/requirements.md +3 -3
  231. package/src/skf-verify-stack/references/synthesize.md +36 -39
  232. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  233. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  234. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  235. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """SKF Rewrite Skill Name — field-scoped, meaning-independent rename transforms.
6
+
7
+ Performs the deterministic in-file name substitutions the rename workflow needs,
8
+ so the LLM never hand-edits JSON or eyeballs "only within the frontmatter". Each
9
+ transform is scoped to a single field / region and applied by exact-key or
10
+ anchored-pattern match, then written atomically (stage to <target>.skf-tmp,
11
+ fsync, rename) in the same call — a process kill mid-rewrite leaves the original
12
+ file intact.
13
+
14
+ Four file kinds (one per --kind), each doing exactly one field-scoped edit:
15
+
16
+ skill-frontmatter Replace the top-level `name:` value inside the YAML
17
+ frontmatter block ONLY (between the leading `---` and the
18
+ first `---` on its own line). Body text is never touched,
19
+ so a legitimate mention of the old name below the closing
20
+ `---` survives. Anchored on `^name:` so a longer key like
21
+ `renamed:` or a nested ` name:` is not matched.
22
+
23
+ metadata-json JSON round-trip: parse, set `name` = <new-name>, re-emit
24
+ with indent=2. Key order is preserved (dict insertion
25
+ order); no manual string surgery, so no risk of reordering
26
+ or dropping keys.
27
+
28
+ provenance-json Same JSON round-trip, but the field is `skill_name`.
29
+
30
+ context-snippet Rewrite the display header `[<old> v...]` -> `[<new> v...]`
31
+ (version suffix preserved) and every `root:` path. Root
32
+ rewrite parses `root: {prefix}{old}/`, preserves the prefix
33
+ verbatim, and swaps the trailing `{old}/` segment for
34
+ `{new}/` — so any IDE prefix (.claude/skills/,
35
+ .windsurf/skills/, draft skills/, ...) is handled without
36
+ enumeration and an old name that is a substring of the
37
+ prefix is left alone. The legacy nested draft form
38
+ `root: skills/{old}/active/{old}/` is flattened to
39
+ `root: skills/{new}/`.
40
+
41
+ Exit codes:
42
+ 0 success (file processed; written iff content changed)
43
+ 1 user error (bad args, invalid new name, target not found)
44
+ 2 operation failure (unparseable structure, atomic write failed)
45
+
46
+ CLI examples:
47
+ python3 skf-rewrite-skill-name.py SKILL.md \
48
+ --kind skill-frontmatter --old-name rename --new-name rename-skill
49
+ python3 skf-rewrite-skill-name.py context-snippet.md \
50
+ --kind context-snippet --old-name rename --new-name rename-skill --dry-run
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import argparse
56
+ import json
57
+ import os
58
+ import re
59
+ import sys
60
+ from pathlib import Path
61
+
62
+ # The module's canonical skill-name rule (same regex as skf-validate-output.py /
63
+ # skf-validate-brief-inputs.py / skf-rename-skill select.md §5).
64
+ NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
65
+
66
+ KINDS = ("skill-frontmatter", "metadata-json", "context-snippet", "provenance-json")
67
+
68
+
69
+ def _die(code: int, message: str) -> None:
70
+ print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
71
+ sys.exit(code)
72
+
73
+
74
+ # --- Atomic write (self-contained; mirrors skf-atomic-write.py cmd_write) -----
75
+
76
+
77
+ def atomic_write_text(target: Path, text: str) -> int:
78
+ """Write `text` to `target` atomically via temp + fsync + rename.
79
+
80
+ Returns the number of bytes written. Raises OSError on failure.
81
+ """
82
+ data = text.encode("utf-8")
83
+ target.parent.mkdir(parents=True, exist_ok=True)
84
+ tmp = target.with_name(target.name + ".skf-tmp")
85
+ # O_BINARY (Windows only; 0 elsewhere) suppresses the text-mode \n -> \r\n
86
+ # translation that would otherwise corrupt verbatim writes on Windows.
87
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_BINARY", 0)
88
+ try:
89
+ fd = os.open(tmp, flags, 0o644)
90
+ try:
91
+ os.write(fd, data)
92
+ os.fsync(fd)
93
+ finally:
94
+ os.close(fd)
95
+ os.replace(tmp, target)
96
+ except OSError:
97
+ if tmp.exists():
98
+ try:
99
+ tmp.unlink()
100
+ except OSError:
101
+ pass
102
+ raise
103
+ return len(data)
104
+
105
+
106
+ # --- Pure transforms (import-friendly for unit tests) -------------------------
107
+
108
+
109
+ def rewrite_frontmatter_name(content: str, new_name: str):
110
+ """Replace the top-level `name:` value inside the frontmatter block only.
111
+
112
+ Returns (new_content, old_value, matched). Raises ValueError if the content
113
+ has no valid `--- ... ---` frontmatter block.
114
+ """
115
+ if not content.startswith("---\n"):
116
+ raise ValueError("SKILL.md has no opening '---' frontmatter delimiter")
117
+
118
+ lines = content.split("\n")
119
+ # Locate the closing '---' on its own line (not a substring inside a value).
120
+ close_start = None # char offset of the start of the closing '---' line
121
+ offset = len(lines[0]) + 1 # start of lines[1]
122
+ for i in range(1, len(lines)):
123
+ if lines[i].rstrip() == "---":
124
+ close_start = offset
125
+ break
126
+ offset += len(lines[i]) + 1
127
+ if close_start is None:
128
+ raise ValueError("SKILL.md has no closing '---' frontmatter delimiter")
129
+
130
+ head = content[:4] # "---\n"
131
+ fm_block = content[4:close_start]
132
+ tail = content[close_start:] # closing '---' onward (body preserved verbatim)
133
+
134
+ out_lines = []
135
+ old_value = None
136
+ matched = False
137
+ for line in fm_block.split("\n"):
138
+ # Anchor on an exact top-level `name` key (no leading indent) so
139
+ # `renamed:` or a nested ` name:` under `metadata:` is never touched.
140
+ m = re.match(r"^(name)(\s*:\s*)(.*)$", line)
141
+ if m and not matched:
142
+ raw = m.group(3).strip()
143
+ quote = ""
144
+ if len(raw) >= 2 and raw[0] in "'\"" and raw[-1] == raw[0]:
145
+ quote = raw[0]
146
+ old_value = raw[1:-1]
147
+ else:
148
+ old_value = raw
149
+ out_lines.append(f"{m.group(1)}{m.group(2)}{quote}{new_name}{quote}")
150
+ matched = True
151
+ else:
152
+ out_lines.append(line)
153
+
154
+ return head + "\n".join(out_lines) + tail, old_value, matched
155
+
156
+
157
+ def rewrite_json_field(content: str, field: str, new_name: str):
158
+ """JSON round-trip: set `field` = new_name, re-emit with indent=2.
159
+
160
+ Returns (new_content, old_value, matched). Raises ValueError on invalid JSON.
161
+ """
162
+ try:
163
+ data = json.loads(content)
164
+ except json.JSONDecodeError as e:
165
+ raise ValueError(f"invalid JSON: {e}") from e
166
+ if not isinstance(data, dict):
167
+ raise ValueError("JSON root is not an object")
168
+ matched = field in data
169
+ old_value = data.get(field)
170
+ data[field] = new_name
171
+ return json.dumps(data, indent=2) + "\n", old_value, matched
172
+
173
+
174
+ def _rewrite_root_path(path: str, old_name: str, new_name: str) -> str:
175
+ """Swap the trailing `{old}/` segment of a skill root for `{new}/`.
176
+
177
+ Legacy nested draft form `{prefix}{old}/active/{old}/` flattens to
178
+ `{prefix}{new}/`. A prefix that merely contains old_name is preserved.
179
+ Returns the path unchanged when it does not end in the old-name segment.
180
+ """
181
+ legacy = f"{old_name}/active/{old_name}/"
182
+ normal = f"{old_name}/"
183
+ if path.endswith(legacy):
184
+ return path[: -len(legacy)] + f"{new_name}/"
185
+ if path.endswith(normal):
186
+ return path[: -len(normal)] + f"{new_name}/"
187
+ return path
188
+
189
+
190
+ def rewrite_context_snippet(content: str, old_name: str, new_name: str):
191
+ """Rewrite the display header and every `root:` path in a context snippet.
192
+
193
+ Returns (new_content, details) where details records what changed. Never
194
+ raises — a snippet missing the header or a root just yields no change there.
195
+ """
196
+ details = {"header_rewritten": False, "roots": []}
197
+
198
+ # Display header: [<old> v<version>] -> [<new> v<version>], suffix preserved.
199
+ header_re = re.compile(r"\[" + re.escape(old_name) + r"(\s+v[^\]]*)\]")
200
+
201
+ def _header_sub(m):
202
+ details["header_rewritten"] = True
203
+ return "[" + new_name + m.group(1) + "]"
204
+
205
+ content = header_re.sub(_header_sub, content)
206
+
207
+ # root: <path> — path is a non-whitespace token.
208
+ root_re = re.compile(r"(root:\s*)(\S+)")
209
+
210
+ def _root_sub(m):
211
+ prefix, path = m.group(1), m.group(2)
212
+ new_path = _rewrite_root_path(path, old_name, new_name)
213
+ if new_path != path:
214
+ details["roots"].append({"old": path, "new": new_path})
215
+ return prefix + new_path
216
+
217
+ content = root_re.sub(_root_sub, content)
218
+ return content, details
219
+
220
+
221
+ # --- CLI orchestration --------------------------------------------------------
222
+
223
+
224
+ def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool) -> dict:
225
+ original = target.read_text(encoding="utf-8")
226
+
227
+ result = {
228
+ "status": "ok",
229
+ "kind": kind,
230
+ "target": str(target),
231
+ "old_name": old_name,
232
+ "new_name": new_name,
233
+ }
234
+
235
+ if kind == "skill-frontmatter":
236
+ new_content, old_value, matched = rewrite_frontmatter_name(original, new_name)
237
+ result["field"] = "name"
238
+ result["matched"] = matched
239
+ result["old_value"] = old_value
240
+ elif kind == "metadata-json":
241
+ new_content, old_value, matched = rewrite_json_field(original, "name", new_name)
242
+ result["field"] = "name"
243
+ result["matched"] = matched
244
+ result["old_value"] = old_value
245
+ elif kind == "provenance-json":
246
+ new_content, old_value, matched = rewrite_json_field(original, "skill_name", new_name)
247
+ result["field"] = "skill_name"
248
+ result["matched"] = matched
249
+ result["old_value"] = old_value
250
+ elif kind == "context-snippet":
251
+ new_content, details = rewrite_context_snippet(original, old_name, new_name)
252
+ result["header_rewritten"] = details["header_rewritten"]
253
+ result["roots_rewritten"] = details["roots"]
254
+ result["matched"] = details["header_rewritten"] or bool(details["roots"])
255
+ else: # pragma: no cover - argparse choices guard this
256
+ raise ValueError(f"unknown kind: {kind}")
257
+
258
+ changed = new_content != original
259
+ result["changed"] = changed
260
+ if dry_run:
261
+ result["dry_run"] = True
262
+ result["new_content"] = new_content
263
+ return result
264
+
265
+ if changed:
266
+ result["bytes"] = atomic_write_text(target, new_content)
267
+ result["wrote"] = str(target)
268
+ else:
269
+ result["wrote"] = None
270
+ return result
271
+
272
+
273
+ def main() -> None:
274
+ parser = argparse.ArgumentParser(
275
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
276
+ )
277
+ parser.add_argument("target", type=Path, help="File to rewrite (SKILL.md, metadata.json, ...)")
278
+ parser.add_argument("--kind", required=True, choices=KINDS, help="File kind / transform to apply")
279
+ parser.add_argument("--old-name", required=True, help="Current skill name")
280
+ parser.add_argument("--new-name", required=True, help="New skill name (kebab-case)")
281
+ parser.add_argument("--dry-run", action="store_true", help="Compute without writing; emit new_content")
282
+ parser.add_argument("--verbose", action="store_true", help="Diagnostics to stderr")
283
+ args = parser.parse_args()
284
+
285
+ if not args.old_name:
286
+ _die(1, "old-name must be non-empty")
287
+ if not NAME_RE.match(args.new_name) or len(args.new_name) > 64:
288
+ _die(1, f"new-name must be kebab-case, 1-64 chars, got: {args.new_name!r}")
289
+
290
+ if not args.target.exists():
291
+ _die(1, f"target not found: {args.target}")
292
+
293
+ try:
294
+ result = process(args.target, args.kind, args.old_name, args.new_name, args.dry_run)
295
+ except ValueError as e:
296
+ _die(2, f"{args.kind} transform failed for {args.target}: {e}")
297
+ except OSError as e:
298
+ _die(2, f"atomic write failed for {args.target}: {e}")
299
+
300
+ if args.verbose:
301
+ print(f"[skf-rewrite-skill-name] {args.kind} changed={result['changed']}", file=sys.stderr)
302
+
303
+ print(json.dumps(result, indent=2))
304
+ sys.exit(0)
305
+
306
+
307
+ if __name__ == "__main__":
308
+ main()