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