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,180 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/armelhbobdad/bmad-module-skill-forge/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json",
|
|
4
|
+
"title": "SKF_VERIFY_STACK_RESULT_JSON envelope (v1)",
|
|
5
|
+
"description": "Single-line JSON document skf-verify-stack emits when {headless_mode} is true, prefixed with the literal 'SKF_VERIFY_STACK_RESULT_JSON: ': once on stdout when step 6 (report) finishes, before it chains to the health check, and once on stderr at every HARD HALT (status error). The shared emitter, skf-emit-result-envelope.py, builds every line from the payload a stage stages in the run folder, folds in the auto-decisions and warnings the run recorded there, derives exit_code from halt_reason, stamps run_id and result_path, and checks the line against this schema, so the model never types it. The skill's references/exit-codes.md describes each field and the halt command.",
|
|
6
|
+
"$defs": {
|
|
7
|
+
"skf-envelope": {
|
|
8
|
+
"description": "The shared emitter's settings for this workflow, read by skf-emit-result-envelope.py: the workflow, the line's prefix, the wrapper, the status a halt carries, the exit code of each halt_reason, the result file stem and the stamp the per-run result file is named after (the run's run_id). Nothing in the schema references this entry, so it constrains no envelope.",
|
|
9
|
+
"const": {
|
|
10
|
+
"workflow": "skf-verify-stack",
|
|
11
|
+
"prefix": "SKF_VERIFY_STACK_RESULT_JSON",
|
|
12
|
+
"wrapper": null,
|
|
13
|
+
"halt_status": "error",
|
|
14
|
+
"exit_codes": {
|
|
15
|
+
"input-missing": 2,
|
|
16
|
+
"input-invalid": 2,
|
|
17
|
+
"skills-folder-missing": 3,
|
|
18
|
+
"forge-folder-unconfigured": 3,
|
|
19
|
+
"resolution-failure": 3,
|
|
20
|
+
"write-failed": 4,
|
|
21
|
+
"insufficient-skills": 5,
|
|
22
|
+
"previous-report-collision": 5,
|
|
23
|
+
"schema-violation": 5,
|
|
24
|
+
"user-cancelled": 6,
|
|
25
|
+
"inventory-unreliable": 7
|
|
26
|
+
},
|
|
27
|
+
"success_exit_code": 0,
|
|
28
|
+
"result_file": "verify-stack-result",
|
|
29
|
+
"result_stamp": "run_id"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"required": [
|
|
36
|
+
"status",
|
|
37
|
+
"report_path",
|
|
38
|
+
"report_latest_path",
|
|
39
|
+
"overall_verdict",
|
|
40
|
+
"coverage_percentage",
|
|
41
|
+
"recommendation_count",
|
|
42
|
+
"exit_code",
|
|
43
|
+
"halt_reason",
|
|
44
|
+
"run_id",
|
|
45
|
+
"result_path"
|
|
46
|
+
],
|
|
47
|
+
"properties": {
|
|
48
|
+
"status": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"enum": ["success", "error"],
|
|
51
|
+
"description": "'success' when step 6 finished: the report passed the feasibility-report check and its -latest copy was written. 'error' on any HARD HALT."
|
|
52
|
+
},
|
|
53
|
+
"report_path": {
|
|
54
|
+
"type": ["string", "null"],
|
|
55
|
+
"minLength": 1,
|
|
56
|
+
"default": null,
|
|
57
|
+
"description": "The run's timestamped feasibility report, feasibility-report-<slug>-<timestamp>.md. On a halt it names the partial report the run wrote so far (step 1 writes it, and steps 2 to 5 fill it in); null when the run halted before step 1 wrote it."
|
|
58
|
+
},
|
|
59
|
+
"report_latest_path": {
|
|
60
|
+
"type": ["string", "null"],
|
|
61
|
+
"minLength": 1,
|
|
62
|
+
"default": null,
|
|
63
|
+
"description": "The feasibility-report-<slug>-latest.md copy of the finished report. Step 6 writes it only after the report passed the feasibility-report check, so it is null on a halt before that, which leaves the previous run's copy in place; the one later halt (the result contract could not be written) names the copy it published."
|
|
64
|
+
},
|
|
65
|
+
"overall_verdict": {
|
|
66
|
+
"type": ["string", "null"],
|
|
67
|
+
"enum": ["FEASIBLE", "CONDITIONALLY_FEASIBLE", "NOT_FEASIBLE", null],
|
|
68
|
+
"default": null,
|
|
69
|
+
"description": "The report's overallVerdict (case-sensitive schema tokens), as the feasibility-report check read it from the finished report. Null on a halt."
|
|
70
|
+
},
|
|
71
|
+
"coverage_percentage": {
|
|
72
|
+
"type": "integer",
|
|
73
|
+
"minimum": 0,
|
|
74
|
+
"default": 0,
|
|
75
|
+
"description": "The report's coveragePercentage, a whole number from 0 to 100 that the coverage tally helper computed, as the feasibility-report check read it from the finished report. 0 on a halt."
|
|
76
|
+
},
|
|
77
|
+
"recommendation_count": {
|
|
78
|
+
"type": "integer",
|
|
79
|
+
"minimum": 0,
|
|
80
|
+
"default": 0,
|
|
81
|
+
"description": "The report's recommendationCount, the count the verdict rollup helper derived from the report's counts (one recommendation per Missing or Replaced technology, per Blocked, Risky or Plausible row, for no integration pair, and per unmet requirement). 0 on a halt."
|
|
82
|
+
},
|
|
83
|
+
"exit_code": {
|
|
84
|
+
"type": "integer",
|
|
85
|
+
"enum": [0, 2, 3, 4, 5, 6, 7],
|
|
86
|
+
"description": "Process exit code matching references/exit-codes.md, derived from halt_reason: null gives 0, input-missing and input-invalid 2, skills-folder-missing, forge-folder-unconfigured and resolution-failure 3, write-failed 4, insufficient-skills, previous-report-collision and schema-violation 5, user-cancelled 6, inventory-unreliable 7."
|
|
87
|
+
},
|
|
88
|
+
"halt_reason": {
|
|
89
|
+
"type": ["string", "null"],
|
|
90
|
+
"enum": [
|
|
91
|
+
null,
|
|
92
|
+
"input-missing",
|
|
93
|
+
"input-invalid",
|
|
94
|
+
"skills-folder-missing",
|
|
95
|
+
"forge-folder-unconfigured",
|
|
96
|
+
"resolution-failure",
|
|
97
|
+
"write-failed",
|
|
98
|
+
"insufficient-skills",
|
|
99
|
+
"previous-report-collision",
|
|
100
|
+
"schema-violation",
|
|
101
|
+
"user-cancelled",
|
|
102
|
+
"inventory-unreliable"
|
|
103
|
+
],
|
|
104
|
+
"description": "Null on success. One of the documented HALT classes on error, so a pipeline branches on the failure class without parsing the prose message."
|
|
105
|
+
},
|
|
106
|
+
"run_id": {
|
|
107
|
+
"type": ["string", "null"],
|
|
108
|
+
"minLength": 1,
|
|
109
|
+
"description": "The run's id, stamped by the emitter from the run folder name: the run's timestamp (YYYYMMDD-HHmmss, UTC), the same stamp the timestamped report's file name carries. Null for a halt before the run folder exists."
|
|
110
|
+
},
|
|
111
|
+
"result_path": {
|
|
112
|
+
"type": ["string", "null"],
|
|
113
|
+
"minLength": 1,
|
|
114
|
+
"description": "The per-run result file the emitter wrote in the forge data folder, verify-stack-result-<YYYYMMDD-HHmmss>.json, named after the run's own stamp (its run_id, the stamp the timestamped report's file name carries; -2, -3, ... appended when a run already took the name), beside its verify-stack-result-latest.json copy. Stamped by the emitter, never typed. Null on every halt, which writes no result file, and when the write failed (warnings say why)."
|
|
115
|
+
},
|
|
116
|
+
"headless_decisions": {
|
|
117
|
+
"type": "array",
|
|
118
|
+
"items": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"additionalProperties": false,
|
|
121
|
+
"required": ["gate", "default_action", "taken_action", "reason"],
|
|
122
|
+
"properties": {
|
|
123
|
+
"gate": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"enum": ["init.previous-report"],
|
|
126
|
+
"description": "The gate that auto-resolved: step 1's offer to compare against the newest earlier report. The gate records its decision in the run sink the moment it decides."
|
|
127
|
+
},
|
|
128
|
+
"default_action": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"minLength": 1,
|
|
131
|
+
"description": "The action documented as the gate's default ('Y' for [Y] use it)."
|
|
132
|
+
},
|
|
133
|
+
"taken_action": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"minLength": 1,
|
|
136
|
+
"description": "The action the run took. In a headless run it equals default_action."
|
|
137
|
+
},
|
|
138
|
+
"reason": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"minLength": 1,
|
|
141
|
+
"description": "Human-readable rationale, such as 'headless: delta comparison against <path>'."
|
|
142
|
+
},
|
|
143
|
+
"evidence": {
|
|
144
|
+
"type": "object",
|
|
145
|
+
"additionalProperties": true,
|
|
146
|
+
"description": "Optional gate-specific data, such as the previous report the run compares against."
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"description": "Optional. One entry per gate that auto-resolved during the run, read from the run sink, so a halt reports the decisions taken before it. Present only when there is at least one."
|
|
151
|
+
},
|
|
152
|
+
"warnings": {
|
|
153
|
+
"type": "array",
|
|
154
|
+
"items": { "type": "string", "minLength": 1 },
|
|
155
|
+
"description": "Optional. Non-fatal anomalies the run recorded in its sink or the emitter raised, one string each, starting with its code: no_technology_referenced, no_live_technology or zero_coverage when the coverage step found no Covered technology (the architecture names none, marks each one for removal, or no skill matches one), skill_modified_mid_run when a paired skill changed after step 1 read it, all_pairs_blocked when every integration pair is Blocked, and result_file_write_failed when a result file could not be written (the emitter's own, or step 6's when it cannot create the forge data folder). Present only when there is at least one."
|
|
156
|
+
},
|
|
157
|
+
"error": {
|
|
158
|
+
"type": "object",
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"required": ["phase", "reason"],
|
|
161
|
+
"properties": {
|
|
162
|
+
"phase": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"minLength": 1,
|
|
165
|
+
"description": "Where the run halted: the step and gate, for example 'init:skills-folder' or 'report:check'."
|
|
166
|
+
},
|
|
167
|
+
"reason": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"minLength": 1,
|
|
170
|
+
"description": "The halt message the step displayed, on one line."
|
|
171
|
+
},
|
|
172
|
+
"path": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"description": "Optional path the failed operation targeted."
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"description": "Present on a HARD HALT only: where the run stopped and why. Pipelines branch on halt_reason and read error for the diagnostic."
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -111,6 +111,40 @@
|
|
|
111
111
|
"reason": { "type": "string", "minLength": 1 },
|
|
112
112
|
"recorded": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" }
|
|
113
113
|
}
|
|
114
|
+
},
|
|
115
|
+
"amendments": {
|
|
116
|
+
"type": "array",
|
|
117
|
+
"description": "Optional additive audit log of scope decisions workflows made after the brief was authored (skf-brief-skill assets/skill-brief-schema.md, Scope Amendments). An entry without category is an auth-doc decision.",
|
|
118
|
+
"items": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"category": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "The workflow path that wrote the entry: auth-doc (also an entry without category), scope-expansion or demo-and-registry."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"registry_path": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"minLength": 1,
|
|
131
|
+
"description": "Optional, for scope.type component-library: the component registry file, relative to the source root. skf-create-skill step 3d reads it instead of detecting a registry, and writes it back when the user confirms or gives one."
|
|
132
|
+
},
|
|
133
|
+
"ui_variants": {
|
|
134
|
+
"type": "array",
|
|
135
|
+
"description": "Optional, for scope.type component-library: the design system variants, the first one primary.",
|
|
136
|
+
"items": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"properties": {
|
|
139
|
+
"name": { "type": "string", "minLength": 1 },
|
|
140
|
+
"package": { "type": "string", "minLength": 1 }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"demo_patterns": {
|
|
145
|
+
"type": "array",
|
|
146
|
+
"description": "Optional, for scope.type component-library: globs of the demo and example files step 3d leaves out. skf-create-skill step 3d reads them instead of detecting demo files, and writes them back when the user confirms or gives them.",
|
|
147
|
+
"items": { "type": "string", "minLength": 1 }
|
|
114
148
|
}
|
|
115
149
|
}
|
|
116
150
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"oneOf": [
|
|
17
17
|
{
|
|
18
18
|
"type": "string",
|
|
19
|
-
"enum": ["npm-workspaces", "pnpm-workspaces", "lerna", "cargo-workspace", "python-multi-package", "generic-folders"]
|
|
19
|
+
"enum": ["npm-workspaces", "pnpm-workspaces", "lerna", "rush", "nx", "cargo-workspace", "python-multi-package", "generic-folders"]
|
|
20
20
|
},
|
|
21
21
|
{ "type": "null" }
|
|
22
22
|
]
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = []
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Atomic Write — Crash-safe artifact writing for skill workflows.
|
|
6
6
|
|
|
7
|
-
Provides
|
|
7
|
+
Provides four CLI subcommands skills can invoke via bash to avoid
|
|
8
8
|
partial-write corruption and active-symlink races.
|
|
9
9
|
|
|
10
10
|
Subcommands:
|
|
@@ -21,19 +21,39 @@ Subcommands:
|
|
|
21
21
|
restore. Supports rollback via --rollback to undo the most recent
|
|
22
22
|
commit by restoring the rollback dir if still present.
|
|
23
23
|
|
|
24
|
-
flip-link
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
flip-link Point the link <link> at <target>: create the new link as
|
|
25
|
+
<link>.skf-tmp-link, then os.replace it over <link>, holding an
|
|
26
|
+
exclusive lock on <link>.skf-lock, which only the flip holding
|
|
27
|
+
it removes. skf-update-active-symlink.py update takes the same
|
|
28
|
+
lock (take_flip_lock), so one flip of a link runs at a time.
|
|
29
|
+
Refuses to replace a <link> that is a real directory or file.
|
|
27
30
|
|
|
28
31
|
Cross-platform: locking branches between fcntl (POSIX) and msvcrt
|
|
29
|
-
(Windows).
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
(Windows). On Windows os.symlink needs Developer Mode or admin; when it
|
|
33
|
+
fails for lack of privilege, flip-link creates a directory junction
|
|
34
|
+
(mklink /J) instead, which needs neither but requires <target> to be an
|
|
35
|
+
existing directory. Windows renames nothing over an existing directory
|
|
36
|
+
link, so on the junction path, and on Windows whenever the rename over the
|
|
37
|
+
old link is refused, replace_link removes the old link first (a junction
|
|
38
|
+
whose folder is gone included) and the flip is not atomic. The Windows
|
|
39
|
+
tests in test-skf-atomic-write.py and test-skf-update-active-symlink.py
|
|
40
|
+
make os.symlink fail as it does without the privilege, so CI's Windows
|
|
41
|
+
runner, which holds it, runs the junction path too.
|
|
32
42
|
|
|
33
43
|
Exit codes:
|
|
34
|
-
0 on success
|
|
35
|
-
1
|
|
36
|
-
|
|
44
|
+
0 on success (JSON on stdout)
|
|
45
|
+
1 when commit-dir has no staging or rollback directory to use (JSON
|
|
46
|
+
error on stderr), and on any unexpected error (a Python traceback on
|
|
47
|
+
stderr); for flip-link that includes Windows refusing both a symlink
|
|
48
|
+
and a junction
|
|
49
|
+
2 on a refused or failed operation, with {"status": "error", "message":
|
|
50
|
+
...} on stderr: write failed, stage-dir could not clear a stale entry,
|
|
51
|
+
commit-dir found a non-directory target or could not swap, flip-link
|
|
52
|
+
found a non-link at <link>, another process holding its lock, a
|
|
53
|
+
lock file Windows kept refusing to open for its short wait while the
|
|
54
|
+
file was being removed, or (on POSIX) a locked lock file that kept
|
|
55
|
+
leaving the path for that same wait; and on bad arguments (argparse
|
|
56
|
+
usage on stderr, no JSON)
|
|
37
57
|
|
|
38
58
|
CLI examples:
|
|
39
59
|
cat metadata.json | python3 skf-atomic-write.py write --target /path/to/metadata.json
|
|
@@ -51,6 +71,7 @@ import os
|
|
|
51
71
|
import shutil
|
|
52
72
|
import subprocess
|
|
53
73
|
import sys
|
|
74
|
+
import time
|
|
54
75
|
from pathlib import Path
|
|
55
76
|
|
|
56
77
|
if os.name == "nt":
|
|
@@ -58,6 +79,18 @@ if os.name == "nt":
|
|
|
58
79
|
else:
|
|
59
80
|
import fcntl
|
|
60
81
|
|
|
82
|
+
# While the flip that held <link>.skf-lock is removing it, Windows refuses a
|
|
83
|
+
# new open of the file with PermissionError. flip-link takes that for a held
|
|
84
|
+
# lock and tries again for up to this long (a flip fails fast on contention
|
|
85
|
+
# by design, so the wait is short), then exits 2 as it does for a held lock.
|
|
86
|
+
# The same wait bounds a POSIX flip that keeps finding the file it locked
|
|
87
|
+
# gone from the path: it opens the path again until then, then exits 2.
|
|
88
|
+
# A caller that gives take_flip_lock a longer `wait` (skf-update-active-symlink.py
|
|
89
|
+
# update) keeps both retries going until that wait ends.
|
|
90
|
+
_IS_WINDOWS = os.name == "nt"
|
|
91
|
+
_LOCK_OPEN_WAIT_SECONDS = 1.0
|
|
92
|
+
_LOCK_OPEN_POLL_SECONDS = 0.02
|
|
93
|
+
|
|
61
94
|
|
|
62
95
|
def _create_symlink_or_junction(target: str, link_path: Path) -> str:
|
|
63
96
|
"""Create a directory link from link_path to target.
|
|
@@ -129,6 +162,16 @@ def _release_lock(fd: int) -> None:
|
|
|
129
162
|
pass
|
|
130
163
|
|
|
131
164
|
|
|
165
|
+
# Keep identical to _names_open_file in gap-ledger.py
|
|
166
|
+
# (test/test-skf-atomic-write.py pins the copy).
|
|
167
|
+
def _names_open_file(fd: int, path: Path) -> bool:
|
|
168
|
+
"""True when `path` still names the file open as `fd`."""
|
|
169
|
+
try:
|
|
170
|
+
return os.path.samestat(os.fstat(fd), os.stat(path))
|
|
171
|
+
except OSError:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
132
175
|
def _die(code: int, message: str) -> None:
|
|
133
176
|
print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
|
|
134
177
|
sys.exit(code)
|
|
@@ -247,13 +290,17 @@ def cmd_commit_dir(target: Path, rollback: bool = False) -> None:
|
|
|
247
290
|
_ok({"committed": str(target)})
|
|
248
291
|
|
|
249
292
|
|
|
293
|
+
# Keep identical to _is_link_or_junction in skf-skill-inventory.py
|
|
294
|
+
# (test/test-skf-skill-inventory.py pins the copies).
|
|
250
295
|
def _is_link_or_junction(p: Path) -> bool:
|
|
251
296
|
"""True for POSIX symlinks AND Windows junctions/symlinks.
|
|
252
297
|
|
|
253
298
|
`Path.is_symlink()` is False for Windows junctions; os.readlink succeeds
|
|
254
299
|
for both symlinks and junctions (since CPython 3.8 on Windows). A regular
|
|
255
300
|
directory raises OSError on readlink, which is the signal we want to
|
|
256
|
-
refuse replacement.
|
|
301
|
+
refuse replacement. On Windows, any other reparse point (a cloud-sync
|
|
302
|
+
placeholder, a deduplicated file, an app execution alias) raises
|
|
303
|
+
ValueError: it does not redirect to another path, so it is not a link.
|
|
257
304
|
"""
|
|
258
305
|
if p.is_symlink():
|
|
259
306
|
return True
|
|
@@ -262,22 +309,186 @@ def _is_link_or_junction(p: Path) -> bool:
|
|
|
262
309
|
try:
|
|
263
310
|
os.readlink(p)
|
|
264
311
|
return True
|
|
265
|
-
except OSError:
|
|
312
|
+
except (OSError, ValueError):
|
|
266
313
|
return False
|
|
267
314
|
|
|
268
315
|
|
|
316
|
+
def _remove_link(p: Path) -> None:
|
|
317
|
+
"""Remove the symlink or junction at p, whether or not the folder it names exists.
|
|
318
|
+
|
|
319
|
+
os.readlink raises for a real folder or file (OSError) and for another
|
|
320
|
+
Windows reparse point (ValueError), so neither is ever removed. os.unlink
|
|
321
|
+
removes a symlink, and on Windows a directory symlink or a junction too;
|
|
322
|
+
os.rmdir is the fallback for a directory link unlink refuses.
|
|
323
|
+
"""
|
|
324
|
+
if not p.is_symlink():
|
|
325
|
+
os.readlink(p)
|
|
326
|
+
try:
|
|
327
|
+
os.unlink(p)
|
|
328
|
+
except OSError:
|
|
329
|
+
os.rmdir(p)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def replace_link(link: Path, target: str, tmp_link: Path | None = None) -> str:
|
|
333
|
+
"""Point the link <link> at <target> through <tmp_link>; return "symlink" or "junction".
|
|
334
|
+
|
|
335
|
+
The caller holds take_flip_lock(link) and has refused a real folder or
|
|
336
|
+
file at <link>. A stale <tmp_link> from a crashed flip is removed first.
|
|
337
|
+
The new link is made at <tmp_link> and renamed over <link>, which is
|
|
338
|
+
atomic for a symlink on POSIX. Windows renames nothing over an existing
|
|
339
|
+
directory link, so the old link is removed first on the junction path,
|
|
340
|
+
and on Windows when the rename is refused: a reader can then find no
|
|
341
|
+
<link> for a moment. A failure removes <tmp_link> and raises.
|
|
342
|
+
"""
|
|
343
|
+
if tmp_link is None:
|
|
344
|
+
tmp_link = link.with_name(link.name + ".skf-tmp-link")
|
|
345
|
+
if os.path.lexists(tmp_link):
|
|
346
|
+
_remove_link(tmp_link)
|
|
347
|
+
link_kind = _create_symlink_or_junction(target, tmp_link)
|
|
348
|
+
try:
|
|
349
|
+
if link_kind == "junction" and os.path.lexists(link):
|
|
350
|
+
_remove_link(link)
|
|
351
|
+
try:
|
|
352
|
+
os.replace(tmp_link, link)
|
|
353
|
+
except OSError:
|
|
354
|
+
if not _IS_WINDOWS or not os.path.lexists(link):
|
|
355
|
+
raise
|
|
356
|
+
_remove_link(link)
|
|
357
|
+
os.replace(tmp_link, link)
|
|
358
|
+
except BaseException:
|
|
359
|
+
try:
|
|
360
|
+
if os.path.lexists(tmp_link):
|
|
361
|
+
_remove_link(tmp_link)
|
|
362
|
+
except (OSError, ValueError):
|
|
363
|
+
pass
|
|
364
|
+
raise
|
|
365
|
+
return link_kind
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class FlipLockHeld(Exception):
|
|
369
|
+
"""Another flip holds <link>.skf-lock; the message names <link>."""
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def take_flip_lock(link: Path, wait: float = 0.0) -> int:
|
|
373
|
+
"""Take <link>.skf-lock, the lock every flip of <link> holds, and return its fd.
|
|
374
|
+
|
|
375
|
+
flip-link and skf-update-active-symlink.py update both take it, so one
|
|
376
|
+
flip of a link runs at a time. `wait` is how long to keep trying a lock
|
|
377
|
+
another flip holds: the call closes the file, sleeps
|
|
378
|
+
_LOCK_OPEN_POLL_SECONDS and opens the path again. flip-link passes 0
|
|
379
|
+
and is refused at once. Raises FlipLockHeld when the lock stays held
|
|
380
|
+
past `wait`, and when the retries cmd_flip_link's docstring describes
|
|
381
|
+
(Windows refusing the open, a locked file that left the path) run out:
|
|
382
|
+
they run until max(_LOCK_OPEN_WAIT_SECONDS, wait). release_flip_lock
|
|
383
|
+
gives the lock back.
|
|
384
|
+
"""
|
|
385
|
+
lock_path = link.with_name(link.name + ".skf-lock")
|
|
386
|
+
held_deadline = time.monotonic() + wait
|
|
387
|
+
deadline = time.monotonic() + max(_LOCK_OPEN_WAIT_SECONDS, wait)
|
|
388
|
+
vanished = False
|
|
389
|
+
while True:
|
|
390
|
+
try:
|
|
391
|
+
lock_fd = os.open(lock_path, os.O_WRONLY | os.O_CREAT, 0o644)
|
|
392
|
+
except PermissionError as exc:
|
|
393
|
+
# Windows refuses to open a file whose removal is pending. A folder
|
|
394
|
+
# at the path, or nothing there twice running (once can be the
|
|
395
|
+
# removal ending just now), is a lasting denial: raise it.
|
|
396
|
+
if not _IS_WINDOWS or os.path.isdir(lock_path):
|
|
397
|
+
raise
|
|
398
|
+
if not os.path.lexists(lock_path):
|
|
399
|
+
if vanished:
|
|
400
|
+
raise
|
|
401
|
+
vanished = True
|
|
402
|
+
continue
|
|
403
|
+
vanished = False
|
|
404
|
+
if time.monotonic() >= deadline:
|
|
405
|
+
raise FlipLockHeld(f"another process holds flip lock on {link} (last open refused: {exc})")
|
|
406
|
+
time.sleep(_LOCK_OPEN_POLL_SECONDS)
|
|
407
|
+
continue
|
|
408
|
+
held = False
|
|
409
|
+
try:
|
|
410
|
+
try:
|
|
411
|
+
_acquire_lock(lock_fd)
|
|
412
|
+
except OSError as e:
|
|
413
|
+
if e.errno != errno.EAGAIN:
|
|
414
|
+
raise
|
|
415
|
+
if time.monotonic() >= held_deadline:
|
|
416
|
+
raise FlipLockHeld(f"another process holds flip lock on {link}")
|
|
417
|
+
held = True
|
|
418
|
+
else:
|
|
419
|
+
# os.name here and in release_flip_lock follows the real OS; _IS_WINDOWS is the test seam of the
|
|
420
|
+
# open retry and of replace_link's remove-first retry.
|
|
421
|
+
if os.name == "nt" or _names_open_file(lock_fd, lock_path):
|
|
422
|
+
return lock_fd
|
|
423
|
+
_release_lock(lock_fd)
|
|
424
|
+
except BaseException:
|
|
425
|
+
os.close(lock_fd)
|
|
426
|
+
raise
|
|
427
|
+
os.close(lock_fd)
|
|
428
|
+
if held:
|
|
429
|
+
time.sleep(_LOCK_OPEN_POLL_SECONDS)
|
|
430
|
+
continue
|
|
431
|
+
if time.monotonic() >= deadline:
|
|
432
|
+
raise FlipLockHeld(f"another process holds flip lock on {link}")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def release_flip_lock(link: Path, lock_fd: int) -> None:
|
|
436
|
+
"""Give back the lock take_flip_lock returned, removing <link>.skf-lock."""
|
|
437
|
+
lock_path = link.with_name(link.name + ".skf-lock")
|
|
438
|
+
# Only the flip holding the lock gets here, so it alone removes the
|
|
439
|
+
# lock file: on POSIX while the lock is still held, so that a flip on
|
|
440
|
+
# the old file sees the path no longer names it; on Windows, which
|
|
441
|
+
# removes no open file, after the release and the close. Best effort:
|
|
442
|
+
# a leftover file is locked and removed by the next flip.
|
|
443
|
+
if os.name != "nt":
|
|
444
|
+
try:
|
|
445
|
+
lock_path.unlink()
|
|
446
|
+
except OSError:
|
|
447
|
+
pass
|
|
448
|
+
_release_lock(lock_fd)
|
|
449
|
+
os.close(lock_fd)
|
|
450
|
+
if os.name == "nt":
|
|
451
|
+
try:
|
|
452
|
+
lock_path.unlink()
|
|
453
|
+
except OSError:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
|
|
269
457
|
def cmd_flip_link(link: Path, target: str) -> None:
|
|
270
458
|
"""Atomically point <link> at <target> using rename-over-symlink pattern.
|
|
271
459
|
|
|
272
460
|
target is the value of the symlink (may be relative, as is convention
|
|
273
|
-
for `active -> 1.0.0`).
|
|
461
|
+
for `active -> 1.0.0`). A lock on <link>.skf-lock, which
|
|
462
|
+
skf-update-active-symlink.py update takes too, prevents concurrent flips.
|
|
274
463
|
|
|
275
464
|
On Windows, os.symlink requires Developer Mode or admin. When that fails
|
|
276
465
|
with PRIVILEGE_NOT_HELD/ACCESS_DENIED the helper falls back to a directory
|
|
277
466
|
junction (no elevation needed); junctions resolve identically for
|
|
278
|
-
skf-skill-inventory's resolve_active_version().
|
|
467
|
+
skf-skill-inventory's resolve_active_version(). replace_link makes the
|
|
468
|
+
new link and renames it over <link>; skf-update-active-symlink.py update
|
|
469
|
+
flips through it too.
|
|
470
|
+
|
|
471
|
+
The lock follows ledger_lock in gap-ledger.py (the same identity check
|
|
472
|
+
and removal order): a fix to one belongs in both. The lock file is
|
|
473
|
+
removed on release, by the flip that held the lock: a flip refused by a
|
|
474
|
+
held lock exits 2 and leaves the file to its holder. On POSIX the
|
|
475
|
+
removal happens while the lock is still held, and a flip checks that
|
|
476
|
+
the file it locked is still the one at the path: otherwise a flip on
|
|
477
|
+
the removed file and a newcomer on a new one would both get a lock. A
|
|
478
|
+
flip whose file is no longer at the path lets it go and opens the path
|
|
479
|
+
again: it goes on as soon as it locks the file that is at the path, and
|
|
480
|
+
exits 2 at once when another flip holds that file. Only when the files
|
|
481
|
+
it locks keep leaving the path until _LOCK_OPEN_WAIT_SECONDS runs out
|
|
482
|
+
does it exit 2 with the "another process holds flip lock" message a
|
|
483
|
+
held lock gives. Windows removes no file that is open, so there the file goes after the
|
|
484
|
+
release, unless another flip has it open. While the removal is under
|
|
485
|
+
way (the remover's own delete handle is open, or a virus scanner's or
|
|
486
|
+
the search indexer's), Windows refuses a new open of the file with
|
|
487
|
+
PermissionError: the flip retries for up to _LOCK_OPEN_WAIT_SECONDS,
|
|
488
|
+
then exits 2 with the held-lock message, naming the last refusal. A
|
|
489
|
+
refusal with a folder at the path, or with nothing there on an
|
|
490
|
+
immediate second try, is a lasting denial and is raised as before.
|
|
279
491
|
"""
|
|
280
|
-
lock_path = link.with_name(link.name + ".skf-lock")
|
|
281
492
|
link.parent.mkdir(parents=True, exist_ok=True)
|
|
282
493
|
|
|
283
494
|
# Refuse only if <link> is a real directory/file (not a symlink or junction).
|
|
@@ -285,56 +496,14 @@ def cmd_flip_link(link: Path, target: str) -> None:
|
|
|
285
496
|
if link.exists() and not _is_link_or_junction(link):
|
|
286
497
|
_die(2, f"refusing to replace non-link: {link}")
|
|
287
498
|
|
|
288
|
-
lock_fd = os.open(lock_path, os.O_WRONLY | os.O_CREAT, 0o644)
|
|
289
|
-
lock_held = False
|
|
290
499
|
try:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
_die(2, f"another process holds flip lock on {link}")
|
|
297
|
-
raise
|
|
298
|
-
|
|
299
|
-
tmp_link = link.with_name(link.name + ".skf-tmp-link")
|
|
300
|
-
if tmp_link.is_symlink() or tmp_link.exists():
|
|
301
|
-
# On Windows a junction returns is_dir()=True, exists()=True but
|
|
302
|
-
# is_symlink()=False — must rmdir/unlink based on type.
|
|
303
|
-
if tmp_link.is_dir() and not tmp_link.is_symlink():
|
|
304
|
-
tmp_link.rmdir()
|
|
305
|
-
else:
|
|
306
|
-
tmp_link.unlink()
|
|
307
|
-
try:
|
|
308
|
-
link_kind = _create_symlink_or_junction(target, tmp_link)
|
|
309
|
-
except OSError as e:
|
|
310
|
-
if os.name == "nt" and getattr(e, "winerror", None) in (1314, 5):
|
|
311
|
-
_die(
|
|
312
|
-
2,
|
|
313
|
-
"symlink/junction creation failed on Windows. Junction fallback "
|
|
314
|
-
"requires the target directory to exist. Either enable Developer "
|
|
315
|
-
"Mode (Settings → Privacy & Security → For Developers) or use WSL2.",
|
|
316
|
-
)
|
|
317
|
-
raise
|
|
318
|
-
# os.replace can swap a symlink-over-symlink atomically, but Windows
|
|
319
|
-
# rejects renaming a directory (junction) over an existing directory
|
|
320
|
-
# (junction or real). Drop the existing link first when junction-based.
|
|
321
|
-
if link_kind == "junction" and (link.is_dir() or link.is_symlink() or link.exists()):
|
|
322
|
-
if link.is_symlink() or not link.is_dir():
|
|
323
|
-
link.unlink()
|
|
324
|
-
else:
|
|
325
|
-
link.rmdir()
|
|
326
|
-
os.replace(tmp_link, link)
|
|
500
|
+
lock_fd = take_flip_lock(link)
|
|
501
|
+
except FlipLockHeld as exc:
|
|
502
|
+
_die(2, str(exc))
|
|
503
|
+
try:
|
|
504
|
+
link_kind = replace_link(link, target)
|
|
327
505
|
finally:
|
|
328
|
-
|
|
329
|
-
_release_lock(lock_fd)
|
|
330
|
-
os.close(lock_fd)
|
|
331
|
-
# Lock file is bookkeeping only — once the fd is closed the lock is
|
|
332
|
-
# gone, so leaving the file around just litters the skill_group dir.
|
|
333
|
-
# Best-effort unlink; a concurrent flipper may have already removed it.
|
|
334
|
-
try:
|
|
335
|
-
lock_path.unlink()
|
|
336
|
-
except OSError:
|
|
337
|
-
pass
|
|
506
|
+
release_flip_lock(link, lock_fd)
|
|
338
507
|
|
|
339
508
|
_ok({"link": str(link), "points_to": target, "kind": link_kind})
|
|
340
509
|
|