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,430 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Promote Staged: move a compiled skill from its staging folder into place, byte for byte.
|
|
6
|
+
|
|
7
|
+
skf-create-skill step 5 writes every artifact of a skill into its staging
|
|
8
|
+
folder, `_bmad-output/.skf-stage/{skill-name}/`, and steps 5a to 6 edit
|
|
9
|
+
them there (doc sources, auto-shard, doc-rot, skill-check --fix, the
|
|
10
|
+
description guard, the provenance fixes). Step 7 publishes that folder.
|
|
11
|
+
Writing each file again from the copies the model holds in context can
|
|
12
|
+
drop an edit a later step made on disk, or a file such as a
|
|
13
|
+
references/full-*.md that auto-shard created, so this helper copies the
|
|
14
|
+
staged bytes instead and reports what it wrote.
|
|
15
|
+
|
|
16
|
+
Subcommand:
|
|
17
|
+
|
|
18
|
+
promote --stage <dir> --package <skill_package> --forge-version <dir>
|
|
19
|
+
[--inventory <extraction inventory JSON> --source-root <dir>]
|
|
20
|
+
[--carry-manual <earlier package>]
|
|
21
|
+
|
|
22
|
+
What goes where:
|
|
23
|
+
|
|
24
|
+
<package>/ SKILL.md, context-snippet.md and metadata.json, every
|
|
25
|
+
file under the staged references/, and scripts/<name>
|
|
26
|
+
and assets/<name> for each scripts_inventory[] and
|
|
27
|
+
assets_inventory[] entry of --inventory, copied from
|
|
28
|
+
<source-root>/<source_file>
|
|
29
|
+
<forge-version>/ provenance-map.json, evidence-report.md and
|
|
30
|
+
extraction-rules.yaml
|
|
31
|
+
|
|
32
|
+
The package is built in `<package>.skf-tmp/` with skf-atomic-write.py
|
|
33
|
+
stage-dir and swapped into place with its commit-dir, so a reader sees the
|
|
34
|
+
earlier package or the new one, never a mix, and a run that compiles a
|
|
35
|
+
version again leaves no file of the earlier package behind. Each workspace
|
|
36
|
+
file goes through skf-atomic-write.py write into --forge-version, which is
|
|
37
|
+
created when missing; nothing else in that folder (earlier result files,
|
|
38
|
+
test reports) is touched. Every promoted file is read back and its SHA-256
|
|
39
|
+
compared with the bytes it came from.
|
|
40
|
+
|
|
41
|
+
Any other file in the staging folder is not promoted and is listed in
|
|
42
|
+
`ignored`, as is a file under references/ that is hidden (a name starting
|
|
43
|
+
with `.`) or that ends in `.skf-tmp`, a write skf-atomic-write.py did not
|
|
44
|
+
finish. A script or asset whose bytes no longer match the content_hash
|
|
45
|
+
the inventory recorded is still copied, with a warning.
|
|
46
|
+
|
|
47
|
+
Hand-written content (--carry-manual). A run that compiles a version
|
|
48
|
+
again replaces the earlier package of that version, so --carry-manual
|
|
49
|
+
names it (usually the --package folder itself) and the helper reads it
|
|
50
|
+
before the swap:
|
|
51
|
+
|
|
52
|
+
- every file under its scripts/[MANUAL]/ and assets/[MANUAL]/ goes into
|
|
53
|
+
the new package at the same path (kind "manual");
|
|
54
|
+
- each [MANUAL] block of its SKILL.md that holds more than whitespace
|
|
55
|
+
and the note create-skill seeds a marker with must be in the staged
|
|
56
|
+
SKILL.md: a block of the same name with the same interior once line
|
|
57
|
+
endings, trailing spaces and surrounding blank lines are set aside
|
|
58
|
+
(create-skill step 5 carries them there, so step 6 validates them).
|
|
59
|
+
A block is an open marker `<!-- [MANUAL:<name>] -->` and the first
|
|
60
|
+
`<!-- [/MANUAL:<name>] -->` after it, as skf-hash-content.py pairs
|
|
61
|
+
them. When one is not there, the earlier SKILL.md is written to
|
|
62
|
+
<forge-version>/manual-backup/SKILL-<UTC time>.md before the swap and
|
|
63
|
+
a warning names the block, so nothing hand-written is lost; each run
|
|
64
|
+
writes a new name, so a later run never replaces an earlier backup.
|
|
65
|
+
|
|
66
|
+
`manual` reports it: `from` (the folder read, or null without the flag),
|
|
67
|
+
`blocks` (the names found in the staged SKILL.md), `missing_blocks`,
|
|
68
|
+
`files` (package-relative paths) and `backup` (the copy's path, or null).
|
|
69
|
+
|
|
70
|
+
Output (stdout, exit 0):
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
"status": "ok",
|
|
74
|
+
"package": "<package, / separators>",
|
|
75
|
+
"forge_version": "<forge-version, / separators>",
|
|
76
|
+
"files": [{"path": "...", "kind": "deliverable|reference|script|asset|manual|workspace",
|
|
77
|
+
"bytes": N, "sha256": "sha256:<hex>"}, ...],
|
|
78
|
+
"counts": {"deliverables": 3, "references": N, "scripts": N, "assets": N,
|
|
79
|
+
"manual": N, "workspace": 3},
|
|
80
|
+
"ignored": ["<staged file not promoted>", ...],
|
|
81
|
+
"manual": {"from": "<folder>"|null, "blocks": ["<name>", ...],
|
|
82
|
+
"missing_blocks": ["<name>", ...], "files": ["scripts/[MANUAL]/...", ...],
|
|
83
|
+
"backup": "<forge-version>/manual-backup/SKILL-<UTC time>.md"|null},
|
|
84
|
+
"warnings": ["..."]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
Exit codes:
|
|
88
|
+
0 promoted and verified
|
|
89
|
+
1 input error, nothing written: a staged file the package or the forge
|
|
90
|
+
folder needs is missing, the inventory cannot be read, a script or
|
|
91
|
+
asset is missing, lies outside --source-root or shares its
|
|
92
|
+
scripts/<name> or assets/<name> with another, --carry-manual is not
|
|
93
|
+
a folder or a file of it cannot be read, or a --carry-manual file
|
|
94
|
+
would replace a promoted file
|
|
95
|
+
2 write failed (one line on stderr names it): a failed backup or package
|
|
96
|
+
swap leaves the earlier package in place, while a workspace file that
|
|
97
|
+
fails after the swap leaves the new package and the workspace files
|
|
98
|
+
written before it
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
from __future__ import annotations
|
|
102
|
+
|
|
103
|
+
import argparse
|
|
104
|
+
import hashlib
|
|
105
|
+
import json
|
|
106
|
+
import os
|
|
107
|
+
import re
|
|
108
|
+
import shutil
|
|
109
|
+
import subprocess
|
|
110
|
+
import sys
|
|
111
|
+
import time
|
|
112
|
+
from pathlib import Path, PurePosixPath
|
|
113
|
+
|
|
114
|
+
ATOMIC_WRITE = Path(__file__).resolve().parent / "skf-atomic-write.py"
|
|
115
|
+
DELIVERABLES = ("SKILL.md", "context-snippet.md", "metadata.json")
|
|
116
|
+
WORKSPACE_FILES = ("provenance-map.json", "evidence-report.md", "extraction-rules.yaml")
|
|
117
|
+
REFERENCES = "references"
|
|
118
|
+
# skf-atomic-write.py's temporary name: a write it did not finish leaves <name>.skf-tmp.
|
|
119
|
+
TMP_SUFFIX = ".skf-tmp"
|
|
120
|
+
# The inventory lists and the package folder each one lands in.
|
|
121
|
+
BUNDLED = (("scripts_inventory", "scripts", "script"), ("assets_inventory", "assets", "asset"))
|
|
122
|
+
# The package folders that hold user-authored files (docs/skill-model.md).
|
|
123
|
+
MANUAL_FOLDERS = ("scripts/[MANUAL]", "assets/[MANUAL]")
|
|
124
|
+
# The forge version folder's backup folder; skf-skill-inventory.py's
|
|
125
|
+
# FORGE_VERSION_FILES names it, so the ownership checks count it as SKF's.
|
|
126
|
+
MANUAL_BACKUP = "manual-backup"
|
|
127
|
+
# The note create-skill seeds a [MANUAL] marker with (compile-assembly-rules.md Section 8).
|
|
128
|
+
SEEDED_NOTE = b"<!-- Add custom notes here. This section is preserved during skill updates. -->"
|
|
129
|
+
# skf-hash-content.py's [MANUAL] markers; the open pattern never matches a close.
|
|
130
|
+
_MANUAL_OPEN_RE = re.compile(rb"<!--\s*\[MANUAL:([^\]]+)\]\s*-->")
|
|
131
|
+
_MANUAL_CLOSE_RE = re.compile(rb"<!--\s*\[/MANUAL:([^\]]+)\]\s*-->")
|
|
132
|
+
ATOMIC_TIMEOUT_SEC = 120
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class InputError(Exception):
|
|
136
|
+
"""Exit 1: nothing was written."""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class WriteError(Exception):
|
|
140
|
+
"""Exit 2: a write failed."""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _sha256(data: bytes) -> str:
|
|
144
|
+
return "sha256:" + hashlib.sha256(data).hexdigest()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _posix(path: Path) -> str:
|
|
148
|
+
return path.as_posix()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _atomic(args: list[str], data: bytes | None = None) -> None:
|
|
152
|
+
"""Run skf-atomic-write.py with `args`; a non-zero exit is a WriteError."""
|
|
153
|
+
try:
|
|
154
|
+
proc = subprocess.run([sys.executable, str(ATOMIC_WRITE), *args], input=data or b"",
|
|
155
|
+
capture_output=True, timeout=ATOMIC_TIMEOUT_SEC, check=False)
|
|
156
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
157
|
+
raise WriteError(f"skf-atomic-write.py {args[0]} did not run: {exc}") from exc
|
|
158
|
+
if proc.returncode != 0:
|
|
159
|
+
detail = proc.stderr.decode("utf-8", "replace").strip().splitlines()
|
|
160
|
+
raise WriteError(f"skf-atomic-write.py {args[0]} failed: {detail[-1] if detail else proc.returncode}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _staged_references(stage: Path) -> list[Path]:
|
|
164
|
+
"""The staged reference files, less a hidden one and a write an interrupted run left half done."""
|
|
165
|
+
folder = stage / REFERENCES
|
|
166
|
+
if not folder.is_dir():
|
|
167
|
+
return []
|
|
168
|
+
return sorted(p for p in folder.rglob("*") if p.is_file() and not p.name.endswith(TMP_SUFFIX)
|
|
169
|
+
and not any(part.startswith(".") for part in p.relative_to(folder).parts))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _bundled_files(inventory: Path | None, source_root: Path | None) -> list[tuple[str, str, Path, str | None]]:
|
|
173
|
+
"""(kind, package-relative path, source file, recorded hash) per inventory script and asset."""
|
|
174
|
+
if inventory is None:
|
|
175
|
+
return []
|
|
176
|
+
try:
|
|
177
|
+
data = json.loads(inventory.read_text(encoding="utf-8"))
|
|
178
|
+
except (OSError, UnicodeDecodeError, ValueError) as exc:
|
|
179
|
+
raise InputError(f"cannot read the inventory {_posix(inventory)}: {exc}") from exc
|
|
180
|
+
if not isinstance(data, dict):
|
|
181
|
+
raise InputError(f"the inventory {_posix(inventory)} is not a JSON object")
|
|
182
|
+
entries: list[tuple[str, str, Path, str | None]] = []
|
|
183
|
+
seen: dict[str, str] = {}
|
|
184
|
+
for key, folder, kind in BUNDLED:
|
|
185
|
+
records = data.get(key) or []
|
|
186
|
+
if not isinstance(records, list):
|
|
187
|
+
raise InputError(f"the inventory's {key} is not a list")
|
|
188
|
+
for record in records:
|
|
189
|
+
if not isinstance(record, dict) or not isinstance(record.get("source_file"), str):
|
|
190
|
+
raise InputError(f"a {key} entry has no source_file: {record!r}")
|
|
191
|
+
if source_root is None:
|
|
192
|
+
raise InputError(f"the inventory lists {key} entries: pass --source-root")
|
|
193
|
+
rel = PurePosixPath(record["source_file"])
|
|
194
|
+
if rel.is_absolute() or ".." in rel.parts or not rel.parts:
|
|
195
|
+
raise InputError(f"{key} entry {record['source_file']!r} is not a path inside --source-root")
|
|
196
|
+
name = record.get("name") or rel.name
|
|
197
|
+
if not isinstance(name, str) or not name or "/" in name or "\\" in name:
|
|
198
|
+
raise InputError(f"{key} entry {record['source_file']!r} has no usable name")
|
|
199
|
+
target = f"{folder}/{name}"
|
|
200
|
+
if target in seen:
|
|
201
|
+
raise InputError(f"{seen[target]!r} and {record['source_file']!r} would both be {target}")
|
|
202
|
+
seen[target] = record["source_file"]
|
|
203
|
+
source = source_root.joinpath(*rel.parts)
|
|
204
|
+
if not source.is_file():
|
|
205
|
+
raise InputError(f"{key} entry {record['source_file']!r} is not a file under --source-root")
|
|
206
|
+
recorded = record.get("content_hash")
|
|
207
|
+
entries.append((kind, target, source, recorded if isinstance(recorded, str) else None))
|
|
208
|
+
return entries
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _manual_blocks(data: bytes) -> list[tuple[str, bytes]]:
|
|
212
|
+
"""(name, interior) of each [MANUAL] block: an open marker paired with the
|
|
213
|
+
first close marker of its name after it; an unclosed one is skipped."""
|
|
214
|
+
closes = [(m.start(), m.group(1).strip()) for m in _MANUAL_CLOSE_RE.finditer(data)]
|
|
215
|
+
blocks = []
|
|
216
|
+
for m in _MANUAL_OPEN_RE.finditer(data):
|
|
217
|
+
name = m.group(1).strip()
|
|
218
|
+
end = next((start for start, close in closes if start >= m.end() and close == name), None)
|
|
219
|
+
if end is not None:
|
|
220
|
+
blocks.append((name.decode("utf-8", "replace"), data[m.end():end]))
|
|
221
|
+
return blocks
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _normalized(interior: bytes) -> bytes:
|
|
225
|
+
"""A block's interior as the check compares it: LF line endings, no
|
|
226
|
+
trailing spaces, no surrounding blank lines (a CRLF checkout of the
|
|
227
|
+
earlier SKILL.md still holds the block an LF staged one carries)."""
|
|
228
|
+
lines = interior.replace(b"\r\n", b"\n").split(b"\n")
|
|
229
|
+
return b"\n".join(line.rstrip() for line in lines).strip()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _hand_written(interior: bytes) -> bool:
|
|
233
|
+
"""A block holds more than whitespace and the note create-skill seeds it with."""
|
|
234
|
+
return _normalized(interior) not in (b"", SEEDED_NOTE)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _read(path: Path) -> bytes:
|
|
238
|
+
"""The bytes of an earlier package file; one it cannot read is an InputError."""
|
|
239
|
+
try:
|
|
240
|
+
return path.read_bytes()
|
|
241
|
+
except OSError as exc:
|
|
242
|
+
raise InputError(f"cannot read {_posix(path)}: {exc}") from exc
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _manual_files(earlier: Path) -> list[tuple[str, bytes]]:
|
|
246
|
+
"""(package-relative path, bytes) of each file under the earlier package's [MANUAL] folders."""
|
|
247
|
+
found = []
|
|
248
|
+
for folder in MANUAL_FOLDERS:
|
|
249
|
+
root = earlier.joinpath(*PurePosixPath(folder).parts)
|
|
250
|
+
if root.is_dir():
|
|
251
|
+
found += [(p.relative_to(earlier).as_posix(), _read(p))
|
|
252
|
+
for p in sorted(root.rglob("*")) if p.is_file()]
|
|
253
|
+
return found
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _held_blocks(earlier_skill_md: bytes, staged_skill_md: bytes) -> tuple[list[str], list[str]]:
|
|
257
|
+
"""The names of the earlier SKILL.md's hand-written blocks the staged one
|
|
258
|
+
holds (same name, same normalized interior), and of those it lacks."""
|
|
259
|
+
staged: dict[str, set[bytes]] = {}
|
|
260
|
+
for name, interior in _manual_blocks(staged_skill_md):
|
|
261
|
+
staged.setdefault(name, set()).add(_normalized(interior))
|
|
262
|
+
held, lacking = [], []
|
|
263
|
+
for name, interior in _manual_blocks(earlier_skill_md):
|
|
264
|
+
if _hand_written(interior):
|
|
265
|
+
(held if _normalized(interior) in staged.get(name, set()) else lacking).append(name)
|
|
266
|
+
return held, lacking
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _backup_target(forge_version: Path) -> Path:
|
|
270
|
+
"""manual-backup/SKILL-<UTC time>.md under the forge version folder, a
|
|
271
|
+
name no earlier backup holds, so a backup the user has not restored
|
|
272
|
+
from yet is never replaced."""
|
|
273
|
+
folder = forge_version / MANUAL_BACKUP
|
|
274
|
+
stamp = time.strftime("%Y%m%d-%H%M%S", time.gmtime())
|
|
275
|
+
target, n = folder / f"SKILL-{stamp}.md", 1
|
|
276
|
+
while os.path.lexists(target):
|
|
277
|
+
n += 1
|
|
278
|
+
target = folder / f"SKILL-{stamp}-{n}.md"
|
|
279
|
+
return target
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def promote(stage: Path, package: Path, forge_version: Path,
|
|
283
|
+
inventory: Path | None = None, source_root: Path | None = None,
|
|
284
|
+
carry_manual: Path | None = None) -> dict:
|
|
285
|
+
"""Promote the staging folder (see the module docstring); raises InputError or WriteError."""
|
|
286
|
+
if not stage.is_dir():
|
|
287
|
+
raise InputError(f"no staging folder at {_posix(stage)}")
|
|
288
|
+
missing = [name for name in (*DELIVERABLES, *WORKSPACE_FILES) if not (stage / name).is_file()]
|
|
289
|
+
if missing:
|
|
290
|
+
raise InputError(f"the staging folder {_posix(stage)} has no {', '.join(missing)}")
|
|
291
|
+
references = _staged_references(stage)
|
|
292
|
+
bundled = _bundled_files(inventory, source_root)
|
|
293
|
+
|
|
294
|
+
plan: list[tuple[str, str, bytes]] = [] # (kind, package-relative path, bytes)
|
|
295
|
+
for name in DELIVERABLES:
|
|
296
|
+
plan.append(("deliverable", name, (stage / name).read_bytes()))
|
|
297
|
+
for path in references:
|
|
298
|
+
plan.append(("reference", path.relative_to(stage).as_posix(), path.read_bytes()))
|
|
299
|
+
warnings: list[str] = []
|
|
300
|
+
for kind, rel, source, recorded in bundled:
|
|
301
|
+
data = source.read_bytes()
|
|
302
|
+
if recorded is not None and recorded != _sha256(data):
|
|
303
|
+
warnings.append(f"{rel}: {_posix(source)} changed since extraction recorded {recorded}")
|
|
304
|
+
plan.append((kind, rel, data))
|
|
305
|
+
workspace = [(name, (stage / name).read_bytes()) for name in WORKSPACE_FILES]
|
|
306
|
+
|
|
307
|
+
# The earlier package's hand-written content, read before the swap deletes it.
|
|
308
|
+
manual = {"from": None, "blocks": [], "missing_blocks": [], "files": [], "backup": None}
|
|
309
|
+
earlier_skill_md = b""
|
|
310
|
+
if carry_manual is not None:
|
|
311
|
+
if not carry_manual.is_dir():
|
|
312
|
+
raise InputError(f"--carry-manual {_posix(carry_manual)} is not a folder")
|
|
313
|
+
if (carry_manual / "SKILL.md").is_file():
|
|
314
|
+
earlier_skill_md = _read(carry_manual / "SKILL.md")
|
|
315
|
+
held, lacking = _held_blocks(earlier_skill_md, (stage / "SKILL.md").read_bytes())
|
|
316
|
+
files = _manual_files(carry_manual)
|
|
317
|
+
taken = {rel for _, rel, _ in plan}
|
|
318
|
+
for rel, data in files:
|
|
319
|
+
if rel in taken:
|
|
320
|
+
raise InputError(f"{rel} of --carry-manual would replace a promoted file")
|
|
321
|
+
plan.append(("manual", rel, data))
|
|
322
|
+
manual.update({"from": _posix(carry_manual), "blocks": held, "missing_blocks": lacking,
|
|
323
|
+
"files": [rel for rel, _ in files]})
|
|
324
|
+
|
|
325
|
+
promoted = {_posix(p.relative_to(stage)) for p in references} | set(DELIVERABLES) | set(WORKSPACE_FILES)
|
|
326
|
+
ignored = sorted(p.relative_to(stage).as_posix() for p in stage.rglob("*")
|
|
327
|
+
if p.is_file() and p.relative_to(stage).as_posix() not in promoted)
|
|
328
|
+
|
|
329
|
+
# A hand-written block the staged SKILL.md lacks: keep the earlier
|
|
330
|
+
# SKILL.md before the swap deletes it.
|
|
331
|
+
if manual["missing_blocks"]:
|
|
332
|
+
target = _backup_target(forge_version)
|
|
333
|
+
_atomic(["write", "--target", str(target)], earlier_skill_md)
|
|
334
|
+
_verified(target, "workspace", earlier_skill_md)
|
|
335
|
+
manual["backup"] = _posix(target)
|
|
336
|
+
warnings.append(f"SKILL.md: the [MANUAL] block(s) {', '.join(manual['missing_blocks'])} of "
|
|
337
|
+
f"{_posix(carry_manual)} are not in the new SKILL.md; the earlier SKILL.md is "
|
|
338
|
+
f"kept at {_posix(target)}")
|
|
339
|
+
|
|
340
|
+
# The package: build it beside its target, then swap it in.
|
|
341
|
+
_atomic(["stage-dir", "--target", str(package)])
|
|
342
|
+
building = package.with_name(package.name + ".skf-tmp")
|
|
343
|
+
try:
|
|
344
|
+
for _, rel, data in plan:
|
|
345
|
+
target = building.joinpath(*PurePosixPath(rel).parts)
|
|
346
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
target.write_bytes(data)
|
|
348
|
+
except OSError as exc:
|
|
349
|
+
shutil.rmtree(building, ignore_errors=True)
|
|
350
|
+
raise WriteError(f"cannot build {_posix(building)}: {exc}") from exc
|
|
351
|
+
_atomic(["commit-dir", "--target", str(package)])
|
|
352
|
+
|
|
353
|
+
# The workspace files, one atomic write each.
|
|
354
|
+
for name, data in workspace:
|
|
355
|
+
_atomic(["write", "--target", str(forge_version / name)], data)
|
|
356
|
+
|
|
357
|
+
files = []
|
|
358
|
+
for kind, rel, data in plan:
|
|
359
|
+
files.append(_verified(package.joinpath(*PurePosixPath(rel).parts), kind, data))
|
|
360
|
+
for name, data in workspace:
|
|
361
|
+
files.append(_verified(forge_version / name, "workspace", data))
|
|
362
|
+
counts = {"deliverables": len(DELIVERABLES), "references": len(references),
|
|
363
|
+
"scripts": sum(1 for kind, *_ in bundled if kind == "script"),
|
|
364
|
+
"assets": sum(1 for kind, *_ in bundled if kind == "asset"),
|
|
365
|
+
"manual": len(manual["files"]), "workspace": len(WORKSPACE_FILES)}
|
|
366
|
+
return {"status": "ok", "package": _posix(package), "forge_version": _posix(forge_version),
|
|
367
|
+
"files": files, "counts": counts, "ignored": ignored, "manual": manual, "warnings": warnings}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _verified(path: Path, kind: str, data: bytes) -> dict:
|
|
371
|
+
"""The promoted file's record, after checking it holds the bytes it came from."""
|
|
372
|
+
try:
|
|
373
|
+
written = path.read_bytes()
|
|
374
|
+
except OSError as exc:
|
|
375
|
+
raise WriteError(f"cannot read back {_posix(path)}: {exc}") from exc
|
|
376
|
+
if written != data:
|
|
377
|
+
raise WriteError(f"{_posix(path)} does not hold the bytes it was promoted from")
|
|
378
|
+
return {"path": _posix(path), "kind": kind, "bytes": len(written), "sha256": _sha256(written)}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
382
|
+
parser = argparse.ArgumentParser(
|
|
383
|
+
prog="skf-promote-staged.py",
|
|
384
|
+
description=__doc__,
|
|
385
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
386
|
+
)
|
|
387
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
388
|
+
p = sub.add_parser("promote", help="copy a staged skill into its package and forge version folders")
|
|
389
|
+
p.add_argument("--stage", required=True, type=Path, help="the staging folder, _bmad-output/.skf-stage/<skill>")
|
|
390
|
+
p.add_argument("--package", required=True, type=Path, help="the skill package folder, {skill_package}")
|
|
391
|
+
p.add_argument("--forge-version", required=True, type=Path, help="the forge version folder, {forge_version}")
|
|
392
|
+
p.add_argument("--inventory", type=Path, default=None,
|
|
393
|
+
help="the extraction inventory JSON whose scripts_inventory and assets_inventory are bundled")
|
|
394
|
+
p.add_argument("--source-root", type=Path, default=None,
|
|
395
|
+
help="the source tree the inventory's source_file paths are relative to")
|
|
396
|
+
p.add_argument("--carry-manual", type=Path, default=None,
|
|
397
|
+
help="the earlier package this one replaces: carry its [MANUAL] files and check "
|
|
398
|
+
"its [MANUAL] blocks are in the staged SKILL.md")
|
|
399
|
+
return parser
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def main(argv: list[str] | None = None) -> int:
|
|
403
|
+
args = _build_parser().parse_args(argv)
|
|
404
|
+
try:
|
|
405
|
+
result = promote(args.stage, args.package, args.forge_version, args.inventory, args.source_root,
|
|
406
|
+
args.carry_manual)
|
|
407
|
+
except InputError as exc:
|
|
408
|
+
print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
|
|
409
|
+
return 1
|
|
410
|
+
except WriteError as exc:
|
|
411
|
+
print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
|
|
412
|
+
return 2
|
|
413
|
+
print(json.dumps(result, indent=2))
|
|
414
|
+
return 0
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _force_utf8(*streams) -> None:
|
|
418
|
+
"""Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
|
|
419
|
+
for stream in streams:
|
|
420
|
+
if hasattr(stream, "reconfigure"):
|
|
421
|
+
errors = getattr(stream, "errors", None)
|
|
422
|
+
if errors is None:
|
|
423
|
+
stream.reconfigure(encoding="utf-8")
|
|
424
|
+
else:
|
|
425
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
if __name__ == "__main__":
|
|
429
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
430
|
+
sys.exit(main())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Provenance Gap Dispatch — Major-Version Scope Reconciliation feeder.
|
|
@@ -17,8 +17,16 @@ deterministic operations before the LLM can prompt the user:
|
|
|
17
17
|
- `promoted` for this path → already in scope (skip)
|
|
18
18
|
- `skipped` for this path → user previously declined (skip)
|
|
19
19
|
- `demoted-include` or `demoted-exclude` → pre-decided demotion
|
|
20
|
+
- `deferred-headless` for this path → a headless run met it and left
|
|
21
|
+
it for a person: unresolved, so the next interactive run asks
|
|
20
22
|
- no matching amendment → unresolved (caller prompts user)
|
|
21
23
|
|
|
24
|
+
A `skipped` amendment whose reason is `headless: no user to prompt`
|
|
25
|
+
is the automatic skip a headless run wrote before `deferred-headless`
|
|
26
|
+
existed. No person declined the path, so it counts as
|
|
27
|
+
`deferred-headless` too (as skf-resolve-authoritative-files.py reads
|
|
28
|
+
the same legacy record).
|
|
29
|
+
|
|
22
30
|
The prose previously asked the LLM to do all three per run. The
|
|
23
31
|
markdown parsing in particular is fiddly (heading lookahead, table
|
|
24
32
|
vs bullet vs definition list formats); keeping it in prose lets
|
|
@@ -45,7 +53,7 @@ Output JSON (stdout):
|
|
|
45
53
|
| "unresolved",
|
|
46
54
|
"prior_action": "promoted" | "skipped"
|
|
47
55
|
| "demoted-include" | "demoted-exclude"
|
|
48
|
-
| null
|
|
56
|
+
| "deferred-headless" | null
|
|
49
57
|
}, ...
|
|
50
58
|
],
|
|
51
59
|
"summary": {
|
|
@@ -59,6 +67,9 @@ Output JSON (stdout):
|
|
|
59
67
|
- `no-candidates` → report exists but Out-of-Scope section is absent/empty
|
|
60
68
|
- `candidates-found` → classified[] is populated; iterate unresolved entries
|
|
61
69
|
|
|
70
|
+
An `unresolved` entry with `prior_action: "deferred-headless"` is one a
|
|
71
|
+
headless run deferred; with `null` no run has decided it yet.
|
|
72
|
+
|
|
62
73
|
Exit codes:
|
|
63
74
|
0 — operation succeeded (any status)
|
|
64
75
|
1 — user error (paths invalid, brief unparseable)
|
|
@@ -67,6 +78,7 @@ Exit codes:
|
|
|
67
78
|
from __future__ import annotations
|
|
68
79
|
|
|
69
80
|
import argparse
|
|
81
|
+
import importlib.util
|
|
70
82
|
import json
|
|
71
83
|
import re
|
|
72
84
|
import sys
|
|
@@ -238,6 +250,34 @@ _PRE_DECIDED_SKIPPED = "pre-decided-skipped"
|
|
|
238
250
|
_PRE_DECIDED_DEMOTED = "pre-decided-demoted"
|
|
239
251
|
_UNRESOLVED = "unresolved"
|
|
240
252
|
|
|
253
|
+
_SKIPPED = "skipped"
|
|
254
|
+
_DEFERRED = "deferred-headless"
|
|
255
|
+
# The reason a headless run gave its automatic skip before deferred-headless
|
|
256
|
+
# existed. Such a skip was never a person's decision.
|
|
257
|
+
_LEGACY_HEADLESS_REASON = "headless: no user to prompt"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
_RESOLVER = None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _resolver():
|
|
264
|
+
"""skf-resolve-authoritative-files.py from this folder, loaded once: its
|
|
265
|
+
parse_brief_yaml reads a brief's YAML, an SKF 1.x brief's trailing `---`
|
|
266
|
+
included. Raises ValueError when it cannot be loaded."""
|
|
267
|
+
global _RESOLVER
|
|
268
|
+
if _RESOLVER is None:
|
|
269
|
+
path = Path(__file__).resolve().parent / "skf-resolve-authoritative-files.py"
|
|
270
|
+
spec = importlib.util.spec_from_file_location("skf_resolve_authoritative_files", path)
|
|
271
|
+
if spec is None or spec.loader is None:
|
|
272
|
+
raise ValueError(f"cannot load {path.name} beside {Path(__file__).name}")
|
|
273
|
+
module = importlib.util.module_from_spec(spec)
|
|
274
|
+
try:
|
|
275
|
+
spec.loader.exec_module(module)
|
|
276
|
+
except (OSError, ImportError, SyntaxError) as exc:
|
|
277
|
+
raise ValueError(f"cannot load {path.name} beside {Path(__file__).name}: {exc}") from exc
|
|
278
|
+
_RESOLVER = module
|
|
279
|
+
return _RESOLVER
|
|
280
|
+
|
|
241
281
|
|
|
242
282
|
def load_brief(brief_path: Path) -> dict:
|
|
243
283
|
"""Parse a skill-brief.yaml file. Raises ValueError on load failure."""
|
|
@@ -245,8 +285,9 @@ def load_brief(brief_path: Path) -> dict:
|
|
|
245
285
|
text = brief_path.read_text(encoding="utf-8")
|
|
246
286
|
except OSError as exc:
|
|
247
287
|
raise ValueError(f"cannot read brief at {brief_path}: {exc}") from exc
|
|
288
|
+
parse_brief_yaml = _resolver().parse_brief_yaml
|
|
248
289
|
try:
|
|
249
|
-
brief =
|
|
290
|
+
brief = parse_brief_yaml(text)
|
|
250
291
|
except yaml.YAMLError as exc:
|
|
251
292
|
raise ValueError(f"brief at {brief_path} is not valid YAML: {exc}") from exc
|
|
252
293
|
if not isinstance(brief, dict):
|
|
@@ -272,7 +313,7 @@ def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
|
|
|
272
313
|
if not isinstance(amend, dict):
|
|
273
314
|
continue
|
|
274
315
|
path = amend.get("path")
|
|
275
|
-
action = amend
|
|
316
|
+
action = _amendment_action(amend)
|
|
276
317
|
if isinstance(path, str) and isinstance(action, str):
|
|
277
318
|
by_path.setdefault(path, []).append(action)
|
|
278
319
|
|
|
@@ -293,20 +334,35 @@ def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
|
|
|
293
334
|
return classified
|
|
294
335
|
|
|
295
336
|
|
|
337
|
+
def _amendment_action(amend: dict) -> str | None:
|
|
338
|
+
"""The action an amendment records, reading a legacy headless skip as a deferral."""
|
|
339
|
+
action = amend.get("action")
|
|
340
|
+
if not isinstance(action, str):
|
|
341
|
+
return None
|
|
342
|
+
reason = amend.get("reason")
|
|
343
|
+
if action == _SKIPPED and isinstance(reason, str) and reason.strip() == _LEGACY_HEADLESS_REASON:
|
|
344
|
+
return _DEFERRED
|
|
345
|
+
return action
|
|
346
|
+
|
|
347
|
+
|
|
296
348
|
def _classify(prior_actions: list[str]) -> tuple[str, str | None]:
|
|
297
349
|
"""Pick the dominant amendment action for a single candidate path.
|
|
298
350
|
|
|
299
351
|
A path may have multiple amendments (e.g. promoted then later
|
|
300
|
-
demoted-exclude). Most recent action wins
|
|
301
|
-
amendment-record-insertion order, so iterate from the end.
|
|
352
|
+
demoted-exclude). Most recent action wins: the list is in
|
|
353
|
+
amendment-record-insertion order, so iterate from the end. A
|
|
354
|
+
deferred-headless amendment decides nothing: the path stays
|
|
355
|
+
unresolved for the next interactive run, which a person answers.
|
|
302
356
|
"""
|
|
303
357
|
for action in reversed(prior_actions):
|
|
304
358
|
if action == "promoted":
|
|
305
359
|
return _ALREADY_IN_SCOPE, "promoted"
|
|
306
|
-
if action ==
|
|
307
|
-
return _PRE_DECIDED_SKIPPED,
|
|
360
|
+
if action == _SKIPPED:
|
|
361
|
+
return _PRE_DECIDED_SKIPPED, _SKIPPED
|
|
308
362
|
if action in ("demoted-include", "demoted-exclude"):
|
|
309
363
|
return _PRE_DECIDED_DEMOTED, action
|
|
364
|
+
if action == _DEFERRED:
|
|
365
|
+
return _UNRESOLVED, _DEFERRED
|
|
310
366
|
return _UNRESOLVED, None
|
|
311
367
|
|
|
312
368
|
|