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
|
@@ -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,48 @@ 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
|
-
the
|
|
109
|
+
mirror skf-atomic-write.py's pattern. On Windows, which refuses to rename
|
|
110
|
+
over a file another process has open, a refused rename is retried for up
|
|
111
|
+
to 5 seconds before the write fails.
|
|
112
|
+
|
|
113
|
+
Registry lock: every subcommand that rewrites forge-tier.yaml
|
|
114
|
+
(write-tools, register-qmd-collection, remove-qmd-collection,
|
|
115
|
+
register-ccc-index, clean-stale) holds `forge-tier.yaml.lock` beside it
|
|
116
|
+
for its one read-modify-write and releases it before it exits, through
|
|
117
|
+
skf-run-lock.py loaded from this script's folder. Concurrent runs never
|
|
118
|
+
lose each other's entries, and no caller takes a lock of its own: the
|
|
119
|
+
lock lives inside one call, never across calls. A call waits up to
|
|
120
|
+
--lock-timeout seconds (30 by default) while another call holds the lock.
|
|
121
|
+
A lock older than 15 seconds was left by a call that was killed, and is
|
|
122
|
+
replaced. An empty lock file holds nothing (the helper never writes one;
|
|
123
|
+
an `flock` on the same path leaves one) and is replaced at once.
|
|
77
124
|
|
|
78
125
|
CLI — invoke via `uv run` so the PEP 723 PyYAML dependency declared
|
|
79
126
|
above is auto-resolved on first call and cached. `docs/getting-started.md`
|
|
@@ -84,18 +131,26 @@ system-wide:
|
|
|
84
131
|
|
|
85
132
|
uv run skf-forge-tier-rw.py read --target /path/forge-tier.yaml
|
|
86
133
|
echo '{...}' | uv run skf-forge-tier-rw.py write-tools --target /path/forge-tier.yaml
|
|
87
|
-
uv run skf-forge-tier-rw.py
|
|
134
|
+
uv run skf-forge-tier-rw.py write-tools --target /path/forge-tier.yaml \\
|
|
135
|
+
--detect-from /run/detect-tools.json --ccc-from /run/ccc-exclusions.json
|
|
136
|
+
uv run skf-forge-tier-rw.py register-qmd-collection --target /path/forge-tier.yaml < entry.json
|
|
137
|
+
uv run skf-forge-tier-rw.py remove-qmd-collection --target /path/forge-tier.yaml \\
|
|
138
|
+
--name foo-extraction
|
|
88
139
|
echo '{...}' | uv run skf-forge-tier-rw.py register-ccc-index --target /path/forge-tier.yaml
|
|
89
140
|
uv run skf-forge-tier-rw.py clean-stale --target /path/forge-tier.yaml \\
|
|
90
|
-
--qmd-live-
|
|
141
|
+
--qmd-live-from /run/qmd-classify.json --prune-missing-ccc-paths
|
|
91
142
|
"""
|
|
92
143
|
|
|
93
144
|
from __future__ import annotations
|
|
94
145
|
|
|
95
146
|
import argparse
|
|
147
|
+
import contextlib
|
|
148
|
+
import importlib.util
|
|
96
149
|
import json
|
|
150
|
+
import math
|
|
97
151
|
import os
|
|
98
152
|
import sys
|
|
153
|
+
import time
|
|
99
154
|
from datetime import datetime, timezone
|
|
100
155
|
from pathlib import Path
|
|
101
156
|
|
|
@@ -103,28 +158,23 @@ import yaml
|
|
|
103
158
|
|
|
104
159
|
|
|
105
160
|
DEFAULT_STALENESS_HOURS = 24
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
#
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
# output_language: ~
|
|
124
|
-
# skill_format_version: ~
|
|
125
|
-
# citation_style: ~
|
|
126
|
-
# confidence_display: ~
|
|
127
|
-
"""
|
|
161
|
+
RUN_LOCK_HELPER = Path(__file__).resolve().parent / "skf-run-lock.py"
|
|
162
|
+
REGISTRY_LOCK_WAIT_SEC = 30.0
|
|
163
|
+
# A call holds the registry lock for milliseconds; one this old was killed.
|
|
164
|
+
REGISTRY_LOCK_STALE_SEC = 15.0
|
|
165
|
+
EXIT_LOCK_BUSY = 3
|
|
166
|
+
# Windows refuses to replace a file another process has open, even one that
|
|
167
|
+
# only reads its attributes: a call about to take the lock checks the file
|
|
168
|
+
# exists, a `read` call opens it with no lock, and a virus scanner may open a
|
|
169
|
+
# just-written file for a moment. _atomic_write retries the rename there for
|
|
170
|
+
# up to this long before failing.
|
|
171
|
+
# The retry runs while the registry lock is held, so it stays well under
|
|
172
|
+
# REGISTRY_LOCK_STALE_SEC, after which a waiting call takes the lock as stale.
|
|
173
|
+
_IS_WINDOWS = os.name == "nt"
|
|
174
|
+
_REPLACE_WAIT_SECONDS = 5.0
|
|
175
|
+
_REPLACE_POLL_SECONDS = 0.02
|
|
176
|
+
# The tool keys write-tools records, in forge-tier.yaml order.
|
|
177
|
+
TOOL_KEYS = ("ast_grep", "gh_cli", "qmd", "ccc")
|
|
128
178
|
|
|
129
179
|
|
|
130
180
|
def _die(code: int, message: str) -> None:
|
|
@@ -155,7 +205,9 @@ def _read_yaml(path: Path) -> dict | None:
|
|
|
155
205
|
|
|
156
206
|
|
|
157
207
|
def _atomic_write(target: Path, content: str) -> None:
|
|
158
|
-
"""Crash-safe write via temp + fsync + rename
|
|
208
|
+
"""Crash-safe write via temp + fsync + rename, as skf-atomic-write.py does.
|
|
209
|
+
On Windows, a rename refused while another process has the file open is
|
|
210
|
+
retried for up to _REPLACE_WAIT_SECONDS."""
|
|
159
211
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
160
212
|
tmp = target.with_name(target.name + ".skf-tmp")
|
|
161
213
|
# O_BINARY (Windows only; 0 elsewhere) suppresses the text-mode \n -> \r\n
|
|
@@ -168,7 +220,15 @@ def _atomic_write(target: Path, content: str) -> None:
|
|
|
168
220
|
os.fsync(fd)
|
|
169
221
|
finally:
|
|
170
222
|
os.close(fd)
|
|
171
|
-
|
|
223
|
+
deadline = time.monotonic() + _REPLACE_WAIT_SECONDS
|
|
224
|
+
while True:
|
|
225
|
+
try:
|
|
226
|
+
os.replace(tmp, target)
|
|
227
|
+
break
|
|
228
|
+
except PermissionError:
|
|
229
|
+
if not _IS_WINDOWS or time.monotonic() >= deadline:
|
|
230
|
+
raise
|
|
231
|
+
time.sleep(_REPLACE_POLL_SECONDS)
|
|
172
232
|
except OSError as e:
|
|
173
233
|
if tmp.exists():
|
|
174
234
|
try:
|
|
@@ -178,6 +238,52 @@ def _atomic_write(target: Path, content: str) -> None:
|
|
|
178
238
|
_die(2, f"atomic write failed for {target}: {e}")
|
|
179
239
|
|
|
180
240
|
|
|
241
|
+
def _load_run_lock():
|
|
242
|
+
"""Load skf-run-lock.py, which installs into the same folder as this script."""
|
|
243
|
+
try:
|
|
244
|
+
spec = importlib.util.spec_from_file_location("skf_run_lock", RUN_LOCK_HELPER)
|
|
245
|
+
if spec is None or spec.loader is None:
|
|
246
|
+
raise ImportError("no import spec")
|
|
247
|
+
module = importlib.util.module_from_spec(spec)
|
|
248
|
+
spec.loader.exec_module(module)
|
|
249
|
+
except (OSError, ImportError) as e:
|
|
250
|
+
_die(2, f"cannot load the run-lock helper {RUN_LOCK_HELPER}: {e}")
|
|
251
|
+
return module
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@contextlib.contextmanager
|
|
255
|
+
def _registry_lock(target: Path, command: str, timeout: float):
|
|
256
|
+
"""Hold `<target>.lock` through one read-modify-write of forge-tier.yaml.
|
|
257
|
+
|
|
258
|
+
Yields the stale lock this call replaced ({"held_by", "held_since"}) or
|
|
259
|
+
None. Exits 3 when another call holds the lock for `timeout` seconds, 2
|
|
260
|
+
when the lock cannot be taken (a folder in its place, an I/O error).
|
|
261
|
+
"""
|
|
262
|
+
run_lock = _load_run_lock()
|
|
263
|
+
lock = target.with_name(target.name + ".lock")
|
|
264
|
+
owner = f"forge-tier-rw:{command}:{run_lock.new_run_id()}"
|
|
265
|
+
try:
|
|
266
|
+
# The empty file an `flock` on this path leaves (create-skill prose
|
|
267
|
+
# before 3.0.0) holds nothing, so it must not make the call wait.
|
|
268
|
+
held = run_lock.acquire_within(lock, owner, REGISTRY_LOCK_STALE_SEC, timeout,
|
|
269
|
+
empty_is_stale=True)
|
|
270
|
+
except run_lock.LockBusy as busy:
|
|
271
|
+
info = busy.result
|
|
272
|
+
_die(EXIT_LOCK_BUSY,
|
|
273
|
+
f"{command}: {lock} is held by {info['held_by'] or 'an unnamed owner'} since "
|
|
274
|
+
f"{info['held_since']}: another run is writing {target.name}. Nothing was "
|
|
275
|
+
f"written; run the command again.")
|
|
276
|
+
except (OSError, ValueError) as e:
|
|
277
|
+
_die(2, f"{command}: cannot take {lock}: {e}")
|
|
278
|
+
try:
|
|
279
|
+
yield held["stale_replaced"]
|
|
280
|
+
finally:
|
|
281
|
+
try:
|
|
282
|
+
run_lock.release(lock, owner)
|
|
283
|
+
except (OSError, ValueError):
|
|
284
|
+
pass # a lock this call could not delete goes stale on its own
|
|
285
|
+
|
|
286
|
+
|
|
181
287
|
def _yaml_block(value, indent: int = 0) -> str:
|
|
182
288
|
"""Dump a value as a YAML fragment, indented by `indent` spaces, no trailing newline."""
|
|
183
289
|
text = yaml.safe_dump(value, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
@@ -191,8 +297,8 @@ def _yaml_block(value, indent: int = 0) -> str:
|
|
|
191
297
|
def render_forge_tier_yaml(payload: dict) -> str:
|
|
192
298
|
"""Render the canonical forge-tier.yaml from a context payload.
|
|
193
299
|
|
|
194
|
-
|
|
195
|
-
|
|
300
|
+
This is the file's one template, human-readable section comments
|
|
301
|
+
included. Sections are emitted in a fixed order
|
|
196
302
|
so re-runs against unchanged inputs produce byte-identical output.
|
|
197
303
|
"""
|
|
198
304
|
tools = payload["tools"]
|
|
@@ -219,7 +325,7 @@ def render_forge_tier_yaml(payload: dict) -> str:
|
|
|
219
325
|
"status": ccc_index.get("status"),
|
|
220
326
|
"staleness_threshold_hours": ccc_index.get("staleness_threshold_hours", DEFAULT_STALENESS_HOURS),
|
|
221
327
|
"file_count": ccc_index.get("file_count"),
|
|
222
|
-
"exclude_patterns": ccc_index.get("exclude_patterns"
|
|
328
|
+
"exclude_patterns": ccc_index.get("exclude_patterns") or [],
|
|
223
329
|
}
|
|
224
330
|
|
|
225
331
|
parts = [
|
|
@@ -257,13 +363,14 @@ def _yaml_scalar(value) -> str:
|
|
|
257
363
|
def _merge_preserved_fields(payload: dict, existing: dict | None) -> dict:
|
|
258
364
|
"""Inject preserved fields from the existing file into the new payload.
|
|
259
365
|
|
|
260
|
-
|
|
366
|
+
Four preservation rules (write-tools in the module docstring):
|
|
261
367
|
- `qmd_collections` array — preserved entirely from existing.
|
|
262
368
|
- `ccc_index_registry` array — preserved entirely from existing.
|
|
263
369
|
- `ccc_index.staleness_threshold_hours` scalar — preserved if user set
|
|
264
370
|
a non-default value; else uses payload value or DEFAULT.
|
|
265
|
-
|
|
266
|
-
|
|
371
|
+
- `ccc_index.exclude_patterns` array — replaced when the payload
|
|
372
|
+
carries a list; kept from the existing file when the payload sends
|
|
373
|
+
null or omits it (an existing value that is not a list becomes []).
|
|
267
374
|
"""
|
|
268
375
|
if existing is None:
|
|
269
376
|
return payload
|
|
@@ -277,6 +384,31 @@ def _merge_preserved_fields(payload: dict, existing: dict | None) -> dict:
|
|
|
277
384
|
payload["ccc_index"]["staleness_threshold_hours"] = existing_ccc.get(
|
|
278
385
|
"staleness_threshold_hours", DEFAULT_STALENESS_HOURS
|
|
279
386
|
)
|
|
387
|
+
if payload["ccc_index"].get("exclude_patterns") is None:
|
|
388
|
+
prior = existing_ccc.get("exclude_patterns")
|
|
389
|
+
payload["ccc_index"]["exclude_patterns"] = prior if isinstance(prior, list) else []
|
|
390
|
+
return payload
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _payload_from(data: dict, **replace) -> dict:
|
|
394
|
+
"""The render payload of an existing forge-tier.yaml, with `replace` applied.
|
|
395
|
+
|
|
396
|
+
render_forge_tier_yaml() emits exactly the six known top-level sections
|
|
397
|
+
(tools, tier, tier_detected_at, ccc_index, ccc_index_registry,
|
|
398
|
+
qmd_collections), so a rewrite from this payload drops any other
|
|
399
|
+
top-level key, as cmd_write_tools does. Update render_forge_tier_yaml()
|
|
400
|
+
and this function together when the schema grows.
|
|
401
|
+
"""
|
|
402
|
+
payload = {
|
|
403
|
+
"tools": data.get("tools", {}),
|
|
404
|
+
"tier": data.get("tier", "Quick"),
|
|
405
|
+
"tier_detected_at": data.get("tier_detected_at",
|
|
406
|
+
datetime.now(timezone.utc).isoformat()),
|
|
407
|
+
"ccc_index": data.get("ccc_index", {}),
|
|
408
|
+
"ccc_index_registry": data.get("ccc_index_registry", []),
|
|
409
|
+
"qmd_collections": data.get("qmd_collections", []),
|
|
410
|
+
}
|
|
411
|
+
payload.update(replace)
|
|
280
412
|
return payload
|
|
281
413
|
|
|
282
414
|
|
|
@@ -291,14 +423,86 @@ def cmd_read(target: Path) -> None:
|
|
|
291
423
|
_ok({"exists": True, "data": data})
|
|
292
424
|
|
|
293
425
|
|
|
294
|
-
def
|
|
295
|
-
|
|
296
|
-
if not raw.strip():
|
|
297
|
-
_die(1, "write-tools: empty stdin (expected JSON payload)")
|
|
426
|
+
def _staged_object(path: Path):
|
|
427
|
+
"""The JSON object a staged helper output holds; None when it holds none (or cannot be read)."""
|
|
298
428
|
try:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
429
|
+
# utf-8-sig: a shell that writes a byte-order mark still stages JSON.
|
|
430
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
431
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
432
|
+
return None
|
|
433
|
+
return value if isinstance(value, dict) else None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _staged_ccc_index(ccc_from: Path | None, ccc_available: bool = False) -> dict:
|
|
437
|
+
"""forge-tier.yaml's `ccc_index` from the merge helper's --result-to file.
|
|
438
|
+
|
|
439
|
+
No file: the run did not prepare ccc (status "none"), or, when
|
|
440
|
+
`ccc_available`, the helper ran and wrote nothing (a usage error, or a
|
|
441
|
+
call its host stopped), so the preparation failed (status "failed"). A
|
|
442
|
+
file holding an error, or no JSON object: the preparation failed too.
|
|
443
|
+
Each keeps the exclusion record (exclude_patterns null). A result
|
|
444
|
+
without an `index` object comes from a run without --build-index,
|
|
445
|
+
which a setup run never makes: that is a usage error.
|
|
446
|
+
"""
|
|
447
|
+
unset = {"indexed_path": None, "last_indexed": None, "file_count": None, "exclude_patterns": None}
|
|
448
|
+
if ccc_from is None or not ccc_from.exists():
|
|
449
|
+
if ccc_available:
|
|
450
|
+
return {**unset, "status": "failed"}
|
|
451
|
+
return {**unset, "status": "none"}
|
|
452
|
+
result = _staged_object(ccc_from)
|
|
453
|
+
if result is None or result.get("status") != "ok":
|
|
454
|
+
return {**unset, "status": "failed"}
|
|
455
|
+
index = result.get("index")
|
|
456
|
+
if not isinstance(index, dict):
|
|
457
|
+
_die(1, f"write-tools: {ccc_from} holds no `index` result (run the merge helper with --build-index)")
|
|
458
|
+
patterns = result.get("effective_patterns")
|
|
459
|
+
return {
|
|
460
|
+
"indexed_path": index.get("indexed_path"),
|
|
461
|
+
"last_indexed": index.get("last_indexed"),
|
|
462
|
+
"status": index.get("status"),
|
|
463
|
+
"file_count": index.get("file_count"),
|
|
464
|
+
"exclude_patterns": patterns if isinstance(patterns, list) else None,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def payload_from_staged(detect_from: Path, ccc_from: Path | None) -> dict:
|
|
469
|
+
"""The write-tools payload from a setup run's staged detector and ccc outputs."""
|
|
470
|
+
detect = _staged_object(detect_from)
|
|
471
|
+
if detect is None:
|
|
472
|
+
_die(1, f"write-tools: {detect_from} holds no skf-detect-tools.py output")
|
|
473
|
+
tools, tier = detect.get("tools"), detect.get("tier")
|
|
474
|
+
if not isinstance(tools, dict) or not isinstance(tier, dict) or not tier.get("calculated"):
|
|
475
|
+
_die(1, f"write-tools: {detect_from} has no `tools` and `tier.calculated`")
|
|
476
|
+
|
|
477
|
+
def probe(key: str) -> dict:
|
|
478
|
+
value = tools.get(key)
|
|
479
|
+
return value if isinstance(value, dict) else {}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
"tools": {
|
|
483
|
+
**{key: probe(key).get("available") is True for key in TOOL_KEYS},
|
|
484
|
+
"ccc_daemon": probe("ccc").get("daemon"),
|
|
485
|
+
"security_scan": probe("security_scan").get("available") is True,
|
|
486
|
+
},
|
|
487
|
+
"tier": tier["calculated"],
|
|
488
|
+
"ccc_index": _staged_ccc_index(ccc_from, probe("ccc").get("available") is True),
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def cmd_write_tools(target: Path, lock_timeout: float = REGISTRY_LOCK_WAIT_SEC,
|
|
493
|
+
detect_from: Path | None = None, ccc_from: Path | None = None) -> None:
|
|
494
|
+
if detect_from is not None:
|
|
495
|
+
payload = payload_from_staged(detect_from, ccc_from)
|
|
496
|
+
else:
|
|
497
|
+
if ccc_from is not None:
|
|
498
|
+
_die(1, "write-tools: --ccc-from needs --detect-from")
|
|
499
|
+
raw = sys.stdin.read()
|
|
500
|
+
if not raw.strip():
|
|
501
|
+
_die(1, "write-tools: empty stdin (expected JSON payload)")
|
|
502
|
+
try:
|
|
503
|
+
payload = json.loads(raw)
|
|
504
|
+
except json.JSONDecodeError as e:
|
|
505
|
+
_die(1, f"write-tools: invalid JSON on stdin: {e}")
|
|
302
506
|
|
|
303
507
|
required = {"tools", "tier", "ccc_index"}
|
|
304
508
|
missing = required - set(payload.keys())
|
|
@@ -307,11 +511,12 @@ def cmd_write_tools(target: Path) -> None:
|
|
|
307
511
|
|
|
308
512
|
payload.setdefault("tier_detected_at", datetime.now(timezone.utc).isoformat())
|
|
309
513
|
|
|
310
|
-
|
|
311
|
-
|
|
514
|
+
with _registry_lock(target, "write-tools", lock_timeout) as stale_replaced:
|
|
515
|
+
existing = _read_yaml(target)
|
|
516
|
+
payload = _merge_preserved_fields(payload, existing)
|
|
312
517
|
|
|
313
|
-
|
|
314
|
-
|
|
518
|
+
rendered = render_forge_tier_yaml(payload)
|
|
519
|
+
_atomic_write(target, rendered)
|
|
315
520
|
_ok({
|
|
316
521
|
"wrote": str(target),
|
|
317
522
|
"preserved_arrays": {
|
|
@@ -319,24 +524,12 @@ def cmd_write_tools(target: Path) -> None:
|
|
|
319
524
|
"ccc_index_registry": len(payload.get("ccc_index_registry", [])),
|
|
320
525
|
},
|
|
321
526
|
"tier": payload["tier"],
|
|
527
|
+
"lock_stale_replaced": stale_replaced,
|
|
322
528
|
})
|
|
323
529
|
|
|
324
530
|
|
|
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.
|
|
531
|
+
def cmd_register_qmd_collection(target: Path,
|
|
532
|
+
lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
340
533
|
raw = sys.stdin.read()
|
|
341
534
|
if not raw.strip():
|
|
342
535
|
_die(1, "register-qmd-collection: empty stdin (expected JSON entry)")
|
|
@@ -351,41 +544,67 @@ def cmd_register_qmd_collection(target: Path) -> None:
|
|
|
351
544
|
if not name or not isinstance(name, str):
|
|
352
545
|
_die(1, "register-qmd-collection: entry must include a non-empty 'name' string")
|
|
353
546
|
|
|
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)
|
|
547
|
+
missing = (f"register-qmd-collection: target does not exist: {target}. "
|
|
548
|
+
f"Run setup workflow first to create forge-tier.yaml.")
|
|
549
|
+
if not target.exists():
|
|
550
|
+
_die(1, missing)
|
|
551
|
+
with _registry_lock(target, "register-qmd-collection", lock_timeout) as stale_replaced:
|
|
552
|
+
data = _read_yaml(target)
|
|
553
|
+
if data is None:
|
|
554
|
+
_die(1, missing)
|
|
555
|
+
|
|
556
|
+
collections = list(data.get("qmd_collections") or [])
|
|
557
|
+
replaced = False
|
|
558
|
+
for i, existing in enumerate(collections):
|
|
559
|
+
if isinstance(existing, dict) and existing.get("name") == name:
|
|
560
|
+
collections[i] = entry
|
|
561
|
+
replaced = True
|
|
562
|
+
break
|
|
563
|
+
if not replaced:
|
|
564
|
+
collections.append(entry)
|
|
565
|
+
|
|
566
|
+
rendered = render_forge_tier_yaml(_payload_from(data, qmd_collections=collections))
|
|
567
|
+
_atomic_write(target, rendered)
|
|
380
568
|
_ok({
|
|
381
569
|
"name": name,
|
|
382
570
|
"action": "replaced" if replaced else "appended",
|
|
383
571
|
"qmd_collections_count": len(collections),
|
|
384
572
|
"wrote": str(target),
|
|
573
|
+
"lock_stale_replaced": stale_replaced,
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def cmd_remove_qmd_collection(target: Path, name: str,
|
|
578
|
+
lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
579
|
+
if not name.strip():
|
|
580
|
+
_die(1, "remove-qmd-collection: --name must be a non-empty collection name")
|
|
581
|
+
|
|
582
|
+
missing = (f"remove-qmd-collection: target does not exist: {target}. "
|
|
583
|
+
f"Run setup workflow first to create forge-tier.yaml.")
|
|
584
|
+
if not target.exists():
|
|
585
|
+
_die(1, missing)
|
|
586
|
+
with _registry_lock(target, "remove-qmd-collection", lock_timeout) as stale_replaced:
|
|
587
|
+
data = _read_yaml(target)
|
|
588
|
+
if data is None:
|
|
589
|
+
_die(1, missing)
|
|
590
|
+
|
|
591
|
+
collections = list(data.get("qmd_collections") or [])
|
|
592
|
+
kept = [e for e in collections if not (isinstance(e, dict) and e.get("name") == name)]
|
|
593
|
+
removed = len(collections) - len(kept)
|
|
594
|
+
if removed:
|
|
595
|
+
rendered = render_forge_tier_yaml(_payload_from(data, qmd_collections=kept))
|
|
596
|
+
_atomic_write(target, rendered)
|
|
597
|
+
_ok({
|
|
598
|
+
"name": name,
|
|
599
|
+
"action": "removed" if removed else "absent",
|
|
600
|
+
"removed_count": removed,
|
|
601
|
+
"qmd_collections_count": len(kept),
|
|
602
|
+
"wrote": str(target) if removed else None,
|
|
603
|
+
"lock_stale_replaced": stale_replaced,
|
|
385
604
|
})
|
|
386
605
|
|
|
387
606
|
|
|
388
|
-
def cmd_register_ccc_index(target: Path) -> None:
|
|
607
|
+
def cmd_register_ccc_index(target: Path, lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
389
608
|
raw = sys.stdin.read()
|
|
390
609
|
if not raw.strip():
|
|
391
610
|
_die(1, "register-ccc-index: empty stdin (expected JSON entry)")
|
|
@@ -403,100 +622,104 @@ def cmd_register_ccc_index(target: Path) -> None:
|
|
|
403
622
|
if not skill_name or not isinstance(skill_name, str):
|
|
404
623
|
_die(1, "register-ccc-index: entry must include a non-empty 'skill_name' string")
|
|
405
624
|
|
|
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)
|
|
625
|
+
missing = (f"register-ccc-index: target does not exist: {target}. "
|
|
626
|
+
f"Run setup workflow first to create forge-tier.yaml.")
|
|
627
|
+
if not target.exists():
|
|
628
|
+
_die(1, missing)
|
|
629
|
+
with _registry_lock(target, "register-ccc-index", lock_timeout) as stale_replaced:
|
|
630
|
+
data = _read_yaml(target)
|
|
631
|
+
if data is None:
|
|
632
|
+
_die(1, missing)
|
|
633
|
+
|
|
634
|
+
registry = list(data.get("ccc_index_registry") or [])
|
|
635
|
+
replaced = False
|
|
636
|
+
for i, existing in enumerate(registry):
|
|
637
|
+
if (isinstance(existing, dict)
|
|
638
|
+
and existing.get("source_repo") == source_repo
|
|
639
|
+
and existing.get("skill_name") == skill_name):
|
|
640
|
+
registry[i] = entry
|
|
641
|
+
replaced = True
|
|
642
|
+
break
|
|
643
|
+
if not replaced:
|
|
644
|
+
registry.append(entry)
|
|
645
|
+
|
|
646
|
+
rendered = render_forge_tier_yaml(_payload_from(data, ccc_index_registry=registry))
|
|
647
|
+
_atomic_write(target, rendered)
|
|
434
648
|
_ok({
|
|
435
649
|
"source_repo": source_repo,
|
|
436
650
|
"skill_name": skill_name,
|
|
437
651
|
"action": "replaced" if replaced else "appended",
|
|
438
652
|
"ccc_index_registry_count": len(registry),
|
|
439
653
|
"wrote": str(target),
|
|
654
|
+
"lock_stale_replaced": stale_replaced,
|
|
440
655
|
})
|
|
441
656
|
|
|
442
657
|
|
|
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
|
|
658
|
+
def live_names_from(path: Path) -> list[str] | None:
|
|
659
|
+
"""The `live_names` of a staged classifier output, or None to skip QMD cleanup.
|
|
478
660
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
661
|
+
A file that is not there (the classifier did not run) or that holds no
|
|
662
|
+
list of names (it failed) gives None, never [], which would remove
|
|
663
|
+
every qmd_collections entry.
|
|
664
|
+
"""
|
|
665
|
+
if not path.exists():
|
|
666
|
+
return None
|
|
667
|
+
names = (_staged_object(path) or {}).get("live_names")
|
|
668
|
+
if not isinstance(names, list) or not all(isinstance(n, str) for n in names):
|
|
669
|
+
return None
|
|
670
|
+
return names
|
|
482
671
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
672
|
+
|
|
673
|
+
def cmd_clean_stale(target: Path, qmd_live_names: list[str] | None,
|
|
674
|
+
prune_missing_ccc_paths: bool,
|
|
675
|
+
lock_timeout: float = REGISTRY_LOCK_WAIT_SEC) -> None:
|
|
676
|
+
missing = f"clean-stale: target does not exist: {target}"
|
|
677
|
+
if not target.exists():
|
|
678
|
+
_die(1, missing)
|
|
679
|
+
with _registry_lock(target, "clean-stale", lock_timeout) as stale_replaced:
|
|
680
|
+
data = _read_yaml(target)
|
|
681
|
+
if data is None:
|
|
682
|
+
_die(1, missing)
|
|
683
|
+
|
|
684
|
+
qmd_removed: list[str] = []
|
|
685
|
+
ccc_removed: list[str] = []
|
|
686
|
+
|
|
687
|
+
if qmd_live_names is not None:
|
|
688
|
+
live_set = set(qmd_live_names)
|
|
689
|
+
kept = []
|
|
690
|
+
for entry in data.get("qmd_collections", []) or []:
|
|
691
|
+
if not isinstance(entry, dict):
|
|
692
|
+
kept.append(entry)
|
|
693
|
+
continue
|
|
694
|
+
name = entry.get("name")
|
|
695
|
+
if name in live_set:
|
|
696
|
+
kept.append(entry)
|
|
697
|
+
else:
|
|
698
|
+
qmd_removed.append(str(name))
|
|
699
|
+
data["qmd_collections"] = kept
|
|
700
|
+
|
|
701
|
+
if prune_missing_ccc_paths:
|
|
702
|
+
kept = []
|
|
703
|
+
for entry in data.get("ccc_index_registry", []) or []:
|
|
704
|
+
if not isinstance(entry, dict):
|
|
705
|
+
kept.append(entry)
|
|
706
|
+
continue
|
|
707
|
+
entry_path = entry.get("path")
|
|
708
|
+
if entry_path and Path(entry_path).exists():
|
|
709
|
+
kept.append(entry)
|
|
710
|
+
else:
|
|
711
|
+
ccc_removed.append(str(entry_path))
|
|
712
|
+
data["ccc_index_registry"] = kept
|
|
713
|
+
|
|
714
|
+
if qmd_removed or ccc_removed:
|
|
715
|
+
# Round-trip through render to preserve the canonical format.
|
|
716
|
+
rendered = render_forge_tier_yaml(_payload_from(data))
|
|
717
|
+
_atomic_write(target, rendered)
|
|
496
718
|
_ok({
|
|
497
719
|
"qmd_removed": qmd_removed,
|
|
498
720
|
"ccc_removed": ccc_removed,
|
|
499
|
-
"wrote":
|
|
721
|
+
"wrote": bool(qmd_removed or ccc_removed),
|
|
722
|
+
"lock_stale_replaced": stale_replaced,
|
|
500
723
|
})
|
|
501
724
|
|
|
502
725
|
|
|
@@ -513,50 +736,91 @@ def main() -> None:
|
|
|
513
736
|
p_read = sub.add_parser("read", help="Read a forge-tier.yaml and emit JSON")
|
|
514
737
|
p_read.add_argument("--target", type=Path, required=True)
|
|
515
738
|
|
|
739
|
+
def add_lock_timeout(p: argparse.ArgumentParser) -> None:
|
|
740
|
+
p.add_argument("--lock-timeout", type=float, default=REGISTRY_LOCK_WAIT_SEC,
|
|
741
|
+
help="seconds to wait while another call holds forge-tier.yaml.lock "
|
|
742
|
+
f"(default {REGISTRY_LOCK_WAIT_SEC:g})")
|
|
743
|
+
|
|
516
744
|
p_write = sub.add_parser("write-tools",
|
|
517
|
-
help="Write a fresh forge-tier.yaml from a JSON payload on stdin"
|
|
745
|
+
help="Write a fresh forge-tier.yaml from a JSON payload on stdin, "
|
|
746
|
+
"or from a setup run's staged outputs")
|
|
518
747
|
p_write.add_argument("--target", type=Path, required=True)
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
748
|
+
p_write.add_argument("--detect-from", type=Path, default=None,
|
|
749
|
+
help="skf-detect-tools.py's staged output; read instead of stdin.")
|
|
750
|
+
p_write.add_argument("--ccc-from", type=Path, default=None,
|
|
751
|
+
help="skf-merge-ccc-exclusions.py's --result-to file (with --detect-from). "
|
|
752
|
+
"Not there: ccc was not prepared this run, or, when the detector "
|
|
753
|
+
"reports ccc available, the preparation failed.")
|
|
754
|
+
add_lock_timeout(p_write)
|
|
523
755
|
|
|
524
756
|
p_clean = sub.add_parser("clean-stale",
|
|
525
757
|
help="Remove stale qmd_collections / ccc_index_registry entries")
|
|
526
758
|
p_clean.add_argument("--target", type=Path, required=True)
|
|
527
|
-
p_clean.
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
759
|
+
live = p_clean.add_mutually_exclusive_group()
|
|
760
|
+
live.add_argument("--qmd-live-from", type=Path, default=None,
|
|
761
|
+
help="skf-qmd-classify-collections.py's staged output. Entries in "
|
|
762
|
+
"qmd_collections whose name is NOT in its live_names are removed. A file "
|
|
763
|
+
"that is not there, or holds no live_names list, skips QMD cleanup.")
|
|
764
|
+
live.add_argument("--qmd-live-names", default=None,
|
|
765
|
+
help="Comma-separated list of currently-live QMD collection names. "
|
|
766
|
+
"Entries in qmd_collections whose name is NOT in this list are removed. "
|
|
767
|
+
"Omit the flag entirely to skip QMD cleanup.")
|
|
531
768
|
p_clean.add_argument("--prune-missing-ccc-paths", action="store_true",
|
|
532
769
|
help="Remove ccc_index_registry entries whose path no longer exists.")
|
|
770
|
+
add_lock_timeout(p_clean)
|
|
533
771
|
|
|
534
772
|
p_register = sub.add_parser("register-qmd-collection",
|
|
535
773
|
help="Append-or-replace a single qmd_collections entry by name")
|
|
536
774
|
p_register.add_argument("--target", type=Path, required=True)
|
|
775
|
+
add_lock_timeout(p_register)
|
|
776
|
+
|
|
777
|
+
p_remove = sub.add_parser("remove-qmd-collection",
|
|
778
|
+
help="Remove the qmd_collections entries with this name (rollback)")
|
|
779
|
+
p_remove.add_argument("--target", type=Path, required=True)
|
|
780
|
+
p_remove.add_argument("--name", required=True, help="the collection name to remove")
|
|
781
|
+
add_lock_timeout(p_remove)
|
|
537
782
|
|
|
538
783
|
p_ccc = sub.add_parser("register-ccc-index",
|
|
539
784
|
help="Append-or-replace a single ccc_index_registry entry by source_repo+skill_name")
|
|
540
785
|
p_ccc.add_argument("--target", type=Path, required=True)
|
|
786
|
+
add_lock_timeout(p_ccc)
|
|
541
787
|
|
|
542
788
|
args = parser.parse_args()
|
|
543
789
|
|
|
790
|
+
lock_timeout = getattr(args, "lock_timeout", REGISTRY_LOCK_WAIT_SEC)
|
|
791
|
+
if not math.isfinite(lock_timeout) or lock_timeout < 0:
|
|
792
|
+
_die(1, f"{args.cmd}: --lock-timeout must be a number of seconds, 0 or more")
|
|
793
|
+
|
|
544
794
|
if args.cmd == "read":
|
|
545
795
|
cmd_read(args.target)
|
|
546
796
|
elif args.cmd == "write-tools":
|
|
547
|
-
cmd_write_tools(args.target)
|
|
548
|
-
elif args.cmd == "init-prefs":
|
|
549
|
-
cmd_init_prefs(args.target)
|
|
797
|
+
cmd_write_tools(args.target, lock_timeout, args.detect_from, args.ccc_from)
|
|
550
798
|
elif args.cmd == "clean-stale":
|
|
551
799
|
live = None
|
|
552
|
-
if args.
|
|
800
|
+
if args.qmd_live_from is not None:
|
|
801
|
+
live = live_names_from(args.qmd_live_from)
|
|
802
|
+
elif args.qmd_live_names is not None:
|
|
553
803
|
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)
|
|
804
|
+
cmd_clean_stale(args.target, live, args.prune_missing_ccc_paths, lock_timeout)
|
|
555
805
|
elif args.cmd == "register-qmd-collection":
|
|
556
|
-
cmd_register_qmd_collection(args.target)
|
|
806
|
+
cmd_register_qmd_collection(args.target, lock_timeout)
|
|
807
|
+
elif args.cmd == "remove-qmd-collection":
|
|
808
|
+
cmd_remove_qmd_collection(args.target, args.name, lock_timeout)
|
|
557
809
|
elif args.cmd == "register-ccc-index":
|
|
558
|
-
cmd_register_ccc_index(args.target)
|
|
810
|
+
cmd_register_ccc_index(args.target, lock_timeout)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def _force_utf8(*streams) -> None:
|
|
814
|
+
"""Reconfigure stdin and the JSON streams to UTF-8 (a Windows console uses cp1252)."""
|
|
815
|
+
for stream in streams:
|
|
816
|
+
if hasattr(stream, "reconfigure"):
|
|
817
|
+
errors = getattr(stream, "errors", None)
|
|
818
|
+
if errors is None:
|
|
819
|
+
stream.reconfigure(encoding="utf-8")
|
|
820
|
+
else:
|
|
821
|
+
stream.reconfigure(encoding="utf-8", errors=errors)
|
|
559
822
|
|
|
560
823
|
|
|
561
824
|
if __name__ == "__main__":
|
|
825
|
+
_force_utf8(sys.stdin, sys.stdout, sys.stderr)
|
|
562
826
|
main()
|