bmad-module-skill-forge 1.3.0 → 1.4.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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/workflows.md +34 -15
  4. package/package.json +2 -2
  5. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +58 -0
  6. package/src/shared/scripts/schemas/skill-brief.v1.json +77 -0
  7. package/src/shared/scripts/schemas/workspace-detection.v1.json +44 -0
  8. package/src/shared/scripts/skf-detect-language.py +277 -0
  9. package/src/shared/scripts/skf-detect-workspaces.py +427 -0
  10. package/src/shared/scripts/skf-emit-brief-result-envelope.py +257 -0
  11. package/src/shared/scripts/skf-extract-public-api.py +29 -0
  12. package/src/shared/scripts/skf-forge-tier-rw.py +73 -0
  13. package/src/shared/scripts/skf-merge-ccc-exclusions.py +14 -0
  14. package/src/shared/scripts/skf-recommend-scope-type.py +369 -0
  15. package/src/shared/scripts/skf-validate-brief-inputs.py +293 -0
  16. package/src/shared/scripts/skf-write-skill-brief.py +509 -0
  17. package/src/skf-audit-skill/steps-c/step-01-init.md +49 -3
  18. package/src/skf-audit-skill/steps-c/step-03-structural-diff.md +6 -5
  19. package/src/skf-brief-skill/SKILL.md +41 -12
  20. package/src/skf-brief-skill/assets/description-voice-examples.md +19 -0
  21. package/src/skf-brief-skill/assets/scope-templates.md +5 -0
  22. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -40
  23. package/src/skf-brief-skill/references/draft-checkpoint.md +46 -0
  24. package/src/skf-brief-skill/references/headless-args.md +22 -0
  25. package/src/skf-brief-skill/references/headless-source-authority-detection.md +26 -0
  26. package/src/skf-brief-skill/references/portfolio-similarity-check.md +35 -0
  27. package/src/skf-brief-skill/references/qmd-collection-registration.md +52 -0
  28. package/src/skf-brief-skill/references/version-resolution.md +46 -0
  29. package/src/skf-brief-skill/steps-c/step-01-gather-intent.md +164 -14
  30. package/src/skf-brief-skill/steps-c/step-02-analyze-target.md +118 -50
  31. package/src/skf-brief-skill/steps-c/step-03-scope-definition.md +48 -5
  32. package/src/skf-brief-skill/steps-c/step-04-confirm-brief.md +33 -19
  33. package/src/skf-brief-skill/steps-c/step-05-write-brief.md +93 -97
  34. package/src/skf-brief-skill/steps-c/step-06-health-check.md +11 -2
  35. package/src/skf-create-skill/steps-c/step-07-generate-artifacts.md +9 -1
  36. package/src/skf-export-skill/steps-c/step-01-load-skill.md +11 -3
  37. package/src/skf-export-skill/steps-c/step-03-generate-snippet.md +8 -2
  38. package/src/skf-export-skill/steps-c/step-04-update-context.md +29 -0
  39. package/src/skf-export-skill/steps-c/step-06-summary.md +12 -0
  40. package/src/skf-setup/steps-c/step-01b-ccc-index.md +10 -3
@@ -0,0 +1,293 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Validate Brief Inputs — pre-pass validation for brief-skill headless invocation.
6
+
7
+ Validates and normalizes the headless argument set passed to skf-brief-skill
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-02
10
+ or step-05.
11
+
12
+ CLI:
13
+ uv run skf-validate-brief-inputs.py --json '{...}'
14
+ echo '{...}' | uv run skf-validate-brief-inputs.py
15
+
16
+ Input (JSON object on stdin or via --json):
17
+ Required:
18
+ target_repo — string (URL or path); error if absent
19
+ skill_name — string (kebab-case); error if absent or malformed
20
+
21
+ Optional with enum constraints:
22
+ source_type — "source" | "docs-only" (default "source")
23
+ source_authority — "official" | "community" | "internal" (default "community")
24
+ scope_type — "full-library" | "specific-modules" | "public-api"
25
+ | "component-library" | "reference-app" | "docs-only"
26
+ scripts_intent — "detect" | "none" | free-text (default "detect")
27
+ assets_intent — "detect" | "none" | free-text (default "detect")
28
+
29
+ Optional with format constraints:
30
+ target_version — loose semver string (matches 1.2.3, 1.2.3-rc.1, 1.2.3+build.5)
31
+
32
+ Conditional:
33
+ doc_urls — required when source_type == "docs-only"
34
+
35
+ Free-text / pass-through:
36
+ scope_hint, language_hint, intent, include, exclude, force
37
+
38
+ Unrecognized keys are passed through unchanged but flagged as warnings.
39
+
40
+ Output (JSON on stdout):
41
+ {
42
+ "valid": bool,
43
+ "errors": [{"field": "...", "message": "..."}, ...],
44
+ "warnings": [{"field": "...", "message": "..."}, ...],
45
+ "normalized": { ...input dict with defaults applied... },
46
+ "halt_reason": "input-missing" | "input-invalid" | null
47
+ }
48
+
49
+ Exit codes:
50
+ 0 — valid (errors empty)
51
+ 1 — invalid (errors present); halt_reason is set
52
+ 2 — internal error (bad JSON input, IO failure)
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import argparse
58
+ import json
59
+ import re
60
+ import sys
61
+ from typing import Any
62
+
63
+ KNOWN_FIELDS = {
64
+ "target_repo",
65
+ "skill_name",
66
+ "source_type",
67
+ "source_authority",
68
+ "scope_type",
69
+ "target_version",
70
+ "doc_urls",
71
+ "scope_hint",
72
+ "language_hint",
73
+ "intent",
74
+ "scripts_intent",
75
+ "assets_intent",
76
+ "include",
77
+ "exclude",
78
+ "force",
79
+ }
80
+ # `preset` is intentionally NOT in KNOWN_FIELDS — it is consumed at the step-01 §8 GATE
81
+ # (the LLM merges the named preset YAML into the args dict and drops the `preset` key
82
+ # before calling the validator). If the key leaks through, the validator's existing
83
+ # unknown-field handling emits `"unrecognized field 'preset' — passed through unchanged"`
84
+ # in `warnings[]` so the missed drop is debuggable rather than silent.
85
+
86
+ VALID_SOURCE_TYPES = {"source", "docs-only"}
87
+ VALID_SOURCE_AUTHORITIES = {"official", "community", "internal"}
88
+ VALID_SCOPE_TYPES = {
89
+ "full-library",
90
+ "specific-modules",
91
+ "public-api",
92
+ "component-library",
93
+ "reference-app",
94
+ "docs-only",
95
+ }
96
+
97
+ KEBAB_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
98
+ # Require full X.Y.Z form (with optional v prefix and pre-release/build suffix).
99
+ # Loose forms like `1`, `1.2`, `v2` are rejected — the user should write the
100
+ # explicit triple. CalVer (e.g. 2024.04.01) is accepted because it satisfies
101
+ # the X.Y.Z shape.
102
+ SEMVER_RE = re.compile(
103
+ r"^v?\d+\.\d+\.\d+([.\-+][0-9A-Za-z][0-9A-Za-z.\-+]*)?$"
104
+ )
105
+ URL_RE = re.compile(r"^https?://", re.IGNORECASE)
106
+
107
+
108
+ def _err(field: str, message: str) -> dict[str, str]:
109
+ return {"field": field, "message": message}
110
+
111
+
112
+ def validate(inp: dict[str, Any]) -> dict[str, Any]:
113
+ errors: list[dict[str, str]] = []
114
+ warnings: list[dict[str, str]] = []
115
+
116
+ # Required: target_repo, skill_name
117
+ target_repo = inp.get("target_repo")
118
+ skill_name = inp.get("skill_name")
119
+ if not target_repo:
120
+ errors.append(_err("target_repo", "missing required argument target_repo"))
121
+ if not skill_name:
122
+ errors.append(_err("skill_name", "missing required argument skill_name"))
123
+
124
+ # skill_name format
125
+ if skill_name and isinstance(skill_name, str) and not KEBAB_RE.match(skill_name):
126
+ errors.append(
127
+ _err(
128
+ "skill_name",
129
+ f"skill_name must be kebab-case (lowercase letters/digits/hyphens, "
130
+ f"no leading or trailing hyphen). Got: {skill_name!r}",
131
+ )
132
+ )
133
+
134
+ # source_type enum
135
+ source_type_raw = inp.get("source_type", "source")
136
+ if source_type_raw not in VALID_SOURCE_TYPES:
137
+ errors.append(
138
+ _err(
139
+ "source_type",
140
+ f"source_type must be one of {sorted(VALID_SOURCE_TYPES)}. Got: {source_type_raw!r}",
141
+ )
142
+ )
143
+ # Treat as default for downstream conditional logic
144
+ source_type = "source"
145
+ else:
146
+ source_type = source_type_raw
147
+
148
+ # source_authority enum
149
+ source_authority_raw = inp.get("source_authority", "community")
150
+ if source_authority_raw not in VALID_SOURCE_AUTHORITIES:
151
+ errors.append(
152
+ _err(
153
+ "source_authority",
154
+ f"source_authority must be one of {sorted(VALID_SOURCE_AUTHORITIES)}. "
155
+ f"Got: {source_authority_raw!r}",
156
+ )
157
+ )
158
+
159
+ # scope_type enum (when present)
160
+ scope_type = inp.get("scope_type")
161
+ if scope_type is not None and scope_type not in VALID_SCOPE_TYPES:
162
+ errors.append(
163
+ _err(
164
+ "scope_type",
165
+ f"scope_type must be one of {sorted(VALID_SCOPE_TYPES)}. Got: {scope_type!r}",
166
+ )
167
+ )
168
+
169
+ # target_version semver-ish
170
+ target_version = inp.get("target_version")
171
+ if target_version is not None:
172
+ if not isinstance(target_version, str):
173
+ errors.append(
174
+ _err(
175
+ "target_version",
176
+ f"target_version must be a string. Got type {type(target_version).__name__}",
177
+ )
178
+ )
179
+ elif not SEMVER_RE.match(target_version):
180
+ errors.append(
181
+ _err(
182
+ "target_version",
183
+ f"target_version does not look like semver. Got: {target_version!r}. "
184
+ f"Expected forms: 1.2.3, v1.2.3, 1.2.3-rc.1, 1.2.3+build.5. "
185
+ f"Partial forms like '1' or '1.2' are not accepted — write the explicit triple.",
186
+ )
187
+ )
188
+
189
+ # docs-only requires doc_urls
190
+ doc_urls = inp.get("doc_urls")
191
+ if source_type == "docs-only" and not doc_urls:
192
+ errors.append(
193
+ _err("doc_urls", "doc_urls is required when source_type is docs-only")
194
+ )
195
+
196
+ # target_repo shape (warning only — script doesn't HEAD-check)
197
+ if isinstance(target_repo, str) and target_repo:
198
+ looks_like_url = URL_RE.match(target_repo) is not None
199
+ looks_like_path = (
200
+ target_repo.startswith("/")
201
+ or target_repo.startswith("./")
202
+ or target_repo.startswith("~")
203
+ )
204
+ if not (looks_like_url or looks_like_path):
205
+ warnings.append(
206
+ _err(
207
+ "target_repo",
208
+ f"target_repo does not look like a URL or absolute/relative path: {target_repo!r}",
209
+ )
210
+ )
211
+
212
+ # Unknown fields → warn
213
+ for key in inp:
214
+ if key not in KNOWN_FIELDS:
215
+ warnings.append(_err(key, f"unrecognized field {key!r} — passed through unchanged"))
216
+
217
+ # Build normalized payload
218
+ normalized: dict[str, Any] = dict(inp)
219
+ normalized.setdefault("source_type", "source")
220
+ # `source_authority` is intentionally NOT setdefault'd here for source-type targets:
221
+ # step-01 §3.3's headless detection branch runs `gh api user` and may resolve to
222
+ # `official` for repos owned by the authenticated user. Stamping a default at
223
+ # validator time would pre-empt the detection because step-01 §8 GATE treats the
224
+ # `normalized` object as the source of truth. Absence is the signal "run detection."
225
+ # The docs-only branch below still forces `community` since detection cannot apply.
226
+ normalized.setdefault("scripts_intent", "detect")
227
+ normalized.setdefault("assets_intent", "detect")
228
+
229
+ # Force community when docs-only
230
+ if normalized.get("source_type") == "docs-only":
231
+ if normalized.get("source_authority") not in (None, "community"):
232
+ warnings.append(
233
+ _err(
234
+ "source_authority",
235
+ f"source_authority forced to 'community' because source_type=docs-only "
236
+ f"(was {normalized['source_authority']!r})",
237
+ )
238
+ )
239
+ normalized["source_authority"] = "community"
240
+
241
+ # halt_reason classification
242
+ if errors:
243
+ missing_required = any(
244
+ e["message"].startswith("missing required") or "is required" in e["message"]
245
+ for e in errors
246
+ )
247
+ halt_reason = "input-missing" if missing_required else "input-invalid"
248
+ else:
249
+ halt_reason = None
250
+
251
+ return {
252
+ "valid": not errors,
253
+ "errors": errors,
254
+ "warnings": warnings,
255
+ "normalized": normalized,
256
+ "halt_reason": halt_reason,
257
+ }
258
+
259
+
260
+ def main() -> int:
261
+ p = argparse.ArgumentParser(prog="skf-validate-brief-inputs")
262
+ p.add_argument(
263
+ "--json",
264
+ help="JSON object as a string. If omitted, the script reads JSON from stdin.",
265
+ )
266
+ args = p.parse_args()
267
+
268
+ raw = args.json
269
+ if raw is None:
270
+ raw = sys.stdin.read()
271
+
272
+ if not raw or not raw.strip():
273
+ sys.stderr.write("skf-validate-brief-inputs: empty input\n")
274
+ return 2
275
+
276
+ try:
277
+ inp = json.loads(raw)
278
+ except json.JSONDecodeError as e:
279
+ sys.stderr.write(f"skf-validate-brief-inputs: invalid JSON input: {e}\n")
280
+ return 2
281
+
282
+ if not isinstance(inp, dict):
283
+ sys.stderr.write("skf-validate-brief-inputs: input must be a JSON object\n")
284
+ return 2
285
+
286
+ result = validate(inp)
287
+ json.dump(result, sys.stdout, indent=2, sort_keys=True)
288
+ sys.stdout.write("\n")
289
+ return 0 if result["valid"] else 1
290
+
291
+
292
+ if __name__ == "__main__":
293
+ sys.exit(main())