bmad-module-skill-forge 2.2.0 → 4.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 +143 -18
- package/docs/verifying-a-skill.md +87 -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 +3 -3
- 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 +31 -59
- 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 +233 -64
- package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
- 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 +772 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
- 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 +21 -3
- 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 +197 -55
- 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 +5247 -96
- package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +700 -0
- package/src/shared/scripts/skf-hash-content.py +180 -20
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
- 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 +176 -28
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
- 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 +155 -14
- 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 +787 -121
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +120 -67
- 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 +23 -2
- 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 +3945 -152
- package/src/shared/scripts/skf-write-skill-brief.py +406 -25
- 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 +28 -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/health-check.md +2 -0
- package/src/skf-audit-skill/references/init.md +149 -210
- package/src/skf-audit-skill/references/re-index.md +103 -110
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +34 -25
- 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 +80 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
- package/src/skf-brief-skill/SKILL.md +37 -26
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +18 -25
- package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +144 -127
- package/src/skf-brief-skill/references/confirm-brief.md +11 -9
- 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 +26 -100
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +7 -10
- 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 +11 -8
- package/src/skf-create-skill/assets/skill-sections.md +21 -11
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
- 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 +119 -70
- 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 +37 -0
- package/src/skf-create-skill/references/extract.md +204 -162
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
- package/src/skf-create-skill/references/extraction-patterns.md +588 -201
- 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 +37 -57
- package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
- 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 +83 -146
- package/src/skf-drop-skill/references/health-check.md +2 -0
- 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 +122 -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 +209 -264
- 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 +164 -91
- 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 +164 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +42 -155
- package/src/skf-test-skill/references/source-access-protocol.md +18 -94
- 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 +380 -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 +1666 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
- package/src/skf-test-skill/scripts/score-signatures.py +479 -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 +133 -56
- 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 +253 -223
- 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 +254 -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 +182 -49
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +102 -35
- package/src/skf-update-skill/references/write.md +357 -155
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
- 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
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Skills Module: is a repository a skills module, and what does it ship.
|
|
6
|
+
|
|
7
|
+
skf-quick-skill wraps a library's public API, but some repositories ship
|
|
8
|
+
agent skills instead of code: a plain Agent Skills package (folders that
|
|
9
|
+
each hold a SKILL.md), and a BMad module, whose module.yaml or
|
|
10
|
+
module-help.csv adds menu codes. Which folder holds the skills, and which
|
|
11
|
+
folders are skills, has one right answer per file listing, so this helper
|
|
12
|
+
computes it from the listing instead of the model walking the tree, and
|
|
13
|
+
reads the staged SKILL.md frontmatter and module-help.csv (with Python's
|
|
14
|
+
yaml and csv modules) into the envelope skf-extract-public-api.py prints
|
|
15
|
+
for a library.
|
|
16
|
+
|
|
17
|
+
CLI:
|
|
18
|
+
uv run skf-skills-module.py sniff --tree-file <file> [--package-json <file>] \\
|
|
19
|
+
[--scope-hint <folder>] [--fetch-list <file>]
|
|
20
|
+
uv run skf-skills-module.py extract --sniff <file> --source-root <dir> \\
|
|
21
|
+
[--package <file>] [--name <name>]
|
|
22
|
+
|
|
23
|
+
sniff
|
|
24
|
+
Reads the repository's file listing (--tree-file: skf-github-probe.py
|
|
25
|
+
tree output, or any listing skf-detect-language.py's --tree-file reads)
|
|
26
|
+
and, with --package-json, the root package.json staged from it. The
|
|
27
|
+
rules:
|
|
28
|
+
- A skill folder holds its own SKILL.md. A module root is a folder that
|
|
29
|
+
directly holds module.yaml or module-help.csv and sits in no skill
|
|
30
|
+
folder (a skill's own assets/module.yaml makes none).
|
|
31
|
+
- The skill folders of a module root are all the folders below it that
|
|
32
|
+
hold a SKILL.md and sit in no other skill folder. The skill folders
|
|
33
|
+
of any other folder are its direct subfolders that hold one.
|
|
34
|
+
- With --scope-hint, that folder is the only candidate, and it counts
|
|
35
|
+
whenever it has skill folders. Otherwise the candidates are the
|
|
36
|
+
repository root and each top-level folder that has skill folders,
|
|
37
|
+
and a candidate counts only when it is a module root or the root
|
|
38
|
+
ships no code. The root ships no code when it has no package
|
|
39
|
+
manifest (package.json, pyproject.toml, setup.py, setup.cfg,
|
|
40
|
+
Cargo.toml, go.mod, pom.xml, build.gradle, build.gradle.kts, Gemfile
|
|
41
|
+
or a *.csproj), or when its only one is a package.json that declares
|
|
42
|
+
no `main` (or an empty one), `exports`, `bin` or `workspaces` and has
|
|
43
|
+
no index.js or index.ts beside it. A root package.json the call was
|
|
44
|
+
not given, or could not read (no --package-json, no file there, or
|
|
45
|
+
not a JSON object), counts as one that ships code. So a library with
|
|
46
|
+
a manifest and one skill/SKILL.md stays a library.
|
|
47
|
+
- When several candidates count, a module root wins, then the one with
|
|
48
|
+
the most skill folders.
|
|
49
|
+
Prints:
|
|
50
|
+
skills_module whether a candidate counts
|
|
51
|
+
skills_root the folder that holds the skill folders ("." for the
|
|
52
|
+
repository root), else null
|
|
53
|
+
module_root whether skills_root is a module root
|
|
54
|
+
skill_folders its skill folders, in path order
|
|
55
|
+
candidates [{"folder", "module_root", "skill_folders" (a count),
|
|
56
|
+
"counts"}] for every candidate looked at
|
|
57
|
+
suggested_scope when the repository is not a skills module: the
|
|
58
|
+
folder a run with no export should name as a scope to
|
|
59
|
+
re-run with (the module root with the most skill
|
|
60
|
+
folders, else the folder with the most), else null
|
|
61
|
+
root_files the files at the repository root
|
|
62
|
+
readme the root README: README.md, else another root file
|
|
63
|
+
named README in any letter case (a .md one first, then
|
|
64
|
+
by name), else null
|
|
65
|
+
asset_dirs the first 10 folders named scripts, bin, assets,
|
|
66
|
+
templates or schemas (within --scope-hint when given)
|
|
67
|
+
asset_dir_count how many such folders there are
|
|
68
|
+
skill_md_count SKILL.md files anywhere in the listing
|
|
69
|
+
truncated the listing was cut short
|
|
70
|
+
warnings [...]
|
|
71
|
+
--fetch-list writes the files extract reads, one path per line, for
|
|
72
|
+
skf-github-fetch.py --patterns-file: each skill folder's SKILL.md, then
|
|
73
|
+
the module-help.csv directly in the skills root when the listing has one
|
|
74
|
+
(an empty file when the repository is not a skills module).
|
|
75
|
+
|
|
76
|
+
extract
|
|
77
|
+
Reads sniff's output (--sniff) and the files --fetch-list named, staged
|
|
78
|
+
under --source-root laid out as in the repository, and prints
|
|
79
|
+
skf-extract-public-api.py's quick-mode envelope for the module:
|
|
80
|
+
exports one {"name", "type": "skill", "brief_description",
|
|
81
|
+
"source_file"} per skill folder, in sniff's order:
|
|
82
|
+
name from the frontmatter `name` (the folder name when
|
|
83
|
+
it has none), brief_description from its
|
|
84
|
+
`description`, whitespace runs made one space; then one
|
|
85
|
+
{"name", "type": "menu-code", "brief_description",
|
|
86
|
+
"source_file"} per module-help.csv row that has a
|
|
87
|
+
menu-code, in file order: the code, then the row's
|
|
88
|
+
description, display-name and the skill it runs, with
|
|
89
|
+
its action when set
|
|
90
|
+
usage_patterns one {"menu_code", "display_name", "description",
|
|
91
|
+
"skill", "action", "args", "preceded_by",
|
|
92
|
+
"followed_by"} per module-help.csv row (empty fields
|
|
93
|
+
null); a row whose skill is empty or starts with `_`
|
|
94
|
+
(the _meta row) gives neither an export nor a pattern
|
|
95
|
+
confidence "high" when every skill folder's frontmatter gave a
|
|
96
|
+
name and a description, else "medium"
|
|
97
|
+
skills, menu_codes the two counts
|
|
98
|
+
skills_root sniff's
|
|
99
|
+
--package takes skf-extract-public-api.py's output for the module's root
|
|
100
|
+
manifest, whose language, package_name, version, description,
|
|
101
|
+
dependencies and warnings the envelope keeps; without it package_name is
|
|
102
|
+
--name (the repository's name) and the rest is empty. A SKILL.md or
|
|
103
|
+
module-help.csv that is missing or cannot be read is named in
|
|
104
|
+
`warnings`.
|
|
105
|
+
|
|
106
|
+
Exit codes:
|
|
107
|
+
0 output printed
|
|
108
|
+
2 usage error: argparse, or an input file that cannot be read, a
|
|
109
|
+
listing that reports a failure, or a --sniff file that is not sniff's
|
|
110
|
+
output ({"status": "error", "message": ...} on stderr)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
from __future__ import annotations
|
|
114
|
+
|
|
115
|
+
import argparse
|
|
116
|
+
import csv
|
|
117
|
+
import importlib.util
|
|
118
|
+
import io
|
|
119
|
+
import json
|
|
120
|
+
import re
|
|
121
|
+
import sys
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
|
|
124
|
+
import yaml
|
|
125
|
+
|
|
126
|
+
MANIFESTS = frozenset({"package.json", "pyproject.toml", "setup.py", "setup.cfg", "Cargo.toml", "go.mod",
|
|
127
|
+
"pom.xml", "build.gradle", "build.gradle.kts", "Gemfile"})
|
|
128
|
+
MODULE_FILES = ("module.yaml", "module-help.csv")
|
|
129
|
+
ASSET_DIR_NAMES = frozenset({"scripts", "bin", "assets", "templates", "schemas"})
|
|
130
|
+
ASSET_DIR_SHOWN = 10
|
|
131
|
+
ROOT = "."
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class UsageError(ValueError):
|
|
135
|
+
"""An input the call cannot run with: exit 2."""
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _load(filename: str):
|
|
139
|
+
path = Path(__file__).resolve().parent / filename
|
|
140
|
+
spec = importlib.util.spec_from_file_location(filename[:-3].replace("-", "_"), path)
|
|
141
|
+
if spec is None or spec.loader is None:
|
|
142
|
+
raise ImportError(f"cannot load {path}")
|
|
143
|
+
module = importlib.util.module_from_spec(spec)
|
|
144
|
+
spec.loader.exec_module(module)
|
|
145
|
+
return module
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --------------------------------------------------------------------------
|
|
149
|
+
# sniff: the skills root from the listing
|
|
150
|
+
# --------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _folder(path: str) -> str:
|
|
154
|
+
return path.rpartition("/")[0]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _ancestors(folder: str) -> list[str]:
|
|
158
|
+
"""The folders above `folder`, the root excluded."""
|
|
159
|
+
parts = folder.split("/") if folder else []
|
|
160
|
+
return ["/".join(parts[:i]) for i in range(1, len(parts))]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _skill_dirs(listing: set[str]) -> set[str]:
|
|
164
|
+
return {_folder(p) for p in listing if p.rpartition("/")[2] == "SKILL.md"}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _is_module_root(folder: str, listing: set[str], skill_dirs: set[str]) -> bool:
|
|
168
|
+
"""Directly holds module.yaml or module-help.csv, and sits in no skill folder."""
|
|
169
|
+
prefix = f"{folder}/" if folder else ""
|
|
170
|
+
holds = any(f"{prefix}{name}" in listing for name in MODULE_FILES)
|
|
171
|
+
in_skill = folder in skill_dirs or any(a in skill_dirs for a in _ancestors(folder))
|
|
172
|
+
return holds and not in_skill
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _skill_folders(folder: str, listing: set[str], skill_dirs: set[str]) -> list[str]:
|
|
176
|
+
prefix = f"{folder}/" if folder else ""
|
|
177
|
+
below = [d for d in skill_dirs if d.startswith(prefix) and d != folder]
|
|
178
|
+
if _is_module_root(folder, listing, skill_dirs):
|
|
179
|
+
return sorted(d for d in below if not any(a in skill_dirs for a in _ancestors(d)))
|
|
180
|
+
return sorted(d for d in below if "/" not in d[len(prefix):])
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _ships_no_code(listing: set[str], package_json: dict | None) -> bool:
|
|
184
|
+
root = {p for p in listing if "/" not in p}
|
|
185
|
+
manifests = {p for p in root if p in MANIFESTS or p.endswith(".csproj")}
|
|
186
|
+
if not manifests:
|
|
187
|
+
return True
|
|
188
|
+
if manifests != {"package.json"} or package_json is None:
|
|
189
|
+
return False
|
|
190
|
+
if package_json.get("main") or any(key in package_json for key in ("exports", "bin", "workspaces")):
|
|
191
|
+
return False
|
|
192
|
+
return not root & {"index.js", "index.ts"}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _suggested_scope(listing: set[str], skill_dirs: set[str]) -> str | None:
|
|
196
|
+
folders = {a for p in listing for a in [*_ancestors(_folder(p)), _folder(p)] if a}
|
|
197
|
+
scored = [(_is_module_root(f, listing, skill_dirs), len(_skill_folders(f, listing, skill_dirs)), f)
|
|
198
|
+
for f in sorted(folders)]
|
|
199
|
+
scored = [s for s in scored if s[1]]
|
|
200
|
+
return max(scored, key=lambda s: s[:2])[2] if scored else None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _readme(root_files: list[str]) -> str | None:
|
|
204
|
+
named = [f for f in root_files if f.lower() == "readme" or f.lower().startswith("readme.")]
|
|
205
|
+
if not named:
|
|
206
|
+
return None
|
|
207
|
+
return min(named, key=lambda f: (f != "README.md", f.lower() != "readme.md", not f.lower().endswith(".md"), f))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _asset_dirs(listing: set[str], scope: str) -> list[str]:
|
|
211
|
+
prefix = f"{scope}/" if scope else ""
|
|
212
|
+
folders = {a for p in listing if p.startswith(prefix) for a in [*_ancestors(_folder(p)), _folder(p)] if a}
|
|
213
|
+
return sorted(f for f in folders if f.startswith(prefix) and f.rpartition("/")[2] in ASSET_DIR_NAMES)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _shown(folder: str) -> str:
|
|
217
|
+
return folder or ROOT
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def sniff(paths: list[str], package_json: dict | None, scope_hint: str | None, truncated: bool = False) -> dict:
|
|
221
|
+
"""The skills-module facts of a file listing (see the module docstring)."""
|
|
222
|
+
listing = set(paths)
|
|
223
|
+
skill_dirs = _skill_dirs(listing)
|
|
224
|
+
scope = scope_hint.strip().strip("/") if scope_hint is not None and scope_hint.strip() not in ("", ".", "./") \
|
|
225
|
+
else None
|
|
226
|
+
candidates = []
|
|
227
|
+
if scope is not None:
|
|
228
|
+
found = _skill_folders(scope, listing, skill_dirs)
|
|
229
|
+
candidates.append((_is_module_root(scope, listing, skill_dirs), found, scope, bool(found)))
|
|
230
|
+
else:
|
|
231
|
+
no_code = _ships_no_code(listing, package_json)
|
|
232
|
+
for folder in ["", *sorted({p.split("/")[0] for p in listing if "/" in p})]:
|
|
233
|
+
found = _skill_folders(folder, listing, skill_dirs)
|
|
234
|
+
if not found:
|
|
235
|
+
continue
|
|
236
|
+
module_root = _is_module_root(folder, listing, skill_dirs)
|
|
237
|
+
candidates.append((module_root, found, folder, module_root or no_code))
|
|
238
|
+
counting = [c for c in candidates if c[3]]
|
|
239
|
+
best = max(counting, key=lambda c: (c[0], len(c[1]))) if counting else None
|
|
240
|
+
root_files = sorted(p for p in listing if "/" not in p)
|
|
241
|
+
assets = _asset_dirs(listing, scope or "")
|
|
242
|
+
warnings = []
|
|
243
|
+
if "package.json" in root_files and package_json is None and scope is None and \
|
|
244
|
+
{p for p in root_files if p in MANIFESTS or p.endswith(".csproj")} == {"package.json"}:
|
|
245
|
+
warnings.append("the root package.json was not read (--package-json): it counts as one that ships code")
|
|
246
|
+
if truncated:
|
|
247
|
+
warnings.append("the listing was cut short: a skill folder it lacks is not counted")
|
|
248
|
+
return {
|
|
249
|
+
"skills_module": best is not None,
|
|
250
|
+
"skills_root": _shown(best[2]) if best else None,
|
|
251
|
+
"module_root": bool(best and best[0]),
|
|
252
|
+
"skill_folders": list(best[1]) if best else [],
|
|
253
|
+
"candidates": [{"folder": _shown(c[2]), "module_root": c[0], "skill_folders": len(c[1]), "counts": c[3]}
|
|
254
|
+
for c in candidates],
|
|
255
|
+
"suggested_scope": None if best else _suggested_scope(listing, skill_dirs),
|
|
256
|
+
"root_files": root_files,
|
|
257
|
+
"readme": _readme(root_files),
|
|
258
|
+
"asset_dirs": assets[:ASSET_DIR_SHOWN],
|
|
259
|
+
"asset_dir_count": len(assets),
|
|
260
|
+
"skill_md_count": sum(1 for p in listing if p.rpartition("/")[2] == "SKILL.md"),
|
|
261
|
+
"truncated": truncated,
|
|
262
|
+
"warnings": warnings,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def fetch_list(result: dict, paths: list[str]) -> list[str]:
|
|
267
|
+
"""The files extract reads: each skill folder's SKILL.md, then the skills root's module-help.csv."""
|
|
268
|
+
if not result["skills_module"]:
|
|
269
|
+
return []
|
|
270
|
+
files = [f"{folder}/SKILL.md" for folder in result["skill_folders"]]
|
|
271
|
+
root = result["skills_root"]
|
|
272
|
+
csv_path = "module-help.csv" if root == ROOT else f"{root}/module-help.csv"
|
|
273
|
+
if csv_path in set(paths):
|
|
274
|
+
files.append(csv_path)
|
|
275
|
+
return files
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# --------------------------------------------------------------------------
|
|
279
|
+
# extract: the envelope from the staged files
|
|
280
|
+
# --------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
FRONTMATTER_RE = re.compile(r"\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)", re.S)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _one_line(value) -> str | None:
|
|
287
|
+
if not isinstance(value, str):
|
|
288
|
+
return None
|
|
289
|
+
text = " ".join(value.split())
|
|
290
|
+
return text or None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def frontmatter(text: str) -> dict:
|
|
294
|
+
"""The YAML frontmatter of a SKILL.md, {} when it has none or it is not a mapping."""
|
|
295
|
+
m = FRONTMATTER_RE.match(text.lstrip(""))
|
|
296
|
+
if not m:
|
|
297
|
+
return {}
|
|
298
|
+
try:
|
|
299
|
+
data = yaml.safe_load(m.group(1))
|
|
300
|
+
except yaml.YAMLError:
|
|
301
|
+
return {}
|
|
302
|
+
return data if isinstance(data, dict) else {}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _read_text(path: Path) -> str | None:
|
|
306
|
+
try:
|
|
307
|
+
return path.read_bytes().decode("utf-8", errors="replace")
|
|
308
|
+
except OSError:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _field(row: dict, name: str) -> str | None:
|
|
313
|
+
value = row.get(name)
|
|
314
|
+
return value.strip() or None if isinstance(value, str) else None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def menu_rows(text: str) -> list[dict]:
|
|
318
|
+
"""The module-help.csv rows that run a skill: the _meta row and rows with no skill left out."""
|
|
319
|
+
rows = []
|
|
320
|
+
for row in csv.DictReader(io.StringIO(text.lstrip(""))):
|
|
321
|
+
skill = _field(row, "skill")
|
|
322
|
+
if skill is None or skill.startswith("_"):
|
|
323
|
+
continue
|
|
324
|
+
rows.append({"menu_code": _field(row, "menu-code"), "display_name": _field(row, "display-name"),
|
|
325
|
+
"description": _field(row, "description"), "skill": skill, "action": _field(row, "action"),
|
|
326
|
+
"args": _field(row, "args"), "preceded_by": _field(row, "preceded-by"),
|
|
327
|
+
"followed_by": _field(row, "followed-by")})
|
|
328
|
+
return rows
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _menu_description(row: dict) -> str:
|
|
332
|
+
parts = [row["description"] or "", f"({row['display_name']})" if row["display_name"] else "",
|
|
333
|
+
f"runs {row['skill']}" + (f" ({row['action']})" if row["action"] else "")]
|
|
334
|
+
return " ".join(p for p in parts if p)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def extract(sniffed: dict, source_root: Path, package: dict | None = None, name: str | None = None) -> dict:
|
|
338
|
+
"""skf-extract-public-api.py's quick-mode envelope for a skills module."""
|
|
339
|
+
package = package or {}
|
|
340
|
+
warnings = [w for w in package.get("warnings", []) if isinstance(w, str)]
|
|
341
|
+
exports, complete = [], True
|
|
342
|
+
for folder in sniffed.get("skill_folders", []):
|
|
343
|
+
rel = f"{folder}/SKILL.md"
|
|
344
|
+
text = _read_text(source_root / rel)
|
|
345
|
+
if text is None:
|
|
346
|
+
warnings.append(f"{rel}: not staged under the source root; the skill is named after its folder")
|
|
347
|
+
meta = frontmatter(text or "")
|
|
348
|
+
skill_name = _one_line(meta.get("name"))
|
|
349
|
+
description = _one_line(meta.get("description"))
|
|
350
|
+
if text is not None and not (skill_name and description):
|
|
351
|
+
warnings.append(f"{rel}: the frontmatter gives no {'name' if not skill_name else 'description'}")
|
|
352
|
+
complete = complete and bool(skill_name and description)
|
|
353
|
+
exports.append({"name": skill_name or folder.rpartition("/")[2], "type": "skill",
|
|
354
|
+
"brief_description": description or "", "source_file": rel})
|
|
355
|
+
root = sniffed.get("skills_root")
|
|
356
|
+
usage_patterns: list[dict] = []
|
|
357
|
+
if root is not None:
|
|
358
|
+
csv_rel = "module-help.csv" if root == ROOT else f"{root}/module-help.csv"
|
|
359
|
+
text = _read_text(source_root / csv_rel)
|
|
360
|
+
if text is not None:
|
|
361
|
+
try:
|
|
362
|
+
usage_patterns = menu_rows(text)
|
|
363
|
+
except csv.Error as e:
|
|
364
|
+
warnings.append(f"{csv_rel}: cannot be read as CSV ({e})")
|
|
365
|
+
for row in usage_patterns:
|
|
366
|
+
if row["menu_code"]:
|
|
367
|
+
exports.append({"name": row["menu_code"], "type": "menu-code",
|
|
368
|
+
"brief_description": _menu_description(row), "source_file": csv_rel})
|
|
369
|
+
skills = sum(1 for e in exports if e["type"] == "skill")
|
|
370
|
+
return {
|
|
371
|
+
"language": package.get("language"),
|
|
372
|
+
"package_name": package.get("package_name") or name,
|
|
373
|
+
"version": package.get("version"),
|
|
374
|
+
"description": package.get("description"),
|
|
375
|
+
"exports": exports,
|
|
376
|
+
"dependencies": package.get("dependencies") or [],
|
|
377
|
+
"usage_patterns": usage_patterns,
|
|
378
|
+
"confidence": "high" if complete and skills else "medium",
|
|
379
|
+
"skills": skills,
|
|
380
|
+
"menu_codes": len(exports) - skills,
|
|
381
|
+
"skills_root": root,
|
|
382
|
+
"warnings": warnings,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# --------------------------------------------------------------------------
|
|
387
|
+
# CLI
|
|
388
|
+
# --------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _read_json(value: str, flag: str) -> dict:
|
|
392
|
+
try:
|
|
393
|
+
with open(value, encoding="utf-8-sig") as fh:
|
|
394
|
+
data = json.load(fh)
|
|
395
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
|
|
396
|
+
raise UsageError(f"cannot read {flag} {value}: {getattr(e, 'strerror', None) or e}") from e
|
|
397
|
+
if not isinstance(data, dict):
|
|
398
|
+
raise UsageError(f"{flag} {value} is not a JSON object")
|
|
399
|
+
return data
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _package_json(value: str | None) -> dict | None:
|
|
403
|
+
"""The staged root package.json, or None when it is absent or unreadable (it then counts as shipping code)."""
|
|
404
|
+
if not value or not Path(value).is_file():
|
|
405
|
+
return None
|
|
406
|
+
try:
|
|
407
|
+
return _read_json(value, "--package-json")
|
|
408
|
+
except UsageError:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
413
|
+
parser = argparse.ArgumentParser(
|
|
414
|
+
prog="skf-skills-module",
|
|
415
|
+
description="Find a repository's skills root from its file listing, and list a skills module's exports.",
|
|
416
|
+
)
|
|
417
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
418
|
+
p_sniff = sub.add_parser("sniff", help="the skills root and the listing facts")
|
|
419
|
+
p_sniff.add_argument("--tree-file", required=True, help="the repository's file listing")
|
|
420
|
+
p_sniff.add_argument("--package-json", help="the root package.json, staged")
|
|
421
|
+
p_sniff.add_argument("--scope-hint", help="the one folder to look at")
|
|
422
|
+
p_sniff.add_argument("--fetch-list", help="write the files extract reads here, one per line")
|
|
423
|
+
p_extract = sub.add_parser("extract", help="the envelope from the staged SKILL.md files and module-help.csv")
|
|
424
|
+
p_extract.add_argument("--sniff", required=True, help="sniff's output, saved")
|
|
425
|
+
p_extract.add_argument("--source-root", required=True, help="the folder the files were staged in")
|
|
426
|
+
p_extract.add_argument("--package", help="skf-extract-public-api.py's output for the root manifest")
|
|
427
|
+
p_extract.add_argument("--name", help="the package name when there is no --package")
|
|
428
|
+
return parser
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def main(argv: list[str] | None = None) -> int:
|
|
432
|
+
args = _build_parser().parse_args(argv)
|
|
433
|
+
try:
|
|
434
|
+
if args.cmd == "sniff":
|
|
435
|
+
try:
|
|
436
|
+
paths, truncated = _load("skf-detect-language.py").read_tree_file(args.tree_file)
|
|
437
|
+
except ValueError as e:
|
|
438
|
+
raise UsageError(str(e)) from e
|
|
439
|
+
out = sniff(paths, _package_json(args.package_json), args.scope_hint, truncated)
|
|
440
|
+
if args.fetch_list:
|
|
441
|
+
try:
|
|
442
|
+
Path(args.fetch_list).write_text("".join(f"{p}\n" for p in fetch_list(out, paths)),
|
|
443
|
+
encoding="utf-8")
|
|
444
|
+
except OSError as e:
|
|
445
|
+
raise UsageError(f"cannot write --fetch-list {args.fetch_list}: {e.strerror or e}") from e
|
|
446
|
+
else:
|
|
447
|
+
sniffed = _read_json(args.sniff, "--sniff")
|
|
448
|
+
if not isinstance(sniffed.get("skill_folders"), list) or "skills_root" not in sniffed:
|
|
449
|
+
raise UsageError(f"--sniff {args.sniff} is not skf-skills-module.py sniff output")
|
|
450
|
+
package = _read_json(args.package, "--package") if args.package else None
|
|
451
|
+
out = extract(sniffed, Path(args.source_root), package, args.name)
|
|
452
|
+
except UsageError as e:
|
|
453
|
+
print(json.dumps({"status": "error", "message": str(e)}), file=sys.stderr)
|
|
454
|
+
return 2
|
|
455
|
+
print(json.dumps(out, indent=2))
|
|
456
|
+
return 0
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _force_utf8(*streams) -> None:
|
|
460
|
+
"""Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
|
|
461
|
+
for stream in streams:
|
|
462
|
+
if hasattr(stream, "reconfigure"):
|
|
463
|
+
errors = getattr(stream, "errors", None)
|
|
464
|
+
if errors is None:
|
|
465
|
+
stream.reconfigure(encoding="utf-8")
|
|
466
|
+
else:
|
|
467
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if __name__ == "__main__":
|
|
471
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
472
|
+
sys.exit(main())
|