bmad-module-skill-forge 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/docs/skill-model.md +26 -32
- package/docs/troubleshooting.md +12 -0
- package/docs/workflows.md +53 -0
- package/package.json +2 -2
- package/src/shared/references/output-contract-schema.md +10 -0
- package/src/shared/scripts/skf-extract-public-api.py +505 -0
- package/src/shared/scripts/skf-render-quick-metadata.py +192 -0
- package/src/shared/scripts/skf-resolve-package.py +264 -0
- package/src/shared/scripts/skf-validate-output.py +24 -7
- package/src/skf-quick-skill/SKILL.md +178 -10
- package/src/skf-quick-skill/assets/skill-template.md +5 -1
- package/src/skf-quick-skill/references/registry-resolution.md +2 -0
- package/src/skf-quick-skill/steps-c/step-01-resolve-target.md +84 -16
- package/src/skf-quick-skill/steps-c/step-02-ecosystem-check.md +3 -3
- package/src/skf-quick-skill/steps-c/step-03-quick-extract.md +86 -43
- package/src/skf-quick-skill/steps-c/step-04-compile.md +49 -56
- package/src/skf-quick-skill/steps-c/step-05-write-and-validate.md +164 -0
- package/src/skf-quick-skill/steps-c/{step-06-write.md → step-06-finalize.md} +15 -7
- package/src/skf-quick-skill/steps-c/step-07-health-check.md +5 -3
- package/src/skf-quick-skill/steps-c/step-05-validate.md +0 -193
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Resolve Package — resolve a package name to a GitHub repository URL.
|
|
6
|
+
|
|
7
|
+
Walks the canonical fallback chain documented in
|
|
8
|
+
`src/skf-quick-skill/references/registry-resolution.md`:
|
|
9
|
+
|
|
10
|
+
1. npm registry (JavaScript/TypeScript)
|
|
11
|
+
2. PyPI registry (Python)
|
|
12
|
+
3. crates.io registry (Rust)
|
|
13
|
+
|
|
14
|
+
Per-call timeout (default 10s); a timeout is treated as a soft failure
|
|
15
|
+
and the resolver falls through to the next entry. Web-search fallback
|
|
16
|
+
is intentionally NOT in this helper — registries are deterministic;
|
|
17
|
+
web search is judgment, and stays in the LLM step.
|
|
18
|
+
|
|
19
|
+
CLI:
|
|
20
|
+
|
|
21
|
+
python3 skf-resolve-package.py <package_name> [--timeout 10]
|
|
22
|
+
|
|
23
|
+
Output JSON (stdout):
|
|
24
|
+
|
|
25
|
+
status: "ok" | "fallthrough"
|
|
26
|
+
package_name: "<input>"
|
|
27
|
+
resolved_url: "https://github.com/<owner>/<repo>" (when status == "ok")
|
|
28
|
+
repo_owner: "<owner>" (when status == "ok")
|
|
29
|
+
repo_name: "<repo>" (when status == "ok")
|
|
30
|
+
registry_used: "npm" | "pypi" | "crates" (when status == "ok")
|
|
31
|
+
registries_tried: ["npm", ...]
|
|
32
|
+
registry_outcomes: {"npm": "ok|404|timeout|error|no-github-link", ...}
|
|
33
|
+
|
|
34
|
+
Exit codes:
|
|
35
|
+
|
|
36
|
+
0 status == "ok"
|
|
37
|
+
1 status == "fallthrough" — every registry replied without a GitHub
|
|
38
|
+
URL; the LLM step should fall back to web search.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import json
|
|
45
|
+
import re
|
|
46
|
+
import socket
|
|
47
|
+
import sys
|
|
48
|
+
import urllib.error
|
|
49
|
+
import urllib.parse
|
|
50
|
+
import urllib.request
|
|
51
|
+
from typing import Optional
|
|
52
|
+
|
|
53
|
+
REGISTRY_TIMEOUT_SECONDS = 10.0
|
|
54
|
+
USER_AGENT = "skf-resolve-package/1.0 (+https://github.com/armelhbobdad/bmad-module-skill-forge)"
|
|
55
|
+
|
|
56
|
+
_GITHUB_URL_RE = re.compile(
|
|
57
|
+
r"https?://(?:www\.)?github\.com/([^/\s]+)/([^/\s.]+?)(?:\.git)?/?$",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_github_url(url: str) -> Optional[tuple[str, str, str]]:
|
|
63
|
+
"""Parse a GitHub URL/string into (canonical_url, owner, repo).
|
|
64
|
+
|
|
65
|
+
Handles the variants registries actually emit:
|
|
66
|
+
- https://github.com/owner/repo
|
|
67
|
+
- http://github.com/owner/repo
|
|
68
|
+
- github.com/owner/repo
|
|
69
|
+
- git+https://github.com/owner/repo.git
|
|
70
|
+
- git@github.com:owner/repo.git
|
|
71
|
+
- github:owner/repo (npm shortcut)
|
|
72
|
+
"""
|
|
73
|
+
if not url or not isinstance(url, str):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
s = url.strip()
|
|
77
|
+
|
|
78
|
+
if s.startswith("github:"):
|
|
79
|
+
rest = s[len("github:") :]
|
|
80
|
+
if "/" in rest:
|
|
81
|
+
owner, _, repo = rest.partition("/")
|
|
82
|
+
repo = repo.removesuffix(".git").rstrip("/")
|
|
83
|
+
if owner and repo:
|
|
84
|
+
return f"https://github.com/{owner}/{repo}", owner, repo
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
if s.startswith("git+"):
|
|
88
|
+
s = s[len("git+") :]
|
|
89
|
+
|
|
90
|
+
if s.startswith("git@github.com:"):
|
|
91
|
+
rest = s[len("git@github.com:") :]
|
|
92
|
+
rest = rest.removesuffix(".git").rstrip("/")
|
|
93
|
+
if "/" in rest:
|
|
94
|
+
owner, _, repo = rest.partition("/")
|
|
95
|
+
if owner and repo:
|
|
96
|
+
return f"https://github.com/{owner}/{repo}", owner, repo
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if s.startswith("github.com/"):
|
|
100
|
+
s = "https://" + s
|
|
101
|
+
|
|
102
|
+
m = _GITHUB_URL_RE.match(s)
|
|
103
|
+
if m:
|
|
104
|
+
owner, repo = m.group(1), m.group(2)
|
|
105
|
+
return f"https://github.com/{owner}/{repo}", owner, repo
|
|
106
|
+
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _http_get_json(url: str, timeout: float) -> tuple[Optional[dict], str]:
|
|
111
|
+
"""Fetch JSON. Returns (payload_or_None, outcome).
|
|
112
|
+
|
|
113
|
+
Outcome values:
|
|
114
|
+
"ok" — valid JSON object returned
|
|
115
|
+
"404" — HTTP 404 (package does not exist on this registry)
|
|
116
|
+
"timeout" — socket / urlopen timed out
|
|
117
|
+
"error" — any other transport / parse error
|
|
118
|
+
"""
|
|
119
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": USER_AGENT})
|
|
120
|
+
try:
|
|
121
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
122
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
123
|
+
if isinstance(payload, dict):
|
|
124
|
+
return payload, "ok"
|
|
125
|
+
return None, "error"
|
|
126
|
+
except urllib.error.HTTPError as e:
|
|
127
|
+
return None, "404" if e.code == 404 else "error"
|
|
128
|
+
except urllib.error.URLError as e:
|
|
129
|
+
if isinstance(getattr(e, "reason", None), (socket.timeout, TimeoutError)):
|
|
130
|
+
return None, "timeout"
|
|
131
|
+
return None, "error"
|
|
132
|
+
except (TimeoutError, socket.timeout):
|
|
133
|
+
return None, "timeout"
|
|
134
|
+
except (ValueError, json.JSONDecodeError):
|
|
135
|
+
return None, "error"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def try_npm(package_name: str, timeout: float) -> tuple[Optional[tuple[str, str, str]], str]:
|
|
139
|
+
"""Try the npm registry. Returns (parsed_or_None, outcome)."""
|
|
140
|
+
encoded = urllib.parse.quote(package_name, safe="@")
|
|
141
|
+
url = f"https://registry.npmjs.org/{encoded}"
|
|
142
|
+
payload, outcome = _http_get_json(url, timeout)
|
|
143
|
+
if payload is None:
|
|
144
|
+
return None, outcome
|
|
145
|
+
candidates: list[str] = []
|
|
146
|
+
repo = payload.get("repository")
|
|
147
|
+
if isinstance(repo, dict) and isinstance(repo.get("url"), str):
|
|
148
|
+
candidates.append(repo["url"])
|
|
149
|
+
elif isinstance(repo, str):
|
|
150
|
+
candidates.append(repo)
|
|
151
|
+
homepage = payload.get("homepage")
|
|
152
|
+
if isinstance(homepage, str):
|
|
153
|
+
candidates.append(homepage)
|
|
154
|
+
for c in candidates:
|
|
155
|
+
parsed = parse_github_url(c)
|
|
156
|
+
if parsed:
|
|
157
|
+
return parsed, "ok"
|
|
158
|
+
return None, "no-github-link"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def try_pypi(package_name: str, timeout: float) -> tuple[Optional[tuple[str, str, str]], str]:
|
|
162
|
+
"""Try the PyPI registry. Returns (parsed_or_None, outcome)."""
|
|
163
|
+
encoded = urllib.parse.quote(package_name, safe="")
|
|
164
|
+
url = f"https://pypi.org/pypi/{encoded}/json"
|
|
165
|
+
payload, outcome = _http_get_json(url, timeout)
|
|
166
|
+
if payload is None:
|
|
167
|
+
return None, outcome
|
|
168
|
+
info = payload.get("info") or {}
|
|
169
|
+
candidates: list[str] = []
|
|
170
|
+
project_urls = info.get("project_urls") or {}
|
|
171
|
+
if isinstance(project_urls, dict):
|
|
172
|
+
for key in ("Source", "Source Code", "Repository", "Homepage"):
|
|
173
|
+
v = project_urls.get(key)
|
|
174
|
+
if isinstance(v, str):
|
|
175
|
+
candidates.append(v)
|
|
176
|
+
home_page = info.get("home_page")
|
|
177
|
+
if isinstance(home_page, str):
|
|
178
|
+
candidates.append(home_page)
|
|
179
|
+
for c in candidates:
|
|
180
|
+
parsed = parse_github_url(c)
|
|
181
|
+
if parsed:
|
|
182
|
+
return parsed, "ok"
|
|
183
|
+
return None, "no-github-link"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def try_crates(package_name: str, timeout: float) -> tuple[Optional[tuple[str, str, str]], str]:
|
|
187
|
+
"""Try the crates.io registry. Returns (parsed_or_None, outcome)."""
|
|
188
|
+
encoded = urllib.parse.quote(package_name, safe="")
|
|
189
|
+
url = f"https://crates.io/api/v1/crates/{encoded}"
|
|
190
|
+
payload, outcome = _http_get_json(url, timeout)
|
|
191
|
+
if payload is None:
|
|
192
|
+
return None, outcome
|
|
193
|
+
crate = payload.get("crate") or {}
|
|
194
|
+
for key in ("repository", "homepage"):
|
|
195
|
+
v = crate.get(key)
|
|
196
|
+
if isinstance(v, str):
|
|
197
|
+
parsed = parse_github_url(v)
|
|
198
|
+
if parsed:
|
|
199
|
+
return parsed, "ok"
|
|
200
|
+
return None, "no-github-link"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
_RESOLVER_NAMES: tuple[tuple[str, str], ...] = (
|
|
204
|
+
("npm", "try_npm"),
|
|
205
|
+
("pypi", "try_pypi"),
|
|
206
|
+
("crates", "try_crates"),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def resolve_package(package_name: str, timeout: float = REGISTRY_TIMEOUT_SECONDS) -> dict:
|
|
211
|
+
registries_tried: list[str] = []
|
|
212
|
+
outcomes: dict[str, str] = {}
|
|
213
|
+
|
|
214
|
+
# Dynamic lookup so test code can monkeypatch try_npm / try_pypi / try_crates
|
|
215
|
+
# without re-binding entries in a module-level tuple of function refs.
|
|
216
|
+
for name, fn_name in _RESOLVER_NAMES:
|
|
217
|
+
registries_tried.append(name)
|
|
218
|
+
fn = globals()[fn_name]
|
|
219
|
+
result, outcome = fn(package_name, timeout)
|
|
220
|
+
outcomes[name] = outcome
|
|
221
|
+
if result is not None:
|
|
222
|
+
url, owner, repo = result
|
|
223
|
+
return {
|
|
224
|
+
"status": "ok",
|
|
225
|
+
"package_name": package_name,
|
|
226
|
+
"resolved_url": url,
|
|
227
|
+
"repo_owner": owner,
|
|
228
|
+
"repo_name": repo,
|
|
229
|
+
"registry_used": name,
|
|
230
|
+
"registries_tried": registries_tried,
|
|
231
|
+
"registry_outcomes": outcomes,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
"status": "fallthrough",
|
|
236
|
+
"package_name": package_name,
|
|
237
|
+
"registries_tried": registries_tried,
|
|
238
|
+
"registry_outcomes": outcomes,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main(argv: list[str]) -> int:
|
|
243
|
+
parser = argparse.ArgumentParser(
|
|
244
|
+
description="Resolve a package name to a GitHub repository URL via npm/PyPI/crates.io.",
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument(
|
|
247
|
+
"package_name",
|
|
248
|
+
help="Package name to resolve (e.g., lodash, @tanstack/query, requests, serde).",
|
|
249
|
+
)
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--timeout",
|
|
252
|
+
type=float,
|
|
253
|
+
default=REGISTRY_TIMEOUT_SECONDS,
|
|
254
|
+
help=f"Per-registry timeout in seconds (default: {REGISTRY_TIMEOUT_SECONDS}).",
|
|
255
|
+
)
|
|
256
|
+
args = parser.parse_args(argv)
|
|
257
|
+
|
|
258
|
+
result = resolve_package(args.package_name, timeout=args.timeout)
|
|
259
|
+
print(json.dumps(result, indent=2))
|
|
260
|
+
return 0 if result["status"] == "ok" else 1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -9,6 +9,7 @@ schema against agentskills.io specification. Outputs JSON validation results.
|
|
|
9
9
|
|
|
10
10
|
CLI: python3 skf-validate-output.py <skill-package-dir>
|
|
11
11
|
python3 skf-validate-output.py <skill-package-dir> --generated-by quick-skill
|
|
12
|
+
python3 skf-validate-output.py <skill-package-dir> --skip-frontmatter
|
|
12
13
|
"""
|
|
13
14
|
|
|
14
15
|
from __future__ import annotations
|
|
@@ -73,7 +74,7 @@ def validate_body_structure(content):
|
|
|
73
74
|
issues = []
|
|
74
75
|
body = content.split("---", 2)[-1] if content.startswith("---") else content
|
|
75
76
|
|
|
76
|
-
required_sections = ["Overview", "Key Exports", "Usage"]
|
|
77
|
+
required_sections = ["Overview", "Description", "Key Exports", "Usage"]
|
|
77
78
|
for section in required_sections:
|
|
78
79
|
pattern = rf"^##\s+.*{re.escape(section)}"
|
|
79
80
|
if not re.search(pattern, body, re.MULTILINE | re.IGNORECASE):
|
|
@@ -155,8 +156,13 @@ def validate_metadata_json(data, generated_by=None):
|
|
|
155
156
|
return issues
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
def validate_skill_package(skill_dir, generated_by=None):
|
|
159
|
-
"""Validate a complete skill package directory.
|
|
159
|
+
def validate_skill_package(skill_dir, generated_by=None, skip_frontmatter=False):
|
|
160
|
+
"""Validate a complete skill package directory.
|
|
161
|
+
|
|
162
|
+
When `skip_frontmatter` is True, the SKILL.md frontmatter pass is omitted —
|
|
163
|
+
intended for callers that already validated frontmatter via skill-check or
|
|
164
|
+
skf-validate-frontmatter.py and only want body / snippet / metadata checks.
|
|
165
|
+
"""
|
|
160
166
|
skill_dir = Path(skill_dir)
|
|
161
167
|
skill_name = skill_dir.name
|
|
162
168
|
|
|
@@ -183,9 +189,14 @@ def validate_skill_package(skill_dir, generated_by=None):
|
|
|
183
189
|
skill_md_path = files["SKILL.md"]
|
|
184
190
|
if skill_md_path.exists():
|
|
185
191
|
content = skill_md_path.read_text(encoding="utf-8")
|
|
186
|
-
|
|
192
|
+
if skip_frontmatter:
|
|
193
|
+
fm_section = {"skipped": "frontmatter validation skipped (--skip-frontmatter)"}
|
|
194
|
+
fm_issues = []
|
|
195
|
+
else:
|
|
196
|
+
fm_issues = validate_frontmatter(content, skill_name)
|
|
197
|
+
fm_section = fm_issues
|
|
187
198
|
body_issues = validate_body_structure(content)
|
|
188
|
-
result["validation"]["skill_md"] = {"frontmatter":
|
|
199
|
+
result["validation"]["skill_md"] = {"frontmatter": fm_section, "body": body_issues}
|
|
189
200
|
for issue in fm_issues + body_issues:
|
|
190
201
|
result["summary"]["total_issues"] += 1
|
|
191
202
|
result["summary"]["by_severity"][issue["severity"]] += 1
|
|
@@ -232,7 +243,11 @@ def validate_skill_package(skill_dir, generated_by=None):
|
|
|
232
243
|
|
|
233
244
|
if __name__ == "__main__":
|
|
234
245
|
if len(sys.argv) < 2:
|
|
235
|
-
print(
|
|
246
|
+
print(
|
|
247
|
+
"Usage: python3 skf-validate-output.py <skill-package-dir> "
|
|
248
|
+
"[--generated-by <generator>] [--skip-frontmatter]",
|
|
249
|
+
file=sys.stderr,
|
|
250
|
+
)
|
|
236
251
|
sys.exit(1)
|
|
237
252
|
|
|
238
253
|
pkg_dir = sys.argv[1]
|
|
@@ -242,6 +257,8 @@ if __name__ == "__main__":
|
|
|
242
257
|
if idx + 1 < len(sys.argv):
|
|
243
258
|
gen_by = sys.argv[idx + 1]
|
|
244
259
|
|
|
245
|
-
|
|
260
|
+
skip_fm = "--skip-frontmatter" in sys.argv
|
|
261
|
+
|
|
262
|
+
result = validate_skill_package(pkg_dir, generated_by=gen_by, skip_frontmatter=skip_fm)
|
|
246
263
|
print(json.dumps(result, indent=2))
|
|
247
264
|
sys.exit(0 if result["result"] == "PASS" else 1)
|
|
@@ -23,6 +23,12 @@ These rules apply to every step in this workflow:
|
|
|
23
23
|
- Only load one step file at a time — never preload future steps
|
|
24
24
|
- Always communicate in `{communication_language}`
|
|
25
25
|
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
|
|
26
|
+
- If `{headless_mode}` is true, emit a single-line JSON progress event to **stderr** at each step's entry and exit so pipeline schedulers can stream live progress instead of post-mortem-parsing the result contract:
|
|
27
|
+
- entry: `{"step":N,"name":"<slug>","status":"start"}`
|
|
28
|
+
- exit (just before chaining to nextStepFile): `{"step":N,"name":"<slug>","status":"done"}`
|
|
29
|
+
- on HARD HALT: `{"step":N,"name":"<slug>","status":"halt","exit":<code>}` instead of "done"
|
|
30
|
+
|
|
31
|
+
`N` is the step number (1–7) and `<slug>` is the kebab portion of the filename after the number — `resolve-target`, `ecosystem-check`, `quick-extract`, `compile`, `write-and-validate`, `finalize`, `health-check`. One line per event; do not pretty-print.
|
|
26
32
|
|
|
27
33
|
## Stages
|
|
28
34
|
|
|
@@ -32,25 +38,187 @@ These rules apply to every step in this workflow:
|
|
|
32
38
|
| 2 | Ecosystem Check | steps-c/step-02-ecosystem-check.md | Yes |
|
|
33
39
|
| 3 | Quick Extract | steps-c/step-03-quick-extract.md | Yes |
|
|
34
40
|
| 4 | Compile | steps-c/step-04-compile.md | No (review) |
|
|
35
|
-
| 5 | Write & Validate | steps-c/step-05-validate.md | Yes |
|
|
36
|
-
| 6 | Finalize | steps-c/step-06-
|
|
41
|
+
| 5 | Write & Validate | steps-c/step-05-write-and-validate.md | Yes |
|
|
42
|
+
| 6 | Finalize | steps-c/step-06-finalize.md | Yes |
|
|
37
43
|
| 7 | Workflow Health Check | steps-c/step-07-health-check.md | Yes |
|
|
38
44
|
|
|
39
45
|
## Invocation Contract
|
|
40
46
|
|
|
41
47
|
| Aspect | Detail |
|
|
42
48
|
|--------|--------|
|
|
43
|
-
| **Inputs** | target (GitHub URL or package name) [required], language_hint [optional], scope_hint [optional] |
|
|
44
|
-
| **
|
|
45
|
-
| **
|
|
49
|
+
| **Inputs** | target (GitHub URL or package name) [required for single-target mode], language_hint [optional], scope_hint [optional] |
|
|
50
|
+
| **Overrides** | `--description`, `--exports`, `--skip-snippet`, `--no-active-pointer`, `--batch <file>`, `--fail-fast` — see On Activation step 3 |
|
|
51
|
+
| **Gates** | step-01: Input Gate [use args]; step-02: Choice Gate [P] (if match); step-04: Review Gate [C/E/S/Q] |
|
|
52
|
+
| **Outputs** | SKILL.md, context-snippet.md, metadata.json, active pointer, result contract (timestamped + `-latest` copy). Snippet and active pointer can be skipped per overrides. |
|
|
46
53
|
| **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
|
|
54
|
+
| **Exit codes** | See "Exit Codes" below |
|
|
55
|
+
|
|
56
|
+
## Exit Codes
|
|
57
|
+
|
|
58
|
+
Every HARD HALT in this workflow exits with a stable, documented code so headless automators can branch on the failure class without grepping message text:
|
|
59
|
+
|
|
60
|
+
| Code | Meaning | Raised by |
|
|
61
|
+
| ---- | ---------------------- | ----------------------------------------------------------- |
|
|
62
|
+
| 0 | success | step-07 (terminal) |
|
|
63
|
+
| 3 | resolution-failure | step-01 §2c (prose input), step-01 §3 (registry chain failed) |
|
|
64
|
+
| 4 | write-failure | step-05 §2 (deliverable write failed) |
|
|
65
|
+
| 5 | overwrite-cancelled | step-05 §1 (user selected [N]) |
|
|
66
|
+
| 6 | compile-cancelled | step-04 §6 (user selected [Q]) |
|
|
67
|
+
| 7 | finalize-blocked | step-06 §1 (active-pointer flip refused — non-link in place) |
|
|
68
|
+
|
|
69
|
+
Reserved: `validator-missing` may be promoted from advisory log to fatal exit code in a future revision; consumers should not assume code 8+ is unused.
|
|
70
|
+
|
|
71
|
+
## Result Contract on HARD HALT
|
|
72
|
+
|
|
73
|
+
In addition to the success-variant result contract written by step-06 §3, every HARD HALT must surface an **error variant** so headless automators don't silently break when `quick-skill-result-latest.json` is missing on failed runs.
|
|
74
|
+
|
|
75
|
+
**Always (every HARD HALT, regardless of phase)** — emit a single line on **stderr**:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
SKF_QUICK_SKILL_RESULT_JSON: {"status":"error","exit_code":<N>,"phase":"<slug>","error":{"code":"<class>","message":"<short>"},"outputs":{},"summary":{},"skill_package":"<path-or-null>"}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
One line, no pretty-print. Matches the prefix-and-envelope convention used by `skf-emit-result-envelope.py`.
|
|
82
|
+
|
|
83
|
+
**Additionally, when `{skill_package}` is known** (HALT at step-05 §1 onward) — write the same JSON object (without the `SKF_QUICK_SKILL_RESULT_JSON: ` prefix) to disk:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
{skill_package}/quick-skill-result-{YYYYMMDD-HHmmss}.json
|
|
87
|
+
{skill_package}/quick-skill-result-latest.json (copy, not symlink)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
so consumers that hardcode the `-latest.json` path see a deterministic file even on failed runs. HALTs at step-01/02/03/04 cannot write to disk because `{skill_package}` is computed only in step-05 §1; for those, the stderr envelope plus exit code is the contract.
|
|
91
|
+
|
|
92
|
+
**Schema:**
|
|
93
|
+
|
|
94
|
+
| Field | Type | Notes |
|
|
95
|
+
| --------------- | -------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
96
|
+
| `status` | string | always `"error"` for HARD HALTs |
|
|
97
|
+
| `exit_code` | integer | matches the Exit Codes table |
|
|
98
|
+
| `phase` | string | step slug where the HALT occurred (e.g. `resolve-target`, `compile`) |
|
|
99
|
+
| `error.code` | string | one of: `resolution-failure`, `write-failure`, `overwrite-cancelled`, `compile-cancelled`, `finalize-blocked` |
|
|
100
|
+
| `error.message` | string | the user-facing message that was displayed |
|
|
101
|
+
| `error.details` | any | optional — phase-specific context (e.g. the failed file path) |
|
|
102
|
+
| `outputs` | object | empty `{}` on early HALTs; partial when files were already written |
|
|
103
|
+
| `summary` | object | empty `{}` on early HALTs |
|
|
104
|
+
| `skill_package` | string \| null | absolute path when known, `null` when HALT preceded step-05 §1 |
|
|
47
105
|
|
|
48
106
|
## On Activation
|
|
49
107
|
|
|
50
|
-
1.
|
|
51
|
-
- `project_name`, `output_folder`, `user_name`, `communication_language`, `document_output_language`
|
|
52
|
-
-
|
|
108
|
+
1. Read `{project-root}/_bmad/skf/config.yaml` and `{forger_root}/preferences.yaml` in parallel (one batched tool-call message — they are independent files), then resolve:
|
|
109
|
+
- From config: `project_name`, `output_folder`, `user_name`, `communication_language`, `document_output_language`, `skills_output_folder`, `forge_data_folder`
|
|
110
|
+
- From preferences: `headless_mode` (default false)
|
|
111
|
+
|
|
112
|
+
2. **Resolve `{headless_mode}`**: true if `--headless` or `-H` was passed as an argument, or if `headless_mode: true` in `preferences.yaml`. Default: false.
|
|
113
|
+
|
|
114
|
+
3. **Parse CLI overrides** — capture optional override flags into the workflow context as `{overrides}`. Each override is opt-in; when omitted, the workflow runs as today.
|
|
115
|
+
|
|
116
|
+
| Flag | Effect |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `--description "<string>"` | Override the LLM-derived description in step-04 §2 (used in SKILL.md frontmatter and metadata.json). Subject to the same agentskills.io length (1–1024 chars) and voice (third-person) checks as extracted descriptions. |
|
|
119
|
+
| `--exports "<name1,name2,...>"` | Override the extracted export list. Parse as comma-separated; trim whitespace per item; skip empty items. Used in step-04 §2 Key Exports and the count-derived metadata stats. |
|
|
120
|
+
| `--skip-snippet` | Skip context-snippet.md generation in step-04 §3 and its write in step-05 §2. Artifact omitted from `outputs`; step-05 §5 advisory snippet validation reports a "skipped" entry. |
|
|
121
|
+
| `--no-active-pointer` | Skip the active-pointer flip in step-06 §1. Deliverables still land in `{skill_package}` but `{skill_group}/active` is not updated. Useful for batch automators that flip pointers in a separate stage. |
|
|
122
|
+
| `--batch <file>` | Run the workflow against a list of targets from a text file rather than a single argument. Implies `--headless` (gates cannot be human-driven across N targets). See "Batch Mode" below for input format and summary contract. Single-target overrides above apply globally to every target in the batch. |
|
|
123
|
+
| `--fail-fast` | Only meaningful with `--batch`. Abort the whole batch on the first per-target failure instead of recording the failure in the summary and proceeding to the next target. |
|
|
124
|
+
|
|
125
|
+
4. **If `--batch` is set**, force `{headless_mode} = true` (log "headless: coerced by --batch" if it was false), read the batch file, and parse the target list per "Batch Mode" below. Continue at step 5; the batch loop documented in "Batch Mode" wraps the step-01 → step-07 pipeline that follows.
|
|
126
|
+
|
|
127
|
+
5. Load, read the full file, and then execute `./steps-c/step-01-resolve-target.md` to begin the workflow. (In batch mode, control returns here for each subsequent target after step-07 completes; see "Batch Mode" below.)
|
|
128
|
+
|
|
129
|
+
## Batch Mode
|
|
130
|
+
|
|
131
|
+
When `--batch <file>` is supplied, quick-skill processes a list of targets from a text file in sequence rather than a single target from arguments. Designed for unattended bulk runs (CI pipelines, mass-rebuilds, the skf-batch-skills meta-workflow when it lands).
|
|
132
|
+
|
|
133
|
+
### Input format
|
|
134
|
+
|
|
135
|
+
One target per line. Empty lines and lines starting with `#` (after optional leading whitespace) are ignored. Each non-empty line has the same shape as the single-target `target` argument, with optional space-separated per-line modifiers:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
# A batch input file.
|
|
139
|
+
lodash
|
|
140
|
+
@vercel/og
|
|
141
|
+
cognee@0.5.0
|
|
142
|
+
https://github.com/foo/bar
|
|
143
|
+
https://github.com/foo/bar@2.1.0-beta
|
|
144
|
+
|
|
145
|
+
# Per-line modifiers — overrides for THIS target only:
|
|
146
|
+
lodash language=javascript scope=src/
|
|
147
|
+
cognee@0.5.0 language=python scope=cognee/api/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Recognised per-line modifiers:
|
|
151
|
+
|
|
152
|
+
| Modifier | Effect (this target only) |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `language=<lang>` | Sets `language_hint` for this target — same effect as the optional `language_hint` input on a single-target run. |
|
|
155
|
+
| `scope=<path>` | Sets `scope_hint` for this target — same effect as the optional `scope_hint` input on a single-target run. |
|
|
156
|
+
|
|
157
|
+
Per-line modifiers shadow the global `--description` / `--exports` / `--skip-snippet` / `--no-active-pointer` overrides only when those override fields are not set. Global overrides apply to every target unless a future modifier extends per-line override syntax.
|
|
158
|
+
|
|
159
|
+
### Execution
|
|
160
|
+
|
|
161
|
+
`--batch` implies `--headless`. The batch loop runs the full quick-skill pipeline (steps 1–7) for each target in file order:
|
|
162
|
+
|
|
163
|
+
1. Set `target`, `target_version`, `language_hint`, `scope_hint` from the batch line into the workflow context.
|
|
164
|
+
2. Execute steps 1–7 per the normal pipeline.
|
|
165
|
+
3. After step-07 completes (success or HARD HALT), record the per-target outcome (target, status, exit_code, skill_package, error.code) into the batch result list.
|
|
166
|
+
4. If `--fail-fast` is set and the target failed, exit the batch loop immediately. Otherwise continue with the next target.
|
|
167
|
+
|
|
168
|
+
Per-target output lands in `{skill_package}/` as today, with the per-target result contract at `{skill_package}/quick-skill-result-latest.json` (success or error variant per "Result Contract on HARD HALT" above).
|
|
169
|
+
|
|
170
|
+
### Batch summary contract
|
|
171
|
+
|
|
172
|
+
After the last target completes (or `--fail-fast` triggers an early exit), write the batch summary at:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
{skills_output_folder}/_batch/quick-skill-batch-{YYYYMMDD-HHmmss}.json
|
|
176
|
+
{skills_output_folder}/_batch/quick-skill-batch-latest.json (copy, not symlink)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Schema:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"skill": "skf-quick-skill",
|
|
184
|
+
"mode": "batch",
|
|
185
|
+
"status": "success | partial | failed",
|
|
186
|
+
"timestamp": "<ISO 8601 UTC>",
|
|
187
|
+
"input_file": "<path passed to --batch>",
|
|
188
|
+
"targets_total": 0,
|
|
189
|
+
"succeeded": 0,
|
|
190
|
+
"failed": 0,
|
|
191
|
+
"fail_fast_triggered": false,
|
|
192
|
+
"results": [
|
|
193
|
+
{
|
|
194
|
+
"target": "<line from batch file>",
|
|
195
|
+
"status": "success | error",
|
|
196
|
+
"exit_code": 0,
|
|
197
|
+
"skill_package": "<absolute path or null>",
|
|
198
|
+
"error_code": null
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`status` resolves as: `"success"` when `failed == 0`; `"partial"` when `failed > 0 && succeeded > 0`; `"failed"` when `succeeded == 0`. `fail_fast_triggered` is `true` only when `--fail-fast` aborted the loop early — `targets_total` then reflects the count actually attempted, not the file's line count.
|
|
205
|
+
|
|
206
|
+
### Headless events
|
|
207
|
+
|
|
208
|
+
Batch mode emits per-target boundary events on stderr in addition to the per-step events documented in Workflow Rules:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
{"batch":<n>,"target":"<target>","status":"start"}
|
|
212
|
+
{"batch":<n>,"target":"<target>","status":"done","exit":<code>}
|
|
213
|
+
{"batch":<n>,"target":"<target>","status":"fail","exit":<code>,"error_code":"<class>"}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`<n>` is the 1-based index of the target in the parsed list. After the loop ends, emit one final batch-summary event:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
{"batch_summary":true,"targets_total":N,"succeeded":K,"failed":M,"status":"<...>","fail_fast_triggered":<bool>}
|
|
220
|
+
```
|
|
53
221
|
|
|
54
|
-
|
|
222
|
+
### Exit code
|
|
55
223
|
|
|
56
|
-
|
|
224
|
+
The batch process exits with code `0` when `failed == 0`, otherwise with the exit code of the first failed target (so automators that already branch on the single-target exit-code map continue to work without batch-specific handling). When `--fail-fast` triggers, the exit code is the failing target's code.
|
|
@@ -101,6 +101,10 @@ Indexed format targeting ~80-120 tokens per skill:
|
|
|
101
101
|
"assets_count": 0
|
|
102
102
|
},
|
|
103
103
|
"dependencies": [],
|
|
104
|
-
"compatibility": "{semver-range}"
|
|
104
|
+
"compatibility": "{semver-range}",
|
|
105
|
+
"provenance": {
|
|
106
|
+
"language_hint": "{language_hint or null}",
|
|
107
|
+
"scope_hint": "{scope_hint or null}"
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
```
|
|
@@ -13,6 +13,8 @@ When the user provides a package name instead of a GitHub URL, use this fallback
|
|
|
13
13
|
|
|
14
14
|
Try each registry in order. Stop at first success.
|
|
15
15
|
|
|
16
|
+
**Per-call timeout:** apply a 10s timeout to each registry HTTP call (15s for the web-search fallback) so a single hung registry cannot stall the workflow under hostile network conditions. Treat a timeout as a soft failure and fall through to the next entry in the chain.
|
|
17
|
+
|
|
16
18
|
#### 1. npm Registry (JavaScript/TypeScript)
|
|
17
19
|
|
|
18
20
|
```
|