bmad-module-skill-forge 2.2.0 → 3.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +49 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +12 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +99 -11
- package/src/shared/scripts/skf-detect-docs.py +694 -2
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +209 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +28 -19
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -90
- package/src/skf-setup/references/detect-and-tier.md +58 -67
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -679
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# /// script
|
|
3
|
-
# requires-python = ">=3.
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Rewrite Skill Name — field-scoped, meaning-independent rename transforms.
|
|
6
6
|
|
|
7
7
|
Performs the deterministic in-file name substitutions the rename workflow needs,
|
|
8
8
|
so the LLM never hand-edits JSON or eyeballs "only within the frontmatter". Each
|
|
9
|
-
transform is scoped to a single field / region
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
file
|
|
9
|
+
transform is scoped to a single field / region (for the context snippet, the
|
|
10
|
+
places its template writes the name) and applied by exact-key, anchored-pattern
|
|
11
|
+
or name-token match, then written atomically (stage to <target>.skf-tmp, fsync,
|
|
12
|
+
rename) in the same call — a process kill mid-rewrite leaves the original file
|
|
13
|
+
intact.
|
|
13
14
|
|
|
14
|
-
Four file kinds (one per --kind), each
|
|
15
|
+
Four file kinds (one per --kind), each editing only the skill's name:
|
|
15
16
|
|
|
16
17
|
skill-frontmatter Replace the top-level `name:` value inside the YAML
|
|
17
18
|
frontmatter block ONLY (between the leading `---` and the
|
|
@@ -23,31 +24,99 @@ Four file kinds (one per --kind), each doing exactly one field-scoped edit:
|
|
|
23
24
|
metadata-json JSON round-trip: parse, set `name` = <new-name>, re-emit
|
|
24
25
|
with indent=2. Key order is preserved (dict insertion
|
|
25
26
|
order); no manual string surgery, so no risk of reordering
|
|
26
|
-
or dropping keys.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
or dropping keys. With --moved-folder FOLDER (repeatable),
|
|
28
|
+
every string value that is a path into FOLDER/<old>/ is
|
|
29
|
+
pointed at FOLDER/<new>/, and the package folder
|
|
30
|
+
FOLDER/<old>/<version>/<old>/ at FOLDER/<new>/<version>/<new>/:
|
|
31
|
+
the rename moves those folders, and skf-verify-no-trace.py
|
|
32
|
+
counts the old name in such a path as a leftover. A path
|
|
33
|
+
counts only where it begins, at the start of the value or
|
|
34
|
+
of a word in it, spelled as FOLDER itself or as its last
|
|
35
|
+
components (a path from the project root, such as
|
|
36
|
+
forge-data/<old>/...); the old name must be a whole path
|
|
37
|
+
segment there, by the verifier's token rule. So a URL, a
|
|
38
|
+
path into another folder and a path that only holds
|
|
39
|
+
FOLDER's name further in stay. The values of the keys that
|
|
40
|
+
name the upstream source or a file in it (UPSTREAM_KEYS:
|
|
41
|
+
the verifier's SOURCE_FACT_KEYS, `source_file` and
|
|
42
|
+
`co_import_files`) never change, even when one holds such
|
|
43
|
+
a path: the rename does not move the source. Keys, every
|
|
44
|
+
other value and file names (`test-report-<old>.md`) stay.
|
|
45
|
+
|
|
46
|
+
provenance-json Same JSON round-trip, but the field is `skill_name`; takes
|
|
47
|
+
--moved-folder the same way.
|
|
48
|
+
|
|
49
|
+
context-snippet Rewrite the name where every SKF snippet template writes
|
|
50
|
+
it, so a snippet that follows a template leaves nothing
|
|
51
|
+
for skf-verify-no-trace.py, which counts any mention there
|
|
52
|
+
as a leftover: the display header `[<old> v...]`, the first
|
|
53
|
+
word of the `|IMPORTANT:` line and its `writing <old> code`
|
|
54
|
+
phrase. In those slots a complete skill-name token of the
|
|
55
|
+
old name becomes the new name, matched by the verifier's
|
|
56
|
+
own rule (_name_token_re, a pinned copy). Nothing else in
|
|
57
|
+
the snippet changes: a field label (`root:`, `|api:`), a
|
|
58
|
+
fixed word of the template ("training data", "SKILL.md")
|
|
59
|
+
or the library's own content that is the old name stays,
|
|
60
|
+
and the verifier then stops the rename rather than let it
|
|
61
|
+
commit a changed snippet. Each `root:` path parses as
|
|
62
|
+
`root: {prefix}{old}/`: the prefix is kept verbatim and
|
|
63
|
+
the trailing `{old}/` segment becomes
|
|
64
|
+
`{new}/`, so any IDE prefix (.claude/skills/,
|
|
35
65
|
.windsurf/skills/, draft skills/, ...) is handled without
|
|
36
|
-
enumeration and
|
|
37
|
-
|
|
66
|
+
enumeration and a prefix that contains the old name is
|
|
67
|
+
never changed. The legacy nested draft form
|
|
38
68
|
`root: skills/{old}/active/{old}/` is flattened to
|
|
39
69
|
`root: skills/{new}/`.
|
|
40
70
|
|
|
71
|
+
Batch mode (--skill-group, no target): the rename workflow's whole in-place
|
|
72
|
+
pass over a copied skill folder in one call, as execute.md section 2 runs it.
|
|
73
|
+
For each version in --versions, in the order given, it moves the package
|
|
74
|
+
folder <skill-group>/<version>/<old>/ to <skill-group>/<version>/<new>/; a
|
|
75
|
+
version without that folder (a manifest version with no folder, or a version
|
|
76
|
+
folder an interrupted run left without a package) is skipped and named in
|
|
77
|
+
`package_warnings`. Then it rewrites, for each version it moved, the
|
|
78
|
+
package's SKILL.md (skill-frontmatter), metadata.json (metadata-json) and
|
|
79
|
+
context-snippet.md (context-snippet) and, with --forge-group, every
|
|
80
|
+
version's <forge-group>/<version>/provenance-map.json (provenance-json),
|
|
81
|
+
package or not, each as the single-file mode does; --moved-folder applies to
|
|
82
|
+
the two JSON kinds. A file that is not there is listed in `missing_files`,
|
|
83
|
+
never a failure. The first move or rewrite that fails stops the batch:
|
|
84
|
+
`status` is "error" and `error` names its `stage` ("inner-rename" or
|
|
85
|
+
"rewrite"), `path` and `message`; the caller rolls the copied folders back.
|
|
86
|
+
|
|
87
|
+
The batch prints one JSON object on stdout, and writes it to --result-to
|
|
88
|
+
FILE too (atomically), the record the rename's report reads back:
|
|
89
|
+
|
|
90
|
+
{"status": "ok"|"error", "old_name", "new_name", "skill_group",
|
|
91
|
+
"forge_group": str|null, "renamed_versions": [...],
|
|
92
|
+
"files_rewritten": [{"kind", "path"}],
|
|
93
|
+
"counts": {"skill-frontmatter": n, "metadata-json": n,
|
|
94
|
+
"context-snippet": n, "provenance-json": n},
|
|
95
|
+
"package_warnings": [...], "missing_files": [...],
|
|
96
|
+
"error": null | {"stage", "path", "message"}}
|
|
97
|
+
|
|
98
|
+
`counts` tallies files_rewritten by kind. A --result-to file that cannot be
|
|
99
|
+
written fails a batch that had not failed yet: `error.stage` is "record",
|
|
100
|
+
with that file as its `path`, so the caller rolls back as for a rewrite.
|
|
101
|
+
|
|
41
102
|
Exit codes:
|
|
42
|
-
0 success (file processed
|
|
43
|
-
1 user error (bad args,
|
|
44
|
-
|
|
103
|
+
0 success (file processed, written iff content changed; or batch done)
|
|
104
|
+
1 user error (bad args, --moved-folder with a kind other than the two JSON
|
|
105
|
+
kinds, invalid new name, target not found)
|
|
106
|
+
2 operation failure (unparseable structure, atomic write failed, or a
|
|
107
|
+
batch move, rewrite or --result-to write that failed)
|
|
45
108
|
|
|
46
109
|
CLI examples:
|
|
47
110
|
python3 skf-rewrite-skill-name.py SKILL.md \
|
|
48
111
|
--kind skill-frontmatter --old-name rename --new-name rename-skill
|
|
49
112
|
python3 skf-rewrite-skill-name.py context-snippet.md \
|
|
50
113
|
--kind context-snippet --old-name rename --new-name rename-skill --dry-run
|
|
114
|
+
python3 skf-rewrite-skill-name.py forge-data/rename/1.0.0/provenance-map.json \
|
|
115
|
+
--kind provenance-json --old-name rename --new-name rename-skill \
|
|
116
|
+
--moved-folder /project/skills --moved-folder /project/forge-data
|
|
117
|
+
python3 skf-rewrite-skill-name.py --skill-group /project/skills/rename-skill \
|
|
118
|
+
--versions 1.0.0,0.9.0 --old-name rename --new-name rename-skill \
|
|
119
|
+
--moved-folder /project/skills --result-to run/rename-rewrite.json
|
|
51
120
|
"""
|
|
52
121
|
|
|
53
122
|
from __future__ import annotations
|
|
@@ -64,6 +133,7 @@ from pathlib import Path
|
|
|
64
133
|
NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
|
|
65
134
|
|
|
66
135
|
KINDS = ("skill-frontmatter", "metadata-json", "context-snippet", "provenance-json")
|
|
136
|
+
JSON_KINDS = ("metadata-json", "provenance-json")
|
|
67
137
|
|
|
68
138
|
|
|
69
139
|
def _die(code: int, message: str) -> None:
|
|
@@ -106,6 +176,17 @@ def atomic_write_text(target: Path, text: str) -> int:
|
|
|
106
176
|
# --- Pure transforms (import-friendly for unit tests) -------------------------
|
|
107
177
|
|
|
108
178
|
|
|
179
|
+
# Keep identical to _name_token_re in skf-verify-no-trace.py
|
|
180
|
+
# (test/test-skf-rewrite-skill-name.py pins the copies).
|
|
181
|
+
def _name_token_re(old_name: str) -> "re.Pattern[str]":
|
|
182
|
+
"""Match old_name only where it is a complete skill-name token.
|
|
183
|
+
|
|
184
|
+
Negative lookbehind/lookahead on `[a-z0-9-]` so `rename` matches inside
|
|
185
|
+
`/rename/` or `"rename"` but not inside `rename-skill` or `renamed`.
|
|
186
|
+
"""
|
|
187
|
+
return re.compile(r"(?<![a-z0-9-])" + re.escape(old_name) + r"(?![a-z0-9-])")
|
|
188
|
+
|
|
189
|
+
|
|
109
190
|
def rewrite_frontmatter_name(content: str, new_name: str):
|
|
110
191
|
"""Replace the top-level `name:` value inside the frontmatter block only.
|
|
111
192
|
|
|
@@ -187,41 +268,285 @@ def _rewrite_root_path(path: str, old_name: str, new_name: str) -> str:
|
|
|
187
268
|
return path
|
|
188
269
|
|
|
189
270
|
|
|
190
|
-
|
|
191
|
-
|
|
271
|
+
# The places every SKF snippet template writes the skill's name, besides the
|
|
272
|
+
# root path: the display header `[<name> v...]`, and on the `|IMPORTANT:` line
|
|
273
|
+
# its first word and the `writing <name> code` phrase. The rest of a snippet is
|
|
274
|
+
# the template's fixed words and field labels (`root:`, `|api:`, "training
|
|
275
|
+
# data", "SKILL.md") or the library's own content, which a rename must not touch.
|
|
276
|
+
_HEADER_SLOT_RE = re.compile(r"\[(?P<slot>[^\]\s]+)(?=\s+v[^\]]*\])")
|
|
277
|
+
_IMPORTANT_LINE_RE = re.compile(r"^\|IMPORTANT:[^\n]*", re.M)
|
|
278
|
+
_IMPORTANT_SLOT_RES = (
|
|
279
|
+
re.compile(r"^\|IMPORTANT:[ \t]*(?P<slot>\S+)"),
|
|
280
|
+
re.compile(r"\bwriting[ \t]+(?P<slot>\S+?)[ \t]+code\b"),
|
|
281
|
+
)
|
|
282
|
+
_ROOT_PATH_RE = re.compile(r"root:\s*(?P<path>\S+)")
|
|
283
|
+
|
|
192
284
|
|
|
193
|
-
|
|
194
|
-
|
|
285
|
+
def rewrite_context_snippet(content: str, old_name: str, new_name: str):
|
|
286
|
+
"""Rewrite the name where SKF's snippet templates write it.
|
|
287
|
+
|
|
288
|
+
skf-verify-no-trace.py counts any complete-token mention of the old name in
|
|
289
|
+
context-snippet.md as a leftover, and every SKF snippet template writes the
|
|
290
|
+
name in its display header, on its `|IMPORTANT:` line (first word, and
|
|
291
|
+
`writing <name> code`) and in its `root:` path. In the header and the
|
|
292
|
+
IMPORTANT-line slots every token _name_token_re matches (the verifier's
|
|
293
|
+
rule) becomes new_name. Each `root:` path goes through _rewrite_root_path,
|
|
294
|
+
which keeps its prefix (the IDE's skill folder) and swaps only the trailing
|
|
295
|
+
`{old}/` segment.
|
|
296
|
+
|
|
297
|
+
Nothing else changes: a field label, a fixed word of the template or the
|
|
298
|
+
library's content that happens to be the old name stays, and the verifier
|
|
299
|
+
then stops the rename (it rolls back with the old skill intact) instead of
|
|
300
|
+
committing a changed snippet.
|
|
301
|
+
|
|
302
|
+
Returns (new_content, details): `header_rewritten` (a `[<old> v...]` header
|
|
303
|
+
was present), `roots` (each root path changed) and `mentions` (how many
|
|
304
|
+
tokens were rewritten in the header and IMPORTANT-line slots). Never
|
|
305
|
+
raises: a snippet with no mention just yields no change.
|
|
306
|
+
"""
|
|
307
|
+
details = {"header_rewritten": False, "roots": [], "mentions": 0}
|
|
308
|
+
token_re = _name_token_re(old_name)
|
|
309
|
+
|
|
310
|
+
# (start, end, kind) of each slot. Root paths are rewritten as paths.
|
|
311
|
+
spans = [(m.start("slot"), m.end("slot"), "header") for m in _HEADER_SLOT_RE.finditer(content)]
|
|
312
|
+
for line in _IMPORTANT_LINE_RE.finditer(content):
|
|
313
|
+
for slot_re in _IMPORTANT_SLOT_RES:
|
|
314
|
+
for m in slot_re.finditer(line.group(0)):
|
|
315
|
+
spans.append((line.start() + m.start("slot"), line.start() + m.end("slot"), "important"))
|
|
316
|
+
spans += [(m.start("path"), m.end("path"), "root") for m in _ROOT_PATH_RE.finditer(content)]
|
|
317
|
+
|
|
318
|
+
out = []
|
|
319
|
+
pos = 0
|
|
320
|
+
for start, end, kind in sorted(set(spans)):
|
|
321
|
+
if start < pos: # inside a slot already rewritten
|
|
322
|
+
continue
|
|
323
|
+
out.append(content[pos:start])
|
|
324
|
+
text = content[start:end]
|
|
325
|
+
if kind == "root":
|
|
326
|
+
new_text = _rewrite_root_path(text, old_name, new_name)
|
|
327
|
+
if new_text != text:
|
|
328
|
+
details["roots"].append({"old": text, "new": new_text})
|
|
329
|
+
else:
|
|
330
|
+
new_text, count = token_re.subn(new_name, text)
|
|
331
|
+
details["mentions"] += count
|
|
332
|
+
if count and kind == "header":
|
|
333
|
+
details["header_rewritten"] = True
|
|
334
|
+
out.append(new_text)
|
|
335
|
+
pos = end
|
|
336
|
+
out.append(content[pos:])
|
|
337
|
+
return "".join(out), details
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# Keep identical to SOURCE_FACT_KEYS in skf-verify-no-trace.py
|
|
341
|
+
# (test/test-skf-rewrite-skill-name.py pins the copies). The keys whose value
|
|
342
|
+
# names the upstream source the skill was made from, not the skill.
|
|
343
|
+
SOURCE_FACT_KEYS = frozenset({
|
|
344
|
+
"source_repo",
|
|
345
|
+
"source_root",
|
|
346
|
+
"source_commit",
|
|
347
|
+
"source_ref",
|
|
348
|
+
"source_package",
|
|
349
|
+
"source_library",
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
# The values rewrite_moved_paths never changes: the upstream source, and the
|
|
353
|
+
# files in it (`source_file` of an export, script, asset or promoted doc, and
|
|
354
|
+
# the `co_import_files` of a stack integration). The rename moves the skill's
|
|
355
|
+
# folders, not the source, so a path such as `skills/<old>/x.py` there is the
|
|
356
|
+
# source's own layout; skf-verify-no-trace.py then reports a `source_file`
|
|
357
|
+
# that names the old name instead of the rename rewriting it.
|
|
358
|
+
UPSTREAM_KEYS = SOURCE_FACT_KEYS | {"source_file", "co_import_files"}
|
|
359
|
+
|
|
360
|
+
# A path begins at the start of a value or of a word in it: never inside a URL
|
|
361
|
+
# or after another folder of a longer path.
|
|
362
|
+
_PATH_START = r"(?<![^\s\"'`(\[<=,;])"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _moved_path_re(folder: str, old_name: str) -> "re.Pattern[str]":
|
|
366
|
+
"""`<folder>/<old>` where a path begins, and the package folder in it.
|
|
367
|
+
|
|
368
|
+
`folder` is a folder the rename moves `<old>` out of
|
|
369
|
+
(`{skills_output_folder}` or `{forge_data_folder}`), as given. The path
|
|
370
|
+
must begin with it, spelled in full or as its last components (so a value
|
|
371
|
+
relative to the project root, `forge-data/<old>/...` or
|
|
372
|
+
`_bmad-output/skills/<old>/...`, matches as well as an absolute one), at the
|
|
373
|
+
start of the value or of a word in it, optionally after `./`. Either
|
|
374
|
+
separator matches. The old name is matched by the verifier's token rule and
|
|
375
|
+
must also end its path segment, so `forge-data/<old>/<v>/test-report-<old>.md`
|
|
376
|
+
matches only at the folder. A following `/<version>/<old>` is the package
|
|
377
|
+
folder the rename also moves (`{skill_group}/{version}/{skill-name}/`).
|
|
195
378
|
"""
|
|
196
|
-
|
|
379
|
+
parts = re.split(r"[/\\]", folder.rstrip("/\\"))
|
|
380
|
+
sep = r"[/\\]"
|
|
381
|
+
spellings = dict.fromkeys(sep.join(re.escape(p) for p in parts[k:]) for k in range(len(parts)))
|
|
382
|
+
heads = "|".join(sorted((s for s in spellings if s), key=len, reverse=True))
|
|
383
|
+
name = _name_token_re(old_name).pattern
|
|
384
|
+
seg_end = r"(?![A-Za-z0-9._-])"
|
|
385
|
+
return re.compile(
|
|
386
|
+
_PATH_START + r"(?P<head>(?:\.[/\\])?(?:" + heads + r")[/\\])" + name + seg_end
|
|
387
|
+
+ r"(?:(?P<mid>[/\\][^/\\\s]+[/\\])" + name + seg_end + r")?"
|
|
388
|
+
)
|
|
197
389
|
|
|
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
390
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return "[" + new_name + m.group(1) + "]"
|
|
391
|
+
def rewrite_moved_paths(content: str, old_name: str, new_name: str, folders):
|
|
392
|
+
"""Point every JSON string value that is a path into a moved folder at the new name.
|
|
204
393
|
|
|
205
|
-
|
|
394
|
+
The rename moves `<folder>/<old>/` to `<folder>/<new>/` for each of
|
|
395
|
+
`folders` (and the package folder `<old>/<version>/<old>/` inside the skill
|
|
396
|
+
folder), so a value such as a test report's
|
|
397
|
+
`forge-data/<old>/<v>/test-report-<old>.md` would name a folder that no
|
|
398
|
+
longer exists, and the verifier would count it as a leftover. A path counts
|
|
399
|
+
where it begins (see _moved_path_re). The values of UPSTREAM_KEYS, at any
|
|
400
|
+
depth, keys and every other value stay; a file named after the skill keeps
|
|
401
|
+
its name, as the rename keeps the file's name. Returns (new_content, paths)
|
|
402
|
+
where paths lists each changed value as {"old", "new"}. Raises ValueError on
|
|
403
|
+
invalid JSON.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
data = json.loads(content)
|
|
407
|
+
except json.JSONDecodeError as e:
|
|
408
|
+
raise ValueError(f"invalid JSON: {e}") from e
|
|
409
|
+
patterns = [_moved_path_re(f, old_name) for f in dict.fromkeys(folders) if _folder_name(f)]
|
|
410
|
+
paths = []
|
|
411
|
+
|
|
412
|
+
def _sub(m):
|
|
413
|
+
return m.group("head") + new_name + ((m.group("mid") + new_name) if m.group("mid") else "")
|
|
414
|
+
|
|
415
|
+
def _walk(node):
|
|
416
|
+
if isinstance(node, dict):
|
|
417
|
+
for key, value in node.items():
|
|
418
|
+
if key not in UPSTREAM_KEYS:
|
|
419
|
+
node[key] = _walk(value)
|
|
420
|
+
elif isinstance(node, list):
|
|
421
|
+
node[:] = [_walk(value) for value in node]
|
|
422
|
+
elif isinstance(node, str):
|
|
423
|
+
new_value = node
|
|
424
|
+
for pattern in patterns:
|
|
425
|
+
new_value = pattern.sub(_sub, new_value)
|
|
426
|
+
if new_value != node:
|
|
427
|
+
paths.append({"old": node, "new": new_value})
|
|
428
|
+
return new_value
|
|
429
|
+
return node
|
|
430
|
+
|
|
431
|
+
data = _walk(data)
|
|
432
|
+
if not paths:
|
|
433
|
+
return content, paths
|
|
434
|
+
return json.dumps(data, indent=2) + "\n", paths
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# --- Batch: every version of a copied skill folder in one call ----------------
|
|
438
|
+
|
|
439
|
+
# The package files each moved version rewrites, in order, then the forge file
|
|
440
|
+
# every version rewrites when the forge folder moves.
|
|
441
|
+
PACKAGE_FILES = (
|
|
442
|
+
("skill-frontmatter", "SKILL.md"),
|
|
443
|
+
("metadata-json", "metadata.json"),
|
|
444
|
+
("context-snippet", "context-snippet.md"),
|
|
445
|
+
)
|
|
446
|
+
FORGE_FILE = ("provenance-json", "provenance-map.json")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _plain_segment(value: str) -> bool:
|
|
450
|
+
"""True for a version or a name that is one folder name and nothing else."""
|
|
451
|
+
return value not in ("", ".", "..") and not any(c in value for c in "/\\:\0")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def run_batch(skill_group: Path, versions, old_name: str, new_name: str,
|
|
455
|
+
forge_group: Path | None = None, moved_folders=()) -> dict:
|
|
456
|
+
"""Move each version's package to the new name, then rewrite its files.
|
|
457
|
+
|
|
458
|
+
See "Batch mode" in the module docstring. Never raises on a file it
|
|
459
|
+
processes: a failure ends the batch with `status` "error".
|
|
460
|
+
"""
|
|
461
|
+
result = {
|
|
462
|
+
"status": "ok",
|
|
463
|
+
"old_name": old_name,
|
|
464
|
+
"new_name": new_name,
|
|
465
|
+
"skill_group": str(skill_group),
|
|
466
|
+
"forge_group": str(forge_group) if forge_group is not None else None,
|
|
467
|
+
"renamed_versions": [],
|
|
468
|
+
"files_rewritten": [],
|
|
469
|
+
"counts": {kind: 0 for kind in KINDS},
|
|
470
|
+
"package_warnings": [],
|
|
471
|
+
"missing_files": [],
|
|
472
|
+
"error": None,
|
|
473
|
+
}
|
|
206
474
|
|
|
207
|
-
|
|
208
|
-
|
|
475
|
+
def fail(stage: str, path: Path, message: str) -> dict:
|
|
476
|
+
result["status"] = "error"
|
|
477
|
+
result["error"] = {"stage": stage, "path": str(path), "message": message}
|
|
478
|
+
return result
|
|
209
479
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if
|
|
214
|
-
|
|
215
|
-
|
|
480
|
+
for version in versions:
|
|
481
|
+
source = skill_group / version / old_name
|
|
482
|
+
target = skill_group / version / new_name
|
|
483
|
+
if not source.is_dir():
|
|
484
|
+
result["package_warnings"].append(
|
|
485
|
+
f"{version}: no {old_name}/ package in {skill_group / version}; skipped")
|
|
486
|
+
continue
|
|
487
|
+
if os.path.lexists(target):
|
|
488
|
+
return fail("inner-rename", target, "the new package folder already exists")
|
|
489
|
+
try:
|
|
490
|
+
os.rename(source, target)
|
|
491
|
+
except OSError as e:
|
|
492
|
+
return fail("inner-rename", source, str(e))
|
|
493
|
+
result["renamed_versions"].append(version)
|
|
494
|
+
|
|
495
|
+
for version in versions:
|
|
496
|
+
files = []
|
|
497
|
+
if version in result["renamed_versions"]:
|
|
498
|
+
files += [(kind, skill_group / version / new_name / name) for kind, name in PACKAGE_FILES]
|
|
499
|
+
if forge_group is not None:
|
|
500
|
+
files.append((FORGE_FILE[0], forge_group / version / FORGE_FILE[1]))
|
|
501
|
+
for kind, path in files:
|
|
502
|
+
if not path.is_file():
|
|
503
|
+
result["missing_files"].append(str(path))
|
|
504
|
+
continue
|
|
505
|
+
moved = moved_folders if kind in JSON_KINDS else ()
|
|
506
|
+
try:
|
|
507
|
+
done = process(path, kind, old_name, new_name, False, moved)
|
|
508
|
+
except ValueError as e:
|
|
509
|
+
return fail("rewrite", path, f"{kind} transform failed: {e}")
|
|
510
|
+
except OSError as e:
|
|
511
|
+
return fail("rewrite", path, f"atomic write failed: {e}")
|
|
512
|
+
if done["wrote"] is not None:
|
|
513
|
+
result["files_rewritten"].append({"kind": kind, "path": done["wrote"]})
|
|
514
|
+
result["counts"][kind] += 1
|
|
515
|
+
return result
|
|
216
516
|
|
|
217
|
-
|
|
218
|
-
|
|
517
|
+
|
|
518
|
+
def _main_batch(args) -> None:
|
|
519
|
+
if not _plain_segment(args.old_name):
|
|
520
|
+
_die(1, f"--old-name must be one folder name, got: {args.old_name!r}")
|
|
521
|
+
versions = [v.strip() for v in args.versions.split(",") if v.strip()]
|
|
522
|
+
bad = [v for v in versions if not _plain_segment(v)]
|
|
523
|
+
if bad:
|
|
524
|
+
_die(1, f"--versions must name version folders, got: {bad!r}")
|
|
525
|
+
if not args.skill_group.is_dir():
|
|
526
|
+
_die(1, f"skill group not found: {args.skill_group}")
|
|
527
|
+
result = run_batch(args.skill_group, versions, args.old_name, args.new_name,
|
|
528
|
+
args.forge_group, args.moved_folder)
|
|
529
|
+
if args.result_to is not None:
|
|
530
|
+
try:
|
|
531
|
+
atomic_write_text(args.result_to, json.dumps(result, indent=2) + "\n")
|
|
532
|
+
except OSError as e:
|
|
533
|
+
if result["error"] is None:
|
|
534
|
+
result["status"] = "error"
|
|
535
|
+
result["error"] = {"stage": "record", "path": str(args.result_to), "message": f"cannot write it: {e}"}
|
|
536
|
+
print(json.dumps(result, indent=2))
|
|
537
|
+
sys.exit(0 if result["status"] == "ok" else 2)
|
|
219
538
|
|
|
220
539
|
|
|
221
540
|
# --- CLI orchestration --------------------------------------------------------
|
|
222
541
|
|
|
223
542
|
|
|
224
|
-
def
|
|
543
|
+
def _folder_name(folder: str) -> str:
|
|
544
|
+
"""The last component of a folder path, whatever its separators."""
|
|
545
|
+
return re.split(r"[/\\]", folder.rstrip("/\\"))[-1]
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool,
|
|
549
|
+
moved_folders=()) -> dict:
|
|
225
550
|
original = target.read_text(encoding="utf-8")
|
|
226
551
|
|
|
227
552
|
result = {
|
|
@@ -251,10 +576,16 @@ def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool
|
|
|
251
576
|
new_content, details = rewrite_context_snippet(original, old_name, new_name)
|
|
252
577
|
result["header_rewritten"] = details["header_rewritten"]
|
|
253
578
|
result["roots_rewritten"] = details["roots"]
|
|
254
|
-
result["
|
|
579
|
+
result["mentions_rewritten"] = details["mentions"]
|
|
580
|
+
result["matched"] = details["mentions"] > 0 or bool(details["roots"])
|
|
255
581
|
else: # pragma: no cover - argparse choices guard this
|
|
256
582
|
raise ValueError(f"unknown kind: {kind}")
|
|
257
583
|
|
|
584
|
+
if kind in JSON_KINDS:
|
|
585
|
+
new_content, paths = rewrite_moved_paths(
|
|
586
|
+
new_content, old_name, new_name, list(moved_folders))
|
|
587
|
+
result["paths_rewritten"] = paths
|
|
588
|
+
|
|
258
589
|
changed = new_content != original
|
|
259
590
|
result["changed"] = changed
|
|
260
591
|
if dry_run:
|
|
@@ -270,28 +601,67 @@ def process(target: Path, kind: str, old_name: str, new_name: str, dry_run: bool
|
|
|
270
601
|
return result
|
|
271
602
|
|
|
272
603
|
|
|
273
|
-
def
|
|
604
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
274
605
|
parser = argparse.ArgumentParser(
|
|
275
606
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
276
607
|
)
|
|
277
|
-
parser.add_argument("target", type=Path,
|
|
278
|
-
|
|
608
|
+
parser.add_argument("target", type=Path, nargs="?",
|
|
609
|
+
help="File to rewrite (SKILL.md, metadata.json, ...); leave out with --skill-group")
|
|
610
|
+
parser.add_argument("--kind", choices=KINDS, help="File kind / transform to apply (single-file mode)")
|
|
279
611
|
parser.add_argument("--old-name", required=True, help="Current skill name")
|
|
280
612
|
parser.add_argument("--new-name", required=True, help="New skill name (kebab-case)")
|
|
613
|
+
parser.add_argument(
|
|
614
|
+
"--moved-folder", action="append", default=[], metavar="FOLDER",
|
|
615
|
+
help="metadata-json / provenance-json only, repeatable: a folder the rename moves <old>/ "
|
|
616
|
+
"to <new>/ in ({skills_output_folder}, and {forge_data_folder} when the forge folder "
|
|
617
|
+
"moves); path values into FOLDER/<old>/ are pointed at FOLDER/<new>/")
|
|
281
618
|
parser.add_argument("--dry-run", action="store_true", help="Compute without writing; emit new_content")
|
|
282
619
|
parser.add_argument("--verbose", action="store_true", help="Diagnostics to stderr")
|
|
620
|
+
batch = parser.add_argument_group("batch mode (see the module docstring)")
|
|
621
|
+
batch.add_argument("--skill-group", type=Path, metavar="FOLDER",
|
|
622
|
+
help="the copied skill folder whose versions to rename, in place of a target")
|
|
623
|
+
batch.add_argument("--versions", metavar="V1,V2",
|
|
624
|
+
help="batch: the comma-separated version folders to rename (required with --skill-group)")
|
|
625
|
+
batch.add_argument("--forge-group", type=Path, metavar="FOLDER",
|
|
626
|
+
help="batch: the copied forge folder whose provenance maps to rewrite")
|
|
627
|
+
batch.add_argument("--result-to", type=Path, metavar="FILE",
|
|
628
|
+
help="batch: also write the result JSON to FILE")
|
|
629
|
+
return parser
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def main() -> None:
|
|
633
|
+
parser = _build_parser()
|
|
283
634
|
args = parser.parse_args()
|
|
284
635
|
|
|
285
636
|
if not args.old_name:
|
|
286
637
|
_die(1, "old-name must be non-empty")
|
|
287
638
|
if not NAME_RE.match(args.new_name) or len(args.new_name) > 64:
|
|
288
639
|
_die(1, f"new-name must be kebab-case, 1-64 chars, got: {args.new_name!r}")
|
|
640
|
+
if any(not _folder_name(f) for f in args.moved_folder):
|
|
641
|
+
_die(1, f"--moved-folder must name a folder, got: {args.moved_folder!r}")
|
|
642
|
+
|
|
643
|
+
batch_only = {"--versions": args.versions, "--forge-group": args.forge_group, "--result-to": args.result_to}
|
|
644
|
+
if args.skill_group is not None:
|
|
645
|
+
if args.target is not None or args.kind is not None or args.dry_run:
|
|
646
|
+
_die(1, "--skill-group takes no target, --kind or --dry-run")
|
|
647
|
+
if args.versions is None:
|
|
648
|
+
_die(1, "--skill-group needs --versions")
|
|
649
|
+
_main_batch(args)
|
|
650
|
+
given = [flag for flag, value in batch_only.items() if value is not None]
|
|
651
|
+
if given:
|
|
652
|
+
_die(1, f"{', '.join(given)} {'needs' if len(given) == 1 else 'need'} --skill-group")
|
|
653
|
+
if args.target is None or args.kind is None:
|
|
654
|
+
_die(1, "pass a target file and --kind, or --skill-group for the batch")
|
|
655
|
+
|
|
656
|
+
if args.moved_folder and args.kind not in JSON_KINDS:
|
|
657
|
+
_die(1, f"--moved-folder applies to {' and '.join(JSON_KINDS)} only, not {args.kind}")
|
|
289
658
|
|
|
290
659
|
if not args.target.exists():
|
|
291
660
|
_die(1, f"target not found: {args.target}")
|
|
292
661
|
|
|
293
662
|
try:
|
|
294
|
-
result = process(args.target, args.kind, args.old_name, args.new_name, args.dry_run
|
|
663
|
+
result = process(args.target, args.kind, args.old_name, args.new_name, args.dry_run,
|
|
664
|
+
args.moved_folder)
|
|
295
665
|
except ValueError as e:
|
|
296
666
|
_die(2, f"{args.kind} transform failed for {args.target}: {e}")
|
|
297
667
|
except OSError as e:
|
|
@@ -304,5 +674,21 @@ def main() -> None:
|
|
|
304
674
|
sys.exit(0)
|
|
305
675
|
|
|
306
676
|
|
|
677
|
+
def _force_utf8(*streams) -> None:
|
|
678
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
679
|
+
|
|
680
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
681
|
+
the --help text or a path may hold.
|
|
682
|
+
"""
|
|
683
|
+
for stream in streams:
|
|
684
|
+
if hasattr(stream, "reconfigure"):
|
|
685
|
+
errors = getattr(stream, "errors", None)
|
|
686
|
+
if errors is None:
|
|
687
|
+
stream.reconfigure(encoding="utf-8")
|
|
688
|
+
else:
|
|
689
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
690
|
+
|
|
691
|
+
|
|
307
692
|
if __name__ == "__main__":
|
|
693
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
308
694
|
main()
|