bmad-module-skill-forge 1.6.0 → 1.8.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 (87) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/docs/workflows.md +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/version-paths.md +4 -3
  6. package/src/shared/health-check.md +1 -1
  7. package/src/shared/scripts/schemas/skill-brief.v1.json +10 -0
  8. package/src/shared/scripts/skf-atomic-write.py +4 -1
  9. package/src/shared/scripts/skf-detect-language.py +34 -0
  10. package/src/shared/scripts/skf-detect-workspaces.py +54 -0
  11. package/src/shared/scripts/skf-enumerate-stack-skills.py +107 -16
  12. package/src/shared/scripts/skf-forge-tier-rw.py +77 -1
  13. package/src/shared/scripts/skf-manifest-ops.py +8 -4
  14. package/src/shared/scripts/skf-merge-ccc-exclusions.py +4 -1
  15. package/src/shared/scripts/skf-qmd-classify-collections.py +29 -3
  16. package/src/shared/scripts/skf-rebuild-managed-sections.py +72 -7
  17. package/src/shared/scripts/skf-scan-skill-md-structure.py +13 -3
  18. package/src/shared/scripts/skf-validate-brief-inputs.py +79 -18
  19. package/src/shared/scripts/skf-validate-brief-schema.py +47 -1
  20. package/src/shared/scripts/skf-validate-frontmatter.py +111 -2
  21. package/src/shared/scripts/skf-write-skill-brief.py +97 -17
  22. package/src/skf-analyze-source/assets/skill-brief-schema.md +11 -2
  23. package/src/skf-analyze-source/references/generate-briefs.md +30 -9
  24. package/src/skf-analyze-source/references/identify-units.md +41 -4
  25. package/src/skf-analyze-source/references/init.md +6 -0
  26. package/src/skf-analyze-source/references/recommend.md +1 -1
  27. package/src/skf-analyze-source/references/scan-project.md +5 -2
  28. package/src/skf-analyze-source/references/unit-detection-heuristics.md +16 -0
  29. package/src/skf-brief-skill/SKILL.md +3 -3
  30. package/src/skf-brief-skill/assets/skill-brief-schema.md +6 -1
  31. package/src/skf-brief-skill/references/analyze-target.md +22 -6
  32. package/src/skf-brief-skill/references/confirm-brief.md +4 -0
  33. package/src/skf-brief-skill/references/gather-intent.md +14 -4
  34. package/src/skf-brief-skill/references/headless-args.md +3 -2
  35. package/src/skf-brief-skill/references/portfolio-similarity-check.md +16 -8
  36. package/src/skf-brief-skill/references/scope-definition.md +20 -2
  37. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  38. package/src/skf-brief-skill/references/write-brief.md +8 -2
  39. package/src/skf-create-skill/assets/compile-assembly-rules.md +19 -0
  40. package/src/skf-create-skill/assets/skill-sections.md +1 -0
  41. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +18 -5
  42. package/src/skf-create-skill/references/compile.md +5 -2
  43. package/src/skf-create-skill/references/extract.md +8 -3
  44. package/src/skf-create-skill/references/extraction-patterns.md +14 -5
  45. package/src/skf-create-skill/references/generate-artifacts.md +10 -11
  46. package/src/skf-create-skill/references/source-resolution-protocols.md +5 -2
  47. package/src/skf-create-skill/references/validate.md +4 -3
  48. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +3 -3
  49. package/src/skf-create-stack-skill/assets/stack-skill-template.md +56 -0
  50. package/src/skf-create-stack-skill/references/compile-stack.md +13 -2
  51. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -0
  52. package/src/skf-create-stack-skill/references/detect-integrations.md +5 -2
  53. package/src/skf-create-stack-skill/references/generate-output.md +49 -9
  54. package/src/skf-create-stack-skill/references/validate.md +8 -5
  55. package/src/skf-export-skill/references/load-skill.md +5 -4
  56. package/src/skf-export-skill/references/manifest-rebuild.md +1 -1
  57. package/src/skf-export-skill/references/token-report.md +2 -2
  58. package/src/skf-export-skill/references/update-context.md +25 -11
  59. package/src/skf-quick-skill/assets/skill-template.md +2 -2
  60. package/src/skf-quick-skill/references/compile.md +4 -2
  61. package/src/skf-quick-skill/references/write-and-validate.md +2 -2
  62. package/src/skf-refine-architecture/SKILL.md +1 -1
  63. package/src/skf-refine-architecture/references/gap-analysis.md +30 -4
  64. package/src/skf-refine-architecture/references/init.md +2 -0
  65. package/src/skf-refine-architecture/references/issue-detection.md +8 -3
  66. package/src/skf-test-skill/references/coherence-check.md +4 -4
  67. package/src/skf-test-skill/references/coverage-check.md +58 -16
  68. package/src/skf-test-skill/references/external-validators.md +7 -7
  69. package/src/skf-test-skill/references/init.md +5 -4
  70. package/src/skf-test-skill/references/score.md +6 -3
  71. package/src/skf-test-skill/references/scoring-rules.md +12 -0
  72. package/src/skf-test-skill/references/source-access-protocol.md +33 -3
  73. package/src/skf-test-skill/scripts/compute-score.py +4 -1
  74. package/src/skf-update-skill/references/detect-changes.md +15 -2
  75. package/src/skf-update-skill/references/merge.md +22 -0
  76. package/src/skf-update-skill/references/re-extract.md +19 -7
  77. package/src/skf-update-skill/references/write.md +3 -0
  78. package/src/skf-verify-stack/SKILL.md +2 -2
  79. package/src/skf-verify-stack/assets/feasibility-report-template.md +1 -1
  80. package/src/skf-verify-stack/references/coverage.md +35 -6
  81. package/src/skf-verify-stack/references/init.md +6 -2
  82. package/src/skf-verify-stack/references/integration-verification-rules.md +1 -1
  83. package/src/skf-verify-stack/references/integrations.md +7 -3
  84. package/src/skf-verify-stack/references/report.md +5 -1
  85. package/src/skf-verify-stack/references/requirements.md +3 -1
  86. package/src/skf-verify-stack/references/synthesize.md +11 -3
  87. package/tools/cli/lib/installer.js +1 -0
@@ -44,14 +44,19 @@ Context payload shape (consumed by `write`):
44
44
  "type": "full-library" | ...,
45
45
  "include": ["src/**/*.ts"],
46
46
  "exclude": ["**/*.test.*"],
47
- "notes": ""
47
+ "notes": "",
48
+ # Conditionally present (preserved verbatim on a ratify/re-write):
49
+ "tier_a_include": ["code/core/src/**"], # stratified-scope monorepos
50
+ "amendments": [{...}] # post-authoring audit log
48
51
  },
49
52
 
50
53
  # Conditionally present:
51
54
  "doc_urls": [{"url": "...", "label": "..."}],
52
55
  "scripts_intent": "detect" | "none" | free-text,
53
56
  "assets_intent": "detect" | "none" | free-text,
54
- "source_authority": "official" | "community" | "internal"
57
+ "source_authority": "official" | "community" | "internal",
58
+ "target_ref": "livekit/v0.7.42", # monorepo tag escape hatch
59
+ "source_ref": "v0.5.0" # auto-resolved git ref
55
60
  }
56
61
 
57
62
  Version precedence (resolved into the rendered YAML's `version` field):
@@ -84,14 +89,18 @@ Flat input form (`--from-flat`):
84
89
  "forge_tier": "Quick",
85
90
  "created": "2026-05-02",
86
91
  "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"
92
+ "scope_type": "full-library",
93
+ "scope_include": ["src/**/*.ts"],
94
+ "scope_exclude": ["**/*.test.*"],
95
+ "scope_notes": "",
96
+ "scope_tier_a_include": null | ["code/core/src/**"],
97
+ "scope_amendments": null | [{...}],
98
+ "doc_urls": null | [...],
99
+ "scripts_intent": null | "detect" | "none" | "...",
100
+ "assets_intent": null | "detect" | "none" | "...",
101
+ "source_authority": null | "official" | "community" | "internal",
102
+ "target_ref": null | "livekit/v0.7.42",
103
+ "source_ref": null | "v0.5.0"
95
104
  }
96
105
 
97
106
  Output (success):
@@ -293,6 +302,42 @@ def validate_context(ctx: dict[str, Any]) -> list[str]:
293
302
  field=f"scope.rationale.{rk}",
294
303
  )
295
304
 
305
+ # scope.tier_a_include — optional narrower tier-A include list for
306
+ # stratified-scope monorepos (read by skf-test-skill for the coverage
307
+ # denominator). Absent/None → key omitted. When present it must be a list
308
+ # of glob strings; the writer preserves it verbatim so a ratify/re-write
309
+ # does not silently drop it.
310
+ tier_a_include = scope.get("tier_a_include")
311
+ if tier_a_include is not None:
312
+ if not isinstance(tier_a_include, list):
313
+ _die("scope.tier_a_include must be an array of glob strings", field="scope.tier_a_include")
314
+ for i, pat in enumerate(tier_a_include):
315
+ if not isinstance(pat, str) or not pat:
316
+ _die(
317
+ f"scope.tier_a_include[{i}] must be a non-empty glob string",
318
+ field="scope.tier_a_include",
319
+ )
320
+
321
+ # scope.amendments — optional additive audit log written post-authoring by
322
+ # skf-create-skill / skf-update-skill. The writer is not the amendments
323
+ # schema authority (those workflows own the entry shape); it only preserves
324
+ # the log verbatim on re-write. Light check: a list of objects.
325
+ amendments = scope.get("amendments")
326
+ if amendments is not None:
327
+ if not isinstance(amendments, list):
328
+ _die("scope.amendments must be an array of amendment objects", field="scope.amendments")
329
+ for i, entry in enumerate(amendments):
330
+ if not isinstance(entry, dict):
331
+ _die(f"scope.amendments[{i}] must be an object", field="scope.amendments")
332
+
333
+ # target_ref / source_ref — optional git refs (top-level). target_ref is a
334
+ # remote-monorepo tag escape hatch; source_ref is the auto-resolved ref.
335
+ # Both must round-trip on a ratify/re-write rather than being dropped.
336
+ for ref_field in ("target_ref", "source_ref"):
337
+ ref_val = ctx.get(ref_field)
338
+ if ref_val is not None and (not isinstance(ref_val, str) or not ref_val):
339
+ _die(f"{ref_field} must be a non-empty string when present", field=ref_field)
340
+
296
341
  # target_version semver shape (when present)
297
342
  tv = ctx.get("target_version")
298
343
  if tv is not None:
@@ -320,6 +365,19 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
320
365
  if source_type == "docs-only":
321
366
  source_authority = "community" # forced
322
367
 
368
+ # Build the scope sub-object. tier_a_include (when present) sits between
369
+ # exclude and notes, matching the schema doc's illustrative order. Absent →
370
+ # the key is omitted, so briefs without it render byte-identically to before.
371
+ scope_obj: dict[str, Any] = {
372
+ "type": ctx["scope"]["type"],
373
+ "include": list(ctx["scope"]["include"]),
374
+ "exclude": list(ctx["scope"]["exclude"]),
375
+ }
376
+ scope_tier_a = ctx["scope"].get("tier_a_include")
377
+ if scope_tier_a is not None:
378
+ scope_obj["tier_a_include"] = list(scope_tier_a)
379
+ scope_obj["notes"] = ctx["scope"]["notes"]
380
+
323
381
  brief: dict[str, Any] = {
324
382
  "name": ctx["name"],
325
383
  "version": resolved_version,
@@ -330,12 +388,7 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
330
388
  "forge_tier": ctx["forge_tier"],
331
389
  "created": ctx["created"],
332
390
  "created_by": ctx["created_by"],
333
- "scope": {
334
- "type": ctx["scope"]["type"],
335
- "include": list(ctx["scope"]["include"]),
336
- "exclude": list(ctx["scope"]["exclude"]),
337
- "notes": ctx["scope"]["notes"],
338
- },
391
+ "scope": scope_obj,
339
392
  }
340
393
 
341
394
  # Conditional: scope.rationale — canonical position is after `notes` and
@@ -353,6 +406,13 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
353
406
  "recorded": scope_rationale["recorded"],
354
407
  }
355
408
 
409
+ # Conditional: scope.amendments — additive audit log, emitted verbatim after
410
+ # rationale (matches the schema doc order). The writer preserves it as-is;
411
+ # the writing authority for entry shape is skf-create-skill / skf-update-skill.
412
+ scope_amendments = ctx["scope"].get("amendments")
413
+ if scope_amendments is not None:
414
+ brief["scope"]["amendments"] = scope_amendments
415
+
356
416
  # Conditional: target_version (must equal version)
357
417
  tv = ctx.get("target_version")
358
418
  if tv is not None:
@@ -364,6 +424,14 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
364
424
  )
365
425
  brief["target_version"] = tv
366
426
 
427
+ # Conditional: target_ref / source_ref git refs — preserved verbatim when
428
+ # present (monorepo tag escape hatch + auto-resolved ref). Emitted after
429
+ # target_version so a ratify/re-write does not strip them.
430
+ for ref_field in ("target_ref", "source_ref"):
431
+ ref_val = ctx.get(ref_field)
432
+ if ref_val is not None:
433
+ brief[ref_field] = ref_val
434
+
367
435
  # Conditional: doc_urls (always emitted when present)
368
436
  doc_urls = ctx.get("doc_urls")
369
437
  if doc_urls:
@@ -412,8 +480,11 @@ def atomic_write(target: Path, content: str) -> int:
412
480
  target.parent.mkdir(parents=True, exist_ok=True)
413
481
  tmp = target.with_name(target.name + ".skf-tmp")
414
482
  encoded = content.encode("utf-8")
483
+ # O_BINARY (Windows only; 0 elsewhere) suppresses the text-mode \n -> \r\n
484
+ # translation that would otherwise corrupt verbatim writes on Windows.
485
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_BINARY", 0)
415
486
  try:
416
- fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
487
+ fd = os.open(tmp, flags, 0o644)
417
488
  try:
418
489
  os.write(fd, encoded)
419
490
  os.fsync(fd)
@@ -439,6 +510,8 @@ _FLAT_SCOPE_KEYS = (
439
510
  "scope_exclude",
440
511
  "scope_notes",
441
512
  "scope_rationale",
513
+ "scope_tier_a_include",
514
+ "scope_amendments",
442
515
  )
443
516
 
444
517
 
@@ -493,6 +566,13 @@ def flat_to_nested(flat: dict[str, Any]) -> dict[str, Any]:
493
566
  # six-subkey object validated by validate_context.
494
567
  if "scope_rationale" in flat and flat["scope_rationale"] is not None:
495
568
  scope["rationale"] = flat["scope_rationale"]
569
+ # Optional stratified-scope tier-A include list and post-authoring
570
+ # amendments log. Same null-drop semantics — preserved on a ratify
571
+ # re-write so the brief's authored surface and audit trail survive.
572
+ if "scope_tier_a_include" in flat and flat["scope_tier_a_include"] is not None:
573
+ scope["tier_a_include"] = flat["scope_tier_a_include"]
574
+ if "scope_amendments" in flat and flat["scope_amendments"] is not None:
575
+ scope["amendments"] = flat["scope_amendments"]
496
576
  nested["scope"] = scope
497
577
  return nested
498
578
 
@@ -27,6 +27,8 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
27
27
  | `scripts_intent` | string | `detect` / `none` / free-text | Describes whether scripts should be extracted. Values: `detect` (auto-detect from source — default when absent), `none` (skip scripts), or a free-text description of expected scripts (e.g., "CLI validation tools in bin/"). |
28
28
  | `assets_intent` | string | `detect` / `none` / free-text | Describes whether assets should be extracted. Values: `detect` (auto-detect from source — default when absent), `none` (skip assets), or a free-text description of expected assets (e.g., "JSON schemas in schemas/"). |
29
29
  | `target_version` | string | Semantic version (`X.Y.Z` or `X.Y.Z-prerelease`) | User-specified target version. When present, overrides auto-detection and becomes the skill's version. Recommended for docs-only skills where auto-detection is unavailable. |
30
+ | `target_ref` | string | Git ref (tag or branch) | Optional. Explicit git ref used verbatim as the resolved `source_ref`, bypassing version-to-tag matching. Escape hatch for monorepo crate tags whose prefix differs from the skill name (e.g. tag `livekit/v0.7.42` for skill `livekit-rust`). Remote sources only. Mutually exclusive with `constituent_refs`. |
31
+ | `constituent_refs` | object | map of path→ref strings | Optional. Per-constituent git ref overrides for composite (multi-repo or multi-ref) sources. Keys must match entries in the analyze-source `project_paths` array. When absent, a single `target_ref` (or auto-detected ref) applies to all paths. Mutually exclusive with `target_ref`. |
30
32
  | `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
31
33
  | `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
32
34
 
@@ -59,11 +61,15 @@ The create-skill workflow (extract) also performs version reconciliation at extr
59
61
 
60
62
  ```yaml
61
63
  scope:
62
- type: full-library | specific-modules | public-api | component-library | docs-only
64
+ type: full-library | specific-modules | public-api | component-library | reference-app | docs-only
63
65
  include:
64
66
  - 'src/**/*.ts' # At least one required
65
67
  exclude:
66
68
  - 'src/**/*.test.ts' # Optional
69
+ # Optional: narrower tier-A include list for stratified-scope monorepos
70
+ # and reference-app pattern surfaces (refined later by skf-brief-skill)
71
+ # tier_a_include:
72
+ # - 'code/core/src/manager-api/**'
67
73
  notes: 'Optional rationale for scope decision'
68
74
  # Additional fields when scope.type is "component-library":
69
75
  # registry_path: "path/to/registry.ts" # Optional — auto-detected if omitted
@@ -83,8 +89,11 @@ scope:
83
89
  | specific-modules | Selected components or packages |
84
90
  | public-api | Only exported interfaces |
85
91
  | component-library | UI component libraries with registries, props-based APIs, and design system variants |
92
+ | reference-app | Whole app whose value is wiring patterns, not public exports (embedded sidecars, CLI demos, integration-pattern demonstrators). Also the home for **language / spec references** — engine- or spec-versioned query languages, grammars, or DSLs (e.g. SurrealQL) whose value is construct idioms, not exports. These have no separate scope type; they ride `reference-app` as a sub-shape (see `skf-create-skill` Language / spec-reference sub-shape) |
86
93
  | docs-only | When source_type is docs-only — no source code available, all content from doc_urls |
87
94
 
95
+ > **Documented vs source language.** The `language` field records the language the skill *documents*. For a language / spec reference this may differ from the source language it is extracted from — e.g. a SurrealQL reference extracted from a Rust engine records `language: surrealql`, not `rust`. Use the documented language and a matching code-fence default (e.g. ` ```surql `) throughout the brief.
96
+
88
97
  ## YAML Template
89
98
 
90
99
  ```yaml
@@ -111,7 +120,7 @@ created_by: '{user_name}'
111
120
  1. **Name uniqueness**: No duplicate names within forge_data_folder
112
121
  2. **Source accessibility**: source_repo path must exist or be reachable
113
122
  3. **Language recognized**: Must be a known programming language
114
- 4. **Scope type valid**: Must match one of the five defined types (full-library, specific-modules, public-api, component-library, docs-only)
123
+ 4. **Scope type valid**: Must match one of the six defined types (full-library, specific-modules, public-api, component-library, reference-app, docs-only)
115
124
  5. **Include patterns**: At least one include glob pattern required (exception: `docs-only` scope, where include patterns are optional since no source code is available)
116
125
  6. **Forge tier match**: Must match value in forge-tier.yaml
117
126
  7. **Docs-only mode**: When `source_type: "docs-only"`, `doc_urls` required (>= 1), `source_repo` optional
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  outputFile: '{forge_data_folder}/analyze-source-report-{project_name}.md'
3
3
  schemaFile: 'assets/skill-brief-schema.md'
4
+ validateBriefSchemaProbeOrder:
5
+ - '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
6
+ - '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
4
7
  nextStepFile: 'health-check.md'
5
8
  ---
6
9
 
@@ -46,30 +49,47 @@ For EACH unit in `confirmed_units`, construct a skill-brief.yaml using:
46
49
  | name | Confirmed name from step 05 recommendation card |
47
50
  | version | Auto-detect from source (see schema Version Detection), fall back to `1.0.0` |
48
51
  | source_repo | `{project_paths[0]}` from frontmatter (or per-unit path if multi-repo) |
49
- | language | Primary language detected in step 03 |
52
+ | language | Language the skill **documents** (primary language detected in step 03). For a language / spec reference this is the *documented* language, which may differ from the source language it is extracted from — e.g. a SurrealQL reference extracted from a Rust engine records `surrealql`, not `rust` (see {schemaFile} "Documented vs source language") |
50
53
  | scope.type | Scope type from step 05 recommendation card |
51
54
  | scope.include | Include patterns from step 05 recommendation card |
52
55
  | scope.exclude | Inferred from heuristics (test files, generated code) |
56
+ | scope.tier_a_include | Optional — narrower tier-A surface for stratified-scope monorepos and `reference-app` pattern surfaces; usually left to skf-brief-skill to refine |
53
57
  | scope.notes | Rationale from step 05 recommendation card |
54
58
  | description | Description from step 05 recommendation card |
55
59
  | forge_tier | `{forge_tier}` from frontmatter |
56
- | created | Current date (ISO format YYYY-MM-DD) |
60
+ | created | Current date as a **quoted** string in ISO format `'YYYY-MM-DD'` — quote it so YAML stores it as text, not a parsed date object (the schema, and the §3a gate, require a string) |
57
61
  | created_by | `{user_name}` from frontmatter |
58
62
 
59
63
  ### 3. Validate Each Brief
60
64
 
61
- For each generated brief, check against {schemaFile} validation rules:
65
+ Validation has two parts: a **deterministic schema gate** (authoritative for structure) and **semantic cross-checks** the schema cannot express. Run the schema gate first.
66
+
67
+ **3a. Deterministic schema gate.** Resolve `{validateBriefSchemaHelper}` from `{validateBriefSchemaProbeOrder}` (first existing path wins; HALT with a clear message if no candidate exists). For each generated brief, pipe the *exact* assembled YAML to the helper:
68
+
69
+ ```bash
70
+ uv run {validateBriefSchemaHelper} - <<'YAML'
71
+ {assembled-brief-yaml}
72
+ YAML
73
+ ```
74
+
75
+ The script returns JSON `{valid, errors[], warnings[], halt_reason, brief}` — the same validator and contract `skf-brief-skill` runs at consumption time, so a brief that passes here will not be rejected there for structural reasons. Apply the result:
76
+
77
+ - **`valid: false`** — the `errors[]` name the offending field (e.g. a `description` mis-indented under `scope:`, which leaves the required top-level `description` absent). Repair the assembled YAML and re-run the helper until `valid: true`. **Never write a brief that has not passed this gate.**
78
+ - **`valid: true`** — carry any non-empty `warnings[]` into the §4 preview, then proceed.
79
+
80
+ This catches structural YAML errors where they are created, rather than letting them surface downstream as a HALT in `skf-brief-skill`'s ratify path.
81
+
82
+ **3b. Semantic cross-checks** (not expressible in the JSON schema — apply in addition to 3a):
62
83
 
63
84
  1. **Name uniqueness** — no duplicate names within the batch or existing skills
64
85
  2. **Source accessible** — project_path exists
65
86
  3. **Language recognized** — valid programming language identifier
66
- 4. **Scope type valid** — matches `full-library`, `specific-modules`, `public-api`, or `component-library`
67
- 5. **Include patterns** — at least one glob pattern present
68
- 6. **Forge tier match** — matches forge_tier from config
87
+ 4. **Include patterns** — at least one glob pattern present (the schema requires the `scope.include` key but does not enforce a non-empty list)
88
+ 5. **Forge tier match** — matches forge_tier from config
69
89
 
70
- **If validation fails for any brief:**
90
+ **If any check fails:**
71
91
  - Document the failure with specific field and reason
72
- - Present to user for correction before writing
92
+ - Repair (3a structural errors) or present to user for correction (3b semantic issues) before writing
73
93
  - Do NOT write invalid briefs
74
94
 
75
95
  ### 4. Present Generation Preview
@@ -99,7 +119,7 @@ Wait for explicit user confirmation before writing files.
99
119
 
100
120
  For each confirmed brief:
101
121
  1. Create directory `{forge_data_folder}/{unit-name}/` if it does not exist
102
- 2. Write `skill-brief.yaml` to `{forge_data_folder}/{unit-name}/skill-brief.yaml`
122
+ 2. Write `skill-brief.yaml` to `{forge_data_folder}/{unit-name}/skill-brief.yaml` — write the exact YAML that passed the §3a schema gate verbatim; do not re-serialize, so the bytes on disk are the bytes that validated
103
123
  3. Verify file was written successfully
104
124
 
105
125
  **IF user modifies (M):**
@@ -124,6 +144,7 @@ For each generated brief, recommend the appropriate next workflow:
124
144
  | Brief has `scope.type: component-library` and registry defines boundaries | create-skill — component boundaries defined by registry |
125
145
  | Brief has `scope.type: specific-modules` or scope needs refinement | brief-skill — refine scope before creating skill |
126
146
  | Brief has `scope.type: public-api` or complex interface | brief-skill — detailed scoping needed |
147
+ | Brief has `scope.type: reference-app` | brief-skill — refine the pattern surface and capture `tier_a_include` before creating skill |
127
148
  | Unit flagged as stack skill candidate | create-stack-skill — after individual skills exist |
128
149
  | Unit flagged as already-skilled | update-skill — refresh existing skill |
129
150
 
@@ -48,6 +48,7 @@ For EACH detected boundary from the scan:
48
48
  - Package Boundary — workspace member or independently versioned
49
49
  - Module Boundary — logical grouping within a package
50
50
  - Library Boundary — third-party with significant project-specific usage
51
+ - Composite Boundary — ≥2 boundaries that only deliver value together (detected in §3b below; not assigned during initial per-boundary classification)
51
52
 
52
53
  **Step C — Assign scope type:**
53
54
  - `full-library` — entire codebase of the unit
@@ -105,9 +106,28 @@ For disqualified candidates, note reason:
105
106
  |------|--------|
106
107
  | {path} | {disqualification reason} |
107
108
 
109
+ ### 3b. Detect Composite Unit Merges
110
+
111
+ After building the classification table, apply the Composite Boundary detection heuristic from {heuristicsFile} against the qualifying units:
112
+
113
+ 1. **Scan for merge candidates:** Among the qualifying units (from `kept[]`), find groups of ≥2 Package or Module boundaries that meet EITHER trigger:
114
+ - **Mutual hard dependency:** Every constituent imports from at least one other constituent in the group, AND no constituent's public API is self-contained
115
+ - **Shared integration surface:** Constituents share types/traits defined in one constituent but consumed by all others, AND the consuming constituents have no independent barrel
116
+
117
+ 2. **If candidate groups are found**, propose each merge:
118
+ - Derive a composite name from the common namespace prefix or repo name
119
+ - List the constituents (boundary names and paths being merged)
120
+ - State the triggering heuristic and evidence
121
+
122
+ 3. **If no candidate groups are found**, skip to §4.
123
+
124
+ **Merge does NOT fire for:** Units already flagged as Stack Skill Candidates in step 4 (map-and-detect §5) — those are multi-unit groupings that deliver value *separately* but are *also* useful together. Composite merges are for units that are *only* useful together (the key distinction). If a group of units is independently useful but commonly combined, it remains as separate units and is flagged as a stack skill candidate later.
125
+
126
+ **This step is a recommendation — not automatic.** Merges are presented to the user in §5 for confirmation (see "Composite Merge Proposals" below). If the user rejects a merge, the constituents remain as separate units in the classification table.
127
+
108
128
  ### 4. Detect Primary Language Per Unit
109
129
 
110
- For each qualifying unit, determine the primary programming language based on:
130
+ For each qualifying unit (including any approved composites from §3b), determine the primary programming language based on:
111
131
  - File extensions in the unit directory
112
132
  - Manifest file type (package.json → JS/TS, Cargo.toml → Rust, go.mod → Go, etc.)
113
133
  - Entry point file extension
@@ -126,20 +146,34 @@ For each qualifying unit, determine the primary programming language based on:
126
146
  **Already-Skilled Units:** {count from existing_skills match}
127
147
  {List with recommendation to run update-skill if source has changed}
128
148
 
149
+ {IF composite merge proposals exist from §3b:}
150
+
151
+ **Composite Merge Proposals:** {count}
152
+
153
+ | # | Composite Name | Constituents | Heuristic | Evidence |
154
+ |---|----------------|--------------|-----------|----------|
155
+ | 1 | {name} | {list of constituent unit names} | {mutual hard dependency / shared integration surface} | {brief evidence} |
156
+
157
+ If approved, each composite replaces its constituents in the classification table as a single Composite Boundary unit. The constituents are recorded in the composite's metadata for downstream workflows (create-skill reads constituents to scope extraction across all member paths).
158
+
159
+ {END IF}
160
+
129
161
  **Notes:**
130
162
  - {Any observations about project structure patterns}
131
163
  - {Any ambiguous boundaries that need user clarification}
132
164
 
133
- Do these classifications look correct? Should any units be added, removed, or reclassified?"
165
+ Do these classifications look correct? Should any units be added, removed, or reclassified?
166
+ {IF composites proposed:} Are the composite merge proposals correct? (Accept/reject each individually.)"
134
167
 
135
- Wait for user feedback. Adjust classifications based on user input.
168
+ Wait for user feedback. Adjust classifications based on user input. For approved composites: remove the constituent rows from the qualifying units table and add a single composite row with `Boundary Type: Composite`, scope type inherited from the dominant constituent, and confidence reflecting the merge heuristic strength.
136
169
 
137
170
  ### 6. Append to Report
138
171
 
139
172
  Append the complete "## Identified Units" section to {outputFile}:
140
173
 
141
174
  Replace the placeholder `[Appended by identify-units]` with:
142
- - Classification table (qualifying units)
175
+ - Classification table (qualifying units, including approved composites)
176
+ - Composite merge details (if any): composite name, constituents list, heuristic, evidence
143
177
  - Disqualification table
144
178
  - Already-skilled units list
145
179
  - Language detection results
@@ -149,8 +183,11 @@ Update {outputFile} frontmatter:
149
183
  ```yaml
150
184
  stepsCompleted: [append 'identify-units' to existing array]
151
185
  lastStep: 'identify-units'
186
+ confirmed_composites: [{list of approved composite merge objects: {name, constituents[], heuristic}}]
152
187
  ```
153
188
 
189
+ (`confirmed_composites` is an empty array when no composites were proposed or all were rejected.)
190
+
154
191
  ### 7. Present MENU OPTIONS
155
192
 
156
193
  Display: "**Select:** [C] Continue to Export Mapping and Integration Detection | [X] Cancel and exit"
@@ -50,6 +50,8 @@ Look for {outputFile}.
50
50
 
51
51
  **Headless flag consumption:** If `project_paths[]` is already populated (e.g. collected by the section-1 stale-collision guard) OR `--project-path <path>` was passed at invocation, set/keep `project_paths[]` (comma-split the flag value if multiple paths were supplied), skip the prompt below, and proceed to validation. Otherwise prompt as today.
52
52
 
53
+ **Per-path ref overrides (`--target-refs`):** If `--target-refs <mapping>` was passed at invocation, parse it as a comma-separated list of `path:ref` pairs (e.g., `owner/repo:v1.0.0,owner/repo2:main`). Build a `constituent_refs` map from the pairs. Each key must match an entry in `project_paths[]` (validated after path collection). When `--target-refs` is absent but multiple `project_paths` exist, set `constituent_refs` to `{}` (empty — all paths use default ref resolution). When only a single path exists, omit `constituent_refs` entirely (use `target_ref` if set on the brief). `constituent_refs` and `target_ref` are mutually exclusive — if both are supplied, HALT with: "`--target-refs` and `--target-ref` are mutually exclusive. Use `--target-refs` for multi-path analysis, or `--target-ref` for single-path."
54
+
53
55
  "**Welcome to Analyze Source — the SKF decomposition engine.**
54
56
 
55
57
  I'll analyze your project to identify discrete skillable units and produce skill-brief.yaml files for each recommended unit.
@@ -71,6 +73,7 @@ Wait for user input.
71
73
  - For each provided path/URL: check that it exists (local) or is accessible (remote)
72
74
  - **IF any invalid:** "Path `{path}` doesn't appear to be valid. Please correct it."
73
75
  - Store as `project_paths[]` array in report frontmatter (single path stored as 1-element array for consistency)
76
+ - **IF `constituent_refs` was built from `--target-refs`:** Validate that every key in the map matches an entry in `project_paths[]`. If any key has no matching path, HALT: "constituent_refs key `{key}` does not match any entry in project_paths."
74
77
 
75
78
  **Collect intent hint** (drives recommendation ranking in Step 5):
76
79
 
@@ -127,6 +130,7 @@ date: '{current_date}'
127
130
  user_name: '{user_name}'
128
131
  project_name: '{project_name}'
129
132
  project_paths: ['{provided_project_path}']
133
+ constituent_refs: {map from --target-refs, or omit if single path}
130
134
  forge_tier: '{detected_tier}'
131
135
  existing_skills: [{list of existing skill names}]
132
136
  intent_hint: '{intent_hint or empty string}'
@@ -136,6 +140,8 @@ stack_skill_candidates: []
136
140
  nextWorkflow: ''
137
141
  ```
138
142
 
143
+ **`constituent_refs` presence rules:** Include the field only when `project_paths` has more than one entry. When present, keys are path strings matching `project_paths[]` entries, values are explicit git refs (tag/branch/commit). Paths with no explicit ref have no entry in the map (default ref resolution applies). Downstream steps (scan-project, brief generation) read this map to resolve per-constituent refs when cloning or reading off-HEAD constituents.
144
+
139
145
  "**Initialization complete.**
140
146
 
141
147
  **Project:** {project_path}
@@ -57,7 +57,7 @@ For EACH qualifying unit, prepare a recommendation card:
57
57
 
58
58
  **Proposed Brief Fields:**
59
59
  - name: {suggested kebab-case name}
60
- - scope.type: {full-library / specific-modules / public-api / component-library}
60
+ - scope.type: {full-library / specific-modules / public-api / component-library / reference-app / docs-only}
61
61
  - scope.include: {suggested glob patterns}
62
62
  - description: {suggested 1-3 sentence description}
63
63
  ```
@@ -28,6 +28,7 @@ To map the complete project structure by scanning directory trees, detecting ser
28
28
 
29
29
  Read {outputFile} frontmatter to obtain:
30
30
  - `project_paths[]` — the root(s) to scan (one or more paths/URLs)
31
+ - `constituent_refs` — optional per-path git ref overrides (present only when `project_paths` has multiple entries and explicit refs were supplied via `--target-refs`)
31
32
  - `forge_tier` — determines scanning depth
32
33
  - Scope hints (if any were provided in step 01)
33
34
 
@@ -37,14 +38,16 @@ Load {heuristicsFile} for reference on detection signals.
37
38
 
38
39
  **Resolve `{scanManifestsHelper}`** from `{scanManifestsProbeOrder}`; first existing path wins. HALT if no candidate exists.
39
40
 
40
- **For each path in `project_paths[]`**, launch a subprocess that scans the project directory structure (aggregate results across all repos with clear repo-level grouping):
41
+ **For each path in `project_paths[]`**, resolve the constituent ref (if any) and launch a subprocess that scans the project directory structure (aggregate results across all repos with clear repo-level grouping):
42
+
43
+ **Per-path ref resolution:** If `constituent_refs` is present and contains an entry for the current path, use that ref. For remote paths, this means cloning or fetching at the specified ref (`git clone --branch {ref} --depth 1` or `git show {ref}:<subpath>` for off-HEAD access). For local paths with a non-HEAD ref, check out or read from the specified ref using `git show {ref}:<path>`. When no `constituent_refs` entry exists for a path, use default ref resolution (HEAD for local, latest tag or HEAD for remote).
41
44
 
42
45
  1. Map the top-level directory tree (2-3 levels deep)
43
46
  2. Identify workspace configuration files (pnpm-workspace.yaml, lerna.json, Cargo.toml [workspace], go.work, etc.)
44
47
  3. Enumerate package manifests deterministically — invoke `uv run {scanManifestsHelper} scan {path}` and parse the JSON envelope. The script returns `{manifests[], total_unique, monorepo, warnings?}` covering npm/python/rust/go/maven/gradle/ruby/composer/swift; record each `{path, ecosystem}` for the manifests catalog in §4 and capture `monorepo` for the boundary-signal pass in §3
45
48
  4. Locate entry point files (index.ts, main.ts, app.ts, main.go, main.rs, __init__.py, etc.)
46
49
  5. Detect service configuration (Dockerfile, docker-compose.yml, kubernetes manifests, serverless.yml) — keep this step LLM-driven; file glob + presence check is sufficient, no parsing required
47
- 6. Return structured findings — file paths and types only, not contents
50
+ 6. Return structured findings — file paths and types only, not contents. When `constituent_refs` was used, include the resolved ref in each repo-level result group: `{path, ref, manifests[], monorepo, warnings?}`
48
51
 
49
52
  **If subprocess unavailable:** Perform directory scanning in main thread using file I/O tools.
50
53
 
@@ -70,6 +70,22 @@ Rules for identifying discrete skillable units within a project. A "skillable un
70
70
  - Props interfaces outnumber function signatures as primary API surface
71
71
  - Scope type: `component-library`
72
72
 
73
+ ### Composite Boundary
74
+ - Two or more Package or Module boundaries that only deliver value together (no constituent is independently useful to the skill consumer)
75
+ - Hard cross-boundary dependency: constituents share types, traits, or interfaces that are not re-exported through a single barrel — consumers must import from multiple constituents to use the integration
76
+ - Common pattern: a set of crates/packages in the same repo that implement a protocol together (e.g., plugin crates for a framework, verification + encoding halves of a cryptographic library)
77
+ - Scope type: inherits from the dominant constituent (typically `full-library` or `specific-modules`)
78
+
79
+ **Detection heuristic (apply after initial classification, before user confirmation):**
80
+ 1. Among the qualifying units, find groups of ≥2 boundaries where EITHER:
81
+ - **Mutual hard dependency:** Every constituent imports from at least one other constituent in the group, AND no constituent's public API is self-contained (removing any one breaks the others)
82
+ - **Shared integration surface:** Constituents share types/traits defined in one constituent but consumed by all others, AND the consuming constituents have no independent barrel (their value depends on the shared definitions)
83
+ 2. For each detected group, propose merging into a single composite unit:
84
+ - Name: `{common-prefix}` or `{integration-name}` (derive from shared namespace or repo name)
85
+ - Constituents: list of merged boundary names and paths
86
+ - Rationale: which heuristic triggered (mutual hard dependency or shared integration surface)
87
+ 3. The merge is a **recommendation** — present to user for confirmation in identify-units §3b
88
+
73
89
  ## Disqualification Rules
74
90
 
75
91
  Do NOT recommend as a skillable unit if:
@@ -13,7 +13,7 @@ A good skill brief sets a tight, cohesive boundary: one capability with 3-8 prim
13
13
 
14
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.
15
15
 
16
- **Ratify path (interactive only).** When the user already has a `skill-brief.yaml` produced by another workflow — typically `skf-analyze-source`'s `generate-briefs` step, where one analyze pass emits several recommended briefs — invoking `/skf-brief-skill` with a path to that brief at the first prompt routes to a fast-path review: gather-intent §3.1a loads the YAML, hydrates the brief context, and jumps straight to step 4 (confirm-brief) where the standard review/edit cycle still applies before step 5 writes. This skips re-deriving fields the upstream draft already supplies and saves the 5-10 minutes a full re-run of intent + analyze + scope would cost.
16
+ **Ratify path.** When the user already has a `skill-brief.yaml` produced by another workflow — typically `skf-analyze-source`'s `generate-briefs` step, where one analyze pass emits several recommended briefs — the brief can be *ratified* (reviewed and rewritten without re-deriving its fields) instead of authored from scratch. **Interactively:** invoke `/skf-brief-skill` with a path to that brief at the first prompt gather-intent §3.1a loads the YAML, hydrates the brief context, and jumps straight to step 4 (confirm-brief) where the standard review/edit cycle still applies before step 5 writes. **Headlessly:** pass `from_brief <path>` — the step 1 §8 GATE runs the same schema validation and hydration and writes through the canonical writer (overwriting in place), enabling a fully headless analyze → brief → create pipeline that preserves the upstream-authored scope. Either path skips re-deriving fields the upstream draft already supplies and saves the 5-10 minutes a full re-run of intent + analyze + scope would cost.
17
17
 
18
18
  ## Conventions
19
19
 
@@ -83,10 +83,10 @@ These rules apply to every step in this workflow:
83
83
 
84
84
  | Aspect | Detail |
85
85
  |--------|--------|
86
- | **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] |
86
+ | **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], `from_brief` [optional: path to a pre-authored `skill-brief.yaml` to *ratify* — when supplied it is the source of truth, `target_repo`/`skill_name` become optional/derived-from-brief, and the run mirrors the interactive §3.1a ratify path: schema-validate, skip analyze-target/scope-definition, write through the canonical writer in place] |
87
87
  | **Gates** | step 1: Input Gate [use args] | step 3: Confirm Gate [C] | step 4: Confirm Gate [C] |
88
88
  | **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 |
89
- | **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) |
89
+ | **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); supplying `from_brief <path>` instead routes the step 1 GATE to a ratify path that schema-validates the pre-authored brief, skips analyze-target/scope-definition, and writes through the canonical writer (overwriting in place, no `--force` needed) rather than deriving a new brief |
90
90
  | **Transient-failure retry** | This workflow does **not** auto-retry network or subprocess failures. A failed `gh` fetch (analyze-target.md §1, portfolio-similarity-check.md), QMD probe, or extraction script is logged and surfaced in the final result envelope as a warning, but the workflow continues with whatever signal it has. Headless pipelines that want retry semantics should wrap the invocation at their orchestrator level (e.g. CI re-runner on non-zero exit). Rationale: brief-skill is read-mostly with one terminal write (the YAML at step 5); a partial-signal retry has more failure modes than just re-running the whole workflow, which is cheap. |
91
91
  | **Exit codes** | See "Exit Codes" below |
92
92
 
@@ -23,6 +23,7 @@
23
23
  | `scripts_intent` | string | `detect` / `none` / free-text | Describes whether scripts should be extracted. Values: `detect` (auto-detect from source — default when absent), `none` (skip scripts), or a free-text description of expected scripts (e.g., "CLI validation tools in bin/"). |
24
24
  | `assets_intent` | string | `detect` / `none` / free-text | Describes whether assets should be extracted. Values: `detect` (auto-detect from source — default when absent), `none` (skip assets), or a free-text description of expected assets (e.g., "JSON schemas in schemas/"). |
25
25
  | `target_version` | string | Semantic version (`X.Y.Z` or `X.Y.Z-prerelease`) | User-specified target version. When present, overrides auto-detection and becomes the skill's version. Recommended for docs-only skills where auto-detection is unavailable. |
26
+ | `target_ref` | string | Git ref (tag or branch) | Optional. Explicit git ref used verbatim as the resolved `source_ref`, bypassing version-to-tag matching. Escape hatch for monorepo crate tags whose prefix differs from the skill name (e.g. tag `livekit/v0.7.42` for skill `livekit-rust`). Remote sources only. |
26
27
  | `source_authority` | string | `official` / `community` / `internal` | Default `community`. Set to `official` only when the skill creator is the library maintainer. Forced to `community` when `source_type: "docs-only"`. |
27
28
  | `source_ref` | string | Git ref (tag/branch/HEAD) | Resolved git ref used for source access. Set automatically during tag resolution — do not set manually. |
28
29
  | `scope.tier_a_include` | array | Glob patterns | Optional. Narrower tier-A include list for stratified-scope monorepo skills. When present, `skf-test-skill` re-derives the coverage denominator from this list instead of the coarse `scope.include`, so the denominator reflects the authoring surface rather than incidentally-matched internal infrastructure. See `skf-test-skill/references/source-access-protocol.md` stratified-scope resolution. |
@@ -136,7 +137,7 @@ scope:
136
137
  | Field | Type | Required | Description |
137
138
  |---|---|---|---|
138
139
  | `path` | string | yes | Relative path (or glob, for `category: "scope-expansion"`) from source root to the file or tree being amended. For `promoted` actions this matches the literal entry added to `scope.include`. |
139
- | `action` | string | yes | One of: `promoted` (path added to `scope.include`), `skipped` (user declined promotion; decision recorded to prevent re-prompting), `demoted-include` (path removed from `scope.include` — only valid with `category: "scope-expansion"`), `demoted-exclude` (path removed from `scope.exclude` — only valid with `category: "scope-expansion"`). |
140
+ | `action` | string | yes | One of: `promoted` (path added to `scope.include`), `skipped` (user declined promotion; decision recorded to prevent re-prompting), `excluded` (path added to `scope.exclude` — only valid with `category: "scope-expansion"`; used by gap-driven rescope to remove an internal / `#[doc(hidden)]` / out-of-scope export from the public surface), `demoted-include` (path removed from `scope.include` — only valid with `category: "scope-expansion"`), `demoted-exclude` (path removed from `scope.exclude` — only valid with `category: "scope-expansion"`). |
140
141
  | `category` | string | no | One of: `auth-doc` (default for entries without this field — the historical sole use case), `scope-expansion`. Distinguishes which workflow path wrote the entry and which writer-rules apply on re-runs. |
141
142
  | `reason` | string | yes | Human-readable sentence explaining the decision. Either user-provided at prompt time or auto-generated. |
142
143
  | `heuristic` | string | conditional | Required for `category: "auth-doc"` — the basename that matched (`llms.txt`, `AGENTS.md`, etc.). Omit for `category: "scope-expansion"`. |
@@ -150,6 +151,8 @@ scope:
150
151
 
151
152
  **Demotion (scope-expansion only):** `demoted-include` removes a previously-promoted path from `scope.include` — used when a prior `[P]` decision is reversed. `demoted-exclude` removes a path from `scope.exclude` — used when a previously excluded path needs to be re-evaluated. Both write the structural change and append the amendment so future runs see the rationale. Demotion is not valid for `category: "auth-doc"`: auth-doc skips already prevent re-prompting without scope mutation.
152
153
 
154
+ **Exclusion (scope-expansion only):** `excluded` adds a path to `scope.exclude` — written by `skf-update-skill` gap-driven rescope (detect-changes §0 rule R1) when a coverage gap's remediation is removal (the export is internal, `#[doc(hidden)]`, or out of scope). The amendment is the audit trail; the `scope.exclude` write is what shrinks the source barrel, so the legitimate scope reduction is expressed in the brief rather than by editing `metadata.stats`. This keeps the reduction visible to `skf-test-skill`'s denominator-deflation check, which re-derives the barrel from `scope.include` filtered by `scope.exclude`. Not valid for `category: "auth-doc"`.
155
+
153
156
  **Backward compatibility:** `scope.amendments` is optional. Briefs without this field validate unchanged. Treat missing as an empty list. Existing entries without `category` are equivalent to `category: "auth-doc"` — readers must default the field when absent.
154
157
 
155
158
  **Who reads `amendments[]`:**
@@ -165,6 +168,7 @@ scope:
165
168
  - `skf-create-skill` §2a (Discovered Authoritative Files Protocol) — `category: "auth-doc"`
166
169
  - `skf-update-skill` §1b (mirror of §2a applied during change detection) — `category: "auth-doc"`
167
170
  - `skf-update-skill` §1c (Major-Version Scope Reconciliation) — `category: "scope-expansion"`
171
+ - `skf-update-skill` gap-driven rescope (detect-changes §0 rule R1) — `category: "scope-expansion"`, `action: "excluded"`
168
172
  - Manual edits by the brief author are permitted but should include all required fields above (and `category` when the entry is not an auth-doc decision).
169
173
 
170
174
  ## YAML Template
@@ -188,6 +192,7 @@ scope:
188
192
  - "{pattern}"
189
193
  notes: "{optional-scope-notes}"
190
194
  # target_version: "X.Y.Z" # Optional: overrides auto-detection when specified
195
+ # target_ref: "livekit/v0.7.42" # Optional: explicit git ref, used verbatim (monorepo crate tags)
191
196
  # source_ref: "v0.5.0" # Auto-resolved — do not set manually
192
197
  # Optional: documentation URLs for T3 content (required when source_type: "docs-only")
193
198
  # doc_urls: