bmad-module-skill-forge 1.8.0 → 2.0.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 (136) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +10 -5
  3. package/docs/_data/pinned.yaml +2 -2
  4. package/docs/agents.md +11 -2
  5. package/docs/architecture.md +5 -4
  6. package/docs/bmad-synergy.md +42 -3
  7. package/docs/campaign.md +172 -0
  8. package/docs/examples.md +7 -3
  9. package/docs/forge-auto.md +90 -0
  10. package/docs/getting-started.md +9 -0
  11. package/docs/how-it-works.md +6 -4
  12. package/docs/index.md +4 -3
  13. package/docs/skill-model.md +1 -1
  14. package/docs/troubleshooting.md +17 -1
  15. package/docs/verifying-a-skill.md +9 -3
  16. package/docs/why-skf.md +1 -1
  17. package/docs/workflows.md +73 -27
  18. package/package.json +2 -2
  19. package/src/module-help.csv +2 -1
  20. package/src/shared/_known-workarounds.yaml +155 -0
  21. package/src/shared/data/language-corpora.json +32 -0
  22. package/src/shared/references/pipeline-contracts.md +14 -2
  23. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +6 -1
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  26. package/src/shared/scripts/skf-detect-docs.py +417 -0
  27. package/src/shared/scripts/skf-detect-language.py +5 -3
  28. package/src/shared/scripts/skf-emit-brief-result-envelope.py +12 -1
  29. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  30. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  31. package/src/shared/scripts/skf-language-corpora.py +100 -0
  32. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  33. package/src/shared/scripts/skf-preapply.py +192 -0
  34. package/src/shared/scripts/skf-shape-detect.py +1114 -0
  35. package/src/shared/scripts/skf-validate-brief-schema.py +2 -5
  36. package/src/shared/scripts/skf-validate-pins.py +368 -0
  37. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  38. package/src/skf-analyze-source/SKILL.md +26 -20
  39. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  40. package/src/skf-analyze-source/references/init.md +30 -0
  41. package/src/skf-analyze-source/references/step-auto-scope.md +677 -0
  42. package/src/skf-analyze-source/references/step-shape-detect.md +86 -0
  43. package/src/skf-audit-skill/SKILL.md +1 -0
  44. package/src/skf-audit-skill/assets/drift-report-template.md +6 -0
  45. package/src/skf-audit-skill/references/init.md +1 -1
  46. package/src/skf-audit-skill/references/report.md +4 -0
  47. package/src/skf-audit-skill/references/severity-classify.md +1 -1
  48. package/src/skf-audit-skill/references/step-doc-drift.md +147 -0
  49. package/src/skf-brief-skill/SKILL.md +7 -3
  50. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  51. package/src/skf-brief-skill/references/analyze-target.md +7 -4
  52. package/src/skf-brief-skill/references/confirm-brief.md +0 -1
  53. package/src/skf-brief-skill/references/gather-intent.md +22 -2
  54. package/src/skf-brief-skill/references/scope-definition.md +2 -1
  55. package/src/skf-brief-skill/references/step-auto-brief.md +188 -0
  56. package/src/skf-brief-skill/references/step-auto-validate.md +209 -0
  57. package/src/skf-brief-skill/references/write-brief.md +2 -3
  58. package/src/skf-campaign/SKILL.md +190 -0
  59. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  60. package/src/skf-campaign/customize.toml +73 -0
  61. package/src/skf-campaign/manifest.yaml +11 -0
  62. package/src/skf-campaign/references/campaign-directive-spec.md +121 -0
  63. package/src/skf-campaign/references/health-check.md +35 -0
  64. package/src/skf-campaign/references/step-01-setup.md +122 -0
  65. package/src/skf-campaign/references/step-02-strategy.md +97 -0
  66. package/src/skf-campaign/references/step-03-pins.md +56 -0
  67. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  68. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  69. package/src/skf-campaign/references/step-06-batch.md +87 -0
  70. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  71. package/src/skf-campaign/references/step-08-verify.md +75 -0
  72. package/src/skf-campaign/references/step-09-refine.md +83 -0
  73. package/src/skf-campaign/references/step-10-export.md +106 -0
  74. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  75. package/src/skf-campaign/references/step-resume.md +114 -0
  76. package/src/skf-campaign/scripts/.gitkeep +0 -0
  77. package/src/skf-campaign/scripts/campaign-deps.py +235 -0
  78. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  79. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  80. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  81. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  82. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  83. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  85. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  86. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  87. package/src/skf-create-skill/SKILL.md +4 -1
  88. package/src/skf-create-skill/assets/compile-assembly-rules.md +22 -0
  89. package/src/skf-create-skill/assets/skill-sections.md +2 -0
  90. package/src/skf-create-skill/references/compile.md +5 -2
  91. package/src/skf-create-skill/references/extract.md +9 -1
  92. package/src/skf-create-skill/references/extraction-patterns.md +3 -1
  93. package/src/skf-create-skill/references/generate-artifacts.md +11 -2
  94. package/src/skf-create-skill/references/health-check.md +2 -2
  95. package/src/skf-create-skill/references/report.md +17 -1
  96. package/src/skf-create-skill/references/source-resolution-protocols.md +1 -1
  97. package/src/skf-create-skill/references/step-auto-shard.md +110 -0
  98. package/src/skf-create-skill/references/step-doc-rot.md +109 -0
  99. package/src/skf-create-skill/references/step-doc-sources.md +123 -0
  100. package/src/skf-create-skill/references/sub/fetch-docs.md +29 -0
  101. package/src/skf-create-skill/references/validate.md +12 -3
  102. package/src/skf-drop-skill/SKILL.md +2 -2
  103. package/src/skf-drop-skill/references/execute.md +20 -9
  104. package/src/skf-drop-skill/references/report.md +2 -0
  105. package/src/skf-drop-skill/references/select.md +7 -2
  106. package/src/skf-forger/SKILL.md +15 -4
  107. package/src/skf-refine-architecture/SKILL.md +2 -1
  108. package/src/skf-refine-architecture/references/compile.md +1 -1
  109. package/src/skf-refine-architecture/references/report.md +1 -1
  110. package/src/skf-rename-skill/SKILL.md +2 -2
  111. package/src/skf-rename-skill/references/execute.md +5 -4
  112. package/src/skf-rename-skill/references/rebuild-context.md +2 -2
  113. package/src/skf-rename-skill/references/select.md +3 -3
  114. package/src/skf-setup/SKILL.md +1 -1
  115. package/src/skf-setup/references/auto-index.md +3 -1
  116. package/src/skf-setup/references/detect-and-tier.md +4 -0
  117. package/src/skf-setup/references/report.md +4 -1
  118. package/src/skf-setup/references/tier-rules.md +1 -1
  119. package/src/skf-test-skill/SKILL.md +8 -7
  120. package/src/skf-test-skill/customize.toml +2 -1
  121. package/src/skf-test-skill/references/coverage-check.md +10 -0
  122. package/src/skf-test-skill/references/external-validators.md +1 -1
  123. package/src/skf-test-skill/references/init.md +16 -1
  124. package/src/skf-test-skill/references/report.md +5 -1
  125. package/src/skf-test-skill/references/score.md +95 -6
  126. package/src/skf-test-skill/references/source-access-protocol.md +13 -3
  127. package/src/skf-test-skill/references/step-hard-gate.md +73 -0
  128. package/src/skf-test-skill/scripts/compute-score.py +4 -3
  129. package/src/skf-test-skill/templates/test-report-template.md +1 -0
  130. package/src/skf-update-skill/customize.toml +0 -9
  131. package/src/skf-update-skill/references/detect-changes.md +33 -3
  132. package/src/skf-update-skill/references/health-check.md +2 -2
  133. package/src/skf-update-skill/references/init.md +1 -0
  134. package/src/skf-update-skill/references/re-extract.md +15 -1
  135. package/src/skf-verify-stack/references/report.md +1 -1
  136. package/tools/cli/lib/ui.js +3 -2
@@ -5,7 +5,7 @@
5
5
  """SKF Validate Brief Schema — schema check for a skill-brief.yaml on disk.
6
6
 
7
7
  Loads `skill-brief.yaml` from a path (or from stdin/inline-YAML), validates
8
- it against `src/shared/scripts/schemas/skill-brief.v1.json`, and applies the
8
+ it against `schemas/skill-brief.v1.json` (sibling of this script), and applies the
9
9
  two conditional rules from `skf-create-skill/references/load-brief.md §3`
10
10
  that the JSON schema doesn't express today:
11
11
 
@@ -56,10 +56,7 @@ import yaml
56
56
  from jsonschema import Draft202012Validator
57
57
 
58
58
 
59
- REPO_ROOT = Path(__file__).resolve().parent.parent.parent.parent
60
- SCHEMA_PATH = (
61
- REPO_ROOT / "src" / "shared" / "scripts" / "schemas" / "skill-brief.v1.json"
62
- )
59
+ SCHEMA_PATH = Path(__file__).resolve().parent / "schemas" / "skill-brief.v1.json"
63
60
 
64
61
 
65
62
  # --------------------------------------------------------------------------
@@ -0,0 +1,368 @@
1
+ # /// script
2
+ # requires-python = ">=3.9"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Validate Pins — resolve and validate version pins for a GitHub repository.
6
+
7
+ Shared pin validation script consumed by skf-analyze-source (AN auto-scope,
8
+ forge-auto --pin) and campaign workflows. Validates that a user-supplied --pin
9
+ resolves to an existing tag or branch, or auto-resolves the latest release tag
10
+ when no --pin is provided.
11
+
12
+ Tag matching priority mirrors source-resolution-protocols.md:
13
+
14
+ 1. Exact match: {pin}
15
+ 2. v-prefixed: v{pin}
16
+ 3. Package scope: {name}@{pin}, @{scope}/{name}@{pin}
17
+ 4. Crate prefix: {name}/v{pin}, {name}/{pin}, {name}-v{pin}
18
+
19
+ CLI:
20
+ uv run src/shared/scripts/skf-validate-pins.py \\
21
+ --repo-url <url> [--pin <version>] [--format tag|branch|any]
22
+
23
+ Input:
24
+ --repo-url GitHub repository URL (required)
25
+ --pin Version pin to validate (optional — resolves latest release when absent)
26
+ --format Restrict matching to tag, branch, or any (optional, default: any)
27
+
28
+ Output (JSON on stdout):
29
+ {
30
+ "status": "valid" | "invalid" | "resolved",
31
+ "pin": "<input_pin_or_null>",
32
+ "resolved_ref": "<matched_tag_or_branch>",
33
+ "ref_type": "tag" | "branch",
34
+ "version": "<semver_or_null>",
35
+ "suggestions": ["<nearest_tags_on_invalid>"]
36
+ }
37
+
38
+ Exit codes:
39
+ 0 valid/resolved (pin matches a tag or branch, or latest release resolved)
40
+ 1 invalid (no match found for the supplied pin, or no releases for auto-resolve)
41
+ 2 error (invalid args, gh not found, network error, etc.)
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import argparse
47
+ import json
48
+ import re
49
+ import subprocess
50
+ import sys
51
+ from typing import Any, Dict, List, Optional, Tuple
52
+
53
+ _GITHUB_URL_RE = re.compile(
54
+ r"https?://(?:www\.)?github\.com/([^/\s]+)/([^/\s.]+?)(?:\.git)?/?$",
55
+ re.IGNORECASE,
56
+ )
57
+
58
+ _SEMVER_RE = re.compile(
59
+ r"^v?(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?(?:\+[a-zA-Z0-9.]+)?)$"
60
+ )
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # gh CLI helpers
65
+ # ---------------------------------------------------------------------------
66
+
67
+ def _run_gh(args: List[str]) -> Optional[str]:
68
+ try:
69
+ result = subprocess.run(
70
+ ["gh"] + args,
71
+ capture_output=True,
72
+ text=True,
73
+ )
74
+ if result.returncode == 0:
75
+ return result.stdout.strip()
76
+ return None
77
+ except FileNotFoundError:
78
+ return None
79
+
80
+
81
+ def _check_gh_available() -> bool:
82
+ try:
83
+ subprocess.run(
84
+ ["gh", "--version"],
85
+ capture_output=True,
86
+ text=True,
87
+ )
88
+ return True
89
+ except FileNotFoundError:
90
+ return False
91
+
92
+
93
+ def _run_git(args: List[str]) -> Optional[str]:
94
+ try:
95
+ result = subprocess.run(
96
+ ["git"] + args,
97
+ capture_output=True,
98
+ text=True,
99
+ )
100
+ if result.returncode == 0:
101
+ return result.stdout.strip()
102
+ return None
103
+ except FileNotFoundError:
104
+ return None
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Version extraction
109
+ # ---------------------------------------------------------------------------
110
+
111
+ def extract_version(tag: str) -> Optional[str]:
112
+ m = _SEMVER_RE.match(tag)
113
+ if m:
114
+ return m.group(1)
115
+ return None
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Tag listing
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def list_tags(owner: str, repo: str, repo_url: str) -> List[str]:
123
+ raw = _run_gh([
124
+ "api", f"repos/{owner}/{repo}/tags",
125
+ "--paginate", "--jq", ".[].name",
126
+ ])
127
+ if raw:
128
+ return [t for t in raw.splitlines() if t.strip()]
129
+
130
+ raw = _run_git(["ls-remote", "--tags", repo_url])
131
+ if raw:
132
+ tags = []
133
+ for line in raw.splitlines():
134
+ parts = line.split("refs/tags/")
135
+ if len(parts) == 2:
136
+ tag = parts[1].strip()
137
+ if not tag.endswith("^{}"):
138
+ tags.append(tag)
139
+ return tags
140
+
141
+ return []
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Branch verification
146
+ # ---------------------------------------------------------------------------
147
+
148
+ def check_branch_exists(repo_url: str, branch: str) -> bool:
149
+ raw = _run_git(["ls-remote", "--heads", repo_url, branch])
150
+ if raw and branch in raw:
151
+ return True
152
+ return False
153
+
154
+
155
+ # ---------------------------------------------------------------------------
156
+ # Tag matching (mirrors source-resolution-protocols.md)
157
+ # ---------------------------------------------------------------------------
158
+
159
+ def _derive_name_from_url(owner: str, repo: str) -> str:
160
+ return repo.lower()
161
+
162
+
163
+ def match_tag(pin: str, tags: List[str], owner: str, repo: str) -> Optional[str]:
164
+ name = _derive_name_from_url(owner, repo)
165
+
166
+ candidates = [
167
+ pin,
168
+ f"v{pin}",
169
+ f"{name}@{pin}",
170
+ f"{name}/v{pin}",
171
+ f"{name}/{pin}",
172
+ f"{name}-v{pin}",
173
+ ]
174
+
175
+ for candidate in candidates:
176
+ if candidate in tags:
177
+ return candidate
178
+
179
+ return None
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Suggestion generation
184
+ # ---------------------------------------------------------------------------
185
+
186
+ def _semver_sort_key(tag: str) -> Tuple:
187
+ v = extract_version(tag)
188
+ if v is None:
189
+ return (0, 0, 0, tag)
190
+ parts = v.split("-")[0].split(".")
191
+ try:
192
+ return tuple(int(p) for p in parts[:3])
193
+ except ValueError:
194
+ return (0, 0, 0, tag)
195
+
196
+
197
+ def generate_suggestions(pin: str, tags: List[str], max_count: int = 5) -> List[str]:
198
+ if not tags:
199
+ return []
200
+
201
+ prefixes_to_try = []
202
+ if "." in pin:
203
+ prefixes_to_try.append(pin.rsplit(".", 1)[0] + ".")
204
+ prefixes_to_try.append(pin.split(".")[0])
205
+ prefixes_to_try.append(pin)
206
+
207
+ for prefix in prefixes_to_try:
208
+ prefix_matches = [t for t in tags if t.startswith(prefix)]
209
+ if prefix_matches:
210
+ sorted_matches = sorted(prefix_matches, key=_semver_sort_key, reverse=True)
211
+ return sorted_matches[:max_count]
212
+
213
+ sorted_tags = sorted(tags, key=_semver_sort_key, reverse=True)
214
+ return sorted_tags[:max_count]
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Latest release resolution
219
+ # ---------------------------------------------------------------------------
220
+
221
+ def resolve_latest_release(owner: str, repo: str) -> Optional[str]:
222
+ raw = _run_gh([
223
+ "api", f"repos/{owner}/{repo}/releases/latest",
224
+ "--jq", ".tag_name",
225
+ ])
226
+ if raw and raw != "null":
227
+ return raw.strip()
228
+
229
+ raw = _run_gh([
230
+ "api", f"repos/{owner}/{repo}/tags",
231
+ "--jq", ".[0].name",
232
+ ])
233
+ if raw and raw != "null":
234
+ return raw.strip()
235
+
236
+ return None
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Core validation logic
241
+ # ---------------------------------------------------------------------------
242
+
243
+ def validate_pin(
244
+ repo_url: str,
245
+ pin: Optional[str] = None,
246
+ format_filter: str = "any",
247
+ ) -> Dict[str, Any]:
248
+ m = _GITHUB_URL_RE.match(repo_url.strip())
249
+ if not m:
250
+ return {
251
+ "status": "invalid",
252
+ "pin": pin,
253
+ "resolved_ref": None,
254
+ "ref_type": None,
255
+ "version": None,
256
+ "suggestions": [],
257
+ }
258
+
259
+ owner, repo = m.group(1), m.group(2)
260
+
261
+ if pin is None:
262
+ tag = resolve_latest_release(owner, repo)
263
+ if tag is None:
264
+ return {
265
+ "status": "invalid",
266
+ "pin": None,
267
+ "resolved_ref": None,
268
+ "ref_type": None,
269
+ "version": None,
270
+ "suggestions": [],
271
+ }
272
+ return {
273
+ "status": "resolved",
274
+ "pin": None,
275
+ "resolved_ref": tag,
276
+ "ref_type": "tag",
277
+ "version": extract_version(tag),
278
+ "suggestions": [],
279
+ }
280
+
281
+ tags: List[str] = []
282
+ if format_filter in ("tag", "any"):
283
+ tags = list_tags(owner, repo, repo_url)
284
+ matched = match_tag(pin, tags, owner, repo)
285
+ if matched:
286
+ return {
287
+ "status": "valid",
288
+ "pin": pin,
289
+ "resolved_ref": matched,
290
+ "ref_type": "tag",
291
+ "version": extract_version(matched),
292
+ "suggestions": [],
293
+ }
294
+
295
+ if format_filter in ("branch", "any"):
296
+ if check_branch_exists(repo_url, pin):
297
+ return {
298
+ "status": "valid",
299
+ "pin": pin,
300
+ "resolved_ref": pin,
301
+ "ref_type": "branch",
302
+ "version": None,
303
+ "suggestions": [],
304
+ }
305
+
306
+ if not tags:
307
+ tags = list_tags(owner, repo, repo_url)
308
+ suggestions = generate_suggestions(pin, tags)
309
+
310
+ return {
311
+ "status": "invalid",
312
+ "pin": pin,
313
+ "resolved_ref": None,
314
+ "ref_type": None,
315
+ "version": None,
316
+ "suggestions": suggestions,
317
+ }
318
+
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # CLI entry point
322
+ # ---------------------------------------------------------------------------
323
+
324
+ def main() -> int:
325
+ parser = argparse.ArgumentParser(
326
+ description="Validate and resolve version pins for a GitHub repository.",
327
+ )
328
+ parser.add_argument("--repo-url", required=True, help="GitHub repository URL")
329
+ parser.add_argument("--pin", default=None, help="Version pin to validate")
330
+ parser.add_argument(
331
+ "--format",
332
+ choices=["tag", "branch", "any"],
333
+ default="any",
334
+ help="Restrict matching to tag, branch, or any (default: any)",
335
+ )
336
+ args = parser.parse_args()
337
+
338
+ if not _check_gh_available():
339
+ json.dump({"error": "gh CLI not found", "code": "GH_NOT_FOUND"}, sys.stderr)
340
+ sys.stderr.write("\n")
341
+ return 2
342
+
343
+ m = _GITHUB_URL_RE.match(args.repo_url.strip())
344
+ if not m:
345
+ json.dump(
346
+ {"error": f"Not a GitHub URL: {args.repo_url}", "code": "INVALID_URL"},
347
+ sys.stderr,
348
+ )
349
+ sys.stderr.write("\n")
350
+ return 2
351
+
352
+ try:
353
+ result = validate_pin(args.repo_url, args.pin, args.format)
354
+ except Exception as exc:
355
+ json.dump({"error": str(exc), "code": "VALIDATION_ERROR"}, sys.stderr)
356
+ sys.stderr.write("\n")
357
+ return 2
358
+
359
+ json.dump(result, sys.stdout, separators=(",", ":"))
360
+ sys.stdout.write("\n")
361
+
362
+ if result["status"] == "invalid":
363
+ return 1
364
+ return 0
365
+
366
+
367
+ if __name__ == "__main__":
368
+ raise SystemExit(main())
@@ -51,7 +51,10 @@ Context payload shape (consumed by `write`):
51
51
  },
52
52
 
53
53
  # Conditionally present:
54
- "doc_urls": [{"url": "...", "label": "..."}],
54
+ "doc_urls": [{"url": "...", "label": "...", "source": "..."}],
55
+ # `source` (optional per #432): language-registry | readme-detection |
56
+ # homepage | pages-api | docs-folder. Threaded through when present;
57
+ # absent → entry renders as {url, label} only (no false drift).
55
58
  "scripts_intent": "detect" | "none" | free-text,
56
59
  "assets_intent": "detect" | "none" | free-text,
57
60
  "source_authority": "official" | "community" | "internal",
@@ -95,7 +98,7 @@ Flat input form (`--from-flat`):
95
98
  "scope_notes": "",
96
99
  "scope_tier_a_include": null | ["code/core/src/**"],
97
100
  "scope_amendments": null | [{...}],
98
- "doc_urls": null | [...],
101
+ "doc_urls": null | [{"url": "...", "label": "...", "source": "..."}],
99
102
  "scripts_intent": null | "detect" | "none" | "...",
100
103
  "assets_intent": null | "detect" | "none" | "...",
101
104
  "source_authority": null | "official" | "community" | "internal",
@@ -433,11 +436,24 @@ def assemble_brief(ctx: dict[str, Any], resolved_version: str) -> dict[str, Any]
433
436
  brief[ref_field] = ref_val
434
437
 
435
438
  # Conditional: doc_urls (always emitted when present)
439
+ #
440
+ # Each entry carries the {url, label} contract plus an OPTIONAL `source`
441
+ # provenance field (issue #432: language-registry | readme-detection |
442
+ # homepage | pages-api | docs-folder). `source` is emitted ONLY when present
443
+ # on the input entry — mirroring the source_authority null-drop discipline
444
+ # below (lines ~448-454): a legacy/hand-authored entry without `source`
445
+ # round-trips to exactly {url, label}, so a ratify/re-write of an old brief
446
+ # stays byte-identical. (An explicit empty-string source is treated as
447
+ # absent here; the schema enum is the real gate and rejects "" upstream.)
436
448
  doc_urls = ctx.get("doc_urls")
437
449
  if doc_urls:
438
- brief["doc_urls"] = [
439
- {"url": e["url"], "label": e.get("label", "")} for e in doc_urls
440
- ]
450
+ rendered_doc_urls: list[dict[str, Any]] = []
451
+ for e in doc_urls:
452
+ entry = {"url": e["url"], "label": e.get("label", "")}
453
+ if e.get("source"):
454
+ entry["source"] = e["source"]
455
+ rendered_doc_urls.append(entry)
456
+ brief["doc_urls"] = rendered_doc_urls
441
457
 
442
458
  # Conditional: scripts_intent / assets_intent — emit when explicitly non-detect
443
459
  for intent_field in ("scripts_intent", "assets_intent"):
@@ -31,26 +31,32 @@ These rules apply to every step in this workflow:
31
31
 
32
32
  ## Stages
33
33
 
34
- | # | Step | File | Auto-proceed |
35
- |---|------|------|--------------|
36
- | 1 | Initialize | references/init.md | Yes |
37
- | 1b | Continue (session resume) | references/continue.md | Yes |
38
- | 2 | Scan Project | references/scan-project.md | No (confirm) |
39
- | 3 | Identify Units | references/identify-units.md | No (confirm) |
40
- | 4 | Map & Detect | references/map-and-detect.md | Yes |
41
- | 5 | Recommend | references/recommend.md | No (confirm) |
42
- | 6 | Generate Briefs | references/generate-briefs.md | Yes |
43
- | 7 | Workflow Health Check | references/health-check.md | Yes |
34
+ | # | Step | File | Auto-proceed | Condition |
35
+ |---|------|------|--------------|-----------|
36
+ | 1 | Initialize | references/init.md | Yes | Always |
37
+ | 1a | Auto-Scope | references/step-auto-scope.md | Yes | `[auto]` mode only. Includes §0b pin resolution — validates and resolves version pin before scoping. Includes §0c coexistence detection — checks for existing skills before proceeding. Includes §0 docs-only URL detection — doc URLs short-circuit auto-scope entirely |
38
+ | 1b | Continue (session resume) | references/continue.md | Yes | Always |
39
+ | 2 | Scan Project | references/scan-project.md | No (confirm) | Interactive mode only |
40
+ | 3 | Identify Units | references/identify-units.md | No (confirm) | Interactive mode only |
41
+ | 4 | Map & Detect | references/map-and-detect.md | Yes | Interactive mode only |
42
+ | 5 | Recommend | references/recommend.md | No (confirm) | Interactive mode only |
43
+ | 6 | Generate Briefs | references/generate-briefs.md | Yes | Interactive mode only |
44
+ | 7 | Workflow Health Check | references/health-check.md | Yes | Always |
45
+
46
+ **Auto mode path:** When `[auto]` flag is present, init (step 1) routes directly to step 1a, which performs manifest scan → shape detection → scope generation → brief write → health check, bypassing steps 2–6. After URL type detection (§0), pin resolution (§0b) validates the `--pin` argument (or resolves the latest release tag) and stores the resolved ref for downstream brief writes. Coexistence detection (§0c) then checks for existing skills matching the target. If found, the user chooses alongside/merge/skip. Headless mode auto-selects alongside. Auto-scope may produce N > 1 confirmed units when a monorepo crosses the decomposition threshold (`package_count > 3`) **and** the §3b cohesion check decides to split, resulting in N briefs and N `brief_paths` in the envelope; a cohesive monorepo merges to one skill instead. A single large library is never decomposed — its size is handled by `skf-create-skill`'s auto-shard. When the target is a documentation URL (not a GitHub repo or local path), auto-scope detects the docs-only input at §0, validates URL reachability, writes a docs-only brief, and emits the envelope without performing source analysis.
47
+
48
+ **Shape detection reference:** `references/step-shape-detect.md` — loaded by step 1a as a reference doc (not a chained step).
44
49
 
45
50
  ## Invocation Contract
46
51
 
47
52
  | Aspect | Detail |
48
53
  |--------|--------|
49
- | **Inputs** | project_path [required], scope_hint [optional] |
50
- | **Headless inputs** | `--project-path <path>` (skip Step 1 project-path prompt), `--scope-hint <text>` (skip Step 1 scope-hint prompt), `--intent-hint <text>` (pre-supply analysis intent; drives recommendation ranking in Step 5) |
54
+ | **Inputs** | project_path [required], scope_hint [optional]. `project_path` can be a GitHub repo URL, a local filesystem path, or a documentation URL for docs-only mode. The URL type heuristic at §0 determines the mode. |
55
+ | **Headless inputs** | `--project-path <path>` (skip Step 1 project-path prompt; accepts documentation URLs for docs-only mode), `--scope-hint <text>` (skip Step 1 scope-hint prompt), `--intent-hint <text>` (pre-supply analysis intent; drives recommendation ranking in Step 5), `--pin <version>` (pin to a specific version tag or branch; accepts semver tags, git tags, and branch names; when absent, resolves to the latest release tag) |
51
56
  | **Headless flag** | `--headless` / `-H` flips every confirm gate to auto-proceed |
52
- | **Gates** | step 2: Confirm Gate [C] | step 3: Confirm Gate [C] | step 5: Confirm Gate [C] |
53
- | **Outputs** | analysis-report.md, skill-brief.yaml files (one per recommended unit); final `SKF_ANALYZE_RESULT_JSON` line on stdout when `{headless_mode}` is true |
57
+ | **Auto flag** | `[auto]` bracket modifier activates auto-scope mode (step 1a). Pipelines pass this as `AN[auto]`. When active, init routes to `step-auto-scope.md` which performs shape detection → scope generation → brief write, bypassing interactive steps 2–6. Requires `--project-path`. |
58
+ | **Gates** | step 2: Confirm Gate [C] | step 3: Confirm Gate [C] | step 5: Confirm Gate [C] (all skipped in auto mode) |
59
+ | **Outputs** | analysis-report.md, skill-brief.yaml files (one per recommended unit); final `SKF_ANALYZE_RESULT_JSON` line on stdout when `{headless_mode}` is true. In auto mode, the envelope includes `"mode":"auto"`. |
54
60
  | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
55
61
  | **Exit codes** | See "Exit Codes" below |
56
62
 
@@ -60,21 +66,21 @@ Every HARD HALT in this workflow exits with a stable code so headless automators
60
66
 
61
67
  | Code | Meaning | Raised by |
62
68
  | ---- | -------------------- | ------------------------------------------------------------------------------------------ |
63
- | 0 | success | step 7 (terminal — health check completion) |
64
- | 2 | input-missing | step 1 §2-3 — required config absent (config.yaml not loadable, project path empty/invalid in headless mode) |
65
- | 3 | resolution-failure | step 1 §2 (`forge-tier.yaml` missing at `{sidecar_path}/forge-tier.yaml`); step 1 §3 (project path does not exist or remote URL inaccessible) |
69
+ | 0 | success / skipped / redirect | step 7 (terminal — health check completion); also covers coexistence `"skipped"` and `"redirect"` statuses from §0c |
70
+ | 2 | input-missing | step 1 §2-3 — required config absent (config.yaml not loadable, project path empty/invalid in headless mode); step 1 §2b — auto mode without `--project-path` |
71
+ | 3 | resolution-failure | step 1 §2 (`forge-tier.yaml` missing at `{sidecar_path}/forge-tier.yaml`); step 1 §3 (project path does not exist or remote URL inaccessible); step 1a §0a (docs-only URL unreachable); step 1a §0b (pin validation failure — `halt_reason: "pin-invalid"` when the supplied `--pin` does not match any tag or branch); step 1a §3 (shape detection script error, exit code 2) |
66
72
  | 4 | write-failure | step 1 §6 (analysis report write failed); step 6 §5 (skill-brief.yaml write failed); step 6 §9 (result contract write failed) |
67
73
  | 6 | user-cancelled | any interactive menu in steps 2/3/5/6 (user selected `[X]` Cancel and exit) |
68
74
 
69
75
  ## Result Contract (Headless)
70
76
 
71
- When `{headless_mode}` is true, step 6 emits a single-line JSON envelope on **stdout** before chaining to step 7, and every HARD HALT emits the same envelope shape on **stderr** with `status: "error"`:
77
+ When `{headless_mode}` is true, step 6 (interactive) or step 1a (auto) emits a single-line JSON envelope on **stdout** before chaining to step 7, and every HARD HALT emits the same envelope shape on **stderr** with `status: "error"`:
72
78
 
73
79
  ```
74
- SKF_ANALYZE_RESULT_JSON: {"status":"success|error","report_path":"…|null","brief_paths":["…"],"unit_counts":{"confirmed":N,"skipped":N,"maybe":N},"exit_code":0,"halt_reason":null}
80
+ SKF_ANALYZE_RESULT_JSON: {"status":"success|error","report_path":"…|null","brief_paths":["…"],"unit_counts":{"confirmed":N,"skipped":N,"maybe":N},"exit_code":0,"halt_reason":null,"mode":"interactive|auto"}
75
81
  ```
76
82
 
77
- `status` is `"success"` on the terminal happy path, `"error"` on any HALT. `halt_reason` is one of: `null` (success), `"input-missing"`, `"forge-tier-missing"`, `"path-invalid"`, `"write-failed"`, `"user-cancelled"`. `exit_code` matches the table above. `brief_paths` is an array of absolute paths to every generated `skill-brief.yaml` (empty array if none were generated). `unit_counts` reports confirmed/skipped/maybe counts from step 5's user decisions.
83
+ `status` is `"success"` on the terminal happy path, `"error"` on any HALT, `"redirect"` when coexistence detection routes to US (merge), or `"skipped"` when the user skips a conflicting target. `halt_reason` is one of: `null` (success), `"input-missing"`, `"forge-tier-missing"`, `"path-invalid"`, `"pin-invalid"`, `"write-failed"`, `"user-cancelled"`. `exit_code` matches the table above. `brief_paths` is an array of absolute paths to every generated `skill-brief.yaml` (empty array if none were generated). `unit_counts` reports confirmed/skipped/maybe counts from step 5's user decisions. `mode` is `"auto"` when the `[auto]` flag was active, `"interactive"` otherwise (omitting `mode` is equivalent to `"interactive"` for backward compatibility). The `coexistence` field (present when §0c triggers) is `"alongside"`, `"merge"`, or `"skip"`, indicating the user's coexistence decision. The `pinned_ref` and `pinned_version` fields (present when §0b resolves a pin) record the resolved git ref and extracted semver version for provenance tracking.
78
84
 
79
85
  ## On Activation
80
86
 
@@ -23,7 +23,7 @@ Defines the output contract for skill-brief.yaml files generated by analyze-sour
23
23
  | Field | Type | Constraint | Description |
24
24
  |--------------------|--------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
25
25
  | source_type | string | `source` or `docs-only` | Default `source`. When `docs-only`: `source_repo` optional, `doc_urls` required |
26
- | doc_urls | array | `{url, label}` objects | Documentation URLs for T3 content. Required when `source_type: "docs-only"` |
26
+ | doc_urls | array | `{url, label, source?}` objects | Documentation URLs for T3 content. Required when `source_type: "docs-only"`. Optional `source` provenance enum: `language-registry` (registry-guaranteed corpus) \| `readme-detection` \| `homepage` \| `pages-api` \| `docs-folder` |
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. |
@@ -46,6 +46,36 @@ Look for {outputFile}.
46
46
 
47
47
  "**Forge tier detected:** {tier} — analysis depth will be calibrated accordingly."
48
48
 
49
+ ### 2b. Auto Mode Check
50
+
51
+ **Check for `[auto]` flag:** If `[auto]` was passed as a bracket modifier in the pipeline context (e.g., `AN[auto]`), set `{auto_mode}` = true.
52
+
53
+ **IF `{auto_mode}` is true:**
54
+
55
+ 1. **Resolve project path:** If `project_paths[]` is already populated (from §1 continuation detection or `--project-path` arg), use it. Otherwise, if `--project-path <path>` was passed at invocation, set `project_paths[]` from it (comma-split if multiple). If neither is available, HARD HALT with exit code 2 (`input-missing`): "**Auto mode requires `--project-path` — no project path available.**"
56
+ 2. **Validate the path(s):** For each provided path/URL, check that it exists (local) or is accessible (remote). If any invalid: HARD HALT with exit code 3 (`resolution-failure`): "**Path `{path}` doesn't appear to be valid.**"
57
+ 3. **Create the analysis report** from {templateFile}. Populate frontmatter:
58
+ ```yaml
59
+ stepsCompleted: ['init']
60
+ lastStep: 'init'
61
+ lastContinued: ''
62
+ date: '{current_date}'
63
+ user_name: '{user_name}'
64
+ project_name: '{project_name}'
65
+ project_paths: ['{provided_project_path}']
66
+ forge_tier: '{detected_tier}'
67
+ existing_skills: []
68
+ confirmed_units: []
69
+ stack_skill_candidates: []
70
+ nextWorkflow: ''
71
+ mode: 'auto'
72
+ ```
73
+ 4. "**Auto mode activated — bypassing interactive analysis.**"
74
+ 5. **Route to auto-scope:** Load, read fully, then execute `references/step-auto-scope.md`. **STOP HERE** — do not continue to §3 or any subsequent section.
75
+
76
+ **IF `{auto_mode}` is NOT true:**
77
+ Continue to §3 as normal — the entire interactive flow below is unchanged.
78
+
49
79
  ### 3. Collect Project Path
50
80
 
51
81
  **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.