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,624 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Update Run State: what an update-skill run wrote, so a halt can undo it.
|
|
6
|
+
|
|
7
|
+
An update writes the skill in one of two ways. A gap-driven repair edits the
|
|
8
|
+
current version in place (SKILL.md, references/, metadata.json and the
|
|
9
|
+
version's provenance map and evidence report, and for a rescope the skill
|
|
10
|
+
brief). Every other mode creates a new version folder and its forge folder
|
|
11
|
+
beside the unchanged previous version. Before the first write, merge.md
|
|
12
|
+
runs `begin`, which records in the run folder what the run is about to
|
|
13
|
+
change: for an in-place repair a snapshot of every file it may write, for a
|
|
14
|
+
new version the folders it creates. A HALT after that runs `rollback`,
|
|
15
|
+
which puts the skill back as it was: it restores the snapshot, or removes
|
|
16
|
+
the folders this run created. write.md runs `finish` once the run's writes
|
|
17
|
+
stand (after its last check), and from then on `rollback` changes
|
|
18
|
+
nothing. A run that died (its run lock went stale) never rolled
|
|
19
|
+
back: the next update's init.md hands the stale lock's owner to
|
|
20
|
+
`rollback --run-root --owner --remove-run-dir`, which finds that run's
|
|
21
|
+
folder by the run id in the owner. Every HALT of an update runs `halt`,
|
|
22
|
+
which does a halt's cleanup in one call.
|
|
23
|
+
|
|
24
|
+
CLI:
|
|
25
|
+
|
|
26
|
+
uv run skf-update-run-state.py begin --run-dir <dir> --mode in-place \\
|
|
27
|
+
--package <skill package> --forge-version <forge version folder> \\
|
|
28
|
+
[--brief <skill-brief.yaml>]
|
|
29
|
+
uv run skf-update-run-state.py begin --run-dir <dir> --mode new-version \\
|
|
30
|
+
--created <folder> [--created <folder> ...] [--staging <folder>] \\
|
|
31
|
+
[--skill-group <skill group>]
|
|
32
|
+
uv run skf-update-run-state.py rollback --run-dir <dir> [--remove-run-dir]
|
|
33
|
+
uv run skf-update-run-state.py rollback --run-root <.skf-run folder> \\
|
|
34
|
+
--owner <the stale lock's owner> --skill <skill> --remove-run-dir
|
|
35
|
+
uv run skf-update-run-state.py finish --run-dir <dir>
|
|
36
|
+
uv run skf-update-run-state.py halt --run-dir <dir> [--tree <tree>] \\
|
|
37
|
+
[--lock <lock file> --owner <owner>] [--emit] < <halt payload>
|
|
38
|
+
|
|
39
|
+
begin (in-place) copies the package folder to <run-dir>/snapshot/package
|
|
40
|
+
(each link copied as a link, never followed), the forge version folder's
|
|
41
|
+
provenance-map.json and evidence-report.md to <run-dir>/snapshot/forge/,
|
|
42
|
+
and the brief to <run-dir>/snapshot/skill-brief.yaml, noting which of the
|
|
43
|
+
files existed. begin (new-version) records each --created folder, which
|
|
44
|
+
must not exist yet (exit 3 names the first that does: a version is never
|
|
45
|
+
overwritten), and the --staging folder, which may (a staging folder an
|
|
46
|
+
interrupted run left is the stage step's to clear). Either way it writes
|
|
47
|
+
<run-dir>/run-state.json and refuses a run folder that already holds one.
|
|
48
|
+
|
|
49
|
+
rollback reads run-state.json. Nothing to do when there is none, when the
|
|
50
|
+
run finished, or when it already rolled back. In place, it rebuilds the
|
|
51
|
+
package from the snapshot beside it and swaps it in, restores each forge
|
|
52
|
+
file and the brief, and removes one the run created. For a new version it
|
|
53
|
+
removes each recorded folder, never a link; once the skill group's
|
|
54
|
+
`active` link names the run's version folder, that version is live and
|
|
55
|
+
every recorded folder is kept (each named, with the reason). With
|
|
56
|
+
--remove-run-dir it then deletes the run folder (an interrupted run's),
|
|
57
|
+
which must be `.skf-run/skf-update-skill-<run id>` with a run id that holds
|
|
58
|
+
no path separator and no `..` (exit 1 otherwise, before anything changes).
|
|
59
|
+
With --run-root and --owner in place of --run-dir, the run folder is
|
|
60
|
+
<run-root>/skf-update-skill-<run id>, the run id taken from an owner
|
|
61
|
+
`update-skill:<skill>:<run id>`; an owner of another workflow or another
|
|
62
|
+
skill, or one with no run id (an older SKF's lock), names no run folder
|
|
63
|
+
and changes nothing (status `no-run-folder`).
|
|
64
|
+
|
|
65
|
+
finish marks the run finished, then deletes the snapshot.
|
|
66
|
+
|
|
67
|
+
halt is a HALT's cleanup, in this order, never stopping on a result: the
|
|
68
|
+
rollback above when the run folder holds a run state; with --tree, the
|
|
69
|
+
private source tree removed (skf-source-tree.py close); with --lock and
|
|
70
|
+
--owner, the run lock released (skf-run-lock.py release, which never
|
|
71
|
+
removes a lock another run holds); with --emit, the halt payload read on
|
|
72
|
+
stdin printed through skf-emit-result-envelope.py emit-halt --workflow
|
|
73
|
+
skf-update-skill (with --run-dir when the folder still exists, so the
|
|
74
|
+
line carries the decisions and warnings the run recorded). Each step it
|
|
75
|
+
could not finish adds a warning to the payload's warnings[] (and to its
|
|
76
|
+
own output): `rollback-incomplete: <path>: <error>`,
|
|
77
|
+
`source-tree-not-removed: <tree> (<close status>)` and
|
|
78
|
+
`run-lock-not-released: <lock>`. The three helpers are read from beside
|
|
79
|
+
this script. Without --emit, stdin is not read.
|
|
80
|
+
|
|
81
|
+
Output: one JSON line,
|
|
82
|
+
{"status": "began" | "exists" | "rolled-back" | "nothing-to-roll-back"
|
|
83
|
+
| "already-rolled-back" | "finished" | "no-run-folder",
|
|
84
|
+
"restored": [paths], "removed": [paths], "kept": [{"path", "why"}],
|
|
85
|
+
"failed": [{"path", "error"}], "run_dir_removed": bool, "path": <exists>}
|
|
86
|
+
and for halt
|
|
87
|
+
{"status": "halted", "rollback": <rollback's status, or "error">,
|
|
88
|
+
"restored", "removed", "kept", "failed": as above,
|
|
89
|
+
"tree": <close's status, or null>, "lock": "released" | "absent" |
|
|
90
|
+
"not-owner" | "failed" | null, "warnings": [...], "emitted": bool}
|
|
91
|
+
followed, with --emit, by the emitter's SKF_UPDATE_RESULT_JSON line.
|
|
92
|
+
|
|
93
|
+
Exit codes:
|
|
94
|
+
0 done (see status); halt: every step ran, its failures in warnings[]
|
|
95
|
+
1 bad arguments, or a run-state.json that cannot be read or written;
|
|
96
|
+
halt: a payload that is not a JSON object, or one the emitter refused
|
|
97
|
+
(its message on stderr): fix the payload and run halt again, which
|
|
98
|
+
redoes nothing already done
|
|
99
|
+
2 a copy, restore or removal failed (each named in failed[]; rollback
|
|
100
|
+
goes on with the rest)
|
|
101
|
+
3 begin: a --created folder already exists
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
from __future__ import annotations
|
|
105
|
+
|
|
106
|
+
import argparse
|
|
107
|
+
import json
|
|
108
|
+
import os
|
|
109
|
+
import shutil
|
|
110
|
+
import stat
|
|
111
|
+
import subprocess
|
|
112
|
+
import sys
|
|
113
|
+
from pathlib import Path
|
|
114
|
+
|
|
115
|
+
STATE = "run-state.json"
|
|
116
|
+
SNAPSHOT = "snapshot"
|
|
117
|
+
FORGE_FILES = ("provenance-map.json", "evidence-report.md")
|
|
118
|
+
TOOL = "skf-update-run-state"
|
|
119
|
+
EXIT_FAILED = 2
|
|
120
|
+
EXIT_EXISTS = 3
|
|
121
|
+
HERE = Path(__file__).resolve().parent
|
|
122
|
+
SOURCE_TREE_HELPER = HERE / "skf-source-tree.py"
|
|
123
|
+
RUN_LOCK_HELPER = HERE / "skf-run-lock.py"
|
|
124
|
+
EMITTER = HERE / "skf-emit-result-envelope.py"
|
|
125
|
+
WORKFLOW = "skf-update-skill"
|
|
126
|
+
OWNER_PREFIX = "update-skill"
|
|
127
|
+
HELPER_TIMEOUT_SEC = 120
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _out(status: str, **fields) -> dict:
|
|
131
|
+
result = {"status": status, "restored": [], "removed": [], "kept": [], "failed": [],
|
|
132
|
+
"run_dir_removed": False}
|
|
133
|
+
result.update(fields)
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _make_writable(func, path, _exc) -> None:
|
|
138
|
+
"""rmtree's error handler: clear a read-only bit (Windows) and retry once."""
|
|
139
|
+
try:
|
|
140
|
+
os.chmod(path, stat.S_IWRITE)
|
|
141
|
+
func(path)
|
|
142
|
+
except OSError:
|
|
143
|
+
raise
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _rmtree(path: Path) -> None:
|
|
147
|
+
if sys.version_info >= (3, 12):
|
|
148
|
+
shutil.rmtree(path, onexc=_make_writable)
|
|
149
|
+
else: # pragma: no cover - Python 3.11
|
|
150
|
+
shutil.rmtree(path, onerror=_make_writable)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _remove(path: Path) -> None:
|
|
154
|
+
"""Remove a file, a link or a folder (the folder's content, never a link's target)."""
|
|
155
|
+
if path.is_symlink() or path.is_file():
|
|
156
|
+
path.unlink()
|
|
157
|
+
elif path.is_dir():
|
|
158
|
+
_rmtree(path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _copy_file(src: Path, dst: Path) -> None:
|
|
162
|
+
"""Copy src over dst through a temporary file and one rename."""
|
|
163
|
+
tmp = dst.with_name(f".{dst.name}.skf-restore-tmp")
|
|
164
|
+
shutil.copy2(src, tmp)
|
|
165
|
+
os.replace(tmp, dst)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _read_state(run_dir: Path) -> dict | None:
|
|
169
|
+
path = run_dir / STATE
|
|
170
|
+
if not path.exists():
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
state = json.loads(path.read_text(encoding="utf-8"))
|
|
174
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
175
|
+
raise ValueError(f"cannot read {path.as_posix()}: {exc}") from exc
|
|
176
|
+
if not isinstance(state, dict) or state.get("tool") != TOOL:
|
|
177
|
+
raise ValueError(f"{path.as_posix()} is not a run state this helper wrote")
|
|
178
|
+
return state
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _write_state(run_dir: Path, state: dict) -> None:
|
|
182
|
+
path = run_dir / STATE
|
|
183
|
+
tmp = run_dir / f".{STATE}.skf-tmp"
|
|
184
|
+
try:
|
|
185
|
+
tmp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
|
|
186
|
+
os.replace(tmp, path)
|
|
187
|
+
except OSError as exc:
|
|
188
|
+
raise ValueError(f"cannot write {path.as_posix()}: {exc}") from exc
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _abs(path: str) -> Path:
|
|
192
|
+
return Path(os.path.abspath(os.path.expanduser(path)))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --------------------------------------------------------------------------
|
|
196
|
+
# begin
|
|
197
|
+
# --------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def begin_in_place(run_dir: Path, package: Path, forge_version: Path, brief: Path | None) -> dict:
|
|
201
|
+
if _read_state(run_dir) is not None:
|
|
202
|
+
raise ValueError(f"{(run_dir / STATE).as_posix()} exists: this run already began writing")
|
|
203
|
+
if not package.is_dir():
|
|
204
|
+
raise ValueError(f"--package is not a folder: {package.as_posix()}")
|
|
205
|
+
snap = run_dir / SNAPSHOT
|
|
206
|
+
failed = []
|
|
207
|
+
try:
|
|
208
|
+
if snap.exists():
|
|
209
|
+
_remove(snap)
|
|
210
|
+
(snap / "forge").mkdir(parents=True)
|
|
211
|
+
shutil.copytree(package, snap / "package", symlinks=True)
|
|
212
|
+
except OSError as exc:
|
|
213
|
+
return _out("began", failed=[{"path": package.as_posix(), "error": str(exc)}])
|
|
214
|
+
forge_files = {}
|
|
215
|
+
for name in FORGE_FILES:
|
|
216
|
+
source = forge_version / name
|
|
217
|
+
forge_files[name] = source.is_file()
|
|
218
|
+
if source.is_file():
|
|
219
|
+
try:
|
|
220
|
+
shutil.copy2(source, snap / "forge" / name)
|
|
221
|
+
except OSError as exc:
|
|
222
|
+
failed.append({"path": source.as_posix(), "error": str(exc)})
|
|
223
|
+
brief_existed = bool(brief and brief.is_file())
|
|
224
|
+
if brief_existed:
|
|
225
|
+
try:
|
|
226
|
+
shutil.copy2(brief, snap / "skill-brief.yaml")
|
|
227
|
+
except OSError as exc:
|
|
228
|
+
failed.append({"path": brief.as_posix(), "error": str(exc)})
|
|
229
|
+
if failed:
|
|
230
|
+
return _out("began", failed=failed)
|
|
231
|
+
_write_state(run_dir, {
|
|
232
|
+
"tool": TOOL, "mode": "in-place", "finished": False, "rolled_back": False,
|
|
233
|
+
"snapshot": {"package": package.as_posix(), "forge_version": forge_version.as_posix(),
|
|
234
|
+
"forge_files": forge_files, "brief": brief.as_posix() if brief else None,
|
|
235
|
+
"brief_existed": brief_existed},
|
|
236
|
+
"created": [], "staging": None, "skill_group": None,
|
|
237
|
+
})
|
|
238
|
+
return _out("began")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def begin_new_version(run_dir: Path, created: list[Path], staging: Path | None, skill_group: Path | None) -> dict:
|
|
242
|
+
if _read_state(run_dir) is not None:
|
|
243
|
+
raise ValueError(f"{(run_dir / STATE).as_posix()} exists: this run already began writing")
|
|
244
|
+
for path in created:
|
|
245
|
+
if os.path.lexists(path):
|
|
246
|
+
return _out("exists", path=path.as_posix())
|
|
247
|
+
_write_state(run_dir, {
|
|
248
|
+
"tool": TOOL, "mode": "new-version", "finished": False, "rolled_back": False, "snapshot": None,
|
|
249
|
+
"created": [p.as_posix() for p in created], "staging": staging.as_posix() if staging else None,
|
|
250
|
+
"skill_group": skill_group.as_posix() if skill_group else None,
|
|
251
|
+
})
|
|
252
|
+
return _out("began")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# --------------------------------------------------------------------------
|
|
256
|
+
# rollback and finish
|
|
257
|
+
# --------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _active_target(skill_group: str | None) -> Path | None:
|
|
261
|
+
"""The folder the group's `active` link resolves to, or None."""
|
|
262
|
+
if not skill_group:
|
|
263
|
+
return None
|
|
264
|
+
link = Path(skill_group) / "active"
|
|
265
|
+
try:
|
|
266
|
+
return link.resolve(strict=True) if os.path.lexists(link) else None
|
|
267
|
+
except OSError:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _restore_in_place(run_dir: Path, snap_state: dict, result: dict) -> None:
|
|
272
|
+
snap = run_dir / SNAPSHOT
|
|
273
|
+
package = Path(snap_state["package"])
|
|
274
|
+
copy = snap / "package"
|
|
275
|
+
if copy.is_dir():
|
|
276
|
+
fresh = package.with_name(f"{package.name}.skf-restore")
|
|
277
|
+
aside = package.with_name(f"{package.name}.skf-rollback")
|
|
278
|
+
try:
|
|
279
|
+
for leftover in (fresh, aside):
|
|
280
|
+
if os.path.lexists(leftover):
|
|
281
|
+
_remove(leftover)
|
|
282
|
+
shutil.copytree(copy, fresh, symlinks=True)
|
|
283
|
+
if os.path.lexists(package):
|
|
284
|
+
os.replace(package, aside)
|
|
285
|
+
os.replace(fresh, package)
|
|
286
|
+
if os.path.lexists(aside):
|
|
287
|
+
_remove(aside)
|
|
288
|
+
result["restored"].append(package.as_posix())
|
|
289
|
+
except OSError as exc:
|
|
290
|
+
result["failed"].append({"path": package.as_posix(), "error": str(exc)})
|
|
291
|
+
forge = Path(snap_state["forge_version"])
|
|
292
|
+
for name, existed in (snap_state.get("forge_files") or {}).items():
|
|
293
|
+
target = forge / name
|
|
294
|
+
try:
|
|
295
|
+
if existed:
|
|
296
|
+
_copy_file(snap / "forge" / name, target)
|
|
297
|
+
result["restored"].append(target.as_posix())
|
|
298
|
+
elif os.path.lexists(target):
|
|
299
|
+
_remove(target)
|
|
300
|
+
result["removed"].append(target.as_posix())
|
|
301
|
+
except OSError as exc:
|
|
302
|
+
result["failed"].append({"path": target.as_posix(), "error": str(exc)})
|
|
303
|
+
brief = snap_state.get("brief")
|
|
304
|
+
if brief:
|
|
305
|
+
target = Path(brief)
|
|
306
|
+
try:
|
|
307
|
+
if snap_state.get("brief_existed"):
|
|
308
|
+
_copy_file(snap / "skill-brief.yaml", target)
|
|
309
|
+
result["restored"].append(target.as_posix())
|
|
310
|
+
elif os.path.lexists(target):
|
|
311
|
+
_remove(target)
|
|
312
|
+
result["removed"].append(target.as_posix())
|
|
313
|
+
except OSError as exc:
|
|
314
|
+
result["failed"].append({"path": target.as_posix(), "error": str(exc)})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _remove_created(state: dict, result: dict) -> None:
|
|
318
|
+
active = _active_target(state.get("skill_group"))
|
|
319
|
+
paths = list(state.get("created") or [])
|
|
320
|
+
if state.get("staging"):
|
|
321
|
+
paths.append(state["staging"])
|
|
322
|
+
live = active is not None and any(
|
|
323
|
+
os.path.lexists(raw) and not Path(raw).is_symlink() and Path(raw).resolve() == active for raw in paths)
|
|
324
|
+
for raw in paths:
|
|
325
|
+
path = Path(raw)
|
|
326
|
+
if not os.path.lexists(path):
|
|
327
|
+
continue
|
|
328
|
+
if path.is_symlink():
|
|
329
|
+
result["kept"].append({"path": path.as_posix(), "why": "a link: this run never creates one"})
|
|
330
|
+
continue
|
|
331
|
+
if live:
|
|
332
|
+
# The active link already names this run's version: it is live,
|
|
333
|
+
# and so is its forge folder.
|
|
334
|
+
why = "the active link names it" if path.resolve() == active else "the active link names its version"
|
|
335
|
+
result["kept"].append({"path": path.as_posix(), "why": why})
|
|
336
|
+
continue
|
|
337
|
+
try:
|
|
338
|
+
_remove(path)
|
|
339
|
+
result["removed"].append(path.as_posix())
|
|
340
|
+
except OSError as exc:
|
|
341
|
+
result["failed"].append({"path": path.as_posix(), "error": str(exc)})
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
RUN_DIR_PREFIX = "skf-update-skill-"
|
|
345
|
+
RUN_ROOT_NAME = ".skf-run"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _is_update_run_dir(run_dir: Path) -> bool:
|
|
349
|
+
"""True for `<...>/.skf-run/skf-update-skill-<run id>`: the only folder --remove-run-dir deletes.
|
|
350
|
+
|
|
351
|
+
init.md builds the path from a run id it reads in a stale lock's owner;
|
|
352
|
+
a run id holding a path separator or `..` must never reach a folder
|
|
353
|
+
outside the run root.
|
|
354
|
+
"""
|
|
355
|
+
name = run_dir.name
|
|
356
|
+
run_id = name[len(RUN_DIR_PREFIX):]
|
|
357
|
+
return (name.startswith(RUN_DIR_PREFIX) and bool(run_id) and run_dir.parent.name == RUN_ROOT_NAME
|
|
358
|
+
and not any(ch in run_id for ch in "/\\") and ".." not in run_id)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def rollback(run_dir: Path, remove_run_dir: bool) -> dict:
|
|
362
|
+
if remove_run_dir and not _is_update_run_dir(run_dir):
|
|
363
|
+
raise ValueError(f"--remove-run-dir deletes only an update run folder (.skf-run/{RUN_DIR_PREFIX}<run id>), "
|
|
364
|
+
f"not {run_dir.as_posix()}")
|
|
365
|
+
state = _read_state(run_dir)
|
|
366
|
+
if state is None:
|
|
367
|
+
result = _out("nothing-to-roll-back")
|
|
368
|
+
elif state.get("finished"):
|
|
369
|
+
result = _out("finished")
|
|
370
|
+
elif state.get("rolled_back"):
|
|
371
|
+
result = _out("already-rolled-back")
|
|
372
|
+
else:
|
|
373
|
+
result = _out("rolled-back")
|
|
374
|
+
if state.get("mode") == "in-place" and isinstance(state.get("snapshot"), dict):
|
|
375
|
+
_restore_in_place(run_dir, state["snapshot"], result)
|
|
376
|
+
else:
|
|
377
|
+
_remove_created(state, result)
|
|
378
|
+
if not result["failed"]:
|
|
379
|
+
_write_state(run_dir, {**state, "rolled_back": True})
|
|
380
|
+
if remove_run_dir and not result["failed"] and run_dir.is_dir():
|
|
381
|
+
try:
|
|
382
|
+
_rmtree(run_dir)
|
|
383
|
+
result["run_dir_removed"] = True
|
|
384
|
+
except OSError as exc:
|
|
385
|
+
result["failed"].append({"path": run_dir.as_posix(), "error": str(exc)})
|
|
386
|
+
return result
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def finish(run_dir: Path) -> dict:
|
|
390
|
+
state = _read_state(run_dir)
|
|
391
|
+
if state is None:
|
|
392
|
+
raise ValueError(f"no {STATE} in {run_dir.as_posix()}: begin never ran")
|
|
393
|
+
# Finished first: a crash before the snapshot goes leaves a finished run
|
|
394
|
+
# whose writes stand, never an unfinished one with no snapshot to restore.
|
|
395
|
+
_write_state(run_dir, {**state, "finished": True})
|
|
396
|
+
snap = run_dir / SNAPSHOT
|
|
397
|
+
failed = []
|
|
398
|
+
if snap.exists():
|
|
399
|
+
try:
|
|
400
|
+
_remove(snap)
|
|
401
|
+
except OSError as exc:
|
|
402
|
+
failed.append({"path": snap.as_posix(), "error": str(exc)})
|
|
403
|
+
return _out("finished", failed=failed)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def run_dir_of_owner(run_root: Path, owner: str, skill: str | None) -> Path | None:
|
|
407
|
+
"""The run folder an update's lock owner names, or None when it names none.
|
|
408
|
+
|
|
409
|
+
An owner is `update-skill:<skill>:<run id>`; one of another workflow or
|
|
410
|
+
skill, or with no run id (an older SKF's lock), has no run folder here.
|
|
411
|
+
"""
|
|
412
|
+
parts = owner.split(":", 2) if isinstance(owner, str) else []
|
|
413
|
+
if len(parts) != 3 or parts[0] != OWNER_PREFIX or not parts[2]:
|
|
414
|
+
return None
|
|
415
|
+
if skill is not None and parts[1] != skill:
|
|
416
|
+
return None
|
|
417
|
+
return run_root / f"{RUN_DIR_PREFIX}{parts[2]}"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# --------------------------------------------------------------------------
|
|
421
|
+
# halt
|
|
422
|
+
# --------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _call(helper: Path, args: list[str], stdin: str | None = None) -> tuple[int | None, str, str]:
|
|
426
|
+
"""Run a helper beside this script: (exit code, stdout, stderr); None when it could not run."""
|
|
427
|
+
if not helper.is_file():
|
|
428
|
+
return None, "", f"{helper.name} is missing beside {Path(__file__).name}; re-install SKF"
|
|
429
|
+
try:
|
|
430
|
+
proc = subprocess.run([sys.executable, str(helper), *args], input=stdin, capture_output=True,
|
|
431
|
+
encoding="utf-8", errors="replace", timeout=HELPER_TIMEOUT_SEC)
|
|
432
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
433
|
+
return None, "", f"{helper.name}: {exc}"
|
|
434
|
+
return proc.returncode, proc.stdout, proc.stderr
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _json_line(text: str) -> dict | None:
|
|
438
|
+
for line in reversed(text.strip().splitlines()):
|
|
439
|
+
try:
|
|
440
|
+
value = json.loads(line)
|
|
441
|
+
except json.JSONDecodeError:
|
|
442
|
+
continue
|
|
443
|
+
return value if isinstance(value, dict) else None
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _first_line(text: str) -> str:
|
|
448
|
+
return next((line.strip() for line in text.splitlines() if line.strip()), "no output")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def halt(run_dir: Path, tree: str | None, lock: str | None, owner: str | None) -> dict:
|
|
452
|
+
"""A HALT's cleanup: rollback, source tree, run lock. Never stops on a result."""
|
|
453
|
+
result = {"status": "halted", "rollback": None, "restored": [], "removed": [], "kept": [], "failed": [],
|
|
454
|
+
"tree": None, "lock": None, "warnings": [], "emitted": False}
|
|
455
|
+
try:
|
|
456
|
+
undone = rollback(run_dir, remove_run_dir=False)
|
|
457
|
+
except ValueError as exc:
|
|
458
|
+
result["rollback"] = "error"
|
|
459
|
+
result["warnings"].append(f"rollback-incomplete: {exc}")
|
|
460
|
+
else:
|
|
461
|
+
result["rollback"] = undone["status"]
|
|
462
|
+
for key in ("restored", "removed", "kept", "failed"):
|
|
463
|
+
result[key] = undone[key]
|
|
464
|
+
for item in undone["failed"]:
|
|
465
|
+
result["warnings"].append(f"rollback-incomplete: {item['path']}: {item['error']}")
|
|
466
|
+
if tree:
|
|
467
|
+
code, out, err = _call(SOURCE_TREE_HELPER, ["close", "--tree", tree])
|
|
468
|
+
closed = _json_line(out) if code == 0 else None
|
|
469
|
+
result["tree"] = closed.get("status") if closed else "failed"
|
|
470
|
+
if result["tree"] not in ("removed", "missing"):
|
|
471
|
+
result["warnings"].append(f"source-tree-not-removed: {tree} "
|
|
472
|
+
f"({result['tree'] if closed else _first_line(err)})")
|
|
473
|
+
if lock and owner:
|
|
474
|
+
code, out, err = _call(RUN_LOCK_HELPER, ["release", "--lock", lock, "--owner", owner])
|
|
475
|
+
released = _json_line(out) if code == 0 else None
|
|
476
|
+
if released is None:
|
|
477
|
+
result["lock"] = "failed"
|
|
478
|
+
result["warnings"].append(f"run-lock-not-released: {lock}")
|
|
479
|
+
else:
|
|
480
|
+
result["lock"] = "released" if released.get("released") else released.get("reason") or "absent"
|
|
481
|
+
return result
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def emit_halt(run_dir: Path, payload: dict, warnings: list[str]) -> tuple[int, str, str]:
|
|
485
|
+
"""Print the halt's line through the shared emitter: (exit code, its stdout, its stderr)."""
|
|
486
|
+
listed = payload.get("warnings")
|
|
487
|
+
payload = {**payload, "warnings": [*(listed if isinstance(listed, list) else []), *warnings]}
|
|
488
|
+
args = ["emit-halt", "--workflow", WORKFLOW]
|
|
489
|
+
if run_dir.is_dir():
|
|
490
|
+
args += ["--run-dir", str(run_dir)]
|
|
491
|
+
code, out, err = _call(EMITTER, args, stdin=json.dumps(payload))
|
|
492
|
+
return (1 if code is None else code), out, err
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
# --------------------------------------------------------------------------
|
|
496
|
+
# CLI
|
|
497
|
+
# --------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
501
|
+
parser = argparse.ArgumentParser(
|
|
502
|
+
prog="skf-update-run-state",
|
|
503
|
+
description="Record what an update-skill run writes, undo it on a halt, and close the window "
|
|
504
|
+
"once the writes stand.",
|
|
505
|
+
)
|
|
506
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
507
|
+
|
|
508
|
+
p_begin = sub.add_parser("begin", help="record what the run is about to write (before the first write)")
|
|
509
|
+
p_begin.add_argument("--run-dir", required=True, help="the run folder")
|
|
510
|
+
p_begin.add_argument("--mode", required=True, choices=("in-place", "new-version"),
|
|
511
|
+
help="in-place (a gap-driven repair) or new-version (every other mode)")
|
|
512
|
+
p_begin.add_argument("--package", help="in-place: the skill package folder to snapshot")
|
|
513
|
+
p_begin.add_argument("--forge-version", help="in-place: the version's forge folder")
|
|
514
|
+
p_begin.add_argument("--brief", help="in-place: the skill brief a rescope amends")
|
|
515
|
+
p_begin.add_argument("--created", action="append", default=[],
|
|
516
|
+
help="new-version: a folder the run creates (repeat); it must not exist yet")
|
|
517
|
+
p_begin.add_argument("--staging", help="new-version: the staging folder, removed with the rest")
|
|
518
|
+
p_begin.add_argument("--skill-group", help="new-version: the skill group, whose active link is never removed")
|
|
519
|
+
|
|
520
|
+
p_rollback = sub.add_parser("rollback", help="undo what the run wrote, unless it finished")
|
|
521
|
+
p_rollback.add_argument("--run-dir", help="the run folder")
|
|
522
|
+
p_rollback.add_argument("--run-root", help="in place of --run-dir: the .skf-run folder, with --owner")
|
|
523
|
+
p_rollback.add_argument("--owner", help="with --run-root: the stale lock's owner, update-skill:<skill>:<run id>")
|
|
524
|
+
p_rollback.add_argument("--skill", help="with --run-root: the skill the owner must name")
|
|
525
|
+
p_rollback.add_argument("--remove-run-dir", action="store_true",
|
|
526
|
+
help="then delete the run folder (an interrupted run's)")
|
|
527
|
+
|
|
528
|
+
p_finish = sub.add_parser("finish", help="the writes stand: close the rollback window, drop the snapshot")
|
|
529
|
+
p_finish.add_argument("--run-dir", required=True, help="the run folder")
|
|
530
|
+
|
|
531
|
+
p_halt = sub.add_parser("halt", help="a HALT's cleanup: rollback, source tree, run lock, then the halt's line")
|
|
532
|
+
p_halt.add_argument("--run-dir", required=True, help="the run folder")
|
|
533
|
+
p_halt.add_argument("--tree", help="the private source tree to remove (skf-source-tree.py open's tree)")
|
|
534
|
+
p_halt.add_argument("--lock", help="the run lock file to release, with --owner")
|
|
535
|
+
p_halt.add_argument("--owner", help="the run lock's owner, as acquire printed it")
|
|
536
|
+
p_halt.add_argument("--emit", action="store_true",
|
|
537
|
+
help="read the halt payload on stdin and print its line through the shared emitter")
|
|
538
|
+
return parser
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _cmd_halt(args: argparse.Namespace, run_dir: Path) -> int:
|
|
542
|
+
if bool(args.lock) != bool(args.owner):
|
|
543
|
+
raise ValueError("--lock and --owner go together")
|
|
544
|
+
payload = None
|
|
545
|
+
if args.emit:
|
|
546
|
+
try:
|
|
547
|
+
payload = json.loads(sys.stdin.read())
|
|
548
|
+
except json.JSONDecodeError as exc:
|
|
549
|
+
raise ValueError(f"the halt payload on stdin is not JSON: {exc}") from exc
|
|
550
|
+
if not isinstance(payload, dict):
|
|
551
|
+
raise ValueError("the halt payload on stdin must be a JSON object")
|
|
552
|
+
result = halt(run_dir, args.tree, args.lock, args.owner)
|
|
553
|
+
if payload is None:
|
|
554
|
+
print(json.dumps(result))
|
|
555
|
+
return 0
|
|
556
|
+
code, out, err = emit_halt(run_dir, payload, result["warnings"])
|
|
557
|
+
result["emitted"] = code == 0
|
|
558
|
+
print(json.dumps(result))
|
|
559
|
+
sys.stdout.write(out)
|
|
560
|
+
sys.stdout.flush()
|
|
561
|
+
if err:
|
|
562
|
+
sys.stderr.write(err)
|
|
563
|
+
return 0 if code == 0 else 1
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def main(argv: list[str] | None = None) -> int:
|
|
567
|
+
parser = _build_parser()
|
|
568
|
+
args = parser.parse_args(argv)
|
|
569
|
+
try:
|
|
570
|
+
if args.cmd == "rollback" and args.run_root:
|
|
571
|
+
if args.run_dir or not args.owner:
|
|
572
|
+
raise ValueError("--run-root takes --owner (and --skill), in place of --run-dir")
|
|
573
|
+
found = run_dir_of_owner(_abs(args.run_root), args.owner, args.skill)
|
|
574
|
+
if found is None:
|
|
575
|
+
print(json.dumps(_out("no-run-folder")))
|
|
576
|
+
return 0
|
|
577
|
+
run_dir = found
|
|
578
|
+
elif args.cmd == "rollback" and (not args.run_dir or args.owner or args.skill):
|
|
579
|
+
raise ValueError("rollback takes --run-dir, or --run-root with --owner")
|
|
580
|
+
else:
|
|
581
|
+
run_dir = _abs(args.run_dir)
|
|
582
|
+
if args.cmd == "halt":
|
|
583
|
+
return _cmd_halt(args, run_dir)
|
|
584
|
+
if args.cmd == "begin":
|
|
585
|
+
if not run_dir.is_dir():
|
|
586
|
+
raise ValueError(f"--run-dir is not a folder: {run_dir.as_posix()}")
|
|
587
|
+
if args.mode == "in-place":
|
|
588
|
+
if not args.package or not args.forge_version or args.created or args.staging:
|
|
589
|
+
raise ValueError("--mode in-place takes --package and --forge-version (and --brief)")
|
|
590
|
+
result = begin_in_place(run_dir, _abs(args.package), _abs(args.forge_version),
|
|
591
|
+
_abs(args.brief) if args.brief else None)
|
|
592
|
+
else:
|
|
593
|
+
if not args.created or args.package or args.forge_version or args.brief:
|
|
594
|
+
raise ValueError("--mode new-version takes --created (and --staging, --skill-group)")
|
|
595
|
+
result = begin_new_version(run_dir, [_abs(p) for p in args.created],
|
|
596
|
+
_abs(args.staging) if args.staging else None,
|
|
597
|
+
_abs(args.skill_group) if args.skill_group else None)
|
|
598
|
+
elif args.cmd == "rollback":
|
|
599
|
+
result = rollback(run_dir, args.remove_run_dir)
|
|
600
|
+
else:
|
|
601
|
+
result = finish(run_dir)
|
|
602
|
+
except ValueError as exc:
|
|
603
|
+
print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
|
|
604
|
+
return 1
|
|
605
|
+
print(json.dumps(result))
|
|
606
|
+
if result["status"] == "exists":
|
|
607
|
+
return EXIT_EXISTS
|
|
608
|
+
return EXIT_FAILED if result["failed"] else 0
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _force_utf8(*streams) -> None:
|
|
612
|
+
"""Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
|
|
613
|
+
for stream in streams:
|
|
614
|
+
if hasattr(stream, "reconfigure"):
|
|
615
|
+
errors = getattr(stream, "errors", None)
|
|
616
|
+
if errors is None:
|
|
617
|
+
stream.reconfigure(encoding="utf-8")
|
|
618
|
+
else:
|
|
619
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
if __name__ == "__main__":
|
|
623
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
624
|
+
sys.exit(main())
|