bmad-module-skill-forge 1.3.0 → 1.4.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.
Files changed (31) 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-recommend-scope-type.py +369 -0
  14. package/src/shared/scripts/skf-validate-brief-inputs.py +293 -0
  15. package/src/shared/scripts/skf-write-skill-brief.py +509 -0
  16. package/src/skf-brief-skill/SKILL.md +41 -12
  17. package/src/skf-brief-skill/assets/description-voice-examples.md +19 -0
  18. package/src/skf-brief-skill/assets/scope-templates.md +5 -0
  19. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -40
  20. package/src/skf-brief-skill/references/draft-checkpoint.md +46 -0
  21. package/src/skf-brief-skill/references/headless-args.md +22 -0
  22. package/src/skf-brief-skill/references/headless-source-authority-detection.md +26 -0
  23. package/src/skf-brief-skill/references/portfolio-similarity-check.md +35 -0
  24. package/src/skf-brief-skill/references/qmd-collection-registration.md +52 -0
  25. package/src/skf-brief-skill/references/version-resolution.md +46 -0
  26. package/src/skf-brief-skill/steps-c/step-01-gather-intent.md +164 -14
  27. package/src/skf-brief-skill/steps-c/step-02-analyze-target.md +118 -50
  28. package/src/skf-brief-skill/steps-c/step-03-scope-definition.md +48 -5
  29. package/src/skf-brief-skill/steps-c/step-04-confirm-brief.md +33 -19
  30. package/src/skf-brief-skill/steps-c/step-05-write-brief.md +93 -97
  31. package/src/skf-brief-skill/steps-c/step-06-health-check.md +11 -2
@@ -0,0 +1,509 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = ["pyyaml"]
4
+ # ///
5
+ """SKF Write Skill Brief — Schema-validated atomic writer for skill-brief.yaml.
6
+
7
+ Replaces the prose-driven YAML emission, version-precedence resolution,
8
+ conditional optional-field rendering, and non-atomic file write currently
9
+ inlined in `src/skf-brief-skill/steps-c/step-05-write-brief.md` §3-§4.
10
+
11
+ Each of those operations is purely deterministic: there is no LLM
12
+ judgement required to render the YAML, decide which optional fields
13
+ appear, resolve target_version vs. detected vs. default, or write the
14
+ file atomically. Keeping that work in prose creates four schema-drift
15
+ seams (key order, YAML formatting, conditional field inclusion rules,
16
+ atomic-write behaviour) that the LLM cannot fully close on every
17
+ invocation. This script is the single source of truth.
18
+
19
+ Subcommand:
20
+
21
+ write Read brief context as JSON on stdin, validate against
22
+ src/shared/scripts/schemas/skill-brief.v1.json, apply
23
+ version-precedence rules, render the canonical YAML, and
24
+ atomically write to --target. Emits a JSON success envelope
25
+ on stdout.
26
+
27
+ Context payload shape (consumed by `write`):
28
+
29
+ {
30
+ "name": "marked",
31
+ "version_resolved": "1.2.3", # OR omit and provide:
32
+ "target_version": "1.2.3" | null,
33
+ "detected_version": "1.2.3" | null,
34
+
35
+ "source_type": "source" | "docs-only",
36
+ "source_repo": "https://github.com/...",
37
+ "language": "javascript",
38
+ "description": "...",
39
+ "forge_tier": "Quick" | "Forge" | "Forge+" | "Deep",
40
+ "created": "2026-05-02", # ISO date
41
+ "created_by": "armel",
42
+
43
+ "scope": {
44
+ "type": "full-library" | ...,
45
+ "include": ["src/**/*.ts"],
46
+ "exclude": ["**/*.test.*"],
47
+ "notes": ""
48
+ },
49
+
50
+ # Conditionally present:
51
+ "doc_urls": [{"url": "...", "label": "..."}],
52
+ "scripts_intent": "detect" | "none" | free-text,
53
+ "assets_intent": "detect" | "none" | free-text,
54
+ "source_authority": "official" | "community" | "internal"
55
+ }
56
+
57
+ Version precedence (resolved into the rendered YAML's `version` field):
58
+ 1. version_resolved if explicitly supplied (caller already ran the
59
+ precedence rule). Used by step-05 when it has confirmed values.
60
+ 2. Otherwise: target_version if non-null.
61
+ 3. Otherwise: detected_version if non-null.
62
+ 4. Otherwise: "1.0.0".
63
+
64
+ When target_version is set, the rendered YAML includes a `target_version`
65
+ field whose value MUST match `version` (the script enforces this
66
+ invariant before write — refuses to emit a brief that violates it).
67
+
68
+ Flat input form (`--from-flat`):
69
+
70
+ Identical semantics, friendlier shape for prose-driven callers — scope
71
+ is split across four top-level keys instead of nested, and every
72
+ optional field can be passed as `null` without the caller deciding
73
+ what to omit. The script translates flat → nested and runs the same
74
+ validator + writer pipeline.
75
+
76
+ {
77
+ "name": "marked",
78
+ "target_version": "1.2.3" | null,
79
+ "detected_version": "1.2.3" | null,
80
+ "source_type": "source",
81
+ "source_repo": "https://github.com/...",
82
+ "language": "javascript",
83
+ "description": "...",
84
+ "forge_tier": "Quick",
85
+ "created": "2026-05-02",
86
+ "created_by": "armel",
87
+ "scope_type": "full-library",
88
+ "scope_include": ["src/**/*.ts"],
89
+ "scope_exclude": ["**/*.test.*"],
90
+ "scope_notes": "",
91
+ "doc_urls": null | [...],
92
+ "scripts_intent": null | "detect" | "none" | "...",
93
+ "assets_intent": null | "detect" | "none" | "...",
94
+ "source_authority": null | "official" | "community" | "internal"
95
+ }
96
+
97
+ Output (success):
98
+
99
+ {
100
+ "status": "ok",
101
+ "brief_path": "/abs/path/skill-brief.yaml",
102
+ "version": "<resolved version>",
103
+ "bytes": <integer>,
104
+ "warnings": ["string", ...]
105
+ }
106
+
107
+ Errors emit `{"status": "error", "message": "...", "field": "..."|null}`
108
+ to stderr and exit non-zero.
109
+
110
+ Exit codes:
111
+ 0 — success
112
+ 1 — validation failure (bad context, schema violation, invariant
113
+ violation, version-precedence underflow with no fallback path)
114
+ 2 — I/O failure (atomic write failed, parent directory not writable)
115
+
116
+ Cross-platform: pure stdlib + PyYAML. Atomic write via temp + fsync +
117
+ rename, mirroring skf-atomic-write.py and the helper in
118
+ skf-forge-tier-rw.py.
119
+ """
120
+
121
+ from __future__ import annotations
122
+
123
+ import argparse
124
+ import json
125
+ import os
126
+ import re
127
+ import sys
128
+ from pathlib import Path
129
+ from typing import Any
130
+
131
+ import yaml
132
+
133
+
134
+ KEBAB_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
135
+ SEMVER_RE = re.compile(
136
+ r"^v?\d+\.\d+\.\d+([.\-+][0-9A-Za-z][0-9A-Za-z.\-+]*)?$"
137
+ )
138
+ ISO_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
139
+
140
+ VALID_SOURCE_TYPES = {"source", "docs-only"}
141
+ VALID_SOURCE_AUTHORITIES = {"official", "community", "internal"}
142
+ VALID_FORGE_TIERS = {"Quick", "Forge", "Forge+", "Deep"}
143
+ VALID_SCOPE_TYPES = {
144
+ "full-library",
145
+ "specific-modules",
146
+ "public-api",
147
+ "component-library",
148
+ "reference-app",
149
+ "docs-only",
150
+ }
151
+
152
+
153
+ def _die(message: str, field: str | None = None, code: int = 1) -> None:
154
+ payload = {"status": "error", "message": message}
155
+ if field is not None:
156
+ payload["field"] = field
157
+ sys.stderr.write(json.dumps(payload) + "\n")
158
+ sys.exit(code)
159
+
160
+
161
+ def resolve_version(ctx: dict[str, Any]) -> str:
162
+ """Apply the version-precedence rule. Returns the resolved version string.
163
+
164
+ Uses `is not None` checks (not truthiness) so an explicitly-supplied
165
+ empty string surfaces as a SEMVER_RE validation failure downstream
166
+ rather than silently falling through to the next precedence level.
167
+ """
168
+ vr = ctx.get("version_resolved")
169
+ if vr is not None:
170
+ return vr
171
+ tv = ctx.get("target_version")
172
+ if tv is not None:
173
+ return tv
174
+ dv = ctx.get("detected_version")
175
+ if dv is not None:
176
+ return dv
177
+ return "1.0.0"
178
+
179
+
180
+ def validate_context(ctx: dict[str, Any]) -> list[str]:
181
+ """Validate the context payload. Raises via _die on hard errors; returns warnings."""
182
+ warnings: list[str] = []
183
+
184
+ # Required string fields
185
+ for field in ("name", "source_repo", "language", "description",
186
+ "forge_tier", "created", "created_by"):
187
+ v = ctx.get(field)
188
+ if not v or not isinstance(v, str):
189
+ _die(f"required field {field!r} missing or not a non-empty string", field=field)
190
+
191
+ # Name must be kebab
192
+ if not KEBAB_RE.match(ctx["name"]):
193
+ _die(
194
+ f"name must be kebab-case (lowercase letters/digits/hyphens, no leading/trailing hyphen). "
195
+ f"Got: {ctx['name']!r}",
196
+ field="name",
197
+ )
198
+
199
+ # forge_tier enum
200
+ if ctx["forge_tier"] not in VALID_FORGE_TIERS:
201
+ _die(
202
+ f"forge_tier must be one of {sorted(VALID_FORGE_TIERS)}. Got: {ctx['forge_tier']!r}",
203
+ field="forge_tier",
204
+ )
205
+
206
+ # created ISO date
207
+ if not ISO_DATE_RE.match(ctx["created"]):
208
+ _die(
209
+ f"created must be an ISO date (YYYY-MM-DD). Got: {ctx['created']!r}",
210
+ field="created",
211
+ )
212
+
213
+ # source_type (default 'source') and conditional doc_urls
214
+ source_type = ctx.get("source_type", "source")
215
+ if source_type not in VALID_SOURCE_TYPES:
216
+ _die(
217
+ f"source_type must be one of {sorted(VALID_SOURCE_TYPES)}. Got: {source_type!r}",
218
+ field="source_type",
219
+ )
220
+
221
+ doc_urls = ctx.get("doc_urls")
222
+ if source_type == "docs-only":
223
+ if not doc_urls or not isinstance(doc_urls, list) or len(doc_urls) == 0:
224
+ _die("source_type=docs-only requires at least one entry in doc_urls", field="doc_urls")
225
+ if doc_urls is not None:
226
+ if not isinstance(doc_urls, list):
227
+ _die("doc_urls must be an array of objects", field="doc_urls")
228
+ for i, entry in enumerate(doc_urls):
229
+ if not isinstance(entry, dict):
230
+ _die(f"doc_urls[{i}] must be an object with at least a 'url' field", field="doc_urls")
231
+ url = entry.get("url")
232
+ if not url or not isinstance(url, str):
233
+ _die(f"doc_urls[{i}].url is required and must be a non-empty string", field="doc_urls")
234
+
235
+ # source_authority (default 'community') with docs-only force rule
236
+ source_authority = ctx.get("source_authority", "community")
237
+ if source_authority not in VALID_SOURCE_AUTHORITIES:
238
+ _die(
239
+ f"source_authority must be one of {sorted(VALID_SOURCE_AUTHORITIES)}. Got: {source_authority!r}",
240
+ field="source_authority",
241
+ )
242
+ if source_type == "docs-only" and source_authority != "community":
243
+ warnings.append(
244
+ f"source_authority forced to 'community' for docs-only (was {source_authority!r})"
245
+ )
246
+
247
+ # scope object
248
+ scope = ctx.get("scope")
249
+ if not isinstance(scope, dict):
250
+ _die("scope must be an object", field="scope")
251
+ for sf in ("type", "include", "exclude", "notes"):
252
+ if sf not in scope:
253
+ _die(f"scope.{sf} is required", field=f"scope.{sf}")
254
+ if scope["type"] not in VALID_SCOPE_TYPES:
255
+ _die(
256
+ f"scope.type must be one of {sorted(VALID_SCOPE_TYPES)}. Got: {scope['type']!r}",
257
+ field="scope.type",
258
+ )
259
+ if not isinstance(scope["include"], list):
260
+ _die("scope.include must be an array of glob strings", field="scope.include")
261
+ if not isinstance(scope["exclude"], list):
262
+ _die("scope.exclude must be an array of glob strings", field="scope.exclude")
263
+ if not isinstance(scope["notes"], str):
264
+ _die("scope.notes must be a string (use empty string when no notes)", field="scope.notes")
265
+
266
+ # target_version semver shape (when present)
267
+ tv = ctx.get("target_version")
268
+ if tv is not None:
269
+ if not isinstance(tv, str) or not SEMVER_RE.match(tv):
270
+ _die(
271
+ f"target_version must be full X.Y.Z semver (with optional v prefix and pre-release/build). "
272
+ f"Got: {tv!r}",
273
+ field="target_version",
274
+ )
275
+
276
+ detected = ctx.get("detected_version")
277
+ if detected is not None and (not isinstance(detected, str) or not SEMVER_RE.match(detected)):
278
+ # Warn rather than HALT — auto-detection upstream may surface odd shapes
279
+ warnings.append(
280
+ f"detected_version {detected!r} is not full X.Y.Z semver — falling through to default 1.0.0"
281
+ )
282
+
283
+ return warnings
284
+
285
+
286
+ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]:
287
+ """Build the final brief dict that will be YAML-dumped, in canonical key order."""
288
+ source_type = ctx.get("source_type", "source")
289
+ source_authority = ctx.get("source_authority", "community")
290
+ if source_type == "docs-only":
291
+ source_authority = "community" # forced
292
+
293
+ brief: dict[str, Any] = {
294
+ "name": ctx["name"],
295
+ "version": resolved_version,
296
+ "source_type": source_type,
297
+ "source_repo": ctx["source_repo"],
298
+ "language": ctx["language"],
299
+ "description": ctx["description"],
300
+ "forge_tier": ctx["forge_tier"],
301
+ "created": ctx["created"],
302
+ "created_by": ctx["created_by"],
303
+ "scope": {
304
+ "type": ctx["scope"]["type"],
305
+ "include": list(ctx["scope"]["include"]),
306
+ "exclude": list(ctx["scope"]["exclude"]),
307
+ "notes": ctx["scope"]["notes"],
308
+ },
309
+ }
310
+
311
+ # Conditional: target_version (must equal version)
312
+ tv = ctx.get("target_version")
313
+ if tv is not None:
314
+ if tv != resolved_version:
315
+ _die(
316
+ f"invariant violation: target_version ({tv!r}) must equal version "
317
+ f"({resolved_version!r}); see references/version-resolution.md",
318
+ field="target_version",
319
+ )
320
+ brief["target_version"] = tv
321
+
322
+ # Conditional: doc_urls (always emitted when present)
323
+ doc_urls = ctx.get("doc_urls")
324
+ if doc_urls:
325
+ brief["doc_urls"] = [
326
+ {"url": e["url"], "label": e.get("label", "")} for e in doc_urls
327
+ ]
328
+
329
+ # Conditional: scripts_intent / assets_intent — emit when explicitly non-detect
330
+ for intent_field in ("scripts_intent", "assets_intent"):
331
+ v = ctx.get(intent_field)
332
+ if v is not None and v != "detect":
333
+ brief[intent_field] = v
334
+
335
+ # Emit source_authority only when non-default — schema lists it as Optional
336
+ # in src/skf-brief-skill/assets/skill-brief-schema.md, and unconditional
337
+ # emission would inject the field into round-tripped briefs that previously
338
+ # omitted it (false-drift signal for diff tooling). Consumers default to
339
+ # "community" when the field is absent.
340
+ if source_authority != "community":
341
+ brief["source_authority"] = source_authority
342
+
343
+ return brief
344
+
345
+
346
+ def render_yaml(brief: dict[str, Any]) -> str:
347
+ """Dump the brief dict as YAML in canonical key order with a leading document marker.
348
+
349
+ The step-05 §3 template shows leading and trailing `---` markers, but those were
350
+ wrapping the example YAML for documentation purposes — actual on-disk YAML uses
351
+ only the leading `---` (or none). A trailing `---` would start a second empty
352
+ document and break callers that use `yaml.safe_load` (which expects a single
353
+ document) — and skf-create-skill / audit-skill / update-skill all use
354
+ `yaml.safe_load`, so consistency with their loaders matters.
355
+ """
356
+ body = yaml.safe_dump(
357
+ brief,
358
+ default_flow_style=False,
359
+ sort_keys=False,
360
+ allow_unicode=True,
361
+ )
362
+ return "---\n" + body
363
+
364
+
365
+ def atomic_write(target: Path, content: str) -> int:
366
+ """Crash-safe write via temp + fsync + rename. Returns bytes written."""
367
+ target.parent.mkdir(parents=True, exist_ok=True)
368
+ tmp = target.with_name(target.name + ".skf-tmp")
369
+ encoded = content.encode("utf-8")
370
+ try:
371
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
372
+ try:
373
+ os.write(fd, encoded)
374
+ os.fsync(fd)
375
+ finally:
376
+ os.close(fd)
377
+ os.replace(tmp, target)
378
+ except OSError as e:
379
+ if tmp.exists():
380
+ try:
381
+ tmp.unlink()
382
+ except OSError:
383
+ pass
384
+ _die(f"atomic write failed for {target}: {e}", code=2)
385
+ return len(encoded)
386
+
387
+
388
+ # Top-level keys that get folded into the nested `scope` sub-object — every
389
+ # other top-level key passes through unchanged so future additions to the
390
+ # schema don't require a translator update.
391
+ _FLAT_SCOPE_KEYS = ("scope_type", "scope_include", "scope_exclude", "scope_notes")
392
+
393
+
394
+ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
395
+ """Translate the flat brief-context shape into the nested shape consumed by validate_context.
396
+
397
+ Flat shape: scope is split across four top-level keys (`scope_type`,
398
+ `scope_include`, `scope_exclude`, `scope_notes`) instead of nested
399
+ under a `scope` object. Optional top-level fields (`doc_urls`,
400
+ `scripts_intent`, `assets_intent`, `source_authority`,
401
+ `target_version`, `detected_version`) may be absent or null —
402
+ they are simply dropped from the nested output, which matches the
403
+ existing validator's `ctx.get(...) is None` semantics.
404
+
405
+ Any unknown top-level keys are passed through unchanged so future
406
+ additions don't require a translator update.
407
+ """
408
+ if not isinstance(flat, dict):
409
+ _die("write --from-flat: payload must be a JSON object")
410
+
411
+ nested: dict[str, Any] = {}
412
+
413
+ # Pass through every top-level key that isn't part of the flat-scope
414
+ # split. Drop None values so optional fields behave as "absent" in
415
+ # the nested form (validate_context uses `is not None` checks).
416
+ for key, value in flat.items():
417
+ if key in _FLAT_SCOPE_KEYS:
418
+ continue
419
+ if value is None:
420
+ continue
421
+ nested[key] = value
422
+
423
+ # Build the nested scope object. All four scope_* keys (type, include,
424
+ # exclude, notes) are required at the schema level — null is
425
+ # intentionally treated as "absent" here (same as omitting the key)
426
+ # so validate_context surfaces `scope.<field> is required` rather
427
+ # than e.g. `scope.notes must be a string`. The `""` valid minimum
428
+ # for scope_notes must therefore be passed as the literal empty
429
+ # string, not null.
430
+ if any(k in flat for k in _FLAT_SCOPE_KEYS):
431
+ scope: dict[str, Any] = {}
432
+ if "scope_type" in flat and flat["scope_type"] is not None:
433
+ scope["type"] = flat["scope_type"]
434
+ if "scope_include" in flat and flat["scope_include"] is not None:
435
+ scope["include"] = flat["scope_include"]
436
+ if "scope_exclude" in flat and flat["scope_exclude"] is not None:
437
+ scope["exclude"] = flat["scope_exclude"]
438
+ if "scope_notes" in flat and flat["scope_notes"] is not None:
439
+ scope["notes"] = flat["scope_notes"]
440
+ nested["scope"] = scope
441
+ return nested
442
+
443
+
444
+ def cmd_write(target: Path, from_flat: bool = False) -> int:
445
+ raw = sys.stdin.read()
446
+ if not raw or not raw.strip():
447
+ _die("write: empty stdin (expected JSON brief context)")
448
+ try:
449
+ ctx = json.loads(raw)
450
+ except json.JSONDecodeError as e:
451
+ _die(f"write: invalid JSON on stdin: {e}")
452
+ if not isinstance(ctx, dict):
453
+ _die("write: context payload must be a JSON object")
454
+
455
+ if from_flat:
456
+ ctx = flat_to_nested(ctx)
457
+
458
+ warnings = validate_context(ctx)
459
+ resolved_version = resolve_version(ctx)
460
+ if not SEMVER_RE.match(resolved_version):
461
+ _die(
462
+ f"resolved version {resolved_version!r} is not full X.Y.Z semver — "
463
+ f"check version_resolved / target_version / detected_version inputs",
464
+ field="version",
465
+ )
466
+
467
+ brief = assemble_brief(ctx, resolved_version)
468
+ content = render_yaml(brief)
469
+ bytes_written = atomic_write(target, content)
470
+
471
+ response = {
472
+ "status": "ok",
473
+ "brief_path": str(target.resolve()),
474
+ "version": resolved_version,
475
+ "bytes": bytes_written,
476
+ "warnings": warnings,
477
+ }
478
+ print(json.dumps(response))
479
+ return 0
480
+
481
+
482
+ def main() -> int:
483
+ parser = argparse.ArgumentParser(
484
+ prog="skf-write-skill-brief",
485
+ description="Schema-validated atomic writer for skill-brief.yaml.",
486
+ )
487
+ sub = parser.add_subparsers(dest="cmd", required=True)
488
+ p_write = sub.add_parser("write", help="Read brief context JSON on stdin, validate, render YAML, atomic write")
489
+ p_write.add_argument("--target", type=Path, required=True, help="Absolute path to skill-brief.yaml")
490
+ p_write.add_argument(
491
+ "--from-flat",
492
+ action="store_true",
493
+ help=(
494
+ "Accept the flat brief-context shape (scope split across "
495
+ "scope_type/scope_include/scope_exclude/scope_notes top-level keys, "
496
+ "optional fields nullable) instead of the nested shape. Eliminates "
497
+ "the conditional-omit logic the LLM currently walks at the §3 "
498
+ "assembly site in step-05."
499
+ ),
500
+ )
501
+
502
+ args = parser.parse_args()
503
+ if args.cmd == "write":
504
+ return cmd_write(args.target, from_flat=args.from_flat)
505
+ return 2
506
+
507
+
508
+ if __name__ == "__main__":
509
+ sys.exit(main())
@@ -1,13 +1,17 @@
1
1
  ---
2
2
  name: skf-brief-skill
3
- description: Design a skill scope through guided discovery. Use when the user requests to "create a skill brief" or "brief a skill."
3
+ description: Design a skill scope through guided discovery. Use when the user requests to "create a skill brief" or "brief a skill".
4
4
  ---
5
5
 
6
6
  # Brief Skill
7
7
 
8
8
  ## Overview
9
9
 
10
- Helps the user define what to skill — target repo, scope, language, inclusion/exclusion patterns — and produces a skill-brief.yaml that drives create-skill. This is the first step in the skill creation pipeline. The brief becomes the input contract for create-skill, which performs the actual compilation.
10
+ Helps the user define what to skill — target repo, scope, language, inclusion/exclusion patterns — and produces a `skill-brief.yaml` that drives create-skill. This is the first step in the skill creation pipeline; the brief is the input contract for create-skill, which performs the actual compilation.
11
+
12
+ A good skill brief sets a tight, cohesive boundary: one capability with 3-8 primary functions, an unambiguous public API surface, and a description short enough to fit in a registry row. Briefs that try to cover several unrelated concerns (e.g. authentication *and* data visualization) compile into skills that no agent can route to confidently — a brief covering too much is a worse failure mode than a brief covering too little, and this workflow steers toward the smaller, sharper version when scope is unclear.
13
+
14
+ Brief-skill is split from create-skill so the scoping conversation runs *once*, on cheap signals (manifests, top-level exports, intent), without paying for AST extraction. Compilation is expensive; scoping decisions are cheap to revise. Keeping them in separate workflows lets a user iterate on the brief, share it for review, and re-run create-skill against the same brief whenever the upstream version moves.
11
15
 
12
16
  ## Role
13
17
 
@@ -20,9 +24,19 @@ These rules apply to every step in this workflow:
20
24
  - Read each step file completely before taking any action
21
25
  - Follow the mandatory sequence in each step exactly — do not skip, reorder, or optimize
22
26
  - Only load one step file at a time — never preload future steps
23
- - Always communicate in `{communication_language}`
27
+ - **Lazy-load references and assets:** `references/*.md` and `assets/*.md` files are loaded inside the section that needs them, not at step entry. If a section is skipped (e.g. `version-resolution.md` when `{extractPublicApiScript}` already returned a version, `scope-templates.md` for the `docs-only` branch that bypasses §2c), do not load that file. Each unnecessary load costs context (~5-10 KB per reference) and biases the LLM toward consulting material the current path does not need.
28
+ - Always communicate in `{communication_language}` (the language for user-facing prose). Written artifact text — the `description`, `notes`, and other free-form fields persisted into `skill-brief.yaml` — is in `{document_output_language}`; per-step rules call this out where it applies (see step-05). The two values may be the same.
24
29
  - If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
25
30
 
31
+ ## On Activation
32
+
33
+ 1. Load config from `{project-root}/_bmad/skf/config.yaml` and resolve:
34
+ - `project_name`, `output_folder`, `user_name`, `communication_language`, `forge_data_folder`, `sidecar_path`
35
+
36
+ 2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
37
+
38
+ 3. Load, read the full file, and execute `./steps-c/step-01-gather-intent.md`.
39
+
26
40
  ## Stages
27
41
 
28
42
  | # | Step | File | Auto-proceed |
@@ -32,22 +46,37 @@ These rules apply to every step in this workflow:
32
46
  | 3 | Scope Definition | steps-c/step-03-scope-definition.md | No (interactive) |
33
47
  | 4 | Confirm Brief | steps-c/step-04-confirm-brief.md | No (confirm) |
34
48
  | 5 | Write Brief | steps-c/step-05-write-brief.md | Yes |
35
- | 6 | Workflow Health Check | steps-c/step-06-health-check.md | Yes |
49
+ | 6 | Workflow Health Check (terminal) | steps-c/step-06-health-check.md | Yes |
36
50
 
37
51
  ## Invocation Contract
38
52
 
39
53
  | Aspect | Detail |
40
54
  |--------|--------|
41
- | **Inputs** | target_repo [required], skill_name [required], scope_hint [optional], language_hint [optional] |
55
+ | **Inputs** | `target_repo` [required], `skill_name` [required], `scope_hint` [optional], `language_hint` [optional], `target_version` [optional], `source_authority` [optional: official/community/internal, default community], `source_type` [optional: source/docs-only, default source], `doc_urls` [optional: list of `url[,label]` for source_type=docs-only or supplemental], `scope_type` [optional: full-library/specific-modules/public-api/component-library/reference-app/docs-only], `include` [optional: comma-separated globs], `exclude` [optional: comma-separated globs], `scripts_intent` [optional: detect/none/free-text, default detect], `assets_intent` [optional: detect/none/free-text, default detect], `intent` [optional: free-text used to derive description], `force` [optional: overwrite existing brief without prompting] |
42
56
  | **Gates** | step-01: Input Gate [use args] | step-03: Confirm Gate [C] | step-04: Confirm Gate [C] |
43
- | **Outputs** | skill-brief.yaml |
44
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
57
+ | **Outputs** | `skill-brief.yaml` at `{forge_data_folder}/{skill-name}/skill-brief.yaml`; final `SKF_BRIEF_RESULT_JSON` line on stdout when `{headless_mode}` is true |
58
+ | **Headless** | All gates auto-resolve with heuristic-driven or default action when `{headless_mode}` is true; pre-supplied inputs consumed at the gates that would otherwise prompt; absent `source_authority` and `scope_type` are resolved by signal-driven detection (see `references/headless-args.md`); existing briefs are preserved unless `--force` was supplied (HALT with `overwrite-cancelled` otherwise) |
59
+ | **Exit codes** | See "Exit Codes" below |
45
60
 
46
- ## On Activation
61
+ ## Exit Codes
47
62
 
48
- 1. Load config from `{project-root}/_bmad/skf/config.yaml` and resolve:
49
- - `project_name`, `output_folder`, `user_name`, `communication_language`, `forge_data_folder`, `sidecar_path`
63
+ Every HARD HALT in this workflow exits with a stable code so headless automators can branch on the failure class without grepping message text:
50
64
 
51
- 2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in preferences.yaml. Default: false.
65
+ | Code | Meaning | Raised by |
66
+ | ---- | -------------------- | ------------------------------------------------------------------------------------------ |
67
+ | 0 | success | step-06 (terminal) |
68
+ | 2 | input-missing / input-invalid | step-01 GATE — required headless arg absent (`target_repo`, `skill_name`, or `doc_urls` when `source_type=docs-only`) → `input-missing`; enum violation, malformed semver, non-kebab `skill_name`, or step-05 brief-context schema validation failure → `input-invalid` |
69
+ | 3 | resolution-failure | step-01 §1 (`forge-tier.yaml` missing); step-02 §1 (target inaccessible / `gh auth` fails) |
70
+ | 4 | write-failure | step-01 §1 pre-flight write probe (data folder unwritable: read-only mount, disk full, permissions denied); step-05 §4 (write to `{forge_data_folder}/{skill-name}/skill-brief.yaml` failed) |
71
+ | 5 | overwrite-cancelled | step-05 §2 (existing brief, `force` not supplied) |
72
+ | 6 | user-cancelled | any interactive menu in step-01/03/04 (user selected `[X]` Cancel and exit) |
73
+
74
+ ## Result Contract (Headless)
75
+
76
+ When `{headless_mode}` is true, step-05 emits a single-line JSON envelope on **stdout** before chaining to step-06, and every HARD HALT emits the same envelope shape on **stderr** with `status: "error"`:
77
+
78
+ ```
79
+ SKF_BRIEF_RESULT_JSON: {"status":"success|error","brief_path":"…|null","skill_name":"…","version":"…|null","language":"…|null","scope_type":"…|null","exit_code":0,"halt_reason":null}
80
+ ```
52
81
 
53
- 3. Load, read the full file, and then execute `./steps-c/step-01-gather-intent.md` to begin the workflow.
82
+ `status` is `"success"` on the terminal happy path, `"error"` on any HALT. `halt_reason` is one of: `null` (success), `"input-missing"`, `"input-invalid"`, `"forge-tier-missing"`, `"target-inaccessible"`, `"gh-auth-failed"`, `"write-failed"`, `"overwrite-cancelled"`, `"user-cancelled"`. `exit_code` matches the table above.
@@ -0,0 +1,19 @@
1
+ # Description Voice Examples
2
+
3
+ Loaded by step-01 §7b only. The five examples below show the *range* of acceptable voices for the `description` field — they vary in lead, structure, and trigger phrasing on purpose. The point is to anchor the LLM to "two facts must come through (what the skill is, when to use it); everything else is voice — do not template-stamp."
4
+
5
+ ## Examples
6
+
7
+ > Render Markdown to HTML using the marked library. Use when the user pastes raw Markdown and wants formatted output, or asks how to convert MD files in a build pipeline.
8
+
9
+ > Stripe API client for Node.js — payment intents, subscriptions, customer portal, webhooks. Triggers on tasks involving Stripe-managed payments, subscription billing, or webhook event handling.
10
+
11
+ > Charts and visualizations powered by D3.js. Reach for this when the user asks to plot data, build interactive graphs, or wants bare D3 control instead of a React-charts abstraction.
12
+
13
+ > Lint Python code with Ruff. Use when the user wants to add or configure Ruff in a Python project, debug rule selectors, or understand why a specific check fired.
14
+
15
+ > Date and time arithmetic via Luxon — parsing, formatting, time zones, durations, intervals. Use when working with dates in ways that exceed `Date.toISOString()` but you don't want a full Moment.js footprint.
16
+
17
+ ## Notes on Voice
18
+
19
+ Each example leads differently (verb / noun / "Charts and..." / verb / noun-phrase) and matches the trigger phrasing ("Use when...", "Triggers on...", "Reach for this when...") to the voice rather than copy-pasting a single template. Compose in that spirit using the gathered material — the target repo, the user's intent, the version if set, and any scope hints — but **do not template-stamp**.
@@ -7,26 +7,31 @@ Present these options to the user for selection:
7
7
  **[F] Full Library** — Include everything. Best for smaller, focused libraries.
8
8
  - All public exports, all modules
9
9
  - Exclude only tests, build artifacts, and internal utilities
10
+ - *Looks like:* `marked` (single-purpose Markdown→HTML); `nanoid` (id generator); `zod` (validation library)
10
11
 
11
12
  **[M] Specific Modules** — Select which modules to include. Best for large libraries where only some parts are relevant.
12
13
  - You choose which modules/directories
13
14
  - Fine-grained control over what's in and out
15
+ - *Looks like:* `lodash` skill scoped to just `lodash/array` and `lodash/string`; `aws-sdk` skill scoped to just S3 and DynamoDB
14
16
 
15
17
  **[P] Public API Only** — Include only the public-facing API surface. Best for libraries with a clear public/private boundary.
16
18
  - Entry points and exported interfaces only
17
19
  - Internal implementation excluded
20
+ - *Looks like:* `stripe` (payment intents, subscriptions, webhooks — not internal HTTP plumbing); `redis` client (connection + commands, not protocol parsers)
18
21
 
19
22
  **[C] Component Library** — Optimized for UI component libraries with registries, props-based APIs, and design system variants.
20
23
  - Component registry as primary API surface (not individual exports)
21
24
  - Props interfaces as API contracts (not function signatures)
22
25
  - Auto-exclude demo/example/story files (with user confirmation)
23
26
  - Variant consolidation across design systems
27
+ - *Looks like:* `shadcn-ui` (Button, Dialog, Form... 50+ components); Material-UI; Carbon Design System
24
28
 
25
29
  **[R] Reference App** — Whole-app pattern-reference skill. Use when the source is a working example app and the skill's value is **wiring patterns** (lifecycle, IPC, build-config, distribution) rather than a public library API.
26
30
  - Pattern surface as primary API slot (not individual exports)
27
31
  - Adoption Steps as primary workflow format (not API-call chains)
28
32
  - Tier 2 organized as `references/pattern-*.md` groupings (not per-function)
29
33
  - Export-count stats are pattern-surface proxies, not library exports
34
+ - *Looks like:* a Tauri starter app (window setup + IPC bridge + build config); a Next.js auth example (route handlers + middleware + session storage wiring)
30
35
 
31
36
  ## Boundary Definitions by Scope Type
32
37