bmad-module-skill-forge 1.4.1 → 1.5.1
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 +0 -8
- package/docs/_data/pinned.yaml +1 -1
- package/docs/_internal/STABILITY.md +1 -1
- package/docs/architecture.md +2 -2
- package/docs/examples.md +1 -1
- package/docs/workflows.md +5 -5
- package/package.json +2 -2
- package/src/README.md +1 -1
- package/src/knowledge/ccc-bridge.md +12 -12
- package/src/knowledge/qmd-registry.md +5 -5
- package/src/shared/health-check.md +5 -5
- package/src/shared/references/description-guard-protocol.md +100 -0
- package/src/shared/references/output-contract-schema.md +1 -1
- package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -6
- package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +9 -3
- package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +149 -0
- package/src/shared/scripts/schemas/skill-brief.v1.json +22 -2
- package/src/shared/scripts/skf-build-change-manifest.py +420 -0
- package/src/shared/scripts/skf-check-workspace-drift.py +321 -0
- package/src/shared/scripts/skf-compare-file-hashes.py +357 -0
- package/src/shared/scripts/skf-description-guard.py +359 -0
- package/src/shared/scripts/skf-detect-language.py +2 -2
- package/src/shared/scripts/skf-detect-scripts-assets.py +613 -0
- package/src/shared/scripts/skf-detect-tools.py +147 -16
- package/src/shared/scripts/skf-detect-workspaces.py +1 -1
- package/src/shared/scripts/skf-disqualify-candidates.py +576 -0
- package/src/shared/scripts/skf-emit-brief-result-envelope.py +3 -3
- package/src/shared/scripts/skf-emit-result-envelope.py +93 -9
- package/src/shared/scripts/skf-enumerate-stack-skills.py +514 -0
- package/src/shared/scripts/skf-forge-tier-rw.py +11 -11
- package/src/shared/scripts/skf-hash-content.py +284 -0
- package/src/shared/scripts/skf-load-provenance.py +295 -0
- package/src/shared/scripts/skf-manifest-ops.py +22 -6
- package/src/shared/scripts/skf-merge-ccc-exclusions.py +34 -4
- package/src/shared/scripts/skf-pair-intersect.py +250 -0
- package/src/shared/scripts/skf-provenance-gap-dispatch.py +433 -0
- package/src/shared/scripts/skf-qmd-classify-collections.py +42 -8
- package/src/shared/scripts/skf-recommend-scope-type.py +2 -2
- package/src/shared/scripts/skf-render-quick-metadata.py +2 -2
- package/src/shared/scripts/skf-resolve-authoritative-files.py +504 -0
- package/src/shared/scripts/skf-scan-manifests.py +738 -0
- package/src/shared/scripts/skf-scan-skill-md-structure.py +375 -0
- package/src/shared/scripts/skf-update-active-symlink.py +315 -0
- package/src/shared/scripts/skf-validate-brief-inputs.py +5 -5
- package/src/shared/scripts/skf-validate-brief-schema.py +348 -0
- package/src/shared/scripts/skf-write-skill-brief.py +61 -5
- package/src/skf-analyze-source/SKILL.md +69 -16
- package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
- package/src/skf-analyze-source/customize.toml +55 -0
- package/src/skf-analyze-source/{steps-c/step-01b-continue.md → references/continue.md} +12 -12
- package/src/skf-analyze-source/{steps-c/step-06-generate-briefs.md → references/generate-briefs.md} +29 -10
- package/src/skf-analyze-source/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
- package/src/skf-analyze-source/{steps-c/step-03-identify-units.md → references/identify-units.md} +48 -14
- package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +32 -9
- package/src/skf-analyze-source/{steps-c/step-04-map-and-detect.md → references/map-and-detect.md} +52 -25
- package/src/skf-analyze-source/{steps-c/step-05-recommend.md → references/recommend.md} +15 -8
- package/src/skf-analyze-source/{steps-c/step-02-scan-project.md → references/scan-project.md} +16 -10
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -0
- package/src/skf-analyze-source/templates/analysis-report-template.md +6 -6
- package/src/skf-audit-skill/SKILL.md +66 -15
- package/src/skf-audit-skill/assets/drift-report-template.md +6 -5
- package/src/skf-audit-skill/customize.toml +49 -0
- package/src/skf-audit-skill/{steps-c/step-07-health-check.md → references/health-check.md} +5 -3
- package/src/skf-audit-skill/{steps-c/step-01-init.md → references/init.md} +63 -19
- package/src/skf-audit-skill/{steps-c/step-02-re-index.md → references/re-index.md} +11 -11
- package/src/skf-audit-skill/{steps-c/step-06-report.md → references/report.md} +25 -7
- package/src/skf-audit-skill/{steps-c/step-04-semantic-diff.md → references/semantic-diff.md} +6 -6
- package/src/skf-audit-skill/{steps-c/step-05-severity-classify.md → references/severity-classify.md} +5 -5
- package/src/skf-audit-skill/references/severity-rules.md +2 -0
- package/src/skf-audit-skill/{steps-c/step-03-structural-diff.md → references/structural-diff.md} +35 -17
- package/src/skf-brief-skill/SKILL.md +49 -19
- package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
- package/src/skf-brief-skill/assets/skill-brief-schema.md +32 -2
- package/src/skf-brief-skill/customize.toml +44 -0
- package/src/skf-brief-skill/{steps-c/step-02-analyze-target.md → references/analyze-target.md} +27 -15
- package/src/skf-brief-skill/{steps-c/step-04-confirm-brief.md → references/confirm-brief.md} +13 -10
- package/src/skf-brief-skill/references/draft-checkpoint.md +4 -4
- package/src/skf-brief-skill/{steps-c/step-01-gather-intent.md → references/gather-intent.md} +35 -29
- package/src/skf-brief-skill/references/headless-args.md +9 -9
- package/src/skf-brief-skill/references/headless-source-authority-detection.md +1 -1
- package/src/skf-brief-skill/{steps-c/step-06-health-check.md → references/health-check.md} +4 -2
- package/src/skf-brief-skill/references/portfolio-similarity-check.md +2 -2
- package/src/skf-brief-skill/references/qmd-collection-registration.md +2 -2
- package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +27 -20
- package/src/skf-brief-skill/references/version-resolution.md +2 -2
- package/src/skf-brief-skill/{steps-c/step-05-write-brief.md → references/write-brief.md} +29 -16
- package/src/skf-create-skill/SKILL.md +25 -19
- package/src/skf-create-skill/assets/compile-assembly-rules.md +3 -3
- package/src/skf-create-skill/assets/skill-sections.md +5 -5
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +11 -11
- package/src/skf-create-skill/customize.toml +35 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +142 -0
- package/src/skf-create-skill/{steps-c/step-05-compile.md → references/compile.md} +16 -16
- package/src/skf-create-skill/{steps-c/step-03d-component-extraction.md → references/component-extraction.md} +16 -16
- package/src/skf-create-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +4 -4
- package/src/skf-create-skill/{steps-c/step-04-enrich.md → references/enrich.md} +3 -3
- package/src/skf-create-skill/{steps-c/step-03-extract.md → references/extract.md} +52 -120
- package/src/skf-create-skill/references/extraction-patterns.md +10 -8
- package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +11 -11
- package/src/skf-create-skill/{steps-c/step-09-health-check.md → references/health-check.md} +4 -2
- package/src/skf-create-skill/{steps-c/step-01-load-brief.md → references/load-brief.md} +40 -32
- package/src/skf-create-skill/{steps-c/step-08-report.md → references/report.md} +8 -8
- package/src/skf-create-skill/references/source-resolution-protocols.md +6 -6
- package/src/skf-create-skill/{steps-c/sub/step-02b-ccc-discover.md → references/sub/ccc-discover.md} +5 -5
- package/src/skf-create-skill/{steps-c/sub/step-03c-fetch-docs.md → references/sub/fetch-docs.md} +28 -12
- package/src/skf-create-skill/{steps-c/sub/step-03b-fetch-temporal.md → references/sub/fetch-temporal.md} +81 -50
- package/src/skf-create-skill/{steps-c/step-06-validate.md → references/validate.md} +48 -28
- package/src/skf-create-stack-skill/SKILL.md +76 -17
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +102 -0
- package/src/skf-create-stack-skill/assets/stack-skill-template.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +46 -0
- package/src/skf-create-stack-skill/{steps-c/step-06-compile-stack.md → references/compile-stack.md} +6 -5
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +3 -1
- package/src/skf-create-stack-skill/{steps-c/step-05-detect-integrations.md → references/detect-integrations.md} +37 -11
- package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +32 -28
- package/src/skf-create-stack-skill/{steps-c/step-07-generate-output.md → references/generate-output.md} +11 -85
- package/src/skf-create-stack-skill/{steps-c/step-10-health-check.md → references/health-check.md} +4 -2
- package/src/skf-create-stack-skill/{steps-c/step-01-init.md → references/init.md} +70 -4
- package/src/skf-create-stack-skill/references/integration-patterns.md +2 -0
- package/src/skf-create-stack-skill/references/manifest-patterns.md +2 -0
- package/src/skf-create-stack-skill/{steps-c/step-04-parallel-extract.md → references/parallel-extract.md} +44 -17
- package/src/skf-create-stack-skill/{steps-c/step-03-rank-and-confirm.md → references/rank-and-confirm.md} +5 -4
- package/src/skf-create-stack-skill/{steps-c/step-09-report.md → references/report.md} +3 -3
- package/src/skf-create-stack-skill/{steps-c/step-08-validate.md → references/validate.md} +6 -6
- package/src/skf-drop-skill/SKILL.md +84 -12
- package/src/skf-drop-skill/customize.toml +57 -0
- package/src/skf-drop-skill/{steps-c/step-02-execute.md → references/execute.md} +49 -34
- package/src/skf-drop-skill/{steps-c/step-04-health-check.md → references/health-check.md} +6 -4
- package/src/skf-drop-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
- package/src/skf-drop-skill/{steps-c/step-01-select.md → references/select.md} +63 -15
- package/src/skf-export-skill/SKILL.md +85 -16
- package/src/skf-export-skill/assets/managed-section-format.md +3 -3
- package/src/skf-export-skill/assets/snippet-format.md +2 -2
- package/src/skf-export-skill/customize.toml +49 -0
- package/src/skf-export-skill/{steps-c/step-03-generate-snippet.md → references/generate-snippet.md} +11 -11
- package/src/skf-export-skill/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
- package/src/skf-export-skill/{steps-c/step-01-load-skill.md → references/load-skill.md} +11 -49
- package/src/skf-export-skill/references/manifest-rebuild.md +68 -0
- package/src/skf-export-skill/references/multi-skill-mode.md +38 -0
- package/src/skf-export-skill/references/orphan-context-detection.md +75 -0
- package/src/skf-export-skill/references/orphan-row-detection.md +102 -0
- package/src/skf-export-skill/{steps-c/step-02-package.md → references/package.md} +7 -7
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +79 -0
- package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +23 -11
- package/src/skf-export-skill/{steps-c/step-05-token-report.md → references/token-report.md} +5 -5
- package/src/skf-export-skill/{steps-c/step-04-update-context.md → references/update-context.md} +109 -113
- package/src/skf-quick-skill/SKILL.md +63 -123
- package/src/skf-quick-skill/customize.toml +44 -0
- package/src/skf-quick-skill/references/batch-mode.md +102 -0
- package/src/skf-quick-skill/{steps-c/step-04-compile.md → references/compile.md} +10 -10
- package/src/skf-quick-skill/{steps-c/step-02-ecosystem-check.md → references/ecosystem-check.md} +5 -5
- package/src/skf-quick-skill/{steps-c/step-06-finalize.md → references/finalize.md} +7 -7
- package/src/skf-quick-skill/{steps-c/step-07-health-check.md → references/health-check.md} +3 -3
- package/src/skf-quick-skill/{steps-c/step-03-quick-extract.md → references/quick-extract.md} +5 -5
- package/src/skf-quick-skill/references/registry-resolution.md +2 -0
- package/src/skf-quick-skill/{steps-c/step-01-resolve-target.md → references/resolve-target.md} +14 -10
- package/src/skf-quick-skill/{steps-c/step-05-write-and-validate.md → references/write-and-validate.md} +4 -4
- package/src/skf-refine-architecture/SKILL.md +86 -16
- package/src/skf-refine-architecture/customize.toml +49 -0
- package/src/skf-refine-architecture/{steps-c/step-05-compile.md → references/compile.md} +4 -4
- package/src/skf-refine-architecture/{steps-c/step-02-gap-analysis.md → references/gap-analysis.md} +5 -5
- package/src/skf-refine-architecture/{steps-c/step-07-health-check.md → references/health-check.md} +6 -4
- package/src/skf-refine-architecture/{steps-c/step-04-improvements.md → references/improvements.md} +5 -5
- package/src/skf-refine-architecture/references/init.md +144 -0
- package/src/skf-refine-architecture/{steps-c/step-03-issue-detection.md → references/issue-detection.md} +5 -5
- package/src/skf-refine-architecture/references/refinement-rules.md +2 -0
- package/src/skf-refine-architecture/{steps-c/step-06-report.md → references/report.md} +14 -5
- package/src/skf-rename-skill/SKILL.md +82 -12
- package/src/skf-rename-skill/customize.toml +52 -0
- package/src/skf-rename-skill/{steps-c/step-02-execute.md → references/execute.md} +94 -99
- package/src/skf-rename-skill/references/health-check.md +30 -0
- package/src/skf-rename-skill/references/rebuild-context.md +110 -0
- package/src/skf-rename-skill/{steps-c/step-03-report.md → references/report.md} +13 -5
- package/src/skf-rename-skill/{steps-c/step-01-select.md → references/select.md} +59 -20
- package/src/skf-setup/SKILL.md +35 -35
- package/src/skf-setup/customize.toml +33 -0
- package/src/skf-setup/{steps-c/step-03-auto-index.md → references/auto-index.md} +10 -18
- package/src/skf-setup/{steps-c/step-01b-ccc-index.md → references/ccc-index.md} +16 -31
- package/src/skf-setup/{steps-c/step-01-detect-and-tier.md → references/detect-and-tier.md} +23 -10
- package/src/skf-setup/{steps-c/step-05-health-check.md → references/health-check.md} +1 -1
- package/src/skf-setup/{steps-c/step-04-report.md → references/report.md} +21 -19
- package/src/skf-setup/references/tier-rules.md +1 -1
- package/src/skf-setup/{steps-c/step-02-write-config.md → references/write-config.md} +14 -13
- package/src/skf-test-skill/SKILL.md +77 -15
- package/src/skf-test-skill/customize.toml +54 -0
- package/src/skf-test-skill/{steps-c/step-04-coherence-check.md → references/coherence-check.md} +37 -73
- package/src/skf-test-skill/{steps-c/step-03-coverage-check.md → references/coverage-check.md} +16 -39
- package/src/skf-test-skill/{steps-c/step-02-detect-mode.md → references/detect-mode.md} +4 -26
- package/src/skf-test-skill/{steps-c/step-04b-external-validators.md → references/external-validators.md} +6 -28
- package/src/skf-test-skill/references/health-check.md +14 -0
- package/src/skf-test-skill/{steps-c/step-01-init.md → references/init.md} +21 -37
- package/src/skf-test-skill/references/migration-section-rules.md +4 -2
- package/src/skf-test-skill/{steps-c/step-06-report.md → references/report.md} +41 -51
- package/src/skf-test-skill/{steps-c/step-05-score.md → references/score.md} +26 -43
- package/src/skf-test-skill/references/scoring-rules.md +8 -6
- package/src/skf-test-skill/references/source-access-protocol.md +7 -5
- package/src/skf-test-skill/scripts/compute-score.py +88 -17
- package/src/skf-test-skill/templates/test-report-template.md +13 -13
- package/src/skf-update-skill/SKILL.md +23 -14
- package/src/skf-update-skill/customize.toml +44 -0
- package/src/skf-update-skill/{steps-c/step-02-detect-changes.md → references/detect-changes.md} +176 -68
- package/src/skf-update-skill/references/health-check.md +32 -0
- package/src/skf-update-skill/{steps-c/step-01-init.md → references/init.md} +56 -8
- package/src/skf-update-skill/references/manual-section-rules.md +4 -0
- package/src/skf-update-skill/references/merge-conflict-rules.md +4 -0
- package/src/skf-update-skill/{steps-c/step-04-merge.md → references/merge.md} +8 -8
- package/src/skf-update-skill/{steps-c/step-03-re-extract.md → references/re-extract.md} +53 -44
- package/src/skf-update-skill/references/remote-source-resolution.md +5 -1
- package/src/skf-update-skill/references/report.md +214 -0
- package/src/skf-update-skill/{steps-c/step-05-validate.md → references/validate.md} +5 -5
- package/src/skf-update-skill/{steps-c/step-06-write.md → references/write.md} +73 -51
- package/src/skf-verify-stack/SKILL.md +89 -17
- package/src/skf-verify-stack/assets/feasibility-report-template.md +4 -4
- package/src/skf-verify-stack/customize.toml +50 -0
- package/src/skf-verify-stack/references/coverage-patterns.md +2 -2
- package/src/skf-verify-stack/{steps-c/step-02-coverage.md → references/coverage.md} +13 -9
- package/src/skf-verify-stack/{steps-c/step-07-health-check.md → references/health-check.md} +7 -5
- package/src/skf-verify-stack/references/init.md +170 -0
- package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
- package/src/skf-verify-stack/{steps-c/step-03-integrations.md → references/integrations.md} +18 -14
- package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +27 -42
- package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +13 -9
- package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +14 -10
- package/tools/validate-docs-drift.js +1 -1
- package/src/skf-refine-architecture/steps-c/step-01-init.md +0 -136
- package/src/skf-rename-skill/steps-c/step-04-health-check.md +0 -22
- package/src/skf-test-skill/steps-c/step-07-health-check.md +0 -25
- package/src/skf-update-skill/steps-c/step-07-report.md +0 -148
- package/src/skf-update-skill/steps-c/step-08-health-check.md +0 -22
- package/src/skf-verify-stack/steps-c/step-01-init.md +0 -178
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Provenance Gap Dispatch — Major-Version Scope Reconciliation feeder.
|
|
6
|
+
|
|
7
|
+
`skf-update-skill/references/detect-changes.md §1c` performs three
|
|
8
|
+
deterministic operations before the LLM can prompt the user:
|
|
9
|
+
|
|
10
|
+
1. **Locate** the latest audit drift report under
|
|
11
|
+
`{forge_data_folder}/{skill_name}/{baseline_version}/drift-report-*.md`
|
|
12
|
+
2. **Parse** the report for an "Out-of-Scope Observations" section
|
|
13
|
+
(either `## Out-of-Scope Observations` at the top level or
|
|
14
|
+
`### Out-of-Scope New Public API` under `## Remediation Suggestions`).
|
|
15
|
+
Extract each candidate's `path` and `evidence`.
|
|
16
|
+
3. **Reconcile** candidates against `brief.scope.amendments[]`:
|
|
17
|
+
- `promoted` for this path → already in scope (skip)
|
|
18
|
+
- `skipped` for this path → user previously declined (skip)
|
|
19
|
+
- `demoted-include` or `demoted-exclude` → pre-decided demotion
|
|
20
|
+
- no matching amendment → unresolved (caller prompts user)
|
|
21
|
+
|
|
22
|
+
The prose previously asked the LLM to do all three per run. The
|
|
23
|
+
markdown parsing in particular is fiddly (heading lookahead, table
|
|
24
|
+
vs bullet vs definition list formats); keeping it in prose lets
|
|
25
|
+
report-format drift produce silent skips. This helper makes
|
|
26
|
+
discovery and classification one bash call.
|
|
27
|
+
|
|
28
|
+
Subcommand:
|
|
29
|
+
dispatch --skill-name <name> --baseline-version <ver> \\
|
|
30
|
+
--forge-data-folder <path> --brief <brief.yaml-path>
|
|
31
|
+
|
|
32
|
+
Output JSON (stdout):
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
"status": "no-report" | "no-candidates" | "candidates-found",
|
|
36
|
+
"report_path": "<abs path>" | null,
|
|
37
|
+
"candidates_total": N,
|
|
38
|
+
"classified": [
|
|
39
|
+
{
|
|
40
|
+
"path": "<glob or file path>",
|
|
41
|
+
"evidence": "<one-liner from report>",
|
|
42
|
+
"status": "already-in-scope"
|
|
43
|
+
| "pre-decided-skipped"
|
|
44
|
+
| "pre-decided-demoted"
|
|
45
|
+
| "unresolved",
|
|
46
|
+
"prior_action": "promoted" | "skipped"
|
|
47
|
+
| "demoted-include" | "demoted-exclude"
|
|
48
|
+
| null
|
|
49
|
+
}, ...
|
|
50
|
+
],
|
|
51
|
+
"summary": {
|
|
52
|
+
"pre_decided_count": N,
|
|
53
|
+
"unresolved_count": N
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
`status` field shortcuts the caller:
|
|
58
|
+
- `no-report` → no drift report exists; §1c skipped entirely
|
|
59
|
+
- `no-candidates` → report exists but Out-of-Scope section is absent/empty
|
|
60
|
+
- `candidates-found` → classified[] is populated; iterate unresolved entries
|
|
61
|
+
|
|
62
|
+
Exit codes:
|
|
63
|
+
0 — operation succeeded (any status)
|
|
64
|
+
1 — user error (paths invalid, brief unparseable)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
from __future__ import annotations
|
|
68
|
+
|
|
69
|
+
import argparse
|
|
70
|
+
import json
|
|
71
|
+
import re
|
|
72
|
+
import sys
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
|
|
75
|
+
import yaml
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --------------------------------------------------------------------------
|
|
79
|
+
# Discovery: locate the latest drift report
|
|
80
|
+
# --------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def discover_drift_report(
|
|
84
|
+
forge_data_folder: Path, skill_name: str, baseline_version: str
|
|
85
|
+
) -> Path | None:
|
|
86
|
+
"""Return the most recent drift-report-*.md under
|
|
87
|
+
{forge_data_folder}/{skill_name}/{baseline_version}/, or None.
|
|
88
|
+
|
|
89
|
+
Reports are sorted by filename DESC — the convention is timestamped
|
|
90
|
+
filenames so lexicographic order == chronological order.
|
|
91
|
+
"""
|
|
92
|
+
search_dir = forge_data_folder / skill_name / baseline_version
|
|
93
|
+
if not search_dir.is_dir():
|
|
94
|
+
return None
|
|
95
|
+
candidates = sorted(
|
|
96
|
+
search_dir.glob("drift-report-*.md"),
|
|
97
|
+
key=lambda p: p.name,
|
|
98
|
+
reverse=True,
|
|
99
|
+
)
|
|
100
|
+
return candidates[0] if candidates else None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
# Parse: extract Out-of-Scope candidates from a drift report
|
|
105
|
+
# --------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
_TOP_LEVEL_HEADING = re.compile(r"^##\s+Out-of-Scope Observations\s*$")
|
|
109
|
+
_NESTED_HEADING = re.compile(r"^###\s+Out-of-Scope New Public API\s*$")
|
|
110
|
+
_REMEDIATION_HEADING = re.compile(r"^##\s+Remediation Suggestions\s*$")
|
|
111
|
+
_ANY_TOP_LEVEL_HEADING = re.compile(r"^##\s+\S")
|
|
112
|
+
_ANY_SUB_LEVEL_HEADING = re.compile(r"^###\s+\S")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def extract_out_of_scope_section(text: str) -> str | None:
|
|
116
|
+
"""Pull the Out-of-Scope block from a drift report. Returns the block
|
|
117
|
+
text (without the heading), or None if no matching section exists.
|
|
118
|
+
|
|
119
|
+
Supports two report shapes:
|
|
120
|
+
- top-level `## Out-of-Scope Observations` heading
|
|
121
|
+
- `### Out-of-Scope New Public API` under `## Remediation Suggestions`
|
|
122
|
+
"""
|
|
123
|
+
lines = text.splitlines()
|
|
124
|
+
n = len(lines)
|
|
125
|
+
|
|
126
|
+
# Pass 1: top-level heading
|
|
127
|
+
for i, line in enumerate(lines):
|
|
128
|
+
if _TOP_LEVEL_HEADING.match(line):
|
|
129
|
+
return _gather_until_next_heading(lines, i + 1, _ANY_TOP_LEVEL_HEADING)
|
|
130
|
+
|
|
131
|
+
# Pass 2: nested heading under Remediation Suggestions
|
|
132
|
+
in_remediation = False
|
|
133
|
+
for i, line in enumerate(lines):
|
|
134
|
+
if _REMEDIATION_HEADING.match(line):
|
|
135
|
+
in_remediation = True
|
|
136
|
+
continue
|
|
137
|
+
if in_remediation and _ANY_TOP_LEVEL_HEADING.match(line):
|
|
138
|
+
in_remediation = False
|
|
139
|
+
continue
|
|
140
|
+
if in_remediation and _NESTED_HEADING.match(line):
|
|
141
|
+
return _gather_until_next_heading(lines, i + 1, _ANY_SUB_LEVEL_HEADING)
|
|
142
|
+
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _gather_until_next_heading(
|
|
147
|
+
lines: list[str], start: int, stop_pattern: re.Pattern[str]
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Collect lines from `start` until the next heading matching stop_pattern."""
|
|
150
|
+
out: list[str] = []
|
|
151
|
+
for i in range(start, len(lines)):
|
|
152
|
+
if stop_pattern.match(lines[i]):
|
|
153
|
+
break
|
|
154
|
+
out.append(lines[i])
|
|
155
|
+
return "\n".join(out).strip()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# --------------------------------------------------------------------------
|
|
159
|
+
# Parse candidates from the section text
|
|
160
|
+
# --------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Bullet-list shape: "- `path/to/thing` — evidence text"
|
|
164
|
+
# (em-dash, en-dash, or plain hyphen as separator)
|
|
165
|
+
_BULLET = re.compile(
|
|
166
|
+
r"^\s*[-*]\s+`(?P<path>[^`]+)`\s*[—–-]\s*(?P<evidence>.+?)\s*$"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Table shape: "| path | evidence |"
|
|
170
|
+
_TABLE_ROW = re.compile(
|
|
171
|
+
r"^\|\s*`?(?P<path>[^|`]+?)`?\s*\|\s*(?P<evidence>[^|]+?)\s*\|"
|
|
172
|
+
)
|
|
173
|
+
_TABLE_SEPARATOR = re.compile(r"^\|\s*[-:]+\s*\|")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def parse_candidates(section_text: str) -> list[dict]:
|
|
177
|
+
r"""Extract (path, evidence) candidates from an Out-of-Scope section.
|
|
178
|
+
|
|
179
|
+
Recognized formats:
|
|
180
|
+
- Bullet lines: ``- `path/to` — evidence``
|
|
181
|
+
- Markdown table: ``| path | evidence |`` (skip header/separator rows)
|
|
182
|
+
"""
|
|
183
|
+
if not section_text:
|
|
184
|
+
return []
|
|
185
|
+
candidates: list[dict] = []
|
|
186
|
+
in_table = False
|
|
187
|
+
table_header_seen = False
|
|
188
|
+
for line in section_text.splitlines():
|
|
189
|
+
stripped = line.strip()
|
|
190
|
+
if not stripped:
|
|
191
|
+
in_table = False
|
|
192
|
+
table_header_seen = False
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
if _TABLE_SEPARATOR.match(stripped):
|
|
196
|
+
in_table = True
|
|
197
|
+
table_header_seen = True
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
if stripped.startswith("|"):
|
|
201
|
+
if not table_header_seen:
|
|
202
|
+
# First table-shaped line is the header — skip
|
|
203
|
+
table_header_seen = True
|
|
204
|
+
continue
|
|
205
|
+
m = _TABLE_ROW.match(stripped)
|
|
206
|
+
if m:
|
|
207
|
+
path = m.group("path").strip()
|
|
208
|
+
evidence = m.group("evidence").strip()
|
|
209
|
+
if path and not _looks_like_table_header(path):
|
|
210
|
+
candidates.append({"path": path, "evidence": evidence})
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
m = _BULLET.match(line)
|
|
214
|
+
if m:
|
|
215
|
+
candidates.append(
|
|
216
|
+
{
|
|
217
|
+
"path": m.group("path").strip(),
|
|
218
|
+
"evidence": m.group("evidence").strip(),
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
return candidates
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _looks_like_table_header(text: str) -> bool:
|
|
225
|
+
"""True if `text` looks like a table-header cell label (e.g. 'path',
|
|
226
|
+
'evidence'). Headers slip past `_TABLE_SEPARATOR` only when the report
|
|
227
|
+
uses a header row without the canonical `|---|---|` separator."""
|
|
228
|
+
return text.lower() in {"path", "evidence", "file", "files", "candidate", "rationale"}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# --------------------------------------------------------------------------
|
|
232
|
+
# Reconcile candidates against brief.scope.amendments[]
|
|
233
|
+
# --------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
_ALREADY_IN_SCOPE = "already-in-scope"
|
|
237
|
+
_PRE_DECIDED_SKIPPED = "pre-decided-skipped"
|
|
238
|
+
_PRE_DECIDED_DEMOTED = "pre-decided-demoted"
|
|
239
|
+
_UNRESOLVED = "unresolved"
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def load_brief(brief_path: Path) -> dict:
|
|
243
|
+
"""Parse a skill-brief.yaml file. Raises ValueError on load failure."""
|
|
244
|
+
try:
|
|
245
|
+
text = brief_path.read_text(encoding="utf-8")
|
|
246
|
+
except OSError as exc:
|
|
247
|
+
raise ValueError(f"cannot read brief at {brief_path}: {exc}") from exc
|
|
248
|
+
try:
|
|
249
|
+
brief = yaml.safe_load(text)
|
|
250
|
+
except yaml.YAMLError as exc:
|
|
251
|
+
raise ValueError(f"brief at {brief_path} is not valid YAML: {exc}") from exc
|
|
252
|
+
if not isinstance(brief, dict):
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"brief at {brief_path} must be a YAML mapping, got "
|
|
255
|
+
f"{type(brief).__name__}"
|
|
256
|
+
)
|
|
257
|
+
return brief
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def reconcile(candidates: list[dict], brief: dict) -> list[dict]:
|
|
261
|
+
"""Classify each candidate against brief.scope.amendments[]."""
|
|
262
|
+
scope = brief.get("scope")
|
|
263
|
+
amendments = []
|
|
264
|
+
if isinstance(scope, dict):
|
|
265
|
+
amendments = scope.get("amendments") or []
|
|
266
|
+
if not isinstance(amendments, list):
|
|
267
|
+
amendments = []
|
|
268
|
+
|
|
269
|
+
# index amendments by path → list of actions (most recent first preserved)
|
|
270
|
+
by_path: dict[str, list[str]] = {}
|
|
271
|
+
for amend in amendments:
|
|
272
|
+
if not isinstance(amend, dict):
|
|
273
|
+
continue
|
|
274
|
+
path = amend.get("path")
|
|
275
|
+
action = amend.get("action")
|
|
276
|
+
if isinstance(path, str) and isinstance(action, str):
|
|
277
|
+
by_path.setdefault(path, []).append(action)
|
|
278
|
+
|
|
279
|
+
classified: list[dict] = []
|
|
280
|
+
for candidate in candidates:
|
|
281
|
+
path = candidate["path"]
|
|
282
|
+
evidence = candidate.get("evidence", "")
|
|
283
|
+
prior_actions = by_path.get(path, [])
|
|
284
|
+
status, prior_action = _classify(prior_actions)
|
|
285
|
+
classified.append(
|
|
286
|
+
{
|
|
287
|
+
"path": path,
|
|
288
|
+
"evidence": evidence,
|
|
289
|
+
"status": status,
|
|
290
|
+
"prior_action": prior_action,
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
return classified
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _classify(prior_actions: list[str]) -> tuple[str, str | None]:
|
|
297
|
+
"""Pick the dominant amendment action for a single candidate path.
|
|
298
|
+
|
|
299
|
+
A path may have multiple amendments (e.g. promoted then later
|
|
300
|
+
demoted-exclude). Most recent action wins — the list is in
|
|
301
|
+
amendment-record-insertion order, so iterate from the end.
|
|
302
|
+
"""
|
|
303
|
+
for action in reversed(prior_actions):
|
|
304
|
+
if action == "promoted":
|
|
305
|
+
return _ALREADY_IN_SCOPE, "promoted"
|
|
306
|
+
if action == "skipped":
|
|
307
|
+
return _PRE_DECIDED_SKIPPED, "skipped"
|
|
308
|
+
if action in ("demoted-include", "demoted-exclude"):
|
|
309
|
+
return _PRE_DECIDED_DEMOTED, action
|
|
310
|
+
return _UNRESOLVED, None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# --------------------------------------------------------------------------
|
|
314
|
+
# Dispatch: orchestrate all three steps
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def dispatch(
|
|
319
|
+
*,
|
|
320
|
+
forge_data_folder: Path,
|
|
321
|
+
skill_name: str,
|
|
322
|
+
baseline_version: str,
|
|
323
|
+
brief_path: Path,
|
|
324
|
+
) -> dict:
|
|
325
|
+
"""Run the full §1c discovery + parse + reconcile pipeline."""
|
|
326
|
+
report = discover_drift_report(forge_data_folder, skill_name, baseline_version)
|
|
327
|
+
if report is None:
|
|
328
|
+
return {
|
|
329
|
+
"status": "no-report",
|
|
330
|
+
"report_path": None,
|
|
331
|
+
"candidates_total": 0,
|
|
332
|
+
"classified": [],
|
|
333
|
+
"summary": {"pre_decided_count": 0, "unresolved_count": 0},
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
text = report.read_text(encoding="utf-8")
|
|
337
|
+
section = extract_out_of_scope_section(text)
|
|
338
|
+
if section is None:
|
|
339
|
+
return {
|
|
340
|
+
"status": "no-candidates",
|
|
341
|
+
"report_path": str(report),
|
|
342
|
+
"candidates_total": 0,
|
|
343
|
+
"classified": [],
|
|
344
|
+
"summary": {"pre_decided_count": 0, "unresolved_count": 0},
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
candidates = parse_candidates(section)
|
|
348
|
+
if not candidates:
|
|
349
|
+
return {
|
|
350
|
+
"status": "no-candidates",
|
|
351
|
+
"report_path": str(report),
|
|
352
|
+
"candidates_total": 0,
|
|
353
|
+
"classified": [],
|
|
354
|
+
"summary": {"pre_decided_count": 0, "unresolved_count": 0},
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
brief = load_brief(brief_path)
|
|
358
|
+
classified = reconcile(candidates, brief)
|
|
359
|
+
|
|
360
|
+
pre_decided = sum(1 for c in classified if c["status"] != _UNRESOLVED)
|
|
361
|
+
unresolved = sum(1 for c in classified if c["status"] == _UNRESOLVED)
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
"status": "candidates-found",
|
|
365
|
+
"report_path": str(report),
|
|
366
|
+
"candidates_total": len(classified),
|
|
367
|
+
"classified": classified,
|
|
368
|
+
"summary": {
|
|
369
|
+
"pre_decided_count": pre_decided,
|
|
370
|
+
"unresolved_count": unresolved,
|
|
371
|
+
},
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# --------------------------------------------------------------------------
|
|
376
|
+
# CLI
|
|
377
|
+
# --------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _cmd_dispatch(args: argparse.Namespace) -> int:
|
|
381
|
+
forge = Path(args.forge_data_folder)
|
|
382
|
+
if not forge.is_dir():
|
|
383
|
+
print(
|
|
384
|
+
f"error: forge-data-folder not a directory: {forge}", file=sys.stderr
|
|
385
|
+
)
|
|
386
|
+
return 1
|
|
387
|
+
brief_path = Path(args.brief)
|
|
388
|
+
if not brief_path.is_file():
|
|
389
|
+
print(f"error: brief not found: {brief_path}", file=sys.stderr)
|
|
390
|
+
return 1
|
|
391
|
+
try:
|
|
392
|
+
result = dispatch(
|
|
393
|
+
forge_data_folder=forge,
|
|
394
|
+
skill_name=args.skill_name,
|
|
395
|
+
baseline_version=args.baseline_version,
|
|
396
|
+
brief_path=brief_path,
|
|
397
|
+
)
|
|
398
|
+
except ValueError as exc:
|
|
399
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
400
|
+
return 1
|
|
401
|
+
json.dump(result, sys.stdout, indent=2)
|
|
402
|
+
sys.stdout.write("\n")
|
|
403
|
+
return 0
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
407
|
+
parser = argparse.ArgumentParser(
|
|
408
|
+
prog="skf-provenance-gap-dispatch",
|
|
409
|
+
description=(
|
|
410
|
+
"Locate the latest drift report, extract Out-of-Scope candidates, "
|
|
411
|
+
"and classify them against brief.scope.amendments[]."
|
|
412
|
+
),
|
|
413
|
+
)
|
|
414
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
415
|
+
|
|
416
|
+
p = sub.add_parser("dispatch", help="run the full §1c pipeline")
|
|
417
|
+
p.add_argument("--skill-name", required=True)
|
|
418
|
+
p.add_argument("--baseline-version", required=True)
|
|
419
|
+
p.add_argument("--forge-data-folder", required=True)
|
|
420
|
+
p.add_argument("--brief", required=True, help="path to skill-brief.yaml")
|
|
421
|
+
p.set_defaults(func=_cmd_dispatch)
|
|
422
|
+
|
|
423
|
+
return parser
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def main(argv: list[str] | None = None) -> int:
|
|
427
|
+
parser = _build_parser()
|
|
428
|
+
args = parser.parse_args(argv)
|
|
429
|
+
return args.func(args)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
raise SystemExit(main())
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
# ///
|
|
5
5
|
"""SKF QMD Classify Collections — Set arithmetic over QMD collection names.
|
|
6
6
|
|
|
7
|
-
Replaces the prose-driven classification logic in `src/skf-setup/
|
|
8
|
-
|
|
7
|
+
Replaces the prose-driven classification logic in `src/skf-setup/references/
|
|
8
|
+
auto-index.md` §3 with one Python invocation. Compares the live
|
|
9
9
|
QMD collections (from `qmd collection list`) against the forge registry
|
|
10
10
|
(`qmd_collections` array in forge-tier.yaml) and classifies each name as
|
|
11
11
|
Healthy / Orphaned / Stale, applying the forge-namespace suffix filter
|
|
12
12
|
added in PR #244 to silently exclude collections owned by unrelated
|
|
13
13
|
tools sharing the QMD daemon.
|
|
14
14
|
|
|
15
|
-
Classification rules (per step
|
|
15
|
+
Classification rules (per step 3 §3):
|
|
16
16
|
|
|
17
17
|
Healthy — name in {forge-suffix-matched live} AND in registry.
|
|
18
18
|
No action needed.
|
|
19
19
|
Orphaned — name in {forge-suffix-matched live} but NOT in registry.
|
|
20
|
-
Flagged for user-prompted removal in step
|
|
20
|
+
Flagged for user-prompted removal in step 3 §4.
|
|
21
21
|
Stale — name in registry but NOT in {all live}. Registry entry
|
|
22
22
|
should be removed.
|
|
23
23
|
Foreign — name in live but does NOT match a forge suffix. Silently
|
|
@@ -175,6 +175,7 @@ def classify(live: list[str], registry: list[str]) -> dict:
|
|
|
175
175
|
stale = sorted(registry_set - all_live_set)
|
|
176
176
|
|
|
177
177
|
return {
|
|
178
|
+
"live_names": sorted(all_live_set),
|
|
178
179
|
"healthy": healthy,
|
|
179
180
|
"orphaned": orphaned,
|
|
180
181
|
"stale": stale,
|
|
@@ -183,6 +184,32 @@ def classify(live: list[str], registry: list[str]) -> dict:
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
|
|
187
|
+
def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
|
|
188
|
+
"""Invoke `qmd collection list` and return (names, error).
|
|
189
|
+
|
|
190
|
+
Owns the CLI parsing so callers (and prompts) don't reinvent it.
|
|
191
|
+
Empty stdout / non-zero exit → ([], error_message). The classifier's
|
|
192
|
+
same-process invocation here is the single source of truth for what
|
|
193
|
+
counts as a "live collection name".
|
|
194
|
+
"""
|
|
195
|
+
import subprocess
|
|
196
|
+
try:
|
|
197
|
+
result = subprocess.run(
|
|
198
|
+
["qmd", "collection", "list"],
|
|
199
|
+
capture_output=True,
|
|
200
|
+
text=True,
|
|
201
|
+
timeout=30,
|
|
202
|
+
check=False,
|
|
203
|
+
)
|
|
204
|
+
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
|
205
|
+
return [], f"qmd collection list failed: {e}"
|
|
206
|
+
if result.returncode != 0:
|
|
207
|
+
return [], f"qmd collection list exited {result.returncode}: {result.stderr.strip() or '<no stderr>'}"
|
|
208
|
+
# Each non-empty line is a collection name (qmd's stable contract).
|
|
209
|
+
names = [line.strip() for line in result.stdout.splitlines() if line.strip()]
|
|
210
|
+
return names, None
|
|
211
|
+
|
|
212
|
+
|
|
186
213
|
def main() -> None:
|
|
187
214
|
parser = argparse.ArgumentParser(
|
|
188
215
|
description="Classify QMD collections vs forge registry.",
|
|
@@ -190,9 +217,10 @@ def main() -> None:
|
|
|
190
217
|
)
|
|
191
218
|
parser.add_argument(
|
|
192
219
|
"--live-names",
|
|
193
|
-
default=
|
|
194
|
-
help="Comma-separated list of collection names currently in QMD "
|
|
195
|
-
"
|
|
220
|
+
default=None,
|
|
221
|
+
help="Comma-separated list of collection names currently in QMD. "
|
|
222
|
+
"If omitted, the script invokes `qmd collection list` itself. "
|
|
223
|
+
"Empty string → no live collections.",
|
|
196
224
|
)
|
|
197
225
|
parser.add_argument(
|
|
198
226
|
"--registry-from-yaml",
|
|
@@ -203,7 +231,13 @@ def main() -> None:
|
|
|
203
231
|
)
|
|
204
232
|
args = parser.parse_args()
|
|
205
233
|
|
|
206
|
-
|
|
234
|
+
if args.live_names is None:
|
|
235
|
+
names, error = fetch_live_names_from_qmd()
|
|
236
|
+
if error is not None:
|
|
237
|
+
_die(2, error)
|
|
238
|
+
live = names
|
|
239
|
+
else:
|
|
240
|
+
live = parse_live_names(args.live_names)
|
|
207
241
|
registry = load_registry_names(args.registry_from_yaml)
|
|
208
242
|
_ok(classify(live, registry))
|
|
209
243
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"""SKF Recommend Scope Type — deterministic 5-rule heuristic ladder.
|
|
6
6
|
|
|
7
7
|
Single source of truth for the scope-type recommendation logic that
|
|
8
|
-
skf-brief-skill step
|
|
8
|
+
skf-brief-skill step 3 §2c applies. Both the interactive recommendation
|
|
9
9
|
and the headless auto-selection paths invoke this script with the same
|
|
10
10
|
inputs to eliminate the drift seam between them.
|
|
11
11
|
|
|
@@ -36,7 +36,7 @@ CLI:
|
|
|
36
36
|
|
|
37
37
|
Input (JSON object on stdin or via --json):
|
|
38
38
|
intent — string (combined intent + scope_hint), default ""
|
|
39
|
-
module_count — integer (top-level modules from step
|
|
39
|
+
module_count — integer (top-level modules from step 2), default 0
|
|
40
40
|
export_count — integer (named exports from manifest), default 0
|
|
41
41
|
tree — list of repo-relative file paths, default []
|
|
42
42
|
entry_files — optional [{path, content}], default null
|
|
@@ -8,9 +8,9 @@ skill-template.md schema with quick-skill-specific population rules.
|
|
|
8
8
|
Pure renderer — no I/O beyond stdin/stdout. Reads a JSON payload of
|
|
9
9
|
extracted state on stdin and emits the corresponding metadata.json
|
|
10
10
|
on stdout. Replaces the hand-assembly the LLM previously did in
|
|
11
|
-
skf-quick-skill step
|
|
11
|
+
skf-quick-skill step 4 §4.
|
|
12
12
|
|
|
13
|
-
Constants vs. input-derived split mirrors step
|
|
13
|
+
Constants vs. input-derived split mirrors step 4's documentation:
|
|
14
14
|
|
|
15
15
|
Constants (always literal):
|
|
16
16
|
skill_type "single"
|