bmad-module-skill-forge 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +51 -49
- package/docs/404.md +10 -8
- package/docs/_data/pinned.yaml +22 -13
- package/docs/_internal/STABILITY.md +58 -35
- package/docs/agents.md +39 -53
- package/docs/architecture.md +65 -51
- package/docs/bmad-synergy.md +53 -51
- package/docs/campaign.md +127 -75
- package/docs/concepts.md +34 -28
- package/docs/examples.md +70 -68
- package/docs/forge-auto.md +34 -31
- package/docs/getting-started.md +118 -57
- package/docs/how-it-works.md +25 -21
- package/docs/index.md +15 -15
- package/docs/skill-model.md +70 -59
- package/docs/troubleshooting.md +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +14 -9
- package/src/shared/references/feasibility-report-schema.md +19 -13
- package/src/shared/references/headless-gate-convention.md +55 -3
- package/src/shared/references/output-contract-schema.md +19 -8
- package/src/shared/references/pipeline-contracts.md +94 -29
- package/src/shared/references/tessl-review.md +77 -0
- package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
- package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
- package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
- package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
- package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
- package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
- package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
- package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
- package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
- package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
- package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
- package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
- package/src/shared/scripts/skf-atomic-write.py +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +117 -9
- package/src/shared/scripts/skf-detect-docs.py +892 -4
- package/src/shared/scripts/skf-detect-language.py +486 -107
- package/src/shared/scripts/skf-detect-registry.py +887 -0
- package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
- package/src/shared/scripts/skf-detect-tools.py +215 -27
- package/src/shared/scripts/skf-detect-workspaces.py +506 -30
- package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
- package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
- package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
- package/src/shared/scripts/skf-extract-public-api.py +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +17 -6
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +55 -18
- package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
- package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
- package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
- package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
- package/src/skf-create-skill/references/validate.md +173 -99
- package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
- package/src/skf-create-stack-skill/SKILL.md +29 -56
- package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
- package/src/skf-create-stack-skill/customize.toml +23 -12
- package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
- package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
- package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
- package/src/skf-create-stack-skill/references/generate-output.md +138 -128
- package/src/skf-create-stack-skill/references/init.md +90 -32
- package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
- package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
- package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
- package/src/skf-create-stack-skill/references/report.md +39 -33
- package/src/skf-create-stack-skill/references/validate.md +20 -39
- package/src/skf-drop-skill/SKILL.md +28 -51
- package/src/skf-drop-skill/customize.toml +44 -30
- package/src/skf-drop-skill/references/execute.md +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -89
- package/src/skf-setup/references/detect-and-tier.md +58 -66
- package/src/skf-setup/references/health-check.md +2 -0
- package/src/skf-setup/references/invocation-contract.md +14 -0
- package/src/skf-setup/references/report.md +43 -166
- package/src/skf-setup/references/tier-rules.md +5 -5
- package/src/skf-setup/references/write-config.md +34 -57
- package/src/skf-test-skill/SKILL.md +17 -57
- package/src/skf-test-skill/assets/output-section-formats.md +33 -34
- package/src/skf-test-skill/customize.toml +27 -13
- package/src/skf-test-skill/references/coherence-check.md +134 -62
- package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
- package/src/skf-test-skill/references/coverage-check.md +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -645
- package/release-audits/v1.0.0-launch-audit.md +0 -728
- package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
- package/src/skf-campaign/manifest.yaml +0 -15
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
- package/src/skf-create-skill/references/ecosystem-check.md +0 -74
- package/src/skf-drop-skill/references/headless-contract.md +0 -18
- package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
- package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
- package/src/skf-update-skill/references/validate.md +0 -124
- package/tools/validate-docs-drift.js +0 -233
- package/tools/validate-docs-links.js +0 -665
|
@@ -1,43 +1,94 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
|
-
"""Campaign Render Kickoff
|
|
6
|
-
|
|
7
|
-
step-05 emits a kickoff message per Tier-A skill.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
{{
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
"""Campaign Render Kickoff: render the per-skill kickoff message, every slot.
|
|
6
|
+
|
|
7
|
+
step-05 emits a kickoff message per Tier-A skill. Each placeholder is data the
|
|
8
|
+
campaign already holds, so this script fills all of them and step-05 presents
|
|
9
|
+
the output as it is. Re-typing them for each of 15+ skills cost output tokens
|
|
10
|
+
and risked paraphrasing or cutting the operator's directive.
|
|
11
|
+
|
|
12
|
+
- From state and brief: campaign name, stage, quality gate, skill identity,
|
|
13
|
+
repo, pin, commit and the dependency-status table.
|
|
14
|
+
- {{workarounds_list}}: one bullet per workaround the pre-apply helper
|
|
15
|
+
applied, read from its log (--workarounds-file, the preapply-log.json
|
|
16
|
+
skf-preapply.py writes): the file it changed, the fingerprint and its
|
|
17
|
+
fix, and the severity. Without the file, the skill's workarounds_applied
|
|
18
|
+
from state.
|
|
19
|
+
- {{brief_summary}}: the skill's target entry in the campaign brief, every
|
|
20
|
+
field it carries (a language or scope hint included).
|
|
21
|
+
- {{directive_content}}: the directive file byte for byte, whatever its
|
|
22
|
+
encoding, or "No directive configured" when --directive-file is absent or
|
|
23
|
+
names no file.
|
|
24
|
+
- {{persistent_facts}}: one bullet per persistent fact, or "None". An entry
|
|
25
|
+
`file:<path-or-glob>` adds the content of each file it names, with
|
|
26
|
+
`{project-root}` replaced by --project-root; an entry `!<entry>` adds
|
|
27
|
+
nothing and drops every earlier entry equal to `<entry>`, so a team or
|
|
28
|
+
personal override drops a bundled default
|
|
29
|
+
(`!file:{project-root}/**/project-context.md`); any other entry is a fact
|
|
30
|
+
sentence.
|
|
17
31
|
|
|
18
32
|
CLI:
|
|
19
33
|
uv run campaign-render-kickoff.py --state-file <p> --brief-file <p> \
|
|
20
|
-
--skill <name> --template <p> [--workarounds
|
|
34
|
+
--skill <name> --template <p> [--workarounds-file <preapply-log.json>] \
|
|
35
|
+
[--facts-json '<json-list>' | --facts-json -] [--project-root <p>] \
|
|
36
|
+
[--directive-file <p>]
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
`--facts-json -` reads the facts from stdin: a JSON list, or the object the
|
|
39
|
+
customization resolver prints for `--key workflow.persistent_facts`.
|
|
40
|
+
|
|
41
|
+
Output: the rendered kickoff markdown on stdout, as UTF-8 bytes. The template
|
|
42
|
+
is filled in one pass, so a `{{...}}` inside the directive or a fact is kept
|
|
43
|
+
as written.
|
|
23
44
|
|
|
24
45
|
Exit codes:
|
|
25
46
|
0 rendered
|
|
26
|
-
2 error
|
|
47
|
+
2 error, as {"error", "code"} on stderr:
|
|
48
|
+
STATE_NOT_FOUND, BRIEF_NOT_FOUND, TEMPLATE_NOT_FOUND
|
|
49
|
+
the input file is missing
|
|
50
|
+
STATE_UNREADABLE, BRIEF_UNREADABLE, TEMPLATE_UNREADABLE
|
|
51
|
+
it cannot be read as UTF-8 text
|
|
52
|
+
PARSE_ERROR state or brief is not a YAML mapping
|
|
53
|
+
SKILL_NOT_FOUND the skill is not in state
|
|
54
|
+
BAD_WORKAROUNDS --workarounds-file cannot be read, or holds no
|
|
55
|
+
applied[] list of workarounds
|
|
56
|
+
BAD_FACTS --facts-json is not a list of strings (nor the
|
|
57
|
+
resolver's object), --project-root is not a
|
|
58
|
+
directory, or a `file:` entry keeps a placeholder
|
|
59
|
+
FACTS_FILE_NOT_FOUND a `file:` path with no glob character names no
|
|
60
|
+
file (a glob that matches nothing adds no fact)
|
|
61
|
+
FACTS_UNREADABLE a facts file cannot be read
|
|
62
|
+
DIRECTIVE_UNREADABLE the directive file cannot be read
|
|
27
63
|
"""
|
|
28
64
|
|
|
29
65
|
from __future__ import annotations
|
|
30
66
|
|
|
31
67
|
import argparse
|
|
68
|
+
import glob
|
|
32
69
|
import json
|
|
70
|
+
import re
|
|
33
71
|
import sys
|
|
34
72
|
from pathlib import Path
|
|
35
73
|
from typing import Any, Dict, List, Optional
|
|
36
74
|
|
|
37
75
|
import yaml
|
|
38
76
|
|
|
39
|
-
|
|
40
|
-
|
|
77
|
+
NO_DIRECTIVE = "No directive configured"
|
|
78
|
+
NO_FACTS = "None"
|
|
79
|
+
FILE_PREFIX = "file:"
|
|
80
|
+
DROP_PREFIX = "!"
|
|
81
|
+
PROJECT_ROOT = "{project-root}"
|
|
82
|
+
# The key the customization resolver prints for --key workflow.persistent_facts
|
|
83
|
+
# (it prints {} when no layer sets it).
|
|
84
|
+
RESOLVER_KEY = "workflow.persistent_facts"
|
|
85
|
+
GLOB_CHARS = ("*", "?", "[")
|
|
86
|
+
# Target fields shown first, in this order; any other field of the entry
|
|
87
|
+
# follows in the entry's own order.
|
|
88
|
+
BRIEF_FIELDS = ("name", "repo_url", "tier", "pin", "depends_on")
|
|
89
|
+
SLOT_RE = re.compile(r"\{\{[a-z_]+\}\}")
|
|
90
|
+
# A placeholder left in a `file:` entry would match no file.
|
|
91
|
+
UNRESOLVED_RE = re.compile(r"\{[A-Za-z][\w-]*\}")
|
|
41
92
|
|
|
42
93
|
|
|
43
94
|
def _err(message: str, code: str) -> int:
|
|
@@ -64,10 +115,134 @@ def _dependency_status_table(skill: Dict[str, Any], skill_map: Dict[str, Dict[st
|
|
|
64
115
|
return "\n".join(rows)
|
|
65
116
|
|
|
66
117
|
|
|
67
|
-
def
|
|
118
|
+
def _code(text: Any) -> str:
|
|
119
|
+
"""Inline code that survives a backtick inside the text."""
|
|
120
|
+
text = str(text)
|
|
121
|
+
return f"`` {text} ``" if "`" in text else f"`{text}`"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _workaround(entry: Any) -> str:
|
|
125
|
+
"""One applied workaround: a pre-apply log entry, or a string from state."""
|
|
126
|
+
if not isinstance(entry, dict):
|
|
127
|
+
return str(entry)
|
|
128
|
+
line = f"{_code(entry.get('fingerprint', ''))} replaced with {_code(entry.get('fix', ''))}"
|
|
129
|
+
if entry.get("file"):
|
|
130
|
+
line = f"{entry['file']}: {line}"
|
|
131
|
+
if entry.get("severity"):
|
|
132
|
+
line += f" ({entry['severity']} severity)"
|
|
133
|
+
return line
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _workarounds_list(workarounds: List[Any]) -> str:
|
|
68
137
|
if not workarounds:
|
|
69
138
|
return "None"
|
|
70
|
-
return "\n".join(f"- {w}" for w in workarounds)
|
|
139
|
+
return "\n".join(f"- {_workaround(w)}" for w in workarounds)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def read_workarounds(path: str) -> List[Any]:
|
|
143
|
+
"""The applied[] list of a pre-apply log. Raises ValueError on any other file."""
|
|
144
|
+
try:
|
|
145
|
+
log = json.loads(Path(path).read_bytes().decode("utf-8-sig"))
|
|
146
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
147
|
+
raise ValueError(f"cannot read {path}: {exc}") from exc
|
|
148
|
+
applied = log.get("applied") if isinstance(log, dict) else None
|
|
149
|
+
if not isinstance(applied, list):
|
|
150
|
+
raise ValueError(f"{path} holds no applied[] list")
|
|
151
|
+
return applied
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _field_value(key: str, value: Any) -> str:
|
|
155
|
+
if value is None or value == "" or value == []:
|
|
156
|
+
return "latest" if key == "pin" else "none"
|
|
157
|
+
if isinstance(value, bool):
|
|
158
|
+
return "true" if value else "false"
|
|
159
|
+
if isinstance(value, list):
|
|
160
|
+
return ", ".join(str(v) for v in value)
|
|
161
|
+
if isinstance(value, dict):
|
|
162
|
+
return json.dumps(value, sort_keys=True)
|
|
163
|
+
return str(value)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _brief_summary(skill_name: str, target: Optional[Dict[str, Any]]) -> str:
|
|
167
|
+
if not target:
|
|
168
|
+
return f"The campaign brief has no target entry for `{skill_name}`."
|
|
169
|
+
keys = [k for k in BRIEF_FIELDS if k in target] + [k for k in target if k not in BRIEF_FIELDS]
|
|
170
|
+
return "\n".join(f"- `{key}`: {_field_value(key, target[key])}" for key in keys)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _bullet(text: str) -> str:
|
|
174
|
+
"""One markdown bullet; a fact of several lines continues indented under it."""
|
|
175
|
+
first, *rest = text.strip().splitlines()
|
|
176
|
+
lines = [f"- {first}"] + [f" {line}" if line.strip() else "" for line in rest]
|
|
177
|
+
return "\n".join(lines)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _facts_list(facts: List[str]) -> str:
|
|
181
|
+
bullets = [_bullet(fact) for fact in facts if fact.strip()]
|
|
182
|
+
return "\n".join(bullets) if bullets else NO_FACTS
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _fact_files(entry: str, pattern: str, root: str) -> List[Path]:
|
|
186
|
+
"""The files a `file:` entry names: the path itself, else every file its glob matches.
|
|
187
|
+
|
|
188
|
+
`{project-root}` in the pattern becomes root, escaped where it joins a glob.
|
|
189
|
+
A path with no glob character that names no file raises FileNotFoundError,
|
|
190
|
+
so a mistyped path stops the kickoff instead of dropping its facts; a glob
|
|
191
|
+
that matches nothing adds no file.
|
|
192
|
+
"""
|
|
193
|
+
path = Path(pattern.replace(PROJECT_ROOT, root))
|
|
194
|
+
if path.is_file():
|
|
195
|
+
return [path]
|
|
196
|
+
if not any(c in pattern for c in GLOB_CHARS):
|
|
197
|
+
raise FileNotFoundError(f"`{entry}` names no file ({path.as_posix()})")
|
|
198
|
+
matches = glob.glob(pattern.replace(PROJECT_ROOT, glob.escape(root)), recursive=True)
|
|
199
|
+
return [Path(p) for p in sorted(matches) if Path(p).is_file()]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_facts(entries: List[str], project_root: Optional[str] = None) -> List[str]:
|
|
203
|
+
"""Turn persistent-facts entries into the facts the kickoff lists.
|
|
204
|
+
|
|
205
|
+
A `!` entry adds nothing and drops every earlier entry equal to what
|
|
206
|
+
follows the `!`, before any `file:` entry is read, so a dropped file is
|
|
207
|
+
never opened. A `file:` entry adds one fact per file it names, headed by
|
|
208
|
+
the file's path. Raises ValueError on an entry that keeps a placeholder
|
|
209
|
+
(as `{project-root}` does when project_root is None), FileNotFoundError
|
|
210
|
+
on a path that names no file, OSError on a file that cannot be read.
|
|
211
|
+
"""
|
|
212
|
+
kept: List[str] = []
|
|
213
|
+
for entry in entries:
|
|
214
|
+
if entry.startswith(DROP_PREFIX):
|
|
215
|
+
dropped = entry[len(DROP_PREFIX):]
|
|
216
|
+
kept = [earlier for earlier in kept if earlier != dropped]
|
|
217
|
+
else:
|
|
218
|
+
kept.append(entry)
|
|
219
|
+
facts: List[str] = []
|
|
220
|
+
for entry in kept:
|
|
221
|
+
if not entry.startswith(FILE_PREFIX):
|
|
222
|
+
facts.append(entry)
|
|
223
|
+
continue
|
|
224
|
+
pattern = entry[len(FILE_PREFIX):].strip()
|
|
225
|
+
left = pattern if project_root is None else pattern.replace(PROJECT_ROOT, "")
|
|
226
|
+
placeholder = UNRESOLVED_RE.search(left)
|
|
227
|
+
if placeholder:
|
|
228
|
+
hint = "; pass --project-root" if placeholder.group(0) == PROJECT_ROOT else ""
|
|
229
|
+
raise ValueError(f"`{entry}` holds the unresolved placeholder `{placeholder.group(0)}`{hint}")
|
|
230
|
+
for path in _fact_files(entry, pattern, project_root or ""):
|
|
231
|
+
text = path.read_bytes().decode("utf-8", errors="replace")
|
|
232
|
+
facts.append(f"From `{path.as_posix()}`:\n\n{text}")
|
|
233
|
+
return facts
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def read_directive(path: Optional[str]) -> Optional[str]:
|
|
237
|
+
"""The directive file's text exactly as stored, or None when no file is there.
|
|
238
|
+
|
|
239
|
+
A byte that is not UTF-8 is kept as a surrogate, which _write_stdout turns
|
|
240
|
+
back into the same byte, so a directive in another encoding comes through
|
|
241
|
+
unchanged.
|
|
242
|
+
"""
|
|
243
|
+
if not path or not Path(path).is_file():
|
|
244
|
+
return None
|
|
245
|
+
return Path(path).read_bytes().decode("utf-8", errors="surrogateescape")
|
|
71
246
|
|
|
72
247
|
|
|
73
248
|
def render_kickoff(
|
|
@@ -75,7 +250,9 @@ def render_kickoff(
|
|
|
75
250
|
brief: Dict[str, Any],
|
|
76
251
|
skill_name: str,
|
|
77
252
|
template: str,
|
|
78
|
-
workarounds: Optional[List[
|
|
253
|
+
workarounds: Optional[List[Any]] = None,
|
|
254
|
+
directive: Optional[str] = None,
|
|
255
|
+
facts: Optional[List[str]] = None,
|
|
79
256
|
) -> str:
|
|
80
257
|
campaign = state.get("campaign", {})
|
|
81
258
|
skills = state.get("skills", [])
|
|
@@ -84,12 +261,13 @@ def render_kickoff(
|
|
|
84
261
|
raise KeyError(f"Skill '{skill_name}' not found in state")
|
|
85
262
|
skill = skill_map[skill_name]
|
|
86
263
|
|
|
87
|
-
targets = {t
|
|
88
|
-
|
|
264
|
+
targets = {t.get("name"): t for t in (brief.get("targets") or []) if isinstance(t, dict)}
|
|
265
|
+
target = targets.get(skill_name)
|
|
266
|
+
repo_url = (target or {}).get("repo_url", "")
|
|
89
267
|
|
|
90
268
|
wa = workarounds if workarounds is not None else (skill.get("workarounds_applied", []) or [])
|
|
91
269
|
|
|
92
|
-
|
|
270
|
+
values = {
|
|
93
271
|
"{{campaign_name}}": str(campaign.get("name", "")),
|
|
94
272
|
"{{current_stage}}": str(campaign.get("current_stage", "")),
|
|
95
273
|
"{{quality_gate_summary}}": _quality_gate_summary(campaign.get("quality_gate", {})),
|
|
@@ -100,59 +278,174 @@ def render_kickoff(
|
|
|
100
278
|
"{{repo_url}}": repo_url,
|
|
101
279
|
"{{workarounds_list}}": _workarounds_list(wa),
|
|
102
280
|
"{{dependency_status_table}}": _dependency_status_table(skill, skill_map),
|
|
281
|
+
"{{brief_summary}}": _brief_summary(skill_name, target),
|
|
282
|
+
"{{persistent_facts}}": _facts_list(facts or []),
|
|
283
|
+
"{{directive_content}}": NO_DIRECTIVE if directive is None else directive,
|
|
103
284
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
285
|
+
# One pass: a filled-in value is never searched again.
|
|
286
|
+
return SLOT_RE.sub(lambda m: values.get(m.group(0), m.group(0)), template)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _facts_entries(raw: str) -> List[Any]:
|
|
290
|
+
"""The entries a --facts-json value holds: a JSON list, or the resolver's object."""
|
|
291
|
+
if not raw.strip():
|
|
292
|
+
raise ValueError("it is empty (with -, the command piped into stdin printed nothing)")
|
|
293
|
+
value = json.loads(raw)
|
|
294
|
+
if isinstance(value, dict) and set(value) <= {RESOLVER_KEY}:
|
|
295
|
+
value = value.get(RESOLVER_KEY, [])
|
|
296
|
+
if not isinstance(value, list):
|
|
297
|
+
raise ValueError(f"not a list, nor an object holding `{RESOLVER_KEY}`")
|
|
298
|
+
return value
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _read_stdin() -> str:
|
|
302
|
+
"""stdin as UTF-8, the encoding the resolver writes; a byte-order mark is dropped."""
|
|
303
|
+
buffer = getattr(sys.stdin, "buffer", None)
|
|
304
|
+
if buffer is None:
|
|
305
|
+
return sys.stdin.read()
|
|
306
|
+
return buffer.read().decode("utf-8-sig")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _write_stdout(text: str) -> None:
|
|
310
|
+
"""Write UTF-8 bytes as they are, so no platform rewrites the directive's line endings.
|
|
311
|
+
|
|
312
|
+
A surrogate read_directive kept for a byte that is not UTF-8 becomes that byte again.
|
|
313
|
+
"""
|
|
314
|
+
buffer = getattr(sys.stdout, "buffer", None)
|
|
315
|
+
if buffer is None:
|
|
316
|
+
sys.stdout.write(text)
|
|
317
|
+
return
|
|
318
|
+
sys.stdout.flush()
|
|
319
|
+
buffer.write(text.encode("utf-8", errors="surrogateescape"))
|
|
320
|
+
buffer.flush()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def run(
|
|
324
|
+
state_file: str,
|
|
325
|
+
brief_file: str,
|
|
326
|
+
skill: str,
|
|
327
|
+
template_file: str,
|
|
328
|
+
workarounds_file: Optional[str],
|
|
329
|
+
facts_json: Optional[str] = None,
|
|
330
|
+
directive_file: Optional[str] = None,
|
|
331
|
+
project_root: Optional[str] = None,
|
|
332
|
+
) -> int:
|
|
333
|
+
texts: Dict[str, str] = {}
|
|
112
334
|
for label, p in (("State", state_file), ("Brief", brief_file), ("Template", template_file)):
|
|
113
335
|
if not Path(p).is_file():
|
|
114
336
|
return _err(f"{label} file not found: {p}", f"{label.upper()}_NOT_FOUND")
|
|
337
|
+
try:
|
|
338
|
+
texts[label] = Path(p).read_text(encoding="utf-8")
|
|
339
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
340
|
+
return _err(f"{label} file unreadable: {p}: {exc}", f"{label.upper()}_UNREADABLE")
|
|
115
341
|
|
|
116
342
|
try:
|
|
117
|
-
state = yaml.safe_load(
|
|
118
|
-
brief = yaml.safe_load(
|
|
343
|
+
state = yaml.safe_load(texts["State"])
|
|
344
|
+
brief = yaml.safe_load(texts["Brief"])
|
|
119
345
|
except yaml.YAMLError as exc:
|
|
120
346
|
return _err(f"Failed to parse YAML: {exc}", "PARSE_ERROR")
|
|
121
|
-
|
|
347
|
+
if not isinstance(state, dict) or not isinstance(brief, dict):
|
|
348
|
+
return _err("State and brief must each be a YAML mapping", "PARSE_ERROR")
|
|
349
|
+
|
|
350
|
+
workarounds: Optional[List[Any]] = None
|
|
351
|
+
if workarounds_file:
|
|
352
|
+
try:
|
|
353
|
+
workarounds = read_workarounds(workarounds_file)
|
|
354
|
+
except ValueError as exc:
|
|
355
|
+
return _err(f"--workarounds-file: {exc}", "BAD_WORKAROUNDS")
|
|
122
356
|
|
|
123
|
-
|
|
124
|
-
if
|
|
357
|
+
entries: List[Any] = []
|
|
358
|
+
if facts_json:
|
|
125
359
|
try:
|
|
126
|
-
|
|
127
|
-
if not isinstance(workarounds, list):
|
|
128
|
-
raise ValueError("not a list")
|
|
360
|
+
entries = _facts_entries(_read_stdin() if facts_json == "-" else facts_json)
|
|
129
361
|
except ValueError as exc:
|
|
130
|
-
return _err(f"--
|
|
362
|
+
return _err(f"--facts-json must be a JSON list of strings: {exc}", "BAD_FACTS")
|
|
363
|
+
if not all(isinstance(e, str) for e in entries):
|
|
364
|
+
return _err("--facts-json must be a JSON list of strings: an entry is not a string", "BAD_FACTS")
|
|
365
|
+
if project_root is not None and (not project_root.strip() or not Path(project_root).is_dir()):
|
|
366
|
+
return _err(f"--project-root is not a directory: '{project_root}'", "BAD_FACTS")
|
|
367
|
+
try:
|
|
368
|
+
facts = load_facts(entries, project_root)
|
|
369
|
+
except ValueError as exc:
|
|
370
|
+
return _err(f"--facts-json: {exc}", "BAD_FACTS")
|
|
371
|
+
except FileNotFoundError as exc:
|
|
372
|
+
return _err(f"Persistent facts file not found: {exc}", "FACTS_FILE_NOT_FOUND")
|
|
373
|
+
except OSError as exc:
|
|
374
|
+
return _err(f"Persistent facts file unreadable: {exc}", "FACTS_UNREADABLE")
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
directive = read_directive(directive_file)
|
|
378
|
+
except OSError as exc:
|
|
379
|
+
return _err(f"Directive file unreadable: {directive_file}: {exc}", "DIRECTIVE_UNREADABLE")
|
|
131
380
|
|
|
132
381
|
try:
|
|
133
|
-
rendered = render_kickoff(state, brief, skill,
|
|
382
|
+
rendered = render_kickoff(state, brief, skill, texts["Template"], workarounds, directive, facts)
|
|
134
383
|
except KeyError as exc:
|
|
135
384
|
return _err(str(exc), "SKILL_NOT_FOUND")
|
|
136
385
|
|
|
137
|
-
|
|
138
|
-
if not rendered.endswith("\n"):
|
|
139
|
-
sys.stdout.write("\n")
|
|
386
|
+
_write_stdout(rendered if rendered.endswith("\n") else rendered + "\n")
|
|
140
387
|
return 0
|
|
141
388
|
|
|
142
389
|
|
|
143
|
-
def
|
|
390
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
144
391
|
parser = argparse.ArgumentParser(
|
|
145
392
|
prog="campaign-render-kickoff",
|
|
146
|
-
description="Render the
|
|
393
|
+
description="Render the campaign kickoff template for one skill, every placeholder filled.",
|
|
147
394
|
)
|
|
148
395
|
parser.add_argument("--state-file", required=True)
|
|
149
396
|
parser.add_argument("--brief-file", required=True)
|
|
150
397
|
parser.add_argument("--skill", required=True, help="skill name (must exist in state)")
|
|
151
398
|
parser.add_argument("--template", required=True, dest="template_file")
|
|
152
|
-
parser.add_argument(
|
|
153
|
-
|
|
154
|
-
|
|
399
|
+
parser.add_argument(
|
|
400
|
+
"--workarounds-file",
|
|
401
|
+
help="the pre-apply log (preapply-log.json) whose applied[] workarounds the kickoff lists",
|
|
402
|
+
)
|
|
403
|
+
parser.add_argument(
|
|
404
|
+
"--facts-json",
|
|
405
|
+
help="persistent facts as a JSON list of sentences, file:<path-or-glob> entries and !<entry> "
|
|
406
|
+
"entries (each drops the earlier entries equal to <entry>), or - to read them from stdin "
|
|
407
|
+
"(a list, or the resolver's output for --key workflow.persistent_facts)",
|
|
408
|
+
)
|
|
409
|
+
parser.add_argument(
|
|
410
|
+
"--project-root",
|
|
411
|
+
help="project root that replaces {project-root} in file: entries",
|
|
412
|
+
)
|
|
413
|
+
parser.add_argument(
|
|
414
|
+
"--directive-file",
|
|
415
|
+
help="directive file to inline byte for byte (absent or missing: 'No directive configured')",
|
|
416
|
+
)
|
|
417
|
+
return parser
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def main(argv: list[str] | None = None) -> int:
|
|
421
|
+
args = build_parser().parse_args(argv)
|
|
422
|
+
return run(
|
|
423
|
+
args.state_file,
|
|
424
|
+
args.brief_file,
|
|
425
|
+
args.skill,
|
|
426
|
+
args.template_file,
|
|
427
|
+
args.workarounds_file,
|
|
428
|
+
args.facts_json,
|
|
429
|
+
args.directive_file,
|
|
430
|
+
args.project_root,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _force_utf8(*streams) -> None:
|
|
435
|
+
"""Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
|
|
436
|
+
|
|
437
|
+
A Windows console pipes them as cp1252, which cannot print every character
|
|
438
|
+
a directive or a fact may hold.
|
|
439
|
+
"""
|
|
440
|
+
for stream in streams:
|
|
441
|
+
if hasattr(stream, "reconfigure"):
|
|
442
|
+
errors = getattr(stream, "errors", None)
|
|
443
|
+
if errors is None:
|
|
444
|
+
stream.reconfigure(encoding="utf-8")
|
|
445
|
+
else:
|
|
446
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
155
447
|
|
|
156
448
|
|
|
157
449
|
if __name__ == "__main__":
|
|
450
|
+
_force_utf8(sys.stdout, sys.stderr)
|
|
158
451
|
raise SystemExit(main())
|