bmad-module-skill-forge 1.4.0 → 1.5.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 +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 +97 -8
- 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-merge-ccc-exclusions.py +45 -1
- 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} +44 -14
- package/src/skf-analyze-source/{steps-c/step-01-init.md → references/init.md} +27 -6
- 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} +12 -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/references/init.md +305 -0
- 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} +29 -16
- package/src/skf-brief-skill/SKILL.md +48 -18
- 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} +5 -5
- 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} +24 -24
- 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 +1 -1
- package/src/skf-brief-skill/{steps-c/step-03-scope-definition.md → references/scope-definition.md} +19 -16
- 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} +11 -10
- 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 +8 -8
- package/src/skf-create-skill/{steps-c/step-07-generate-artifacts.md → references/generate-artifacts.md} +17 -9
- 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} +46 -26
- 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} +32 -11
- package/src/skf-create-stack-skill/{steps-c/step-02-detect-manifests.md → references/detect-manifests.md} +28 -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} +40 -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} +18 -12
- 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} +21 -51
- 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 +74 -0
- package/src/skf-export-skill/{steps-c/step-06-summary.md → references/summary.md} +30 -6
- 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} +116 -91
- 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} +14 -22
- 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} +28 -71
- 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} +7 -7
- 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 +166 -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} +12 -12
- package/src/skf-verify-stack/{steps-c/step-06-report.md → references/report.md} +22 -41
- package/src/skf-verify-stack/{steps-c/step-04-requirements.md → references/requirements.md} +7 -7
- package/src/skf-verify-stack/{steps-c/step-05-synthesize.md → references/synthesize.md} +8 -8
- package/tools/validate-docs-drift.js +1 -1
- package/src/skf-audit-skill/steps-c/step-01-init.md +0 -221
- 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
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
Validates and normalizes the headless argument set passed to skf-brief-skill
|
|
8
8
|
before the workflow's interactive sequence runs. Catches malformed inputs at
|
|
9
|
-
point of capture instead of letting them surface 5+ minutes later in step
|
|
10
|
-
or step
|
|
9
|
+
point of capture instead of letting them surface 5+ minutes later in step 2
|
|
10
|
+
or step 5.
|
|
11
11
|
|
|
12
12
|
CLI:
|
|
13
13
|
uv run skf-validate-brief-inputs.py --json '{...}'
|
|
@@ -77,7 +77,7 @@ KNOWN_FIELDS = {
|
|
|
77
77
|
"exclude",
|
|
78
78
|
"force",
|
|
79
79
|
}
|
|
80
|
-
# `preset` is intentionally NOT in KNOWN_FIELDS — it is consumed at the step
|
|
80
|
+
# `preset` is intentionally NOT in KNOWN_FIELDS — it is consumed at the step 1 §8 GATE
|
|
81
81
|
# (the LLM merges the named preset YAML into the args dict and drops the `preset` key
|
|
82
82
|
# before calling the validator). If the key leaks through, the validator's existing
|
|
83
83
|
# unknown-field handling emits `"unrecognized field 'preset' — passed through unchanged"`
|
|
@@ -218,9 +218,9 @@ def validate(inp: dict[str, Any]) -> dict[str, Any]:
|
|
|
218
218
|
normalized: dict[str, Any] = dict(inp)
|
|
219
219
|
normalized.setdefault("source_type", "source")
|
|
220
220
|
# `source_authority` is intentionally NOT setdefault'd here for source-type targets:
|
|
221
|
-
# step
|
|
221
|
+
# step 1 §3.3's headless detection branch runs `gh api user` and may resolve to
|
|
222
222
|
# `official` for repos owned by the authenticated user. Stamping a default at
|
|
223
|
-
# validator time would pre-empt the detection because step
|
|
223
|
+
# validator time would pre-empt the detection because step 1 §8 GATE treats the
|
|
224
224
|
# `normalized` object as the source of truth. Absence is the signal "run detection."
|
|
225
225
|
# The docs-only branch below still forces `community` since detection cannot apply.
|
|
226
226
|
normalized.setdefault("scripts_intent", "detect")
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = ["pyyaml", "jsonschema>=4.0"]
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Validate Brief Schema — schema check for a skill-brief.yaml on disk.
|
|
6
|
+
|
|
7
|
+
Loads `skill-brief.yaml` from a path (or from stdin/inline-YAML), validates
|
|
8
|
+
it against `src/shared/scripts/schemas/skill-brief.v1.json`, and applies the
|
|
9
|
+
two conditional rules from `skf-create-skill/references/load-brief.md §3`
|
|
10
|
+
that the JSON schema doesn't express today:
|
|
11
|
+
|
|
12
|
+
- When `source_type == "docs-only"`: `doc_urls` must have ≥1 entry, and
|
|
13
|
+
`source_authority` must be `community` (the prose says it is "forced
|
|
14
|
+
to community" — we surface a warning when it disagrees rather than
|
|
15
|
+
silently rewriting).
|
|
16
|
+
- `version` must be present AND non-empty AND not whitespace-only. The
|
|
17
|
+
schema rejects empty strings via its semver pattern, but a string that
|
|
18
|
+
only contains whitespace would slip past a naive presence check; we
|
|
19
|
+
catch that case explicitly so the downstream directory-resolution code
|
|
20
|
+
doesn't produce `{name}//` paths.
|
|
21
|
+
|
|
22
|
+
The script produces skill-friendly error messages — translating raw
|
|
23
|
+
jsonschema diagnostics into the "Brief validation failed: ..." form the
|
|
24
|
+
calling stage prose already uses. Calling stages can read `errors[]`
|
|
25
|
+
directly and forward the `message` field to the user.
|
|
26
|
+
|
|
27
|
+
CLI:
|
|
28
|
+
uv run skf-validate-brief-schema.py <path/to/skill-brief.yaml>
|
|
29
|
+
cat brief.yaml | uv run skf-validate-brief-schema.py -
|
|
30
|
+
uv run skf-validate-brief-schema.py --yaml '<inline yaml>'
|
|
31
|
+
|
|
32
|
+
Output (JSON on stdout):
|
|
33
|
+
{
|
|
34
|
+
"valid": bool,
|
|
35
|
+
"errors": [{"field": "...", "message": "..."}, ...],
|
|
36
|
+
"warnings": [{"field": "...", "message": "..."}, ...],
|
|
37
|
+
"halt_reason": "brief-missing" | "brief-malformed" | "brief-invalid" | null,
|
|
38
|
+
"brief": { ...parsed YAML if loadable, else null... }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
Exit codes:
|
|
42
|
+
0 — valid (errors empty)
|
|
43
|
+
1 — invalid (errors non-empty) OR file/yaml load failed
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import json
|
|
50
|
+
import sys
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from typing import Any
|
|
53
|
+
|
|
54
|
+
import yaml
|
|
55
|
+
from jsonschema import Draft202012Validator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
|
59
|
+
SCHEMA_PATH = (
|
|
60
|
+
REPO_ROOT / "src" / "shared" / "scripts" / "schemas" / "skill-brief.v1.json"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --------------------------------------------------------------------------
|
|
65
|
+
# Loading
|
|
66
|
+
# --------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_schema() -> dict:
|
|
70
|
+
"""Read the skill-brief schema from its canonical path."""
|
|
71
|
+
return json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_brief_text(text: str) -> tuple[dict | None, str | None]:
|
|
75
|
+
"""Parse YAML text. Returns (brief, error_message)."""
|
|
76
|
+
try:
|
|
77
|
+
brief = yaml.safe_load(text)
|
|
78
|
+
except yaml.YAMLError as exc:
|
|
79
|
+
return None, f"Brief is not valid YAML: {exc}"
|
|
80
|
+
if brief is None:
|
|
81
|
+
return None, "Brief is empty"
|
|
82
|
+
if not isinstance(brief, dict):
|
|
83
|
+
return None, f"Brief must be a YAML mapping; got {type(brief).__name__}"
|
|
84
|
+
return brief, None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------------------------------
|
|
88
|
+
# Error translation
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _field_path(error_path: tuple) -> str:
|
|
93
|
+
"""Render a jsonschema error path as `a.b[0].c`."""
|
|
94
|
+
parts: list[str] = []
|
|
95
|
+
for p in error_path:
|
|
96
|
+
if isinstance(p, int):
|
|
97
|
+
parts.append(f"[{p}]")
|
|
98
|
+
else:
|
|
99
|
+
parts.append(f".{p}" if parts else str(p))
|
|
100
|
+
return "".join(parts) or "(root)"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _translate_jsonschema_error(err) -> dict:
|
|
104
|
+
"""Produce a skill-friendly error record from a jsonschema ValidationError."""
|
|
105
|
+
field = _field_path(err.absolute_path)
|
|
106
|
+
validator = err.validator
|
|
107
|
+
inst = err.instance
|
|
108
|
+
|
|
109
|
+
if validator == "required":
|
|
110
|
+
missing = err.message.split("'")[1] if "'" in err.message else "(unknown)"
|
|
111
|
+
return {
|
|
112
|
+
"field": missing,
|
|
113
|
+
"message": (
|
|
114
|
+
f"Brief validation failed: missing required field `{missing}`. "
|
|
115
|
+
f"Update your skill-brief.yaml and re-run."
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
if validator == "pattern":
|
|
119
|
+
return {
|
|
120
|
+
"field": field,
|
|
121
|
+
"message": (
|
|
122
|
+
f"Brief validation failed: `{field}` field `{inst}` does not "
|
|
123
|
+
f"match required pattern `{err.validator_value}`. "
|
|
124
|
+
f"Update your skill-brief.yaml and re-run."
|
|
125
|
+
),
|
|
126
|
+
}
|
|
127
|
+
if validator == "enum":
|
|
128
|
+
return {
|
|
129
|
+
"field": field,
|
|
130
|
+
"message": (
|
|
131
|
+
f"Brief validation failed: `{field}` field `{inst}` is not one of "
|
|
132
|
+
f"{err.validator_value}. Update your skill-brief.yaml and re-run."
|
|
133
|
+
),
|
|
134
|
+
}
|
|
135
|
+
if validator == "type":
|
|
136
|
+
expected = err.validator_value
|
|
137
|
+
actual = type(inst).__name__
|
|
138
|
+
return {
|
|
139
|
+
"field": field,
|
|
140
|
+
"message": (
|
|
141
|
+
f"Brief validation failed: `{field}` field has type `{actual}`, "
|
|
142
|
+
f"expected `{expected}`. Update your skill-brief.yaml and re-run."
|
|
143
|
+
),
|
|
144
|
+
}
|
|
145
|
+
if validator == "minLength":
|
|
146
|
+
return {
|
|
147
|
+
"field": field,
|
|
148
|
+
"message": (
|
|
149
|
+
f"Brief validation failed: `{field}` field must be non-empty. "
|
|
150
|
+
f"Update your skill-brief.yaml and re-run."
|
|
151
|
+
),
|
|
152
|
+
}
|
|
153
|
+
if validator == "minItems":
|
|
154
|
+
return {
|
|
155
|
+
"field": field,
|
|
156
|
+
"message": (
|
|
157
|
+
f"Brief validation failed: `{field}` field must contain at least "
|
|
158
|
+
f"{err.validator_value} item(s). Update your skill-brief.yaml and re-run."
|
|
159
|
+
),
|
|
160
|
+
}
|
|
161
|
+
# Fallback — preserve raw message but with consistent prefix
|
|
162
|
+
return {
|
|
163
|
+
"field": field,
|
|
164
|
+
"message": f"Brief validation failed: `{field}` — {err.message}",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
# Conditional rules from §3 prose
|
|
170
|
+
# --------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _docs_only_rules(brief: dict) -> tuple[list[dict], list[dict]]:
|
|
174
|
+
"""Apply the docs-only conditional rules.
|
|
175
|
+
|
|
176
|
+
Returns (errors, warnings).
|
|
177
|
+
"""
|
|
178
|
+
errors: list[dict] = []
|
|
179
|
+
warnings: list[dict] = []
|
|
180
|
+
if brief.get("source_type") != "docs-only":
|
|
181
|
+
return errors, warnings
|
|
182
|
+
|
|
183
|
+
doc_urls = brief.get("doc_urls")
|
|
184
|
+
if not isinstance(doc_urls, list) or len(doc_urls) == 0:
|
|
185
|
+
errors.append(
|
|
186
|
+
{
|
|
187
|
+
"field": "doc_urls",
|
|
188
|
+
"message": (
|
|
189
|
+
"Brief validation failed: `doc_urls` must have at least one "
|
|
190
|
+
"entry when `source_type` is `docs-only`. "
|
|
191
|
+
"Update your skill-brief.yaml and re-run."
|
|
192
|
+
),
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
source_authority = brief.get("source_authority")
|
|
197
|
+
if source_authority not in (None, "community"):
|
|
198
|
+
warnings.append(
|
|
199
|
+
{
|
|
200
|
+
"field": "source_authority",
|
|
201
|
+
"message": (
|
|
202
|
+
f"`source_authority` is `{source_authority}` but will be treated "
|
|
203
|
+
f"as `community` because `source_type` is `docs-only`."
|
|
204
|
+
),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
return errors, warnings
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _version_non_empty_rule(brief: dict) -> list[dict]:
|
|
211
|
+
"""The §3 prose calls out version-whitespace-only as a hard error."""
|
|
212
|
+
version = brief.get("version")
|
|
213
|
+
if isinstance(version, str) and version.strip() == "" and version != "":
|
|
214
|
+
# purely whitespace — schema's minLength check on patterns won't catch
|
|
215
|
+
# leading/trailing whitespace that strips to empty
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
"field": "version",
|
|
219
|
+
"message": (
|
|
220
|
+
"Brief validation failed: `version` field is required and "
|
|
221
|
+
"must be non-empty. Update your skill-brief.yaml and re-run."
|
|
222
|
+
),
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# --------------------------------------------------------------------------
|
|
229
|
+
# Main validate
|
|
230
|
+
# --------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def validate_brief(brief: dict) -> dict:
|
|
234
|
+
"""Validate a parsed brief against the schema + conditional rules."""
|
|
235
|
+
schema = load_schema()
|
|
236
|
+
validator = Draft202012Validator(schema)
|
|
237
|
+
|
|
238
|
+
errors: list[dict] = []
|
|
239
|
+
warnings: list[dict] = []
|
|
240
|
+
|
|
241
|
+
for err in sorted(validator.iter_errors(brief), key=lambda e: e.absolute_path):
|
|
242
|
+
errors.append(_translate_jsonschema_error(err))
|
|
243
|
+
|
|
244
|
+
cond_errors, cond_warnings = _docs_only_rules(brief)
|
|
245
|
+
errors.extend(cond_errors)
|
|
246
|
+
warnings.extend(cond_warnings)
|
|
247
|
+
|
|
248
|
+
errors.extend(_version_non_empty_rule(brief))
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"valid": not errors,
|
|
252
|
+
"errors": errors,
|
|
253
|
+
"warnings": warnings,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# --------------------------------------------------------------------------
|
|
258
|
+
# CLI
|
|
259
|
+
# --------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _emit(envelope: dict) -> None:
|
|
263
|
+
json.dump(envelope, sys.stdout, indent=2)
|
|
264
|
+
sys.stdout.write("\n")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _envelope_load_error(reason: str, message: str) -> dict:
|
|
268
|
+
return {
|
|
269
|
+
"valid": False,
|
|
270
|
+
"errors": [{"field": "(file)", "message": message}],
|
|
271
|
+
"warnings": [],
|
|
272
|
+
"halt_reason": reason,
|
|
273
|
+
"brief": None,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _envelope_invalid(brief: dict, errors: list[dict], warnings: list[dict]) -> dict:
|
|
278
|
+
return {
|
|
279
|
+
"valid": False,
|
|
280
|
+
"errors": errors,
|
|
281
|
+
"warnings": warnings,
|
|
282
|
+
"halt_reason": "brief-invalid",
|
|
283
|
+
"brief": brief,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _envelope_valid(brief: dict, warnings: list[dict]) -> dict:
|
|
288
|
+
return {
|
|
289
|
+
"valid": True,
|
|
290
|
+
"errors": [],
|
|
291
|
+
"warnings": warnings,
|
|
292
|
+
"halt_reason": None,
|
|
293
|
+
"brief": brief,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def main(argv: list[str] | None = None) -> int:
|
|
298
|
+
parser = argparse.ArgumentParser(
|
|
299
|
+
prog="skf-validate-brief-schema",
|
|
300
|
+
description="Validate a skill-brief.yaml against the SKF brief schema.",
|
|
301
|
+
)
|
|
302
|
+
src = parser.add_mutually_exclusive_group(required=True)
|
|
303
|
+
src.add_argument(
|
|
304
|
+
"path",
|
|
305
|
+
nargs="?",
|
|
306
|
+
help="path to skill-brief.yaml; pass `-` to read from stdin",
|
|
307
|
+
)
|
|
308
|
+
src.add_argument(
|
|
309
|
+
"--yaml",
|
|
310
|
+
dest="inline_yaml",
|
|
311
|
+
help="inline YAML text to validate (alternative to a path)",
|
|
312
|
+
)
|
|
313
|
+
args = parser.parse_args(argv)
|
|
314
|
+
|
|
315
|
+
if args.inline_yaml is not None:
|
|
316
|
+
text = args.inline_yaml
|
|
317
|
+
elif args.path == "-":
|
|
318
|
+
text = sys.stdin.read()
|
|
319
|
+
else:
|
|
320
|
+
brief_path = Path(args.path)
|
|
321
|
+
if not brief_path.is_file():
|
|
322
|
+
_emit(
|
|
323
|
+
_envelope_load_error(
|
|
324
|
+
"brief-missing",
|
|
325
|
+
f"Brief not found at `{brief_path}`. Run [BS] Brief Skill to "
|
|
326
|
+
"create one, or use [QS] Quick Skill for brief-less generation.",
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
return 1
|
|
330
|
+
text = brief_path.read_text(encoding="utf-8")
|
|
331
|
+
|
|
332
|
+
brief, load_err = load_brief_text(text)
|
|
333
|
+
if load_err is not None:
|
|
334
|
+
_emit(_envelope_load_error("brief-malformed", load_err))
|
|
335
|
+
return 1
|
|
336
|
+
assert brief is not None
|
|
337
|
+
|
|
338
|
+
result = validate_brief(brief)
|
|
339
|
+
if not result["valid"]:
|
|
340
|
+
_emit(_envelope_invalid(brief, result["errors"], result["warnings"]))
|
|
341
|
+
return 1
|
|
342
|
+
|
|
343
|
+
_emit(_envelope_valid(brief, result["warnings"]))
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
if __name__ == "__main__":
|
|
348
|
+
raise SystemExit(main())
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Replaces the prose-driven YAML emission, version-precedence resolution,
|
|
8
8
|
conditional optional-field rendering, and non-atomic file write currently
|
|
9
|
-
inlined in `src/skf-brief-skill/
|
|
9
|
+
inlined in `src/skf-brief-skill/references/write-brief.md` §3-§4.
|
|
10
10
|
|
|
11
11
|
Each of those operations is purely deterministic: there is no LLM
|
|
12
12
|
judgement required to render the YAML, decide which optional fields
|
|
@@ -56,7 +56,7 @@ Context payload shape (consumed by `write`):
|
|
|
56
56
|
|
|
57
57
|
Version precedence (resolved into the rendered YAML's `version` field):
|
|
58
58
|
1. version_resolved if explicitly supplied (caller already ran the
|
|
59
|
-
precedence rule). Used by step
|
|
59
|
+
precedence rule). Used by step 5 when it has confirmed values.
|
|
60
60
|
2. Otherwise: target_version if non-null.
|
|
61
61
|
3. Otherwise: detected_version if non-null.
|
|
62
62
|
4. Otherwise: "1.0.0".
|
|
@@ -263,6 +263,36 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
|
|
|
263
263
|
if not isinstance(scope["notes"], str):
|
|
264
264
|
_die("scope.notes must be a string (use empty string when no notes)", field="scope.notes")
|
|
265
265
|
|
|
266
|
+
# scope.rationale — optional authoring-time scope-type decision record.
|
|
267
|
+
# Absent/None → field is simply not present (same null-drop path as
|
|
268
|
+
# doc_urls). When present it must be a complete six-subkey object.
|
|
269
|
+
rationale = scope.get("rationale")
|
|
270
|
+
if rationale is not None:
|
|
271
|
+
if not isinstance(rationale, dict):
|
|
272
|
+
_die("scope.rationale must be an object when present", field="scope.rationale")
|
|
273
|
+
_RATIONALE_STR_KEYS = ("recommended", "chosen", "heuristic", "reason", "recorded")
|
|
274
|
+
for rk in (*_RATIONALE_STR_KEYS, "accepted_recommendation"):
|
|
275
|
+
if rk not in rationale:
|
|
276
|
+
_die(f"scope.rationale.{rk} is required", field=f"scope.rationale.{rk}")
|
|
277
|
+
for rk in _RATIONALE_STR_KEYS:
|
|
278
|
+
if not isinstance(rationale[rk], str) or not rationale[rk]:
|
|
279
|
+
_die(
|
|
280
|
+
f"scope.rationale.{rk} must be a non-empty string",
|
|
281
|
+
field=f"scope.rationale.{rk}",
|
|
282
|
+
)
|
|
283
|
+
if not isinstance(rationale["accepted_recommendation"], bool):
|
|
284
|
+
_die(
|
|
285
|
+
"scope.rationale.accepted_recommendation must be a boolean",
|
|
286
|
+
field="scope.rationale.accepted_recommendation",
|
|
287
|
+
)
|
|
288
|
+
for rk in ("recommended", "chosen"):
|
|
289
|
+
if rationale[rk] not in VALID_SCOPE_TYPES:
|
|
290
|
+
_die(
|
|
291
|
+
f"scope.rationale.{rk} must be one of {sorted(VALID_SCOPE_TYPES)}. "
|
|
292
|
+
f"Got: {rationale[rk]!r}",
|
|
293
|
+
field=f"scope.rationale.{rk}",
|
|
294
|
+
)
|
|
295
|
+
|
|
266
296
|
# target_version semver shape (when present)
|
|
267
297
|
tv = ctx.get("target_version")
|
|
268
298
|
if tv is not None:
|
|
@@ -308,6 +338,21 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
|
|
|
308
338
|
},
|
|
309
339
|
}
|
|
310
340
|
|
|
341
|
+
# Conditional: scope.rationale — canonical position is after `notes` and
|
|
342
|
+
# before `amendments` (amendments is appended post-authoring by other
|
|
343
|
+
# workflows and is never emitted here, so appending after notes yields the
|
|
344
|
+
# canonical order). Absent → key omitted, matching the legacy-brief default.
|
|
345
|
+
scope_rationale = ctx["scope"].get("rationale")
|
|
346
|
+
if scope_rationale is not None:
|
|
347
|
+
brief["scope"]["rationale"] = {
|
|
348
|
+
"recommended": scope_rationale["recommended"],
|
|
349
|
+
"chosen": scope_rationale["chosen"],
|
|
350
|
+
"accepted_recommendation": scope_rationale["accepted_recommendation"],
|
|
351
|
+
"heuristic": scope_rationale["heuristic"],
|
|
352
|
+
"reason": scope_rationale["reason"],
|
|
353
|
+
"recorded": scope_rationale["recorded"],
|
|
354
|
+
}
|
|
355
|
+
|
|
311
356
|
# Conditional: target_version (must equal version)
|
|
312
357
|
tv = ctx.get("target_version")
|
|
313
358
|
if tv is not None:
|
|
@@ -346,7 +391,7 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
|
|
|
346
391
|
def render_yaml(brief: dict[str, Any]) -> str:
|
|
347
392
|
"""Dump the brief dict as YAML in canonical key order with a leading document marker.
|
|
348
393
|
|
|
349
|
-
The step
|
|
394
|
+
The step 5 §3 template shows leading and trailing `---` markers, but those were
|
|
350
395
|
wrapping the example YAML for documentation purposes — actual on-disk YAML uses
|
|
351
396
|
only the leading `---` (or none). A trailing `---` would start a second empty
|
|
352
397
|
document and break callers that use `yaml.safe_load` (which expects a single
|
|
@@ -388,7 +433,13 @@ def atomic_write(target: Path, content: str) -> int:
|
|
|
388
433
|
# Top-level keys that get folded into the nested `scope` sub-object — every
|
|
389
434
|
# other top-level key passes through unchanged so future additions to the
|
|
390
435
|
# schema don't require a translator update.
|
|
391
|
-
_FLAT_SCOPE_KEYS = (
|
|
436
|
+
_FLAT_SCOPE_KEYS = (
|
|
437
|
+
"scope_type",
|
|
438
|
+
"scope_include",
|
|
439
|
+
"scope_exclude",
|
|
440
|
+
"scope_notes",
|
|
441
|
+
"scope_rationale",
|
|
442
|
+
)
|
|
392
443
|
|
|
393
444
|
|
|
394
445
|
def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -437,6 +488,11 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
|
|
|
437
488
|
scope["exclude"] = flat["scope_exclude"]
|
|
438
489
|
if "scope_notes" in flat and flat["scope_notes"] is not None:
|
|
439
490
|
scope["notes"] = flat["scope_notes"]
|
|
491
|
+
# Optional authoring-time rationale. Null/absent → key dropped (same
|
|
492
|
+
# null-drop semantics as doc_urls); when present it carries the full
|
|
493
|
+
# six-subkey object validated by validate_context.
|
|
494
|
+
if "scope_rationale" in flat and flat["scope_rationale"] is not None:
|
|
495
|
+
scope["rationale"] = flat["scope_rationale"]
|
|
440
496
|
nested["scope"] = scope
|
|
441
497
|
return nested
|
|
442
498
|
|
|
@@ -495,7 +551,7 @@ def main() -> int:
|
|
|
495
551
|
"scope_type/scope_include/scope_exclude/scope_notes top-level keys, "
|
|
496
552
|
"optional fields nullable) instead of the nested shape. Eliminates "
|
|
497
553
|
"the conditional-omit logic the LLM currently walks at the §3 "
|
|
498
|
-
"assembly site in step
|
|
554
|
+
"assembly site in step 5."
|
|
499
555
|
),
|
|
500
556
|
)
|
|
501
557
|
|
|
@@ -9,6 +9,14 @@ description: Discover what to skill in a large repo and produce recommended skil
|
|
|
9
9
|
|
|
10
10
|
Analyzes a large repo or multi-service project to identify discrete skillable units, map exports and integration points, and produce recommended skill-brief.yaml files as the primary entry point for brownfield onboarding. The analysis must be thorough enough to produce actionable briefs, but scoped enough to avoid overwhelming the user with false positives. Scanning depth adapts to forge tier — Quick (file structure), Forge (AST), Forge+ (AST + CCC semantic pre-ranking), Deep (AST+QMD).
|
|
11
11
|
|
|
12
|
+
## Conventions
|
|
13
|
+
|
|
14
|
+
- Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
|
|
15
|
+
- `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates.
|
|
16
|
+
- `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
|
|
17
|
+
- `{project-root}`-prefixed paths resolve from the project working directory.
|
|
18
|
+
- `{skill-name}` resolves to the skill directory's basename.
|
|
19
|
+
|
|
12
20
|
## Role
|
|
13
21
|
|
|
14
22
|
You are a source code analyst and decomposition architect collaborating with a developer onboarding an existing project. You bring expertise in codebase analysis, service boundary detection, and skill scoping, while the user brings their domain knowledge. Work together as equals.
|
|
@@ -17,35 +25,56 @@ You are a source code analyst and decomposition architect collaborating with a d
|
|
|
17
25
|
|
|
18
26
|
These rules apply to every step in this workflow:
|
|
19
27
|
|
|
20
|
-
- Read each step file completely before taking any action
|
|
21
|
-
- Follow the mandatory sequence in each step exactly — do not skip, reorder, or optimize
|
|
22
28
|
- Only load one step file at a time — never preload future steps
|
|
23
|
-
-
|
|
24
|
-
- If any instruction references a subprocess or tool you lack, achieve the outcome in your main context thread
|
|
25
|
-
- Always communicate in `{communication_language}`
|
|
29
|
+
- Always communicate in `{communication_language}` (the language for user-facing prose). Written artifact text — the per-unit recommendation `description` and `scope.notes` persisted into `skill-brief.yaml` — is in `{document_output_language}`; per-step rules call this out where it applies. The two values may be the same.
|
|
26
30
|
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
|
|
27
31
|
|
|
28
32
|
## Stages
|
|
29
33
|
|
|
30
34
|
| # | Step | File | Auto-proceed |
|
|
31
35
|
|---|------|------|--------------|
|
|
32
|
-
| 1 | Initialize |
|
|
33
|
-
| 1b | Continue (session resume) |
|
|
34
|
-
| 2 | Scan Project |
|
|
35
|
-
| 3 | Identify Units |
|
|
36
|
-
| 4 | Map & Detect |
|
|
37
|
-
| 5 | Recommend |
|
|
38
|
-
| 6 | Generate Briefs |
|
|
39
|
-
| 7 | Workflow Health Check |
|
|
36
|
+
| 1 | Initialize | references/init.md | Yes |
|
|
37
|
+
| 1b | Continue (session resume) | references/continue.md | Yes |
|
|
38
|
+
| 2 | Scan Project | references/scan-project.md | No (confirm) |
|
|
39
|
+
| 3 | Identify Units | references/identify-units.md | No (confirm) |
|
|
40
|
+
| 4 | Map & Detect | references/map-and-detect.md | Yes |
|
|
41
|
+
| 5 | Recommend | references/recommend.md | No (confirm) |
|
|
42
|
+
| 6 | Generate Briefs | references/generate-briefs.md | Yes |
|
|
43
|
+
| 7 | Workflow Health Check | references/health-check.md | Yes |
|
|
40
44
|
|
|
41
45
|
## Invocation Contract
|
|
42
46
|
|
|
43
47
|
| Aspect | Detail |
|
|
44
48
|
|--------|--------|
|
|
45
49
|
| **Inputs** | project_path [required], scope_hint [optional] |
|
|
46
|
-
| **
|
|
47
|
-
| **
|
|
50
|
+
| **Headless inputs** | `--project-path <path>` (skip Step 1 project-path prompt), `--scope-hint <text>` (skip Step 1 scope-hint prompt), `--intent-hint <text>` (pre-supply analysis intent; drives recommendation ranking in Step 5) |
|
|
51
|
+
| **Headless flag** | `--headless` / `-H` flips every confirm gate to auto-proceed |
|
|
52
|
+
| **Gates** | step 2: Confirm Gate [C] | step 3: Confirm Gate [C] | step 5: Confirm Gate [C] |
|
|
53
|
+
| **Outputs** | analysis-report.md, skill-brief.yaml files (one per recommended unit); final `SKF_ANALYZE_RESULT_JSON` line on stdout when `{headless_mode}` is true |
|
|
48
54
|
| **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
|
|
55
|
+
| **Exit codes** | See "Exit Codes" below |
|
|
56
|
+
|
|
57
|
+
## Exit Codes
|
|
58
|
+
|
|
59
|
+
Every HARD HALT in this workflow exits with a stable code so headless automators can branch on the failure class without grepping message text:
|
|
60
|
+
|
|
61
|
+
| Code | Meaning | Raised by |
|
|
62
|
+
| ---- | -------------------- | ------------------------------------------------------------------------------------------ |
|
|
63
|
+
| 0 | success | step 7 (terminal — health check completion) |
|
|
64
|
+
| 2 | input-missing | step 1 §2-3 — required config absent (config.yaml not loadable, project path empty/invalid in headless mode) |
|
|
65
|
+
| 3 | resolution-failure | step 1 §2 (`forge-tier.yaml` missing at `{sidecar_path}/forge-tier.yaml`); step 1 §3 (project path does not exist or remote URL inaccessible) |
|
|
66
|
+
| 4 | write-failure | step 1 §6 (analysis report write failed); step 6 §5 (skill-brief.yaml write failed); step 6 §9 (result contract write failed) |
|
|
67
|
+
| 6 | user-cancelled | any interactive menu in steps 2/3/5/6 (user selected `[X]` Cancel and exit) |
|
|
68
|
+
|
|
69
|
+
## Result Contract (Headless)
|
|
70
|
+
|
|
71
|
+
When `{headless_mode}` is true, step 6 emits a single-line JSON envelope on **stdout** before chaining to step 7, and every HARD HALT emits the same envelope shape on **stderr** with `status: "error"`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
SKF_ANALYZE_RESULT_JSON: {"status":"success|error","report_path":"…|null","brief_paths":["…"],"unit_counts":{"confirmed":N,"skipped":N,"maybe":N},"exit_code":0,"halt_reason":null}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`status` is `"success"` on the terminal happy path, `"error"` on any HALT. `halt_reason` is one of: `null` (success), `"input-missing"`, `"forge-tier-missing"`, `"path-invalid"`, `"write-failed"`, `"user-cancelled"`. `exit_code` matches the table above. `brief_paths` is an array of absolute paths to every generated `skill-brief.yaml` (empty array if none were generated). `unit_counts` reports confirmed/skipped/maybe counts from step 5's user decisions.
|
|
49
78
|
|
|
50
79
|
## On Activation
|
|
51
80
|
|
|
@@ -54,4 +83,28 @@ These rules apply to every step in this workflow:
|
|
|
54
83
|
|
|
55
84
|
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
|
|
56
85
|
|
|
57
|
-
3.
|
|
86
|
+
3. **Resolve workflow customization.** Run:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python3 {project-root}/_bmad/scripts/resolve_customization.py \
|
|
90
|
+
--skill {skill-root} --key workflow
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append):
|
|
94
|
+
|
|
95
|
+
- `{skill-root}/customize.toml` — bundled defaults
|
|
96
|
+
- `_bmad/custom/<skill-name>.toml` under `{project-root}` — team overrides (committed)
|
|
97
|
+
- `_bmad/custom/<skill-name>.user.toml` under `{project-root}` — personal overrides (gitignored)
|
|
98
|
+
|
|
99
|
+
If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly — the bundled defaults are an empty string for each path scalar.
|
|
100
|
+
|
|
101
|
+
Apply the path-scalar fallback now so stage files don't have to repeat the conditional logic. For each scalar, if the merged value is empty or absent, use the bundled default:
|
|
102
|
+
|
|
103
|
+
- `{unitDetectionHeuristicsPath}` ← `workflow.unit_detection_heuristics_path` if non-empty, else `references/unit-detection-heuristics.md`
|
|
104
|
+
- `{briefSchemaPath}` ← `workflow.brief_schema_path` if non-empty, else `assets/skill-brief-schema.md`
|
|
105
|
+
- `{analysisReportTemplatePath}` ← `workflow.analysis_report_template_path` if non-empty, else `templates/analysis-report-template.md`
|
|
106
|
+
- `{onCompleteCommand}` ← `workflow.on_complete` if non-empty, else empty string (no-op — workflow skips the hook invocation)
|
|
107
|
+
|
|
108
|
+
Stash all four as workflow-context variables. Stage files reference `{unitDetectionHeuristicsPath}` / `{briefSchemaPath}` / `{analysisReportTemplatePath}` / `{onCompleteCommand}` directly — no conditional at the usage site. Empty-string overrides cleanly fall through to the bundled default; non-empty values let orgs swap in house-style copies (or wire in pipeline hooks) without forking the skill.
|
|
109
|
+
|
|
110
|
+
4. Load, read the full file, and then execute `references/init.md` to begin the workflow.
|
|
@@ -49,7 +49,7 @@ If the source is a remote GitHub repo, use `gh api repos/{owner}/{repo}/contents
|
|
|
49
49
|
|
|
50
50
|
If detection succeeds, use the detected version. If it fails or returns a non-semver value, fall back to `"1.0.0"`.
|
|
51
51
|
|
|
52
|
-
The create-skill workflow (
|
|
52
|
+
The create-skill workflow (extract) also performs version reconciliation at extraction time — if the source version has changed since the brief was created, the extraction step warns and uses the source version.
|
|
53
53
|
|
|
54
54
|
**Target version override:** When `target_version` is present in the brief, it takes precedence over auto-detection. Auto-detection still runs for informational purposes (displayed as "Detected version" alongside the user-specified "Target version"), but the `target_version` value is used as the brief's `version` field. This is particularly useful for docs-only skills (where no package manifest exists) and when the user wants to compile a skill for a specific older version.
|
|
55
55
|
|