bmad-module-skill-forge 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +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 +135 -18
- package/docs/verifying-a-skill.md +83 -57
- package/docs/why-skf.md +24 -22
- package/docs/workflows.md +274 -110
- package/package.json +18 -10
- package/src/forger/preferences.yaml +8 -1
- package/src/knowledge/agentskills-spec.md +2 -0
- package/src/knowledge/ccc-bridge.md +40 -24
- package/src/knowledge/confidence-tiers.md +9 -5
- package/src/knowledge/overview.md +3 -3
- package/src/knowledge/progressive-capability.md +8 -8
- package/src/knowledge/provenance-tracking.md +9 -5
- package/src/knowledge/qmd-registry.md +2 -2
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +18 -11
- package/src/knowledge/split-body-strategy.md +0 -3
- package/src/knowledge/tool-resolution.md +4 -2
- package/src/knowledge/version-paths.md +101 -15
- package/src/module-help.csv +3 -3
- package/src/module.yaml +10 -0
- package/src/shared/data/ast-grep-recipes.yaml +608 -0
- package/src/shared/data/ide-context-files.json +30 -0
- package/src/shared/health-check.md +32 -12
- package/src/shared/references/description-guard-protocol.md +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 +30 -14
- package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
- package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
- package/src/shared/scripts/skf-check-unit-records.py +184 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
- package/src/shared/scripts/skf-classify-changed-files.py +694 -0
- package/src/shared/scripts/skf-comention-pairs.py +1333 -123
- package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
- package/src/shared/scripts/skf-count-imports.py +1867 -0
- package/src/shared/scripts/skf-count-tokens.py +1 -1
- package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
- package/src/shared/scripts/skf-description-guard.py +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 +1 -1
- 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 +4084 -85
- package/src/shared/scripts/skf-extraction-inventory.py +746 -0
- package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
- package/src/shared/scripts/skf-fetch-temporal.py +674 -0
- package/src/shared/scripts/skf-find-cycles.py +97 -29
- package/src/shared/scripts/skf-find-test-report.py +476 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
- package/src/shared/scripts/skf-github-fetch.py +365 -0
- package/src/shared/scripts/skf-github-probe.py +661 -0
- package/src/shared/scripts/skf-hash-content.py +166 -12
- package/src/shared/scripts/skf-language-corpora.py +1 -1
- package/src/shared/scripts/skf-load-provenance.py +159 -27
- package/src/shared/scripts/skf-manifest-ops.py +1 -1
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
- package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
- package/src/shared/scripts/skf-names-present.py +221 -0
- package/src/shared/scripts/skf-pair-intersect.py +119 -28
- package/src/shared/scripts/skf-parse-gaps.py +1384 -0
- package/src/shared/scripts/skf-preapply.py +1 -1
- package/src/shared/scripts/skf-preflight.py +161 -27
- package/src/shared/scripts/skf-promote-staged.py +430 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
- package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
- package/src/shared/scripts/skf-quick-batch.py +774 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
- package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
- package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
- package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
- package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
- package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
- package/src/shared/scripts/skf-resolve-package.py +523 -106
- package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
- package/src/shared/scripts/skf-run-lock.py +577 -0
- package/src/shared/scripts/skf-scan-manifests.py +998 -57
- package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
- package/src/shared/scripts/skf-severity-classify.py +712 -108
- package/src/shared/scripts/skf-shape-detect.py +298 -204
- package/src/shared/scripts/skf-shard-body.py +9 -8
- package/src/shared/scripts/skf-skill-inventory.py +2412 -89
- package/src/shared/scripts/skf-skills-module.py +472 -0
- package/src/shared/scripts/skf-source-tree.py +1796 -0
- package/src/shared/scripts/skf-structural-diff.py +670 -119
- package/src/shared/scripts/skf-tessl-review.py +706 -0
- package/src/shared/scripts/skf-update-active-symlink.py +5 -5
- package/src/shared/scripts/skf-update-run-state.py +624 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
- package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
- package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
- package/src/shared/scripts/skf-validate-output.py +712 -64
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +116 -10
- package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
- package/src/shared/scripts/skf-write-skill-brief.py +367 -19
- package/src/shared/tool-requirements.yaml +201 -0
- package/src/skf-analyze-source/SKILL.md +41 -36
- package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
- package/src/skf-analyze-source/customize.toml +36 -24
- package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
- package/src/skf-analyze-source/references/continue.md +2 -13
- package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
- package/src/skf-analyze-source/references/generate-briefs.md +121 -55
- package/src/skf-analyze-source/references/headless-contract.md +42 -15
- package/src/skf-analyze-source/references/identify-units.md +96 -110
- package/src/skf-analyze-source/references/init.md +73 -71
- package/src/skf-analyze-source/references/map-and-detect.md +123 -105
- package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
- package/src/skf-analyze-source/references/recommend.md +51 -70
- package/src/skf-analyze-source/references/scan-project.md +60 -84
- package/src/skf-analyze-source/references/scan-root.md +39 -0
- package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
- package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
- package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
- package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
- package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
- package/src/skf-audit-skill/SKILL.md +43 -51
- package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
- package/src/skf-audit-skill/customize.toml +41 -20
- package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
- package/src/skf-audit-skill/references/doc-drift.md +141 -0
- package/src/skf-audit-skill/references/headless-contract.md +29 -0
- package/src/skf-audit-skill/references/init.md +148 -210
- package/src/skf-audit-skill/references/re-index.md +96 -108
- package/src/skf-audit-skill/references/report.md +96 -33
- package/src/skf-audit-skill/references/semantic-diff.md +27 -18
- package/src/skf-audit-skill/references/severity-classify.md +79 -65
- package/src/skf-audit-skill/references/severity-rules.md +15 -6
- package/src/skf-audit-skill/references/structural-diff.md +75 -90
- package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
- package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
- package/src/skf-brief-skill/SKILL.md +36 -25
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/scope-templates.md +17 -24
- package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
- package/src/skf-brief-skill/customize.toml +48 -20
- package/src/skf-brief-skill/references/analyze-target.md +142 -125
- package/src/skf-brief-skill/references/confirm-brief.md +10 -8
- package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
- package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
- package/src/skf-brief-skill/references/gather-intent.md +87 -182
- package/src/skf-brief-skill/references/headless-args.md +84 -16
- package/src/skf-brief-skill/references/health-check.md +1 -1
- package/src/skf-brief-skill/references/invocation-contract.md +18 -13
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
- package/src/skf-brief-skill/references/scope-definition.md +123 -47
- package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
- package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
- package/src/skf-brief-skill/references/version-resolution.md +3 -2
- package/src/skf-brief-skill/references/write-brief.md +66 -57
- package/src/skf-campaign/SKILL.md +31 -34
- package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
- package/src/skf-campaign/customize.toml +47 -16
- package/src/skf-campaign/references/campaign-contracts.md +39 -29
- package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
- package/src/skf-campaign/references/health-check.md +7 -16
- package/src/skf-campaign/references/step-01-setup.md +46 -84
- package/src/skf-campaign/references/step-02-strategy.md +31 -30
- package/src/skf-campaign/references/step-03-pins.md +26 -18
- package/src/skf-campaign/references/step-04-provenance.md +24 -23
- package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
- package/src/skf-campaign/references/step-06-batch.md +41 -34
- package/src/skf-campaign/references/step-07-capstone.md +40 -25
- package/src/skf-campaign/references/step-08-verify.md +28 -29
- package/src/skf-campaign/references/step-09-refine.md +29 -30
- package/src/skf-campaign/references/step-10-export.md +48 -36
- package/src/skf-campaign/references/step-11-maintenance.md +31 -34
- package/src/skf-campaign/references/step-resume.md +37 -68
- package/src/skf-campaign/scripts/campaign-deps.py +57 -18
- package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
- package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
- package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
- package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
- package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
- package/src/skf-campaign/scripts/campaign-report.py +204 -37
- package/src/skf-campaign/scripts/campaign-state.py +1128 -0
- package/src/skf-campaign/scripts/campaign-status.py +10 -74
- package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
- package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
- package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
- package/src/skf-campaign/templates/campaign-report-template.md +5 -1
- package/src/skf-campaign/templates/kickoff-template.md +0 -14
- package/src/skf-create-skill/SKILL.md +27 -20
- package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
- package/src/skf-create-skill/assets/skill-sections.md +15 -5
- package/src/skf-create-skill/customize.toml +20 -8
- package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
- package/src/skf-create-skill/references/batch-mode.md +81 -0
- package/src/skf-create-skill/references/ccc-index-check.md +27 -0
- package/src/skf-create-skill/references/compile.md +107 -29
- package/src/skf-create-skill/references/component-extraction.md +113 -91
- package/src/skf-create-skill/references/enrich.md +41 -23
- package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
- package/src/skf-create-skill/references/extract.md +200 -161
- package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
- package/src/skf-create-skill/references/extraction-patterns.md +584 -196
- package/src/skf-create-skill/references/generate-artifacts.md +145 -93
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +53 -36
- package/src/skf-create-skill/references/relabel-rule.md +12 -0
- package/src/skf-create-skill/references/report.md +28 -59
- package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
- package/src/skf-create-skill/references/step-auto-shard.md +20 -93
- package/src/skf-create-skill/references/step-doc-rot.md +44 -60
- package/src/skf-create-skill/references/step-doc-sources.md +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 +79 -146
- package/src/skf-drop-skill/references/invocation-contract.md +64 -0
- package/src/skf-drop-skill/references/report.md +46 -30
- package/src/skf-drop-skill/references/select.md +118 -124
- package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
- package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
- package/src/skf-export-skill/SKILL.md +40 -62
- package/src/skf-export-skill/assets/managed-section-format.md +14 -50
- package/src/skf-export-skill/assets/snippet-format.md +3 -1
- package/src/skf-export-skill/customize.toml +42 -13
- package/src/skf-export-skill/references/generate-snippet.md +54 -46
- package/src/skf-export-skill/references/health-check.md +2 -0
- package/src/skf-export-skill/references/invocation-contract.md +15 -0
- package/src/skf-export-skill/references/load-skill.md +88 -62
- package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
- package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
- package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
- package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
- package/src/skf-export-skill/references/package.md +15 -73
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
- package/src/skf-export-skill/references/result-envelope.md +31 -16
- package/src/skf-export-skill/references/summary.md +41 -17
- package/src/skf-export-skill/references/token-report.md +4 -12
- package/src/skf-export-skill/references/update-context.md +118 -237
- package/src/skf-forger/SKILL.md +61 -37
- package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
- package/src/skf-forger/customize.toml +43 -0
- package/src/skf-forger/references/pipeline-mode.md +73 -18
- package/src/skf-forger/scripts/forge-status.py +299 -0
- package/src/skf-forger/scripts/parse-pipeline.py +322 -54
- package/src/skf-forger/scripts/pipeline-gate.py +428 -0
- package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
- package/src/skf-quick-skill/SKILL.md +54 -53
- package/src/skf-quick-skill/assets/skill-template.md +4 -2
- package/src/skf-quick-skill/customize.toml +51 -20
- package/src/skf-quick-skill/references/batch-mode.md +74 -21
- package/src/skf-quick-skill/references/compile.md +30 -31
- package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
- package/src/skf-quick-skill/references/finalize.md +69 -18
- package/src/skf-quick-skill/references/halt-contract.md +41 -29
- package/src/skf-quick-skill/references/health-check.md +5 -3
- package/src/skf-quick-skill/references/quick-extract.md +114 -45
- package/src/skf-quick-skill/references/registry-resolution.md +9 -64
- package/src/skf-quick-skill/references/resolve-target.md +116 -66
- package/src/skf-quick-skill/references/write-and-validate.md +79 -40
- package/src/skf-refine-architecture/SKILL.md +43 -53
- package/src/skf-refine-architecture/customize.toml +49 -20
- package/src/skf-refine-architecture/references/compile.md +97 -54
- package/src/skf-refine-architecture/references/exit-codes.md +30 -0
- package/src/skf-refine-architecture/references/finding-storage.md +21 -0
- package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
- package/src/skf-refine-architecture/references/improvements.md +14 -20
- package/src/skf-refine-architecture/references/init.md +98 -57
- package/src/skf-refine-architecture/references/issue-detection.md +32 -39
- package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
- package/src/skf-refine-architecture/references/report.md +65 -54
- package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
- package/src/skf-rename-skill/SKILL.md +19 -54
- package/src/skf-rename-skill/customize.toml +32 -26
- package/src/skf-rename-skill/references/execute.md +230 -184
- package/src/skf-rename-skill/references/exit-codes.md +7 -7
- package/src/skf-rename-skill/references/health-check.md +1 -7
- package/src/skf-rename-skill/references/invocation-contract.md +25 -0
- package/src/skf-rename-skill/references/report.md +63 -52
- package/src/skf-rename-skill/references/select.md +197 -115
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
- package/src/skf-setup/SKILL.md +26 -26
- package/src/skf-setup/customize.toml +14 -10
- package/src/skf-setup/references/auto-index.md +50 -63
- package/src/skf-setup/references/ccc-index.md +29 -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 +217 -254
- package/src/skf-test-skill/references/detect-mode.md +5 -5
- package/src/skf-test-skill/references/external-validators.md +94 -82
- package/src/skf-test-skill/references/health-check.md +6 -7
- package/src/skf-test-skill/references/init.md +163 -88
- package/src/skf-test-skill/references/invocation-contract.md +66 -0
- package/src/skf-test-skill/references/migration-section-rules.md +5 -2
- package/src/skf-test-skill/references/report.md +162 -142
- package/src/skf-test-skill/references/score.md +80 -107
- package/src/skf-test-skill/references/scoring-rules.md +41 -155
- package/src/skf-test-skill/references/source-access-protocol.md +19 -92
- package/src/skf-test-skill/references/step-hard-gate.md +51 -33
- package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
- package/src/skf-test-skill/scripts/build-result-context.py +287 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
- package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
- package/src/skf-test-skill/scripts/compute-score.py +223 -45
- package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
- package/src/skf-test-skill/scripts/hard-gate.py +203 -0
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
- package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
- package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
- package/src/skf-test-skill/scripts/score-signatures.py +459 -0
- package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
- package/src/skf-test-skill/templates/test-report-template.md +21 -15
- package/src/skf-update-skill/SKILL.md +38 -25
- package/src/skf-update-skill/customize.toml +27 -12
- package/src/skf-update-skill/references/detect-changes.md +233 -222
- package/src/skf-update-skill/references/gap-driven.md +336 -0
- package/src/skf-update-skill/references/health-check.md +31 -5
- package/src/skf-update-skill/references/init.md +250 -84
- package/src/skf-update-skill/references/invocation-contract.md +12 -0
- package/src/skf-update-skill/references/manual-section-rules.md +6 -23
- package/src/skf-update-skill/references/merge.md +180 -47
- package/src/skf-update-skill/references/re-extract.md +53 -262
- package/src/skf-update-skill/references/report.md +100 -35
- package/src/skf-update-skill/references/write.md +331 -149
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
- package/src/skf-verify-stack/SKILL.md +23 -42
- package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
- package/src/skf-verify-stack/customize.toml +55 -22
- package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
- package/src/skf-verify-stack/references/coverage.md +67 -64
- package/src/skf-verify-stack/references/exit-codes.md +21 -8
- package/src/skf-verify-stack/references/init.md +107 -62
- package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
- package/src/skf-verify-stack/references/integrations.md +106 -65
- package/src/skf-verify-stack/references/report.md +59 -42
- package/src/skf-verify-stack/references/requirements.md +32 -9
- package/src/skf-verify-stack/references/synthesize.md +53 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
- package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
- package/tools/cli/commands/install.js +11 -4
- package/tools/cli/commands/status.js +15 -3
- package/tools/cli/commands/update.js +11 -4
- package/tools/cli/lib/tool-check.js +320 -0
- package/tools/cli/lib/version-check.js +22 -9
- package/tools/cli/skf-cli.js +4 -1
- package/.gitattributes +0 -16
- package/.nvmrc +0 -1
- package/docs/_internal/RELEASING.md +0 -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
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# /// script
|
|
2
|
-
# requires-python = ">=3.
|
|
2
|
+
# requires-python = ">=3.11"
|
|
3
3
|
# dependencies = ["pyyaml"]
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF Forge Tier RW — Read/write primitives for forger-sidecar YAML files.
|
|
6
6
|
|
|
7
7
|
Replaces the prose-driven YAML emission in `src/skf-setup/references/
|
|
8
|
-
write-config.md` §1
|
|
9
|
-
step 3 §
|
|
10
|
-
of truth for the on-disk forge-tier.yaml schema (
|
|
11
|
-
|
|
8
|
+
write-config.md` §1 (and the prose-driven cleanup logic in
|
|
9
|
+
step 3 §4) with one Python invocation. The script is the source
|
|
10
|
+
of truth for the on-disk forge-tier.yaml schema (the canonical
|
|
11
|
+
template is render_forge_tier_yaml below) and guarantees
|
|
12
12
|
that registry arrays are PRESERVED across rewrites — losing
|
|
13
13
|
`qmd_collections` or `ccc_index_registry` would break every
|
|
14
14
|
downstream skill (skf-create-skill, skf-audit-skill, skf-update-skill,
|
|
@@ -21,14 +21,31 @@ Subcommands:
|
|
|
21
21
|
payload with status=ok so first-run callers can branch.
|
|
22
22
|
|
|
23
23
|
write-tools Write a fresh forge-tier.yaml from a JSON context payload
|
|
24
|
-
on stdin
|
|
25
|
-
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
on stdin, or, with --detect-from, from the outputs a
|
|
25
|
+
setup run staged in its run folder (below). Preserves
|
|
26
|
+
`qmd_collections`, `ccc_index_registry`, and the
|
|
27
|
+
user-customizable `ccc_index.staleness_threshold_hours`
|
|
28
|
+
from the existing file (if any) by reading it first, then
|
|
29
|
+
merging, and `ccc_index.exclude_patterns` when the payload
|
|
30
|
+
sends null (the SKF exclusion record kept across runs that
|
|
31
|
+
did not reconcile it).
|
|
32
|
+
|
|
33
|
+
--detect-from <detect-tools.json>
|
|
34
|
+
skf-detect-tools.py's output: `tools` (each tool's
|
|
35
|
+
`available`, ccc's `daemon`) and `tier.calculated`.
|
|
36
|
+
--ccc-from <ccc-exclusions.json>
|
|
37
|
+
skf-merge-ccc-exclusions.py's --result-to file, run
|
|
38
|
+
with --build-index: its `index` object (status,
|
|
39
|
+
indexed_path, last_indexed, file_count) and its
|
|
40
|
+
`effective_patterns` become `ccc_index`. A file that
|
|
41
|
+
is not there means the run did not prepare ccc
|
|
42
|
+
(status "none"), unless detect-tools.json reports
|
|
43
|
+
ccc available: the helper then wrote no result, so
|
|
44
|
+
the preparation failed (status "failed"), as with a
|
|
45
|
+
file that holds an error, or no JSON object.
|
|
46
|
+
Either way the exclusion record is kept.
|
|
47
|
+
With --detect-from the subcommand reads nothing on stdin,
|
|
48
|
+
so no value passes through the caller's hands.
|
|
32
49
|
|
|
33
50
|
register-qmd-collection
|
|
34
51
|
Append-or-replace a single entry in the `qmd_collections`
|
|
@@ -37,11 +54,19 @@ Subcommands:
|
|
|
37
54
|
the same `name` is replaced, otherwise appended). All
|
|
38
55
|
other forge-tier state (tools / tier / ccc_index /
|
|
39
56
|
ccc_index_registry / other qmd_collections entries) is
|
|
40
|
-
preserved verbatim. Used by skf-brief-skill step 5 §
|
|
57
|
+
preserved verbatim. Used by skf-brief-skill step 5 §3b
|
|
41
58
|
and skf-create-skill to register Deep-tier QMD
|
|
42
59
|
collections without re-rendering the whole file in
|
|
43
60
|
prose.
|
|
44
61
|
|
|
62
|
+
remove-qmd-collection
|
|
63
|
+
Remove every `qmd_collections` entry whose `name` is
|
|
64
|
+
--name. All other forge-tier state is preserved verbatim.
|
|
65
|
+
The rollback of register-qmd-collection, for a caller
|
|
66
|
+
whose `qmd collection add` failed after it removed the
|
|
67
|
+
old collection, so the registry matches QMD again. A
|
|
68
|
+
name with no entry changes nothing (action "absent").
|
|
69
|
+
|
|
45
70
|
register-ccc-index
|
|
46
71
|
Append-or-replace a single entry in the
|
|
47
72
|
`ccc_index_registry` array. Reads the entry as JSON on
|
|
@@ -54,26 +79,46 @@ Subcommands:
|
|
|
54
79
|
re-rendering the whole file in prose.
|
|
55
80
|
|
|
56
81
|
clean-stale Two cleanup operations gated by flags:
|
|
57
|
-
--qmd-live-
|
|
58
|
-
entries whose `name` is not in the
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
--qmd-live-from <qmd-classify.json>: remove
|
|
83
|
+
qmd_collections entries whose `name` is not in the
|
|
84
|
+
`live_names` list of skf-qmd-classify-collections.py's
|
|
85
|
+
staged output. A file that is not there, or holds no
|
|
86
|
+
`live_names` list (the classifier did not run, or
|
|
87
|
+
failed), skips QMD cleanup: an empty list would read
|
|
88
|
+
as "nothing is live" and empty the registry.
|
|
89
|
+
--qmd-live-names a,b,c: the same with the list given
|
|
90
|
+
inline (mutually exclusive with --qmd-live-from).
|
|
62
91
|
--prune-missing-ccc-paths — remove ccc_index_registry
|
|
63
92
|
entries whose `path` no longer exists on disk.
|
|
64
|
-
Both
|
|
93
|
+
Both kinds of cleanup can run in the same invocation.
|
|
65
94
|
|
|
66
95
|
Output schema (the read subcommand and the response from every write):
|
|
67
96
|
|
|
68
97
|
{"status": "ok", "version": "v1", ...subcommand-specific fields...}
|
|
69
98
|
|
|
99
|
+
The subcommands that take the registry lock (below) also return
|
|
100
|
+
`lock_stale_replaced`: null, or the {"held_by", "held_since"} of the
|
|
101
|
+
stale lock they replaced.
|
|
102
|
+
|
|
70
103
|
Errors emit `{"status": "error", "message": "..."}` to stderr and
|
|
71
|
-
exit non-zero (1 for user error, 2 for I/O failure
|
|
104
|
+
exit non-zero (1 for user error, 2 for I/O failure or a registry lock
|
|
105
|
+
that cannot be taken, 3 when another call held the registry lock for the
|
|
106
|
+
whole --lock-timeout; nothing was written).
|
|
72
107
|
|
|
73
108
|
Cross-platform: pure stdlib + PyYAML. Atomic writes via temp + rename
|
|
74
|
-
mirror skf-atomic-write.py's pattern.
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
mirror skf-atomic-write.py's pattern.
|
|
110
|
+
|
|
111
|
+
Registry lock: every subcommand that rewrites forge-tier.yaml
|
|
112
|
+
(write-tools, register-qmd-collection, remove-qmd-collection,
|
|
113
|
+
register-ccc-index, clean-stale) holds `forge-tier.yaml.lock` beside it
|
|
114
|
+
for its one read-modify-write and releases it before it exits, through
|
|
115
|
+
skf-run-lock.py loaded from this script's folder. Concurrent runs never
|
|
116
|
+
lose each other's entries, and no caller takes a lock of its own: the
|
|
117
|
+
lock lives inside one call, never across calls. A call waits up to
|
|
118
|
+
--lock-timeout seconds (30 by default) while another call holds the lock.
|
|
119
|
+
A lock older than 15 seconds was left by a call that was killed, and is
|
|
120
|
+
replaced. An empty lock file holds nothing (the helper never writes one;
|
|
121
|
+
an `flock` on the same path leaves one) and is replaced at once.
|
|
77
122
|
|
|
78
123
|
CLI — invoke via `uv run` so the PEP 723 PyYAML dependency declared
|
|
79
124
|
above is auto-resolved on first call and cached. `docs/getting-started.md`
|
|
@@ -84,16 +129,23 @@ system-wide:
|
|
|
84
129
|
|
|
85
130
|
uv run skf-forge-tier-rw.py read --target /path/forge-tier.yaml
|
|
86
131
|
echo '{...}' | uv run skf-forge-tier-rw.py write-tools --target /path/forge-tier.yaml
|
|
87
|
-
uv run skf-forge-tier-rw.py
|
|
132
|
+
uv run skf-forge-tier-rw.py write-tools --target /path/forge-tier.yaml \\
|
|
133
|
+
--detect-from /run/detect-tools.json --ccc-from /run/ccc-exclusions.json
|
|
134
|
+
uv run skf-forge-tier-rw.py register-qmd-collection --target /path/forge-tier.yaml < entry.json
|
|
135
|
+
uv run skf-forge-tier-rw.py remove-qmd-collection --target /path/forge-tier.yaml \\
|
|
136
|
+
--name foo-extraction
|
|
88
137
|
echo '{...}' | uv run skf-forge-tier-rw.py register-ccc-index --target /path/forge-tier.yaml
|
|
89
138
|
uv run skf-forge-tier-rw.py clean-stale --target /path/forge-tier.yaml \\
|
|
90
|
-
--qmd-live-
|
|
139
|
+
--qmd-live-from /run/qmd-classify.json --prune-missing-ccc-paths
|
|
91
140
|
"""
|
|
92
141
|
|
|
93
142
|
from __future__ import annotations
|
|
94
143
|
|
|
95
144
|
import argparse
|
|
145
|
+
import contextlib
|
|
146
|
+
import importlib.util
|
|
96
147
|
import json
|
|
148
|
+
import math
|
|
97
149
|
import os
|
|
98
150
|
import sys
|
|
99
151
|
from datetime import datetime, timezone
|
|
@@ -103,28 +155,13 @@ import yaml
|
|
|
103
155
|
|
|
104
156
|
|
|
105
157
|
DEFAULT_STALENESS_HOURS = 24
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Passive context injection (set to false to skip snippet generation and CLAUDE.md updates during export)
|
|
114
|
-
passive_context: true
|
|
115
|
-
|
|
116
|
-
# Headless mode (set to true to skip confirmation gates in all workflows)
|
|
117
|
-
headless_mode: false
|
|
118
|
-
|
|
119
|
-
# Compact greeting (set to true to skip the full capabilities table on session start)
|
|
120
|
-
compact_greeting: false
|
|
121
|
-
|
|
122
|
-
# Reserved for future use — these fields are not yet consumed by any workflow step
|
|
123
|
-
# output_language: ~
|
|
124
|
-
# skill_format_version: ~
|
|
125
|
-
# citation_style: ~
|
|
126
|
-
# confidence_display: ~
|
|
127
|
-
"""
|
|
158
|
+
RUN_LOCK_HELPER = Path(__file__).resolve().parent / "skf-run-lock.py"
|
|
159
|
+
REGISTRY_LOCK_WAIT_SEC = 30.0
|
|
160
|
+
# A call holds the registry lock for milliseconds; one this old was killed.
|
|
161
|
+
REGISTRY_LOCK_STALE_SEC = 15.0
|
|
162
|
+
EXIT_LOCK_BUSY = 3
|
|
163
|
+
# The tool keys write-tools records, in forge-tier.yaml order.
|
|
164
|
+
TOOL_KEYS = ("ast_grep", "gh_cli", "qmd", "ccc")
|
|
128
165
|
|
|
129
166
|
|
|
130
167
|
def _die(code: int, message: str) -> None:
|
|
@@ -178,6 +215,52 @@ def _atomic_write(target: Path, content: str) -> None:
|
|
|
178
215
|
_die(2, f"atomic write failed for {target}: {e}")
|
|
179
216
|
|
|
180
217
|
|
|
218
|
+
def _load_run_lock():
|
|
219
|
+
"""Load skf-run-lock.py, which installs into the same folder as this script."""
|
|
220
|
+
try:
|
|
221
|
+
spec = importlib.util.spec_from_file_location("skf_run_lock", RUN_LOCK_HELPER)
|
|
222
|
+
if spec is None or spec.loader is None:
|
|
223
|
+
raise ImportError("no import spec")
|
|
224
|
+
module = importlib.util.module_from_spec(spec)
|
|
225
|
+
spec.loader.exec_module(module)
|
|
226
|
+
except (OSError, ImportError) as e:
|
|
227
|
+
_die(2, f"cannot load the run-lock helper {RUN_LOCK_HELPER}: {e}")
|
|
228
|
+
return module
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@contextlib.contextmanager
|
|
232
|
+
def _registry_lock(target: Path, command: str, timeout: float):
|
|
233
|
+
"""Hold `<target>.lock` through one read-modify-write of forge-tier.yaml.
|
|
234
|
+
|
|
235
|
+
Yields the stale lock this call replaced ({"held_by", "held_since"}) or
|
|
236
|
+
None. Exits 3 when another call holds the lock for `timeout` seconds, 2
|
|
237
|
+
when the lock cannot be taken (a folder in its place, an I/O error).
|
|
238
|
+
"""
|
|
239
|
+
run_lock = _load_run_lock()
|
|
240
|
+
lock = target.with_name(target.name + ".lock")
|
|
241
|
+
owner = f"forge-tier-rw:{command}:{run_lock.new_run_id()}"
|
|
242
|
+
try:
|
|
243
|
+
# The empty file an `flock` on this path leaves (create-skill prose
|
|
244
|
+
# before 3.0.0) holds nothing, so it must not make the call wait.
|
|
245
|
+
held = run_lock.acquire_within(lock, owner, REGISTRY_LOCK_STALE_SEC, timeout,
|
|
246
|
+
empty_is_stale=True)
|
|
247
|
+
except run_lock.LockBusy as busy:
|
|
248
|
+
info = busy.result
|
|
249
|
+
_die(EXIT_LOCK_BUSY,
|
|
250
|
+
f"{command}: {lock} is held by {info['held_by'] or 'an unnamed owner'} since "
|
|
251
|
+
f"{info['held_since']}: another run is writing {target.name}. Nothing was "
|
|
252
|
+
f"written; run the command again.")
|
|
253
|
+
except (OSError, ValueError) as e:
|
|
254
|
+
_die(2, f"{command}: cannot take {lock}: {e}")
|
|
255
|
+
try:
|
|
256
|
+
yield held["stale_replaced"]
|
|
257
|
+
finally:
|
|
258
|
+
try:
|
|
259
|
+
run_lock.release(lock, owner)
|
|
260
|
+
except (OSError, ValueError):
|
|
261
|
+
pass # a lock this call could not delete goes stale on its own
|
|
262
|
+
|
|
263
|
+
|
|
181
264
|
def _yaml_block(value, indent: int = 0) -> str:
|
|
182
265
|
"""Dump a value as a YAML fragment, indented by `indent` spaces, no trailing newline."""
|
|
183
266
|
text = yaml.safe_dump(value, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
@@ -191,8 +274,8 @@ def _yaml_block(value, indent: int = 0) -> str:
|
|
|
191
274
|
def render_forge_tier_yaml(payload: dict) -> str:
|
|
192
275
|
"""Render the canonical forge-tier.yaml from a context payload.
|
|
193
276
|
|
|
194
|
-
|
|
195
|
-
|
|
277
|
+
This is the file's one template, human-readable section comments
|
|
278
|
+
included. Sections are emitted in a fixed order
|
|
196
279
|
so re-runs against unchanged inputs produce byte-identical output.
|
|
197
280
|
"""
|
|
198
281
|
tools = payload["tools"]
|
|
@@ -219,7 +302,7 @@ def render_forge_tier_yaml(payload: dict) -> str:
|
|
|
219
302
|
"status": ccc_index.get("status"),
|
|
220
303
|
"staleness_threshold_hours": ccc_index.get("staleness_threshold_hours", DEFAULT_STALENESS_HOURS),
|
|
221
304
|
"file_count": ccc_index.get("file_count"),
|
|
222
|
-
"exclude_patterns": ccc_index.get("exclude_patterns"
|
|
305
|
+
"exclude_patterns": ccc_index.get("exclude_patterns") or [],
|
|
223
306
|
}
|
|
224
307
|
|
|
225
308
|
parts = [
|
|
@@ -257,13 +340,14 @@ def _yaml_scalar(value) -> str:
|
|
|
257
340
|
def _merge_preserved_fields(payload: dict, existing: dict | None) -> dict:
|
|
258
341
|
"""Inject preserved fields from the existing file into the new payload.
|
|
259
342
|
|
|
260
|
-
|
|
343
|
+
Four preservation rules (write-tools in the module docstring):
|
|
261
344
|
- `qmd_collections` array — preserved entirely from existing.
|
|
262
345
|
- `ccc_index_registry` array — preserved entirely from existing.
|
|
263
346
|
- `ccc_index.staleness_threshold_hours` scalar — preserved if user set
|
|
264
347
|
a non-default value; else uses payload value or DEFAULT.
|
|
265
|
-
|
|
266
|
-
|
|
348
|
+
- `ccc_index.exclude_patterns` array — replaced when the payload
|
|
349
|
+
carries a list; kept from the existing file when the payload sends
|
|
350
|
+
null or omits it (an existing value that is not a list becomes []).
|
|
267
351
|
"""
|
|
268
352
|
if existing is None:
|
|
269
353
|
return payload
|
|
@@ -277,6 +361,31 @@ def _merge_preserved_fields(payload: dict, existing: dict | None) -> dict:
|
|
|
277
361
|
payload["ccc_index"]["staleness_threshold_hours"] = existing_ccc.get(
|
|
278
362
|
"staleness_threshold_hours", DEFAULT_STALENESS_HOURS
|
|
279
363
|
)
|
|
364
|
+
if payload["ccc_index"].get("exclude_patterns") is None:
|
|
365
|
+
prior = existing_ccc.get("exclude_patterns")
|
|
366
|
+
payload["ccc_index"]["exclude_patterns"] = prior if isinstance(prior, list) else []
|
|
367
|
+
return payload
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _payload_from(data: dict, **replace) -> dict:
|
|
371
|
+
"""The render payload of an existing forge-tier.yaml, with `replace` applied.
|
|
372
|
+
|
|
373
|
+
render_forge_tier_yaml() emits exactly the six known top-level sections
|
|
374
|
+
(tools, tier, tier_detected_at, ccc_index, ccc_index_registry,
|
|
375
|
+
qmd_collections), so a rewrite from this payload drops any other
|
|
376
|
+
top-level key, as cmd_write_tools does. Update render_forge_tier_yaml()
|
|
377
|
+
and this function together when the schema grows.
|
|
378
|
+
"""
|
|
379
|
+
payload = {
|
|
380
|
+
"tools": data.get("tools", {}),
|
|
381
|
+
"tier": data.get("tier", "Quick"),
|
|
382
|
+
"tier_detected_at": data.get("tier_detected_at",
|
|
383
|
+
datetime.now(timezone.utc).isoformat()),
|
|
384
|
+
"ccc_index": data.get("ccc_index", {}),
|
|
385
|
+
"ccc_index_registry": data.get("ccc_index_registry", []),
|
|
386
|
+
"qmd_collections": data.get("qmd_collections", []),
|
|
387
|
+
}
|
|
388
|
+
payload.update(replace)
|
|
280
389
|
return payload
|
|
281
390
|
|
|
282
391
|
|
|
@@ -291,14 +400,86 @@ def cmd_read(target: Path) -> None:
|
|
|
291
400
|
_ok({"exists": True, "data": data})
|
|
292
401
|
|
|
293
402
|
|
|
294
|
-
def
|
|
295
|
-
|
|
296
|
-
if not raw.strip():
|
|
297
|
-
_die(1, "write-tools: empty stdin (expected JSON payload)")
|
|
403
|
+
def _staged_object(path: Path):
|
|
404
|
+
"""The JSON object a staged helper output holds; None when it holds none (or cannot be read)."""
|
|
298
405
|
try:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
406
|
+
# utf-8-sig: a shell that writes a byte-order mark still stages JSON.
|
|
407
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
408
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
409
|
+
return None
|
|
410
|
+
return value if isinstance(value, dict) else None
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _staged_ccc_index(ccc_from: Path | None, ccc_available: bool = False) -> dict:
|
|
414
|
+
"""forge-tier.yaml's `ccc_index` from the merge helper's --result-to file.
|
|
415
|
+
|
|
416
|
+
No file: the run did not prepare ccc (status "none"), or, when
|
|
417
|
+
`ccc_available`, the helper ran and wrote nothing (a usage error, or a
|
|
418
|
+
call its host stopped), so the preparation failed (status "failed"). A
|
|
419
|
+
file holding an error, or no JSON object: the preparation failed too.
|
|
420
|
+
Each keeps the exclusion record (exclude_patterns null). A result
|
|
421
|
+
without an `index` object comes from a run without --build-index,
|
|
422
|
+
which a setup run never makes: that is a usage error.
|
|
423
|
+
"""
|
|
424
|
+
unset = {"indexed_path": None, "last_indexed": None, "file_count": None, "exclude_patterns": None}
|
|
425
|
+
if ccc_from is None or not ccc_from.exists():
|
|
426
|
+
if ccc_available:
|
|
427
|
+
return {**unset, "status": "failed"}
|
|
428
|
+
return {**unset, "status": "none"}
|
|
429
|
+
result = _staged_object(ccc_from)
|
|
430
|
+
if result is None or result.get("status") != "ok":
|
|
431
|
+
return {**unset, "status": "failed"}
|
|
432
|
+
index = result.get("index")
|
|
433
|
+
if not isinstance(index, dict):
|
|
434
|
+
_die(1, f"write-tools: {ccc_from} holds no `index` result (run the merge helper with --build-index)")
|
|
435
|
+
patterns = result.get("effective_patterns")
|
|
436
|
+
return {
|
|
437
|
+
"indexed_path": index.get("indexed_path"),
|
|
438
|
+
"last_indexed": index.get("last_indexed"),
|
|
439
|
+
"status": index.get("status"),
|
|
440
|
+
"file_count": index.get("file_count"),
|
|
441
|
+
"exclude_patterns": patterns if isinstance(patterns, list) else None,
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def payload_from_staged(detect_from: Path, ccc_from: Path | None) -> dict:
|
|
446
|
+
"""The write-tools payload from a setup run's staged detector and ccc outputs."""
|
|
447
|
+
detect = _staged_object(detect_from)
|
|
448
|
+
if detect is None:
|
|
449
|
+
_die(1, f"write-tools: {detect_from} holds no skf-detect-tools.py output")
|
|
450
|
+
tools, tier = detect.get("tools"), detect.get("tier")
|
|
451
|
+
if not isinstance(tools, dict) or not isinstance(tier, dict) or not tier.get("calculated"):
|
|
452
|
+
_die(1, f"write-tools: {detect_from} has no `tools` and `tier.calculated`")
|
|
453
|
+
|
|
454
|
+
def probe(key: str) -> dict:
|
|
455
|
+
value = tools.get(key)
|
|
456
|
+
return value if isinstance(value, dict) else {}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
"tools": {
|
|
460
|
+
**{key: probe(key).get("available") is True for key in TOOL_KEYS},
|
|
461
|
+
"ccc_daemon": probe("ccc").get("daemon"),
|
|
462
|
+
"security_scan": probe("security_scan").get("available") is True,
|
|
463
|
+
},
|
|
464
|
+
"tier": tier["calculated"],
|
|
465
|
+
"ccc_index": _staged_ccc_index(ccc_from, probe("ccc").get("available") is True),
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def cmd_write_tools(target: Path, lock_timeout: float = REGISTRY_LOCK_WAIT_SEC,
|
|
470
|
+
detect_from: Path | None = None, ccc_from: Path | None = None) -> None:
|
|
471
|
+
if detect_from is not None:
|
|
472
|
+
payload = payload_from_staged(detect_from, ccc_from)
|
|
473
|
+
else:
|
|
474
|
+
if ccc_from is not None:
|
|
475
|
+
_die(1, "write-tools: --ccc-from needs --detect-from")
|
|
476
|
+
raw = sys.stdin.read()
|
|
477
|
+
if not raw.strip():
|
|
478
|
+
_die(1, "write-tools: empty stdin (expected JSON payload)")
|
|
479
|
+
try:
|
|
480
|
+
payload = json.loads(raw)
|
|
481
|
+
except json.JSONDecodeError as e:
|
|
482
|
+
_die(1, f"write-tools: invalid JSON on stdin: {e}")
|
|
302
483
|
|
|
303
484
|
required = {"tools", "tier", "ccc_index"}
|
|
304
485
|
missing = required - set(payload.keys())
|
|
@@ -307,11 +488,12 @@ def cmd_write_tools(target: Path) -> None:
|
|
|
307
488
|
|
|
308
489
|
payload.setdefault("tier_detected_at", datetime.now(timezone.utc).isoformat())
|
|
309
490
|
|
|
310
|
-
|
|
311
|
-
|
|
491
|
+
with _registry_lock(target, "write-tools", lock_timeout) as stale_replaced:
|
|
492
|
+
existing = _read_yaml(target)
|
|
493
|
+
payload = _merge_preserved_fields(payload, existing)
|
|
312
494
|
|
|
313
|
-
|
|
314
|
-
|
|
495
|
+
rendered = render_forge_tier_yaml(payload)
|
|
496
|
+
_atomic_write(target, rendered)
|
|
315
497
|
_ok({
|
|
316
498
|
"wrote": str(target),
|
|
317
499
|
"preserved_arrays": {
|
|
@@ -319,24 +501,12 @@ def cmd_write_tools(target: Path) -> None:
|
|
|
319
501
|
"ccc_index_registry": len(payload.get("ccc_index_registry", [])),
|
|
320
502
|
},
|
|
321
503
|
"tier": payload["tier"],
|
|
504
|
+
"lock_stale_replaced": stale_replaced,
|
|
322
505
|
})
|
|
323
506
|
|
|
324
507
|
|
|
325
|
-
def
|
|
326
|
-
|
|
327
|
-
_ok({"exists": True, "wrote": False, "path": str(target)})
|
|
328
|
-
return
|
|
329
|
-
_atomic_write(target, PREFERENCES_TEMPLATE)
|
|
330
|
-
_ok({"exists": True, "wrote": True, "path": str(target), "first_run": True})
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
def cmd_register_qmd_collection(target: Path) -> None:
|
|
334
|
-
# Note: render_forge_tier_yaml() emits exactly the six known top-level
|
|
335
|
-
# sections (tools, tier, tier_detected_at, ccc_index, ccc_index_registry,
|
|
336
|
-
# qmd_collections). If a future schema revision adds a new top-level key,
|
|
337
|
-
# this subcommand will silently drop it — same limitation that already
|
|
338
|
-
# affects cmd_write_tools and cmd_clean_stale. Update render_forge_tier_yaml()
|
|
339
|
-
# AND those three subcommands together when the schema grows.
|
|
508
|
+
def cmd_register_qmd_collection(target: Path,
|
|
509
|
+
lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
340
510
|
raw = sys.stdin.read()
|
|
341
511
|
if not raw.strip():
|
|
342
512
|
_die(1, "register-qmd-collection: empty stdin (expected JSON entry)")
|
|
@@ -351,41 +521,67 @@ def cmd_register_qmd_collection(target: Path) -> None:
|
|
|
351
521
|
if not name or not isinstance(name, str):
|
|
352
522
|
_die(1, "register-qmd-collection: entry must include a non-empty 'name' string")
|
|
353
523
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"ccc_index_registry": data.get("ccc_index_registry", []),
|
|
376
|
-
"qmd_collections": collections,
|
|
377
|
-
}
|
|
378
|
-
rendered = render_forge_tier_yaml(payload)
|
|
379
|
-
_atomic_write(target, rendered)
|
|
524
|
+
missing = (f"register-qmd-collection: target does not exist: {target}. "
|
|
525
|
+
f"Run setup workflow first to create forge-tier.yaml.")
|
|
526
|
+
if not target.exists():
|
|
527
|
+
_die(1, missing)
|
|
528
|
+
with _registry_lock(target, "register-qmd-collection", lock_timeout) as stale_replaced:
|
|
529
|
+
data = _read_yaml(target)
|
|
530
|
+
if data is None:
|
|
531
|
+
_die(1, missing)
|
|
532
|
+
|
|
533
|
+
collections = list(data.get("qmd_collections") or [])
|
|
534
|
+
replaced = False
|
|
535
|
+
for i, existing in enumerate(collections):
|
|
536
|
+
if isinstance(existing, dict) and existing.get("name") == name:
|
|
537
|
+
collections[i] = entry
|
|
538
|
+
replaced = True
|
|
539
|
+
break
|
|
540
|
+
if not replaced:
|
|
541
|
+
collections.append(entry)
|
|
542
|
+
|
|
543
|
+
rendered = render_forge_tier_yaml(_payload_from(data, qmd_collections=collections))
|
|
544
|
+
_atomic_write(target, rendered)
|
|
380
545
|
_ok({
|
|
381
546
|
"name": name,
|
|
382
547
|
"action": "replaced" if replaced else "appended",
|
|
383
548
|
"qmd_collections_count": len(collections),
|
|
384
549
|
"wrote": str(target),
|
|
550
|
+
"lock_stale_replaced": stale_replaced,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def cmd_remove_qmd_collection(target: Path, name: str,
|
|
555
|
+
lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
556
|
+
if not name.strip():
|
|
557
|
+
_die(1, "remove-qmd-collection: --name must be a non-empty collection name")
|
|
558
|
+
|
|
559
|
+
missing = (f"remove-qmd-collection: target does not exist: {target}. "
|
|
560
|
+
f"Run setup workflow first to create forge-tier.yaml.")
|
|
561
|
+
if not target.exists():
|
|
562
|
+
_die(1, missing)
|
|
563
|
+
with _registry_lock(target, "remove-qmd-collection", lock_timeout) as stale_replaced:
|
|
564
|
+
data = _read_yaml(target)
|
|
565
|
+
if data is None:
|
|
566
|
+
_die(1, missing)
|
|
567
|
+
|
|
568
|
+
collections = list(data.get("qmd_collections") or [])
|
|
569
|
+
kept = [e for e in collections if not (isinstance(e, dict) and e.get("name") == name)]
|
|
570
|
+
removed = len(collections) - len(kept)
|
|
571
|
+
if removed:
|
|
572
|
+
rendered = render_forge_tier_yaml(_payload_from(data, qmd_collections=kept))
|
|
573
|
+
_atomic_write(target, rendered)
|
|
574
|
+
_ok({
|
|
575
|
+
"name": name,
|
|
576
|
+
"action": "removed" if removed else "absent",
|
|
577
|
+
"removed_count": removed,
|
|
578
|
+
"qmd_collections_count": len(kept),
|
|
579
|
+
"wrote": str(target) if removed else None,
|
|
580
|
+
"lock_stale_replaced": stale_replaced,
|
|
385
581
|
})
|
|
386
582
|
|
|
387
583
|
|
|
388
|
-
def cmd_register_ccc_index(target: Path) -> None:
|
|
584
|
+
def cmd_register_ccc_index(target: Path, lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
389
585
|
raw = sys.stdin.read()
|
|
390
586
|
if not raw.strip():
|
|
391
587
|
_die(1, "register-ccc-index: empty stdin (expected JSON entry)")
|
|
@@ -403,100 +599,104 @@ def cmd_register_ccc_index(target: Path) -> None:
|
|
|
403
599
|
if not skill_name or not isinstance(skill_name, str):
|
|
404
600
|
_die(1, "register-ccc-index: entry must include a non-empty 'skill_name' string")
|
|
405
601
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
"ccc_index_registry": registry,
|
|
430
|
-
"qmd_collections": data.get("qmd_collections", []),
|
|
431
|
-
}
|
|
432
|
-
rendered = render_forge_tier_yaml(payload)
|
|
433
|
-
_atomic_write(target, rendered)
|
|
602
|
+
missing = (f"register-ccc-index: target does not exist: {target}. "
|
|
603
|
+
f"Run setup workflow first to create forge-tier.yaml.")
|
|
604
|
+
if not target.exists():
|
|
605
|
+
_die(1, missing)
|
|
606
|
+
with _registry_lock(target, "register-ccc-index", lock_timeout) as stale_replaced:
|
|
607
|
+
data = _read_yaml(target)
|
|
608
|
+
if data is None:
|
|
609
|
+
_die(1, missing)
|
|
610
|
+
|
|
611
|
+
registry = list(data.get("ccc_index_registry") or [])
|
|
612
|
+
replaced = False
|
|
613
|
+
for i, existing in enumerate(registry):
|
|
614
|
+
if (isinstance(existing, dict)
|
|
615
|
+
and existing.get("source_repo") == source_repo
|
|
616
|
+
and existing.get("skill_name") == skill_name):
|
|
617
|
+
registry[i] = entry
|
|
618
|
+
replaced = True
|
|
619
|
+
break
|
|
620
|
+
if not replaced:
|
|
621
|
+
registry.append(entry)
|
|
622
|
+
|
|
623
|
+
rendered = render_forge_tier_yaml(_payload_from(data, ccc_index_registry=registry))
|
|
624
|
+
_atomic_write(target, rendered)
|
|
434
625
|
_ok({
|
|
435
626
|
"source_repo": source_repo,
|
|
436
627
|
"skill_name": skill_name,
|
|
437
628
|
"action": "replaced" if replaced else "appended",
|
|
438
629
|
"ccc_index_registry_count": len(registry),
|
|
439
630
|
"wrote": str(target),
|
|
631
|
+
"lock_stale_replaced": stale_replaced,
|
|
440
632
|
})
|
|
441
633
|
|
|
442
634
|
|
|
443
|
-
def
|
|
444
|
-
|
|
445
|
-
data = _read_yaml(target)
|
|
446
|
-
if data is None:
|
|
447
|
-
_die(1, f"clean-stale: target does not exist: {target}")
|
|
448
|
-
|
|
449
|
-
qmd_removed: list[str] = []
|
|
450
|
-
ccc_removed: list[str] = []
|
|
451
|
-
|
|
452
|
-
if qmd_live_names is not None:
|
|
453
|
-
live_set = set(qmd_live_names)
|
|
454
|
-
kept = []
|
|
455
|
-
for entry in data.get("qmd_collections", []) or []:
|
|
456
|
-
if not isinstance(entry, dict):
|
|
457
|
-
kept.append(entry)
|
|
458
|
-
continue
|
|
459
|
-
name = entry.get("name")
|
|
460
|
-
if name in live_set:
|
|
461
|
-
kept.append(entry)
|
|
462
|
-
else:
|
|
463
|
-
qmd_removed.append(str(name))
|
|
464
|
-
data["qmd_collections"] = kept
|
|
465
|
-
|
|
466
|
-
if prune_missing_ccc_paths:
|
|
467
|
-
kept = []
|
|
468
|
-
for entry in data.get("ccc_index_registry", []) or []:
|
|
469
|
-
if not isinstance(entry, dict):
|
|
470
|
-
kept.append(entry)
|
|
471
|
-
continue
|
|
472
|
-
entry_path = entry.get("path")
|
|
473
|
-
if entry_path and Path(entry_path).exists():
|
|
474
|
-
kept.append(entry)
|
|
475
|
-
else:
|
|
476
|
-
ccc_removed.append(str(entry_path))
|
|
477
|
-
data["ccc_index_registry"] = kept
|
|
635
|
+
def live_names_from(path: Path) -> list[str] | None:
|
|
636
|
+
"""The `live_names` of a staged classifier output, or None to skip QMD cleanup.
|
|
478
637
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
638
|
+
A file that is not there (the classifier did not run) or that holds no
|
|
639
|
+
list of names (it failed) gives None, never [], which would remove
|
|
640
|
+
every qmd_collections entry.
|
|
641
|
+
"""
|
|
642
|
+
if not path.exists():
|
|
643
|
+
return None
|
|
644
|
+
names = (_staged_object(path) or {}).get("live_names")
|
|
645
|
+
if not isinstance(names, list) or not all(isinstance(n, str) for n in names):
|
|
646
|
+
return None
|
|
647
|
+
return names
|
|
482
648
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
649
|
+
|
|
650
|
+
def cmd_clean_stale(target: Path, qmd_live_names: list[str] | None,
|
|
651
|
+
prune_missing_ccc_paths: bool,
|
|
652
|
+
lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
653
|
+
missing = f"clean-stale: target does not exist: {target}"
|
|
654
|
+
if not target.exists():
|
|
655
|
+
_die(1, missing)
|
|
656
|
+
with _registry_lock(target, "clean-stale", lock_timeout) as stale_replaced:
|
|
657
|
+
data = _read_yaml(target)
|
|
658
|
+
if data is None:
|
|
659
|
+
_die(1, missing)
|
|
660
|
+
|
|
661
|
+
qmd_removed: list[str] = []
|
|
662
|
+
ccc_removed: list[str] = []
|
|
663
|
+
|
|
664
|
+
if qmd_live_names is not None:
|
|
665
|
+
live_set = set(qmd_live_names)
|
|
666
|
+
kept = []
|
|
667
|
+
for entry in data.get("qmd_collections", []) or []:
|
|
668
|
+
if not isinstance(entry, dict):
|
|
669
|
+
kept.append(entry)
|
|
670
|
+
continue
|
|
671
|
+
name = entry.get("name")
|
|
672
|
+
if name in live_set:
|
|
673
|
+
kept.append(entry)
|
|
674
|
+
else:
|
|
675
|
+
qmd_removed.append(str(name))
|
|
676
|
+
data["qmd_collections"] = kept
|
|
677
|
+
|
|
678
|
+
if prune_missing_ccc_paths:
|
|
679
|
+
kept = []
|
|
680
|
+
for entry in data.get("ccc_index_registry", []) or []:
|
|
681
|
+
if not isinstance(entry, dict):
|
|
682
|
+
kept.append(entry)
|
|
683
|
+
continue
|
|
684
|
+
entry_path = entry.get("path")
|
|
685
|
+
if entry_path and Path(entry_path).exists():
|
|
686
|
+
kept.append(entry)
|
|
687
|
+
else:
|
|
688
|
+
ccc_removed.append(str(entry_path))
|
|
689
|
+
data["ccc_index_registry"] = kept
|
|
690
|
+
|
|
691
|
+
if qmd_removed or ccc_removed:
|
|
692
|
+
# Round-trip through render to preserve the canonical format.
|
|
693
|
+
rendered = render_forge_tier_yaml(_payload_from(data))
|
|
694
|
+
_atomic_write(target, rendered)
|
|
496
695
|
_ok({
|
|
497
696
|
"qmd_removed": qmd_removed,
|
|
498
697
|
"ccc_removed": ccc_removed,
|
|
499
|
-
"wrote":
|
|
698
|
+
"wrote": bool(qmd_removed or ccc_removed),
|
|
699
|
+
"lock_stale_replaced": stale_replaced,
|
|
500
700
|
})
|
|
501
701
|
|
|
502
702
|
|
|
@@ -513,50 +713,91 @@ def main() -> None:
|
|
|
513
713
|
p_read = sub.add_parser("read", help="Read a forge-tier.yaml and emit JSON")
|
|
514
714
|
p_read.add_argument("--target", type=Path, required=True)
|
|
515
715
|
|
|
716
|
+
def add_lock_timeout(p: argparse.ArgumentParser) -> None:
|
|
717
|
+
p.add_argument("--lock-timeout", type=float, default=REGISTRY_LOCK_WAIT_SEC,
|
|
718
|
+
help="seconds to wait while another call holds forge-tier.yaml.lock "
|
|
719
|
+
f"(default {REGISTRY_LOCK_WAIT_SEC:g})")
|
|
720
|
+
|
|
516
721
|
p_write = sub.add_parser("write-tools",
|
|
517
|
-
help="Write a fresh forge-tier.yaml from a JSON payload on stdin"
|
|
722
|
+
help="Write a fresh forge-tier.yaml from a JSON payload on stdin, "
|
|
723
|
+
"or from a setup run's staged outputs")
|
|
518
724
|
p_write.add_argument("--target", type=Path, required=True)
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
725
|
+
p_write.add_argument("--detect-from", type=Path, default=None,
|
|
726
|
+
help="skf-detect-tools.py's staged output; read instead of stdin.")
|
|
727
|
+
p_write.add_argument("--ccc-from", type=Path, default=None,
|
|
728
|
+
help="skf-merge-ccc-exclusions.py's --result-to file (with --detect-from). "
|
|
729
|
+
"Not there: ccc was not prepared this run, or, when the detector "
|
|
730
|
+
"reports ccc available, the preparation failed.")
|
|
731
|
+
add_lock_timeout(p_write)
|
|
523
732
|
|
|
524
733
|
p_clean = sub.add_parser("clean-stale",
|
|
525
734
|
help="Remove stale qmd_collections / ccc_index_registry entries")
|
|
526
735
|
p_clean.add_argument("--target", type=Path, required=True)
|
|
527
|
-
p_clean.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
736
|
+
live = p_clean.add_mutually_exclusive_group()
|
|
737
|
+
live.add_argument("--qmd-live-from", type=Path, default=None,
|
|
738
|
+
help="skf-qmd-classify-collections.py's staged output. Entries in "
|
|
739
|
+
"qmd_collections whose name is NOT in its live_names are removed. A file "
|
|
740
|
+
"that is not there, or holds no live_names list, skips QMD cleanup.")
|
|
741
|
+
live.add_argument("--qmd-live-names", default=None,
|
|
742
|
+
help="Comma-separated list of currently-live QMD collection names. "
|
|
743
|
+
"Entries in qmd_collections whose name is NOT in this list are removed. "
|
|
744
|
+
"Omit the flag entirely to skip QMD cleanup.")
|
|
531
745
|
p_clean.add_argument("--prune-missing-ccc-paths", action="store_true",
|
|
532
746
|
help="Remove ccc_index_registry entries whose path no longer exists.")
|
|
747
|
+
add_lock_timeout(p_clean)
|
|
533
748
|
|
|
534
749
|
p_register = sub.add_parser("register-qmd-collection",
|
|
535
750
|
help="Append-or-replace a single qmd_collections entry by name")
|
|
536
751
|
p_register.add_argument("--target", type=Path, required=True)
|
|
752
|
+
add_lock_timeout(p_register)
|
|
753
|
+
|
|
754
|
+
p_remove = sub.add_parser("remove-qmd-collection",
|
|
755
|
+
help="Remove the qmd_collections entries with this name (rollback)")
|
|
756
|
+
p_remove.add_argument("--target", type=Path, required=True)
|
|
757
|
+
p_remove.add_argument("--name", required=True, help="the collection name to remove")
|
|
758
|
+
add_lock_timeout(p_remove)
|
|
537
759
|
|
|
538
760
|
p_ccc = sub.add_parser("register-ccc-index",
|
|
539
761
|
help="Append-or-replace a single ccc_index_registry entry by source_repo+skill_name")
|
|
540
762
|
p_ccc.add_argument("--target", type=Path, required=True)
|
|
763
|
+
add_lock_timeout(p_ccc)
|
|
541
764
|
|
|
542
765
|
args = parser.parse_args()
|
|
543
766
|
|
|
767
|
+
lock_timeout = getattr(args, "lock_timeout", REGISTRY_LOCK_WAIT_SEC)
|
|
768
|
+
if not math.isfinite(lock_timeout) or lock_timeout < 0:
|
|
769
|
+
_die(1, f"{args.cmd}: --lock-timeout must be a number of seconds, 0 or more")
|
|
770
|
+
|
|
544
771
|
if args.cmd == "read":
|
|
545
772
|
cmd_read(args.target)
|
|
546
773
|
elif args.cmd == "write-tools":
|
|
547
|
-
cmd_write_tools(args.target)
|
|
548
|
-
elif args.cmd == "init-prefs":
|
|
549
|
-
cmd_init_prefs(args.target)
|
|
774
|
+
cmd_write_tools(args.target, lock_timeout, args.detect_from, args.ccc_from)
|
|
550
775
|
elif args.cmd == "clean-stale":
|
|
551
776
|
live = None
|
|
552
|
-
if args.
|
|
777
|
+
if args.qmd_live_from is not None:
|
|
778
|
+
live = live_names_from(args.qmd_live_from)
|
|
779
|
+
elif args.qmd_live_names is not None:
|
|
553
780
|
live = [n.strip() for n in args.qmd_live_names.split(",") if n.strip()]
|
|
554
|
-
cmd_clean_stale(args.target, live, args.prune_missing_ccc_paths)
|
|
781
|
+
cmd_clean_stale(args.target, live, args.prune_missing_ccc_paths, lock_timeout)
|
|
555
782
|
elif args.cmd == "register-qmd-collection":
|
|
556
|
-
cmd_register_qmd_collection(args.target)
|
|
783
|
+
cmd_register_qmd_collection(args.target, lock_timeout)
|
|
784
|
+
elif args.cmd == "remove-qmd-collection":
|
|
785
|
+
cmd_remove_qmd_collection(args.target, args.name, lock_timeout)
|
|
557
786
|
elif args.cmd == "register-ccc-index":
|
|
558
|
-
cmd_register_ccc_index(args.target)
|
|
787
|
+
cmd_register_ccc_index(args.target, lock_timeout)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _force_utf8(*streams) -> None:
|
|
791
|
+
"""Reconfigure stdin and the JSON streams to UTF-8 (a Windows console uses cp1252)."""
|
|
792
|
+
for stream in streams:
|
|
793
|
+
if hasattr(stream, "reconfigure"):
|
|
794
|
+
errors = getattr(stream, "errors", None)
|
|
795
|
+
if errors is None:
|
|
796
|
+
stream.reconfigure(encoding="utf-8")
|
|
797
|
+
else:
|
|
798
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
559
799
|
|
|
560
800
|
|
|
561
801
|
if __name__ == "__main__":
|
|
802
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
562
803
|
main()
|