bmad-module-skill-forge 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.nvmrc +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +34 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +59 -28
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +51 -31
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +42 -10
- package/src/skf-drop-skill/references/select.md +88 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +27 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +13 -10
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +107 -78
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +109 -75
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +38 -20
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +131 -1
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +12 -24
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
package/.nvmrc
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
24
|
package/docs/_data/pinned.yaml
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# Enforced two ways: (1) release.yaml bumps this line in the same commit
|
|
27
27
|
# as package.json + marketplace.json on every release; (2) validate-docs-drift.js
|
|
28
28
|
# cross-checks this value against package.json.version and fails on mismatch.
|
|
29
|
-
skf_version: "2.0.
|
|
29
|
+
skf_version: "2.0.1"
|
|
30
30
|
|
|
31
31
|
# Path to the oh-my-skills repo, resolved relative to this repo's root.
|
|
32
32
|
# Override at runtime with the OMS environment variable if oh-my-skills
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-module-skill-forge",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.1",
|
|
5
5
|
"description": "BMAD module — Turn code and docs into instructions AI agents can actually follow. Progressive capability tiers (Quick/Forge/Forge+/Deep).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"test:cli": "node test/test-cli-integration.js",
|
|
54
54
|
"test:install": "node test/test-installation-components.js",
|
|
55
55
|
"test:knowledge": "node test/test-knowledge-base.js",
|
|
56
|
-
"test:python": "uv run --with pytest --with pyyaml --with jsonschema pytest test/test-compute-score-contract.py test/test-skf-preflight.py test/test-skf-skill-inventory.py test/test-skf-validate-output.py test/test-skf-validate-frontmatter.py test/test-skf-manifest-ops.py test/test-skf-rebuild-managed-sections.py test/test-skf-atomic-write.py test/test-skf-severity-classify.py test/test-skf-structural-diff.py test/test-skf-detect-tools.py test/test-skf-forge-tier-rw.py test/test-skf-emit-result-envelope.py test/test-skf-qmd-classify-collections.py test/test-skf-merge-ccc-exclusions.py test/test-skf-resolve-package.py test/test-skf-extract-public-api.py test/test-skf-render-quick-metadata.py test/test-skf-validate-brief-inputs.py test/test-skf-emit-brief-result-envelope.py test/test-skf-write-skill-brief.py test/test-skf-detect-workspaces.py test/test-skf-recommend-scope-type.py test/test-skf-detect-language.py test/test-skf-language-corpora.py test/test-skf-merge-doc-urls.py test/test-skf-derive-assembly-shape.py test/test-skf-description-guard.py test/test-skf-detect-scripts-assets.py test/test-skf-hash-content.py test/test-skf-validate-brief-schema.py test/test-skf-check-workspace-drift.py test/test-skf-update-active-symlink.py test/test-skf-build-change-manifest.py test/test-skf-provenance-gap-dispatch.py test/test-skf-resolve-authoritative-files.py test/test-skf-scan-manifests.py test/test-skf-pair-intersect.py test/test-skf-enumerate-stack-skills.py test/test-skf-compare-file-hashes.py test/test-skf-load-provenance.py test/test-skf-scan-skill-md-structure.py test/test-skf-disqualify-candidates.py test/test-skf-chain-reachability.py test/test-skf-shape-detect.py test/test-skf-detect-docs.py test/test-skf-step-doc-sources.py test/test-skf-step-doc-drift.py test/test-skf-step-hard-gate.py test/test-skf-per-pipeline-thresholds.py test/test-skf-evidence-report-fallback.py test/test-skf-validate-pins.py test/test-skf-auto-shard.py test/test-skf-preapply.py test/test-skf-step-doc-rot.py test/test-skf-campaign-state.py test/test-skf-campaign-pins.py test/test-skf-campaign-deps.py test/test-skf-campaign-report.py test/test-skf-campaign-stepfiles.py test/test-skf-campaign-validate-state.py test/test-skf-campaign-provenance.py test/test-skf-campaign-parse-manifest.py test/test-skf-campaign-render-kickoff.py -v",
|
|
56
|
+
"test:python": "uv run --with pytest --with pyyaml --with jsonschema pytest test/test-compute-score-contract.py test/test-skf-preflight.py test/test-skf-skill-inventory.py test/test-skf-validate-output.py test/test-skf-validate-frontmatter.py test/test-skf-manifest-ops.py test/test-skf-rebuild-managed-sections.py test/test-skf-atomic-write.py test/test-skf-severity-classify.py test/test-skf-structural-diff.py test/test-skf-detect-tools.py test/test-skf-forge-tier-rw.py test/test-skf-emit-result-envelope.py test/test-skf-qmd-classify-collections.py test/test-skf-merge-ccc-exclusions.py test/test-skf-resolve-package.py test/test-skf-extract-public-api.py test/test-skf-render-quick-metadata.py test/test-skf-validate-brief-inputs.py test/test-skf-emit-brief-result-envelope.py test/test-skf-write-skill-brief.py test/test-skf-detect-workspaces.py test/test-skf-recommend-scope-type.py test/test-skf-detect-language.py test/test-skf-language-corpora.py test/test-skf-merge-doc-urls.py test/test-skf-derive-assembly-shape.py test/test-skf-description-guard.py test/test-skf-detect-scripts-assets.py test/test-skf-hash-content.py test/test-skf-validate-brief-schema.py test/test-skf-check-workspace-drift.py test/test-skf-update-active-symlink.py test/test-skf-build-change-manifest.py test/test-skf-provenance-gap-dispatch.py test/test-skf-resolve-authoritative-files.py test/test-skf-scan-manifests.py test/test-skf-pair-intersect.py test/test-skf-enumerate-stack-skills.py test/test-skf-compare-file-hashes.py test/test-skf-load-provenance.py test/test-skf-scan-skill-md-structure.py test/test-skf-disqualify-candidates.py test/test-skf-chain-reachability.py test/test-skf-shape-detect.py test/test-skf-detect-docs.py test/test-skf-step-doc-sources.py test/test-skf-step-doc-drift.py test/test-skf-step-hard-gate.py test/test-skf-per-pipeline-thresholds.py test/test-skf-evidence-report-fallback.py test/test-skf-validate-pins.py test/test-skf-auto-shard.py test/test-skf-preapply.py test/test-skf-step-doc-rot.py test/test-skf-campaign-state.py test/test-skf-campaign-pins.py test/test-skf-campaign-deps.py test/test-skf-campaign-report.py test/test-skf-campaign-stepfiles.py test/test-skf-campaign-validate-state.py test/test-skf-campaign-provenance.py test/test-skf-campaign-parse-manifest.py test/test-skf-campaign-render-kickoff.py test/test-skf-campaign-render-batch.py test/test-skf-shard-body.py test/test-skf-render-metadata-stats.py test/test-skf-comention-pairs.py test/test-skf-count-tokens.py test/test-skf-reconcile-coverage.py test/test-skf-verify-provenance-completeness.py test/test-skf-find-cycles.py test/test-skf-validate-feasibility-report.py test/test-skf-rewrite-skill-name.py test/test-skf-verify-no-trace.py test/test-skf-validate-inventory.py test/test-skf-check-metadata-coherence.py test/test-skf-scan-doc-rot.py test/test-skf-aggregate-coherence.py test/test-skf-dir-sizes.py test/test-skf-parse-pipeline.py test/test-skf-validate-rename-name.py test/test-skf-verify-declared-numerator.py test/test-skf-combine-external-scores.py test/test-skf-new-file-diff.py test/test-skf-coverage-tally.py test/test-skf-report-delta.py test/test-skf-verdict-rollup.py -v",
|
|
57
57
|
"test:schemas": "node test/test-agent-schema.js",
|
|
58
58
|
"test:workflow": "node test/test-workflow-state.js",
|
|
59
59
|
"validate:refs": "node tools/validate-file-refs.js --strict",
|
|
@@ -143,7 +143,7 @@ EX → agentskills.io bundle + context snippets (flat platform paths)
|
|
|
143
143
|
|
|
144
144
|
VS → feasibility-report-{project_name}.md (verdict + integration verdicts)
|
|
145
145
|
↓ (report consumed by RA)
|
|
146
|
-
RA → refined-architecture-{
|
|
146
|
+
RA → refined-architecture-{arch_project_name}.md (gaps filled, issues flagged, improvements suggested)
|
|
147
147
|
↓ (refined doc consumed by SS compose-mode)
|
|
148
148
|
SS (compose) → SKILL.md (stack skill synthesized from individual skills + architecture)
|
|
149
149
|
```
|
package/src/module-help.csv
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
module,skill,display-name,menu-code,description,action,args,phase,
|
|
1
|
+
module,skill,display-name,menu-code,description,action,args,phase,preceded-by,followed-by,required,output-location,outputs
|
|
2
2
|
skf,skf-forger,Ferris — Skill Forge Agent,FF,"Skill compilation specialist — the forge master. Invoke to access all SKF workflows via guided menu.",,,anytime,,,false,,
|
|
3
3
|
skf,skf-setup,Setup Forge,SF,"Initialize forge environment — detect tools and set capability tier (Quick/Forge/Forge+/Deep)",,,anytime,,"skf-analyze-source,skf-brief-skill,skf-quick-skill,skf-verify-stack,skf-campaign",false,,forge-tier.yaml
|
|
4
4
|
skf,skf-campaign,Campaign,CA,"Orchestrate multi-library skill production across sessions — dependency tracking, file-based state, and resume",,resume [--from=<skill>],anytime,skf-setup,,false,forge_data_folder,_campaign-state.yaml
|
|
@@ -10,7 +10,7 @@ skf,skf-create-stack-skill,Stack Skill,SS,"Consolidated project stack skill with
|
|
|
10
10
|
skf,skf-update-skill,Update Skill,US,"Smart regeneration preserving [MANUAL] sections after source changes",,,anytime,skf-export-skill,,false,skills_output_folder,SKILL.md
|
|
11
11
|
skf,skf-audit-skill,Audit Skill,AS,"Drift detection between skill and current source code",,,anytime,skf-export-skill,,false,,drift report
|
|
12
12
|
skf,skf-verify-stack,Verify Stack,VS,"Pre-code stack feasibility verification against architecture and PRD documents",,,anytime,skf-setup,skf-refine-architecture,false,forge_data_folder,feasibility-report-{project_name}.md
|
|
13
|
-
skf,skf-refine-architecture,Refine Architecture,RA,"Improve architecture doc using verified skill data and VS feasibility findings",,,anytime,skf-verify-stack,,false,output_folder,refined-architecture-{
|
|
13
|
+
skf,skf-refine-architecture,Refine Architecture,RA,"Improve architecture doc using verified skill data and VS feasibility findings",,,anytime,skf-verify-stack,,false,output_folder,refined-architecture-{arch_project_name}.md
|
|
14
14
|
skf,skf-test-skill,Test Skill,TS,"Cognitive completeness verification — quality gate before export",,,anytime,"skf-create-skill,skf-create-stack-skill,skf-quick-skill",skf-export-skill,false,,completeness score
|
|
15
15
|
skf,skf-export-skill,Export Skill,EX,"Package for distribution and inject context into CLAUDE.md/AGENTS.md/.cursorrules",,,anytime,skf-test-skill,"skf-update-skill,skf-audit-skill,skf-rename-skill,skf-drop-skill",false,skills_output_folder,agentskills.io package
|
|
16
16
|
skf,skf-rename-skill,Rename Skill,RS,"Rename a skill across all its versions — transactional copy-verify-delete with platform context rebuild",,,anytime,skf-export-skill,,false,skills_output_folder,renamed skill group
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF Co-mention Pairs — compute compose-mode co-mention pairs from an
|
|
7
|
+
architecture document with the same precision guards the prompt used to run
|
|
8
|
+
by hand.
|
|
9
|
+
|
|
10
|
+
`detect-integrations.md` §2 (compose branch) asks the LLM to scan an
|
|
11
|
+
architecture markdown document for skill-name co-mentions using three
|
|
12
|
+
deterministic guards:
|
|
13
|
+
|
|
14
|
+
1. Word-boundary matching (`\\b{skill_name}\\b`, case-insensitive) — no
|
|
15
|
+
substring false positives (`react` must not match inside `reactive`).
|
|
16
|
+
2. Section filtering — paragraphs governed by an H1/H2 header that
|
|
17
|
+
normalises to one of {introduction, overview, glossary, table of
|
|
18
|
+
contents, references, appendix, index} are excluded, and heading text
|
|
19
|
+
itself is never a co-mention source.
|
|
20
|
+
3. Two-paragraph minimum — a pair (A, B) qualifies only when at least two
|
|
21
|
+
distinct body paragraphs each contain word-boundary matches for both A
|
|
22
|
+
and B.
|
|
23
|
+
|
|
24
|
+
Given a fixed architecture doc and skill-name set the qualifying-pair set is
|
|
25
|
+
fully determined, so this belongs in a deterministic helper rather than
|
|
26
|
+
in-prompt regex/counting. It mirrors the co-import subprocess the code-mode
|
|
27
|
+
branch already delegates and the `skf-pair-intersect.py` pre-pass. The LLM
|
|
28
|
+
still does the semantic composition (integration-section authoring, tier
|
|
29
|
+
inheritance, VS-verdict parsing) — the script only emits the qualifying pairs
|
|
30
|
+
and their evidence paragraphs.
|
|
31
|
+
|
|
32
|
+
Subcommand:
|
|
33
|
+
comention --doc <md-file-or-'-'> --skills <json-file-or-'-'>
|
|
34
|
+
--doc path to the architecture markdown document, or '-' for stdin.
|
|
35
|
+
--skills path to a JSON array of loaded skill names
|
|
36
|
+
(e.g. `["react", "express"]`), or '-' for stdin.
|
|
37
|
+
(--doc and --skills cannot both read from stdin.)
|
|
38
|
+
|
|
39
|
+
Emit JSON:
|
|
40
|
+
{
|
|
41
|
+
"pairs": [
|
|
42
|
+
{
|
|
43
|
+
"a": "<skill>", "b": "<skill>",
|
|
44
|
+
"paragraph_count": N,
|
|
45
|
+
"evidence": [{"header": "<governing-header-or-null>",
|
|
46
|
+
"excerpt": "<collapsed paragraph text>"}, ...]
|
|
47
|
+
},
|
|
48
|
+
...
|
|
49
|
+
],
|
|
50
|
+
"excluded_section_count": M
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Only unordered pairs (a < b lexicographically) with paragraph_count >= 2
|
|
54
|
+
are emitted. `pairs[]` is sorted by paragraph_count DESC, then (a, b)
|
|
55
|
+
ASC — stable, mirroring skf-pair-intersect.py. `evidence[]` is in
|
|
56
|
+
document order. `excluded_section_count` is the number of DISTINCT
|
|
57
|
+
excluded H1/H2 sections (by normalised header) that governed at least
|
|
58
|
+
one body paragraph — a diagnostic for the caller, not a gate.
|
|
59
|
+
|
|
60
|
+
CLI examples:
|
|
61
|
+
uv run skf-comention-pairs.py comention --doc arch.md --skills skills.json
|
|
62
|
+
echo '["react","express"]' | uv run skf-comention-pairs.py comention \\
|
|
63
|
+
--doc arch.md --skills -
|
|
64
|
+
|
|
65
|
+
Exit codes:
|
|
66
|
+
0 — operation succeeded (including: no qualifying pairs → empty list)
|
|
67
|
+
1 — user error (bad JSON, missing/unreadable file, both inputs on stdin,
|
|
68
|
+
malformed skills array)
|
|
69
|
+
2 — unexpected internal error
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
from __future__ import annotations
|
|
73
|
+
|
|
74
|
+
import argparse
|
|
75
|
+
import json
|
|
76
|
+
import re
|
|
77
|
+
import sys
|
|
78
|
+
from pathlib import Path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Closed exclusion set — governing H1/H2 headers that normalise to one of
|
|
82
|
+
# these drop their paragraphs from co-mention analysis (they typically
|
|
83
|
+
# enumerate all libraries without describing an integration).
|
|
84
|
+
EXCLUDED_HEADERS = frozenset({
|
|
85
|
+
"introduction",
|
|
86
|
+
"overview",
|
|
87
|
+
"glossary",
|
|
88
|
+
"table of contents",
|
|
89
|
+
"references",
|
|
90
|
+
"appendix",
|
|
91
|
+
"index",
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# ATX header line: 1-6 leading hashes followed by at least one space/tab.
|
|
95
|
+
# A line like `#foo` (no space) is NOT a header per CommonMark and is treated
|
|
96
|
+
# as body text. Capture group 1 = hashes (level), group 2 = header text.
|
|
97
|
+
_HEADER_RE = re.compile(r"^(#{1,6})[ \t]+(.*)$")
|
|
98
|
+
|
|
99
|
+
_EXCERPT_LIMIT = 240
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class UserError(Exception):
|
|
103
|
+
"""A caller-facing input error → exit code 1."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# --------------------------------------------------------------------------
|
|
107
|
+
# Markdown parsing
|
|
108
|
+
# --------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _strip_closing_hashes(text: str) -> str:
|
|
112
|
+
"""Remove a closed-ATX trailing run of hashes (e.g. `Overview ##`)."""
|
|
113
|
+
return re.sub(r"[ \t]*#+[ \t]*$", "", text)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def normalize_header(text: str) -> str:
|
|
117
|
+
"""Normalise a header for exclusion-set comparison: lowercase, collapse
|
|
118
|
+
internal whitespace, strip trailing punctuation."""
|
|
119
|
+
t = text.strip().lower()
|
|
120
|
+
t = re.sub(r"\s+", " ", t).strip()
|
|
121
|
+
t = t.rstrip(".,:;!?").strip()
|
|
122
|
+
return t
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_body_paragraphs(doc_text: str) -> list[tuple[str | None, str]]:
|
|
126
|
+
"""Split the document into body paragraphs, each tagged with the raw text
|
|
127
|
+
of its governing (nearest-preceding) H1/H2 header (or None).
|
|
128
|
+
|
|
129
|
+
Paragraphs are maximal runs of consecutive non-blank, non-header lines,
|
|
130
|
+
separated by blank lines. Header lines of ANY level are never emitted as
|
|
131
|
+
paragraphs (headings are not a co-mention source). Only H1/H2 headers
|
|
132
|
+
update the governing header; H3-H6 leave it unchanged.
|
|
133
|
+
"""
|
|
134
|
+
paragraphs: list[tuple[str | None, str]] = []
|
|
135
|
+
governing: str | None = None
|
|
136
|
+
buf: list[str] = []
|
|
137
|
+
|
|
138
|
+
def _flush() -> None:
|
|
139
|
+
if buf:
|
|
140
|
+
text = "\n".join(buf).strip()
|
|
141
|
+
if text:
|
|
142
|
+
paragraphs.append((governing, text))
|
|
143
|
+
buf.clear()
|
|
144
|
+
|
|
145
|
+
for raw_line in doc_text.splitlines():
|
|
146
|
+
m = _HEADER_RE.match(raw_line)
|
|
147
|
+
if m is not None:
|
|
148
|
+
# A header terminates the current paragraph; the header line
|
|
149
|
+
# itself is not body content.
|
|
150
|
+
_flush()
|
|
151
|
+
level = len(m.group(1))
|
|
152
|
+
header_text = _strip_closing_hashes(m.group(2)).strip()
|
|
153
|
+
if level <= 2:
|
|
154
|
+
governing = header_text if header_text else None
|
|
155
|
+
continue
|
|
156
|
+
if raw_line.strip() == "":
|
|
157
|
+
_flush()
|
|
158
|
+
continue
|
|
159
|
+
buf.append(raw_line)
|
|
160
|
+
_flush()
|
|
161
|
+
return paragraphs
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# --------------------------------------------------------------------------
|
|
165
|
+
# Co-mention analysis
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _make_excerpt(text: str, limit: int = _EXCERPT_LIMIT) -> str:
|
|
170
|
+
"""Collapse whitespace and truncate for a stable, compact evidence quote."""
|
|
171
|
+
collapsed = re.sub(r"\s+", " ", text).strip()
|
|
172
|
+
if len(collapsed) > limit:
|
|
173
|
+
return collapsed[: limit - 3].rstrip() + "..."
|
|
174
|
+
return collapsed
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def analyze(doc_text: str, skill_names: list[str]) -> dict:
|
|
178
|
+
"""Compute qualifying co-mention pairs and their evidence.
|
|
179
|
+
|
|
180
|
+
Deterministic: same (doc_text, skill_names) → identical output.
|
|
181
|
+
"""
|
|
182
|
+
names = sorted(set(skill_names))
|
|
183
|
+
patterns = {
|
|
184
|
+
n: re.compile(r"\b" + re.escape(n) + r"\b", re.IGNORECASE)
|
|
185
|
+
for n in names
|
|
186
|
+
}
|
|
187
|
+
paragraphs = parse_body_paragraphs(doc_text)
|
|
188
|
+
|
|
189
|
+
excluded_sections: set[str] = set()
|
|
190
|
+
# (a, b) -> list of evidence dicts, one per qualifying paragraph
|
|
191
|
+
pair_evidence: dict[tuple[str, str], list[dict]] = {}
|
|
192
|
+
|
|
193
|
+
for governing, text in paragraphs:
|
|
194
|
+
if governing is not None:
|
|
195
|
+
norm = normalize_header(governing)
|
|
196
|
+
if norm in EXCLUDED_HEADERS:
|
|
197
|
+
excluded_sections.add(norm)
|
|
198
|
+
continue
|
|
199
|
+
# names is sorted, so `present` is sorted → pairs are (a < b)
|
|
200
|
+
present = [n for n in names if patterns[n].search(text)]
|
|
201
|
+
if len(present) < 2:
|
|
202
|
+
continue
|
|
203
|
+
excerpt = _make_excerpt(text)
|
|
204
|
+
evidence_entry = {"header": governing, "excerpt": excerpt}
|
|
205
|
+
for i in range(len(present)):
|
|
206
|
+
for j in range(i + 1, len(present)):
|
|
207
|
+
key = (present[i], present[j])
|
|
208
|
+
pair_evidence.setdefault(key, []).append(evidence_entry)
|
|
209
|
+
|
|
210
|
+
pairs: list[dict] = []
|
|
211
|
+
for (a, b), evidence in pair_evidence.items():
|
|
212
|
+
if len(evidence) >= 2:
|
|
213
|
+
pairs.append({
|
|
214
|
+
"a": a,
|
|
215
|
+
"b": b,
|
|
216
|
+
"paragraph_count": len(evidence),
|
|
217
|
+
"evidence": evidence,
|
|
218
|
+
})
|
|
219
|
+
pairs.sort(key=lambda p: (-p["paragraph_count"], p["a"], p["b"]))
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"pairs": pairs,
|
|
223
|
+
"excluded_section_count": len(excluded_sections),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# --------------------------------------------------------------------------
|
|
228
|
+
# CLI
|
|
229
|
+
# --------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _read_source(source: str, label: str) -> str:
|
|
233
|
+
"""Read text from a file path or stdin (if source == '-')."""
|
|
234
|
+
if source == "-":
|
|
235
|
+
try:
|
|
236
|
+
return sys.stdin.read()
|
|
237
|
+
except OSError as exc:
|
|
238
|
+
raise UserError(f"failed to read {label} from stdin: {exc}") from exc
|
|
239
|
+
path = Path(source)
|
|
240
|
+
if not path.is_file():
|
|
241
|
+
raise UserError(f"{label} file not found: {path}")
|
|
242
|
+
try:
|
|
243
|
+
return path.read_text(encoding="utf-8")
|
|
244
|
+
except OSError as exc:
|
|
245
|
+
raise UserError(f"failed to read {label} file {path}: {exc}") from exc
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def parse_skills(raw_text: str) -> list[str]:
|
|
249
|
+
"""Parse and validate the --skills JSON array of non-empty strings."""
|
|
250
|
+
try:
|
|
251
|
+
data = json.loads(raw_text)
|
|
252
|
+
except json.JSONDecodeError as exc:
|
|
253
|
+
raise UserError(f"malformed JSON in --skills input: {exc}") from exc
|
|
254
|
+
if not isinstance(data, list):
|
|
255
|
+
raise UserError(
|
|
256
|
+
"--skills input must be a JSON array of strings; "
|
|
257
|
+
f"got {type(data).__name__}"
|
|
258
|
+
)
|
|
259
|
+
names: list[str] = []
|
|
260
|
+
for idx, item in enumerate(data):
|
|
261
|
+
if not isinstance(item, str) or not item:
|
|
262
|
+
raise UserError(
|
|
263
|
+
f"--skills[{idx}] must be a non-empty string; got {item!r}"
|
|
264
|
+
)
|
|
265
|
+
names.append(item)
|
|
266
|
+
return names
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _cmd_comention(args: argparse.Namespace) -> int:
|
|
270
|
+
if args.doc == "-" and args.skills == "-":
|
|
271
|
+
raise UserError(
|
|
272
|
+
"--doc and --skills cannot both read from stdin ('-')"
|
|
273
|
+
)
|
|
274
|
+
# Read the file-backed input first so a single stdin source stays intact.
|
|
275
|
+
if args.doc == "-":
|
|
276
|
+
skills_text = _read_source(args.skills, "--skills")
|
|
277
|
+
doc_text = _read_source(args.doc, "--doc")
|
|
278
|
+
else:
|
|
279
|
+
doc_text = _read_source(args.doc, "--doc")
|
|
280
|
+
skills_text = _read_source(args.skills, "--skills")
|
|
281
|
+
skill_names = parse_skills(skills_text)
|
|
282
|
+
|
|
283
|
+
result = analyze(doc_text, skill_names)
|
|
284
|
+
if args.verbose:
|
|
285
|
+
print(
|
|
286
|
+
f"analyzed {len(set(skill_names))} distinct skill names; "
|
|
287
|
+
f"{len(result['pairs'])} qualifying pairs; "
|
|
288
|
+
f"{result['excluded_section_count']} excluded sections",
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
291
|
+
json.dump(result, sys.stdout, indent=2)
|
|
292
|
+
sys.stdout.write("\n")
|
|
293
|
+
return 0
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
297
|
+
parser = argparse.ArgumentParser(
|
|
298
|
+
prog="skf-comention-pairs",
|
|
299
|
+
description=(
|
|
300
|
+
"Compute compose-mode co-mention pairs from an architecture "
|
|
301
|
+
"document (detect-integrations.md §2 compose branch)."
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
305
|
+
|
|
306
|
+
p_cm = sub.add_parser(
|
|
307
|
+
"comention",
|
|
308
|
+
help=(
|
|
309
|
+
"emit qualifying co-mention pairs (>=2 body paragraphs, "
|
|
310
|
+
"word-boundary, section-filtered) as JSON"
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
p_cm.add_argument(
|
|
314
|
+
"--doc",
|
|
315
|
+
required=True,
|
|
316
|
+
help="path to the architecture markdown document, or '-' for stdin",
|
|
317
|
+
)
|
|
318
|
+
p_cm.add_argument(
|
|
319
|
+
"--skills",
|
|
320
|
+
required=True,
|
|
321
|
+
help=(
|
|
322
|
+
"path to a JSON array of loaded skill names, or '-' for stdin. "
|
|
323
|
+
"Shape: [\"<skill>\", ...]"
|
|
324
|
+
),
|
|
325
|
+
)
|
|
326
|
+
p_cm.add_argument(
|
|
327
|
+
"--verbose",
|
|
328
|
+
action="store_true",
|
|
329
|
+
help="print a one-line summary to stderr",
|
|
330
|
+
)
|
|
331
|
+
p_cm.set_defaults(func=_cmd_comention)
|
|
332
|
+
|
|
333
|
+
return parser
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main(argv: list[str] | None = None) -> int:
|
|
337
|
+
parser = _build_parser()
|
|
338
|
+
args = parser.parse_args(argv)
|
|
339
|
+
try:
|
|
340
|
+
return args.func(args)
|
|
341
|
+
except UserError as exc:
|
|
342
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
343
|
+
return 1
|
|
344
|
+
except Exception as exc: # noqa: BLE001 — last-resort guard → exit 2
|
|
345
|
+
print(f"internal error: {exc}", file=sys.stderr)
|
|
346
|
+
return 2
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Count Tokens — Deterministic per-artifact token/word metrics for a skill package.
|
|
6
|
+
|
|
7
|
+
Pre-pass that measures the exported artifacts of a single skill package and the
|
|
8
|
+
shared managed section, so the export skill's token report (references/token-report.md)
|
|
9
|
+
and snippet token check (references/generate-snippet.md §4) render exact numbers from
|
|
10
|
+
JSON instead of re-reading every reference file to word-count it in-prompt.
|
|
11
|
+
|
|
12
|
+
Metrics:
|
|
13
|
+
- words: whitespace-split word count (`len(text.split())`) — the report's "Words" column
|
|
14
|
+
- tokens: char-over-four estimate (`len(text) // 4`) — the SKF-wide convention already
|
|
15
|
+
used by skf-validate-output.py, so every SKF token number agrees
|
|
16
|
+
|
|
17
|
+
Measured artifacts (all relative to the positional skill-package dir):
|
|
18
|
+
- context-snippet.md — the compressed passive-context snippet (if present)
|
|
19
|
+
- SKILL.md — the active skill document
|
|
20
|
+
- metadata.json — machine-readable metadata
|
|
21
|
+
- references/* — every file under references/ (recursively), summed as references_total
|
|
22
|
+
- managed section — the complete <!-- SKF:BEGIN ... --> ... <!-- SKF:END --> block,
|
|
23
|
+
extracted from the first --target-file that contains one
|
|
24
|
+
(shared across the batch; measured once)
|
|
25
|
+
|
|
26
|
+
package_total sums context-snippet.md + SKILL.md + metadata.json + references_total and
|
|
27
|
+
DELIBERATELY EXCLUDES the managed-section row — the managed section is a shared all-skills
|
|
28
|
+
cost reported separately, and including it would double-count this skill's own snippet
|
|
29
|
+
(see references/token-report.md §1).
|
|
30
|
+
|
|
31
|
+
CLI:
|
|
32
|
+
python3 skf-count-tokens.py <skill-package-dir>
|
|
33
|
+
python3 skf-count-tokens.py <skill-package-dir> --target-file CLAUDE.md
|
|
34
|
+
python3 skf-count-tokens.py <skill-package-dir> --target-file CLAUDE.md --target-file AGENTS.md
|
|
35
|
+
python3 skf-count-tokens.py <skill-package-dir> -o report.json
|
|
36
|
+
|
|
37
|
+
Exit codes: 0 = metrics emitted, 2 = error (skill package dir missing / not a directory).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import argparse
|
|
43
|
+
import json
|
|
44
|
+
import re
|
|
45
|
+
import sys
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
# Mirrors skf-rebuild-managed-sections.py: a BEGIN marker may carry attributes
|
|
49
|
+
# (e.g. `updated:2026-07-13`) before its closing `-->`; the END marker is fixed.
|
|
50
|
+
MANAGED_SECTION_RE = re.compile(
|
|
51
|
+
r"<!-- SKF:BEGIN[^>]*-->.*?<!-- SKF:END -->",
|
|
52
|
+
re.DOTALL,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Core per-skill artifacts, in report order. (references/ is handled separately.)
|
|
56
|
+
CORE_ARTIFACTS = [
|
|
57
|
+
("context-snippet.md", "context-snippet"),
|
|
58
|
+
("SKILL.md", "skill-md"),
|
|
59
|
+
("metadata.json", "metadata"),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def estimate_tokens(text: str) -> int:
|
|
64
|
+
"""Char-over-four token estimate — the SKF-wide convention (see skf-validate-output.py)."""
|
|
65
|
+
return len(text) // 4
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def count_words(text: str) -> int:
|
|
69
|
+
"""Whitespace-split word count — the token report's 'Words' column."""
|
|
70
|
+
return len(text.split())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def measure(text: str) -> dict:
|
|
74
|
+
"""Return the {words, tokens} metrics for a piece of text."""
|
|
75
|
+
return {"words": count_words(text), "tokens": estimate_tokens(text)}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_text(path: Path) -> str:
|
|
79
|
+
# encoding is pinned to utf-8 so non-ASCII content is not mojibaked on
|
|
80
|
+
# Windows (cp1252 default).
|
|
81
|
+
return path.read_text(encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def extract_managed_section(text: str) -> str | None:
|
|
85
|
+
"""Return the complete BEGIN..END managed-section span (markers included), or None."""
|
|
86
|
+
m = MANAGED_SECTION_RE.search(text)
|
|
87
|
+
return m.group(0) if m else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def measure_reference_files(references_dir: Path) -> tuple[list[dict], dict]:
|
|
91
|
+
"""Measure every file under references/ (recursively, sorted). Returns (per-file rows, total)."""
|
|
92
|
+
rows: list[dict] = []
|
|
93
|
+
total_words = 0
|
|
94
|
+
total_tokens = 0
|
|
95
|
+
if references_dir.is_dir():
|
|
96
|
+
for path in sorted(references_dir.rglob("*"), key=lambda p: p.as_posix()):
|
|
97
|
+
if not path.is_file():
|
|
98
|
+
continue
|
|
99
|
+
text = _read_text(path)
|
|
100
|
+
metrics = measure(text)
|
|
101
|
+
rows.append(
|
|
102
|
+
{
|
|
103
|
+
"path": path.relative_to(references_dir.parent).as_posix(),
|
|
104
|
+
"role": "reference",
|
|
105
|
+
"exists": True,
|
|
106
|
+
**metrics,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
total_words += metrics["words"]
|
|
110
|
+
total_tokens += metrics["tokens"]
|
|
111
|
+
total = {"count": len(rows), "words": total_words, "tokens": total_tokens}
|
|
112
|
+
return rows, total
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def count_package(skill_dir: Path, target_files: list[Path] | None = None) -> dict:
|
|
116
|
+
"""Measure a skill package's artifacts and the shared managed section.
|
|
117
|
+
|
|
118
|
+
Returns the metrics dict (see module docstring for the shape). Missing core
|
|
119
|
+
artifacts are reported with exists=False and zero metrics so package_total is
|
|
120
|
+
always well-defined and callers can render 'N/A' rows without guessing.
|
|
121
|
+
"""
|
|
122
|
+
skill_dir = Path(skill_dir)
|
|
123
|
+
target_files = target_files or []
|
|
124
|
+
|
|
125
|
+
files: list[dict] = []
|
|
126
|
+
|
|
127
|
+
# Core per-skill artifacts.
|
|
128
|
+
for name, role in CORE_ARTIFACTS:
|
|
129
|
+
path = skill_dir / name
|
|
130
|
+
if path.is_file():
|
|
131
|
+
metrics = measure(_read_text(path))
|
|
132
|
+
files.append({"path": name, "role": role, "exists": True, **metrics})
|
|
133
|
+
else:
|
|
134
|
+
files.append({"path": name, "role": role, "exists": False, "words": 0, "tokens": 0})
|
|
135
|
+
|
|
136
|
+
# references/ — per-file rows + summed total.
|
|
137
|
+
ref_rows, references_total = measure_reference_files(skill_dir / "references")
|
|
138
|
+
files.extend(ref_rows)
|
|
139
|
+
|
|
140
|
+
# package_total = snippet + SKILL.md + metadata + references_total; EXCLUDES managed section.
|
|
141
|
+
core_by_role = {row["role"]: row for row in files if row["role"] in {"context-snippet", "skill-md", "metadata"}}
|
|
142
|
+
package_total = {
|
|
143
|
+
"words": sum(core_by_role[r]["words"] for r in core_by_role) + references_total["words"],
|
|
144
|
+
"tokens": sum(core_by_role[r]["tokens"] for r in core_by_role) + references_total["tokens"],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Managed section — first --target-file that actually contains a block wins.
|
|
148
|
+
managed_section = {"present": False, "source_file": None, "words": 0, "tokens": 0}
|
|
149
|
+
for tf in target_files:
|
|
150
|
+
tf = Path(tf)
|
|
151
|
+
if not tf.is_file():
|
|
152
|
+
continue
|
|
153
|
+
section = extract_managed_section(_read_text(tf))
|
|
154
|
+
if section is not None:
|
|
155
|
+
managed_section = {
|
|
156
|
+
"present": True,
|
|
157
|
+
"source_file": tf.as_posix(),
|
|
158
|
+
**measure(section),
|
|
159
|
+
}
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"status": "ok",
|
|
164
|
+
"skill_package": skill_dir.as_posix(),
|
|
165
|
+
"files": files,
|
|
166
|
+
"references_total": references_total,
|
|
167
|
+
"managed_section": managed_section,
|
|
168
|
+
"package_total": package_total,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main(argv: list[str] | None = None) -> int:
|
|
173
|
+
parser = argparse.ArgumentParser(
|
|
174
|
+
prog="skf-count-tokens.py",
|
|
175
|
+
description=(
|
|
176
|
+
"Deterministic per-artifact word/token metrics for a skill package. Emits JSON "
|
|
177
|
+
"with per-file rows, references_total, the shared managed-section block, and a "
|
|
178
|
+
"package_total that excludes the managed section (see references/token-report.md §1). "
|
|
179
|
+
"tokens = len(text)//4 (SKF-wide convention); words = whitespace-split count."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
parser.add_argument(
|
|
183
|
+
"skill_package",
|
|
184
|
+
help="Resolved skill-package directory (holds context-snippet.md, SKILL.md, metadata.json, references/)",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--target-file",
|
|
188
|
+
action="append",
|
|
189
|
+
default=[],
|
|
190
|
+
metavar="PATH",
|
|
191
|
+
help="Context file (CLAUDE.md / AGENTS.md / .cursorrules) to extract the managed "
|
|
192
|
+
"section from; repeatable — the first file with a BEGIN..END block is measured.",
|
|
193
|
+
)
|
|
194
|
+
parser.add_argument("-o", "--output", metavar="PATH", help="Write JSON here (default: stdout)")
|
|
195
|
+
parser.add_argument("--verbose", action="store_true", help="Emit diagnostics to stderr")
|
|
196
|
+
args = parser.parse_args(argv)
|
|
197
|
+
|
|
198
|
+
skill_dir = Path(args.skill_package)
|
|
199
|
+
if not skill_dir.is_dir():
|
|
200
|
+
print(f"error: skill package directory not found: {skill_dir}", file=sys.stderr)
|
|
201
|
+
return 2
|
|
202
|
+
|
|
203
|
+
if args.verbose:
|
|
204
|
+
print(f"measuring skill package: {skill_dir}", file=sys.stderr)
|
|
205
|
+
if args.target_file:
|
|
206
|
+
print(f"managed-section candidates: {args.target_file}", file=sys.stderr)
|
|
207
|
+
|
|
208
|
+
result = count_package(skill_dir, [Path(p) for p in args.target_file])
|
|
209
|
+
|
|
210
|
+
payload = json.dumps(result, indent=2)
|
|
211
|
+
if args.output:
|
|
212
|
+
Path(args.output).write_text(payload + "\n", encoding="utf-8")
|
|
213
|
+
if args.verbose:
|
|
214
|
+
print(f"wrote {args.output}", file=sys.stderr)
|
|
215
|
+
else:
|
|
216
|
+
print(payload)
|
|
217
|
+
|
|
218
|
+
return 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if __name__ == "__main__":
|
|
222
|
+
sys.exit(main())
|