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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.nvmrc +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +34 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +59 -28
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +51 -31
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +42 -10
- package/src/skf-drop-skill/references/select.md +88 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +27 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +13 -10
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +107 -78
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +109 -75
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +38 -20
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +131 -1
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +12 -24
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- 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()
|