bmad-module-skill-forge 1.5.1 → 1.6.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/package.json +1 -1
- package/src/shared/scripts/skf-description-guard.py +33 -80
- package/src/skf-analyze-source/references/continue.md +3 -1
- package/src/skf-brief-skill/SKILL.md +2 -0
- package/src/skf-brief-skill/references/gather-intent.md +60 -1
- package/src/skf-brief-skill/references/write-brief.md +5 -1
- package/src/skf-create-skill/assets/compile-assembly-rules.md +1 -1
- package/src/skf-create-skill/assets/skill-sections.md +1 -1
- package/src/skf-test-skill/SKILL.md +1 -1
- package/src/skf-update-skill/references/init.md +8 -1
package/docs/_data/pinned.yaml
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# Enforced two ways: (1) release.yaml bumps this line in the same commit
|
|
27
27
|
# as package.json + marketplace.json on every release; (2) validate-docs-drift.js
|
|
28
28
|
# cross-checks this value against package.json.version and fails on mismatch.
|
|
29
|
-
skf_version: "1.
|
|
29
|
+
skf_version: "1.6.0"
|
|
30
30
|
|
|
31
31
|
# Path to the oh-my-skills repo, resolved relative to this repo's root.
|
|
32
32
|
# Override at runtime with the OMS environment variable if oh-my-skills
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bmad-module-skill-forge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.6.0",
|
|
5
5
|
"description": "BMAD module — Turn code and docs into instructions AI agents can actually follow. Progressive capability tiers (Quick/Forge/Forge+/Deep).",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"bmad",
|
|
@@ -160,17 +160,45 @@ def is_diverged(diff_kind: str) -> bool:
|
|
|
160
160
|
def restore_description(skill_md: Path, captured: str) -> None:
|
|
161
161
|
"""Atomically rewrite SKILL.md so its frontmatter `description` equals captured.
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
Parses the entire frontmatter via PyYAML, replaces the top-level
|
|
164
|
+
`description` value, and re-emits the frontmatter via `yaml.safe_dump`.
|
|
165
|
+
This guarantees valid YAML output regardless of the source representation
|
|
166
|
+
(inline, double-quoted, single-quoted, folded `>`, literal `|`), and
|
|
167
|
+
avoids the line-level pitfalls a previous implementation had with folded
|
|
168
|
+
block scalars and nested `description:` keys in sibling mappings.
|
|
169
|
+
|
|
170
|
+
Key order is preserved (PyYAML's `safe_dump` honours dict insertion order;
|
|
171
|
+
this module's `requires-python = ">=3.10"` guarantees ordered dicts).
|
|
172
|
+
Quoting style of *other* fields may change to whatever `safe_dump`
|
|
173
|
+
chooses for each scalar — downstream readers parse YAML, so any valid
|
|
174
|
+
YAML emission is acceptable.
|
|
167
175
|
"""
|
|
168
176
|
text = skill_md.read_text(encoding="utf-8")
|
|
169
177
|
leading, fm_yaml, body = _split_frontmatter(text)
|
|
170
178
|
if not fm_yaml:
|
|
171
179
|
raise ValueError(f"cannot restore: no frontmatter in {skill_md}")
|
|
172
180
|
|
|
173
|
-
|
|
181
|
+
try:
|
|
182
|
+
fm = yaml.safe_load(fm_yaml)
|
|
183
|
+
except yaml.YAMLError as exc:
|
|
184
|
+
raise ValueError(f"frontmatter in {skill_md} is not valid YAML: {exc}") from exc
|
|
185
|
+
if not isinstance(fm, dict):
|
|
186
|
+
raise ValueError(f"frontmatter in {skill_md} is not a mapping")
|
|
187
|
+
if "description" not in fm:
|
|
188
|
+
raise ValueError(f"frontmatter in {skill_md} has no `description` field")
|
|
189
|
+
|
|
190
|
+
fm["description"] = captured
|
|
191
|
+
|
|
192
|
+
# `width=10**9` keeps the description on one line regardless of length;
|
|
193
|
+
# PyYAML otherwise inserts line breaks at ~80 chars which would re-introduce
|
|
194
|
+
# folded-scalar continuation lines — the exact failure mode this rewrite fixes.
|
|
195
|
+
new_fm = yaml.safe_dump(
|
|
196
|
+
fm,
|
|
197
|
+
sort_keys=False,
|
|
198
|
+
default_flow_style=False,
|
|
199
|
+
allow_unicode=True,
|
|
200
|
+
width=10**9,
|
|
201
|
+
).rstrip("\n")
|
|
174
202
|
new_text = f"{leading}{new_fm}\n---\n{body}"
|
|
175
203
|
|
|
176
204
|
# atomic: write to a sibling temp file, fsync, rename
|
|
@@ -191,81 +219,6 @@ def restore_description(skill_md: Path, captured: str) -> None:
|
|
|
191
219
|
raise
|
|
192
220
|
|
|
193
221
|
|
|
194
|
-
def _rewrite_description_line(fm_yaml: str, new_description: str) -> str:
|
|
195
|
-
"""Replace the `description:` value in a YAML frontmatter block.
|
|
196
|
-
|
|
197
|
-
Preserves other keys verbatim. Handles three common shapes:
|
|
198
|
-
description: single-line value
|
|
199
|
-
description: "double-quoted value"
|
|
200
|
-
description: | # block scalar (folded variant: >)
|
|
201
|
-
multi-line
|
|
202
|
-
value here
|
|
203
|
-
"""
|
|
204
|
-
lines = fm_yaml.split("\n")
|
|
205
|
-
out: list[str] = []
|
|
206
|
-
i = 0
|
|
207
|
-
quoted = _yaml_quote_inline(new_description)
|
|
208
|
-
replaced = False
|
|
209
|
-
while i < len(lines):
|
|
210
|
-
line = lines[i]
|
|
211
|
-
if not replaced and _is_description_key_line(line):
|
|
212
|
-
stripped = line.lstrip()
|
|
213
|
-
indent = line[: len(line) - len(stripped)]
|
|
214
|
-
# detect block-scalar indicator (| or >)
|
|
215
|
-
after_key = stripped[len("description:") :].lstrip()
|
|
216
|
-
if after_key.startswith("|") or after_key.startswith(">"):
|
|
217
|
-
# skip block-scalar continuation lines (deeper indent than the key line)
|
|
218
|
-
key_indent = len(indent)
|
|
219
|
-
i += 1
|
|
220
|
-
while i < len(lines):
|
|
221
|
-
nxt = lines[i]
|
|
222
|
-
if nxt.strip() == "":
|
|
223
|
-
# blank lines belong to the block scalar
|
|
224
|
-
i += 1
|
|
225
|
-
continue
|
|
226
|
-
nxt_indent = len(nxt) - len(nxt.lstrip())
|
|
227
|
-
if nxt_indent <= key_indent:
|
|
228
|
-
break
|
|
229
|
-
i += 1
|
|
230
|
-
out.append(f"{indent}description: {quoted}")
|
|
231
|
-
replaced = True
|
|
232
|
-
continue
|
|
233
|
-
out.append(f"{indent}description: {quoted}")
|
|
234
|
-
replaced = True
|
|
235
|
-
i += 1
|
|
236
|
-
continue
|
|
237
|
-
out.append(line)
|
|
238
|
-
i += 1
|
|
239
|
-
if not replaced:
|
|
240
|
-
raise ValueError("description key not found while rewriting frontmatter")
|
|
241
|
-
return "\n".join(out)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _is_description_key_line(line: str) -> bool:
|
|
245
|
-
stripped = line.lstrip()
|
|
246
|
-
return stripped.startswith("description:") and (
|
|
247
|
-
len(stripped) == len("description:") or stripped[len("description:")] in (" ", "\t", "")
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _yaml_quote_inline(value: str) -> str:
|
|
252
|
-
"""Emit a YAML scalar suitable as the inline value of `description: `.
|
|
253
|
-
|
|
254
|
-
Uses double-quoted form so control characters and embedded quotes are
|
|
255
|
-
safe. Block scalars (| / >) are not used — the description field is a
|
|
256
|
-
single semantic string and downstream readers (skill-check, agentskills)
|
|
257
|
-
expect inline form.
|
|
258
|
-
"""
|
|
259
|
-
escaped = (
|
|
260
|
-
value.replace("\\", "\\\\")
|
|
261
|
-
.replace("\"", "\\\"")
|
|
262
|
-
.replace("\n", "\\n")
|
|
263
|
-
.replace("\r", "\\r")
|
|
264
|
-
.replace("\t", "\\t")
|
|
265
|
-
)
|
|
266
|
-
return f'"{escaped}"'
|
|
267
|
-
|
|
268
|
-
|
|
269
222
|
# --------------------------------------------------------------------------
|
|
270
223
|
# CLI
|
|
271
224
|
# --------------------------------------------------------------------------
|
|
@@ -6,6 +6,7 @@ nextStepOptions:
|
|
|
6
6
|
step 4: 'map-and-detect.md'
|
|
7
7
|
step 5: 'recommend.md'
|
|
8
8
|
step 6: 'generate-briefs.md'
|
|
9
|
+
step 7: 'health-check.md'
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -61,8 +62,9 @@ Map the last completed step to the next step file:
|
|
|
61
62
|
| identify-units | map-and-detect |
|
|
62
63
|
| map-and-detect | recommend |
|
|
63
64
|
| recommend | generate-briefs |
|
|
65
|
+
| generate-briefs | health-check |
|
|
64
66
|
|
|
65
|
-
**IF
|
|
67
|
+
**IF `health-check` is in `stepsCompleted`:**
|
|
66
68
|
"**This analysis appears to be complete.** All steps have been finished. Would you like to start a new analysis?"
|
|
67
69
|
|
|
68
70
|
### 5. Update and Route
|
|
@@ -13,6 +13,8 @@ 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.
|
|
17
|
+
|
|
16
18
|
## Conventions
|
|
17
19
|
|
|
18
20
|
- Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'analyze-target.md'
|
|
3
|
+
ratifyTargetFile: 'confirm-brief.md'
|
|
3
4
|
forgeTierFile: '{sidecar_path}/forge-tier.yaml'
|
|
4
5
|
descriptionVoiceExamplesFile: 'assets/description-voice-examples.md'
|
|
5
6
|
headlessArgsFile: 'references/headless-args.md'
|
|
@@ -9,6 +10,9 @@ draftCheckpointFile: 'references/draft-checkpoint.md'
|
|
|
9
10
|
validateBriefInputsProbeOrder:
|
|
10
11
|
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-inputs.py'
|
|
11
12
|
- '{project-root}/src/shared/scripts/skf-validate-brief-inputs.py'
|
|
13
|
+
validateBriefSchemaProbeOrder:
|
|
14
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-schema.py'
|
|
15
|
+
- '{project-root}/src/shared/scripts/skf-validate-brief-schema.py'
|
|
12
16
|
emitBriefEnvelopeProbeOrder:
|
|
13
17
|
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
14
18
|
- '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
@@ -84,7 +88,7 @@ Let's get started."
|
|
|
84
88
|
|
|
85
89
|
### 3. Gather Target Repository
|
|
86
90
|
|
|
87
|
-
This section has
|
|
91
|
+
This section has four sub-flows. Execute exactly one branch — 3.1a *or* 3.2 *or* 3.3 — based on the user's response in 3.1, then end with the shared confirmation (3.1a is terminal for §3 and jumps directly to confirm-brief.md). Do not mix branches.
|
|
88
92
|
|
|
89
93
|
#### 3.1 Collect target
|
|
90
94
|
|
|
@@ -94,6 +98,7 @@ Provide one of:
|
|
|
94
98
|
- A **GitHub URL** (e.g., `https://github.com/org/repo`)
|
|
95
99
|
- A **local path** (e.g., `/path/to/project`)
|
|
96
100
|
- **Documentation URLs** for a docs-only skill (e.g., `https://docs.stripe.com/api`) — use this when no source code is available (SaaS, closed-source)
|
|
101
|
+
- A **path to an existing `skill-brief.yaml`** (file path or a directory containing one) — use this to ratify a brief produced by another workflow (e.g. `skf-analyze-source`) without re-deriving fields
|
|
97
102
|
|
|
98
103
|
Or type `cancel` / `exit` / `[X]` to leave without writing anything.
|
|
99
104
|
|
|
@@ -102,10 +107,64 @@ Or type `cancel` / `exit` / `[X]` to leave without writing anything.
|
|
|
102
107
|
Wait for user response. Branch on the response:
|
|
103
108
|
|
|
104
109
|
- Empty input, `cancel`, `exit`, `[X]`, `q`, or `:q` → Display `"Cancelled — no brief was written."` and HALT (exit code 6, `halt_reason: "user-cancelled"`). Cancellation here is non-destructive — no files have been written yet by step 1. Headless mode never reaches this branch (the GATE in §8 short-circuits the interactive sub-flows).
|
|
110
|
+
- Path that resolves to an existing `skill-brief.yaml` (file path ending in `skill-brief.yaml` that exists, OR a directory containing a `skill-brief.yaml`) → §3.1a
|
|
105
111
|
- Documentation URLs only (no source location) → §3.2
|
|
106
112
|
- GitHub URL or local filesystem path → §3.3
|
|
107
113
|
- Any other free-form question (e.g. "what is this?", "show me an example", "how does SKF work?") → answer briefly, re-display the prompt
|
|
108
114
|
|
|
115
|
+
#### 3.1a Branch — Ratify existing brief
|
|
116
|
+
|
|
117
|
+
This branch handles the AN→BS handoff: another workflow (typically `skf-analyze-source`) has already produced a `skill-brief.yaml`, and the user wants to review and confirm it without re-running gather-intent / analyze-target / scope-definition. **This branch is interactive-only** — headless mode reaches step 1 via §8's GATE with `target_repo`/`skill_name` args, not via this §3.1a path. (A headless ratify equivalent could be added later via a `from_brief` argument; out of scope here.)
|
|
118
|
+
|
|
119
|
+
Resolve the path:
|
|
120
|
+
|
|
121
|
+
- If the user's input ends in `skill-brief.yaml` and points at an existing file → that is the brief path.
|
|
122
|
+
- Otherwise (input was a directory) → the brief path is `<input>/skill-brief.yaml`.
|
|
123
|
+
|
|
124
|
+
**Validate the brief against the schema** before presenting it. Resolve `{validateBriefSchemaHelper}` from `{validateBriefSchemaProbeOrder}` (first existing path wins; HALT if no candidate exists), then:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
uv run {validateBriefSchemaHelper} <resolved-brief-path>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The script returns JSON `{valid, errors[], warnings[], halt_reason, brief}`. Apply the result:
|
|
131
|
+
|
|
132
|
+
- **`valid: false`** — surface the `errors[]` messages and the `halt_reason` to the user, then re-display the §3.1 prompt for a corrected path (or another target altogether). Do not HALT — the user may simply have pointed at the wrong file. Example: `"**Brief at `{path}` is invalid:** {first error message}. Pick a different brief, or supply a repo / docs URL instead."`
|
|
133
|
+
- **`valid: true`** — proceed with the parsed `brief` payload.
|
|
134
|
+
|
|
135
|
+
Surface any non-empty `warnings[]` as a single grouped line (`"**Brief validation warnings:** {joined warnings}"`), then present the ratify menu:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
**Existing brief detected at `{path}`.**
|
|
139
|
+
|
|
140
|
+
- **Name:** {brief.name}
|
|
141
|
+
- **Target:** {brief.source_repo}
|
|
142
|
+
- **Description:** "{brief.description}"
|
|
143
|
+
- **Created:** {brief.created} by {brief.created_by}
|
|
144
|
+
- **Scope:** {brief.scope.type}
|
|
145
|
+
|
|
146
|
+
Pick one:
|
|
147
|
+
[R] Ratify — review in step 4 and write (overwriting this file once approved)
|
|
148
|
+
[F] Start fresh — discard this brief and re-prompt for a target
|
|
149
|
+
[X] Cancel and exit
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Wait for user response. Branch:
|
|
153
|
+
|
|
154
|
+
- **[R] Ratify** — Confirm overwrite up front: store `ratify_mode: true` and `ratify_source_path: <resolved-brief-path>` in workflow context. Hydrate the brief context variables from the parsed `brief` payload so step 4 has the same field set it normally derives from steps 1-3:
|
|
155
|
+
- `name` ← `brief.name`; `version` ← `brief.version`; `target_version` ← `brief.target_version`
|
|
156
|
+
- `source_repo` ← `brief.source_repo`; `source_type` ← `brief.source_type`; `source_authority` ← `brief.source_authority`; `doc_urls` ← `brief.doc_urls`
|
|
157
|
+
- `language` ← `brief.language`; `description` ← `brief.description`; `forge_tier` ← `brief.forge_tier`
|
|
158
|
+
- `created` ← `brief.created`; `created_by` ← `brief.created_by`
|
|
159
|
+
- `scope.type` / `scope.include` / `scope.exclude` / `scope.notes` / `scope.rationale` ← `brief.scope.*`
|
|
160
|
+
- `scripts_intent` ← `brief.scripts_intent`; `assets_intent` ← `brief.assets_intent`
|
|
161
|
+
|
|
162
|
+
Then load, read entirely, and execute `{ratifyTargetFile}` — bypassing §3.1b/§3.2/§3.3, §3b, §4, §5, §6, §7, §7b, and §8 entirely. Skip step 2 (analyze-target) and step 3 (scope-definition) — both would re-derive fields already on disk. The forward chain resumes at step 4 (confirm-brief) where the user gets the standard review pass and can still adjust fields inline via §4.
|
|
163
|
+
|
|
164
|
+
- **[F] Start fresh** — discard the loaded brief and re-display §3.1 above (the user is now at the same point as if they had typed nothing).
|
|
165
|
+
- **[X] Cancel** — Display `"Cancelled — no brief was written."` and HALT (exit code 6, `halt_reason: "user-cancelled"`). Non-destructive.
|
|
166
|
+
- **Any other input** — treat as a fresh §3.1 response and re-evaluate the routing branches above (a typed GitHub URL after seeing the menu means "I changed my mind, brief this repo instead").
|
|
167
|
+
|
|
109
168
|
#### 3.2 Branch — Documentation URLs (docs-only)
|
|
110
169
|
|
|
111
170
|
- Set `source_type: "docs-only"` in the brief data
|
|
@@ -52,7 +52,11 @@ The script's atomic-write helper creates parent directories as needed (`mkdir -p
|
|
|
52
52
|
|
|
53
53
|
Before writing, check whether the resolved target path already exists.
|
|
54
54
|
|
|
55
|
-
**
|
|
55
|
+
**Ratify path (`ratify_mode: true` in workflow context):**
|
|
56
|
+
|
|
57
|
+
The user already confirmed overwrite up-front at step 1 §3.1a when they chose `[R] Ratify` against the same file. Skip the interactive prompt below; log a single-line `brief-skill: ratify-mode auto-overwriting existing brief at {path}` and proceed to §3. Resuming the prompt would be friction — the user has by now also reviewed and approved the brief at step 4. This auto-accept does not apply to headless mode (handled below) nor to the standard interactive path (also below).
|
|
58
|
+
|
|
59
|
+
**Interactive (`{headless_mode}` is false, `ratify_mode` not set):**
|
|
56
60
|
|
|
57
61
|
If the file exists, present:
|
|
58
62
|
|
|
@@ -15,7 +15,7 @@ description: >
|
|
|
15
15
|
**Frontmatter rules:**
|
|
16
16
|
|
|
17
17
|
- `name`: lowercase alphanumeric + hyphens only, must match the skill output directory name. Prefer gerund form (`processing-pdfs`, `analyzing-spreadsheets`) for clarity.
|
|
18
|
-
- `description`: non-empty, max 1024 chars, optimized for agent discovery. **MUST use third-person voice** ("Processes Excel files..." not "I can help you..." or "You can use this to..."). Inconsistent point-of-view causes discovery problems since the description is injected into the system prompt.
|
|
18
|
+
- `description`: non-empty, max 1024 chars, optimized for agent discovery. **MUST use third-person voice** ("Processes Excel files..." not "I can help you..." or "You can use this to...") AND **MUST include a trigger phrase** so agents know when to match — one of `Use when …`, `Triggers on …`, or `Reach for this when …`. When using `Use when `, follow with a **gerund** (`Use when building/processing/analyzing X…`) or a noun-phrase clause (`Use when the user requests to "X"`); never a bare indicative verb like `builds`/`processes`, because `skill-check --fix` will prepend a literal `Use when ` to a description missing the trigger phrase, producing ungrammatical output (`Use when builds X…`). Inconsistent point-of-view or a missing trigger phrase causes discovery problems since the description is injected into the system prompt. See `skf-brief-skill/assets/description-voice-examples.md` for the canonical voice palette.
|
|
19
19
|
- **`description` must NOT contain angle brackets** — neither standalone placeholders like `<name>`, `<component>`, `<path>`, nor inline generics like `` `Array<T>` `` or `` `Meta<typeof X>` ``. Both `skill-check`'s `description_field` validator and `tessl`'s deterministic description check parse the frontmatter description as a raw string and reject any `<` or `>`, regardless of markdown context (backticks do NOT protect content here). A rejected description fails the review with 0% score. When the natural phrasing would use angle brackets:
|
|
20
20
|
- Prefer the curly-brace form in prose: `{name}`, `{component-id}`, `{path}` — readable and tessl-safe.
|
|
21
21
|
- Uppercase placeholders are also acceptable: `NAME`, `COMPONENT_ID`.
|
|
@@ -18,7 +18,7 @@ description: >
|
|
|
18
18
|
**Frontmatter rules (agentskills.io specification):**
|
|
19
19
|
|
|
20
20
|
- `name`: 1-64 characters, lowercase alphanumeric + hyphens only, must match parent directory name. Prefer gerund form (`processing-pdfs`, `analyzing-spreadsheets`) for clarity; noun phrases and action-oriented forms are acceptable alternatives.
|
|
21
|
-
- `description`: 1-1024 characters, trigger-optimized for agent matching. MUST use third-person voice ("Processes..." not "I can..." or "You can...").
|
|
21
|
+
- `description`: 1-1024 characters, trigger-optimized for agent matching. MUST use third-person voice ("Processes..." not "I can..." or "You can...") AND include a trigger phrase ("Use when ...", "Triggers on ...", "Reach for this when ..."). When using "Use when ", follow with a gerund (`Use when building/processing/analyzing X…`) or noun-phrase clause — never a bare indicative verb, since `skill-check --fix` will prepend a literal "Use when " to descriptions missing the phrase. See `skf-brief-skill/assets/description-voice-examples.md`.
|
|
22
22
|
- Only 6 fields permitted: `name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`
|
|
23
23
|
- `version` and `author` belong in metadata.json, NOT in frontmatter
|
|
24
24
|
|
|
@@ -51,7 +51,7 @@ These rules apply to every step in this workflow:
|
|
|
51
51
|
|--------|--------|
|
|
52
52
|
| **Inputs** | skill_name [required]; optional flags: `--allow-workspace-drift`, `--no-discovery` (skip §4b Discovery Testing), `--no-health-check` (skip §7 health-check dispatch), `--tier=<Quick\|Forge\|Forge+\|Deep>` (bypass forge-tier.yaml sidecar requirement), `--threshold=<N>` (override pass threshold; CLI wins over `workflow.default_threshold` scalar) |
|
|
53
53
|
| **Gates** | step 6: Confirm Gate [C] |
|
|
54
|
-
| **Outputs** | test-report-{skill_name}.md with completeness score and result (PASS/FAIL); per-run `skf-test-skill-result-{run_id}.json` and `skf-test-skill-result-latest.json` written atomically under `{forge_version}/` |
|
|
54
|
+
| **Outputs** | per-run `test-report-{skill_name}-{run_id}.md` with completeness score and result (PASS/FAIL); per-run `skf-test-skill-result-{run_id}.json` and `skf-test-skill-result-latest.json` written atomically under `{forge_version}/` — downstream consumers (export-skill, update-skill `--from-test-report`) glob `test-report-{skill_name}-*.md` and pick newest by parsed ISO timestamp |
|
|
55
55
|
| **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true |
|
|
56
56
|
| **Exit codes** | See "Exit Codes" below |
|
|
57
57
|
|
|
@@ -42,7 +42,14 @@ Provide either:
|
|
|
42
42
|
Resolve the path to an absolute skill folder location.
|
|
43
43
|
|
|
44
44
|
**If `--from-test-report` was provided (or user references a test report):**
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
`skf-test-skill` writes timestamped test-report filenames (`test-report-{skill_name}-{ISO-TIMESTAMP}-{HASH}.md`) — there is no exact-name `test-report-{skill_name}.md` on disk. Locate the most recent report by glob, mirroring `skf-export-skill/references/load-skill.md §4b`:
|
|
47
|
+
|
|
48
|
+
1. Glob `{forge_data_folder}/{skill_name}/{active_version}/test-report-{skill_name}-*.md` (i.e. `{forge_version}/test-report-{skill_name}-*.md`). Sort matches descending by the parsed ISO-timestamp segment in the filename (`YYYYMMDDTHHMMSSZ` between the skill name and the hash — `sort -r` on the filename works because the timestamp is the first variable component). Take the first match.
|
|
49
|
+
2. If the versioned glob returns nothing, fall back to the same glob at the flat path `{forge_data_folder}/{skill_name}/test-report-{skill_name}-*.md`. Pick the newest by parsed timestamp.
|
|
50
|
+
3. If neither glob returns anything, look for the stable companion `skf-test-skill-result-latest.json` in the same two directories (versioned first, then flat). Read the report path from `outputs[]` per the canonical contract documented at `shared/references/output-contract-schema.md` (resolved by skf-test-skill step 6 §4c) and load that file.
|
|
51
|
+
|
|
52
|
+
If a report is located, set `test_report_path` in context to the resolved absolute path and set `update_mode: gap-driven`. Surface the actual file picked in the message (e.g. `test-report-{skill_name}-20260507T050917Z-487606-9b2f.md`) so an operator can navigate to the report from the log. If all three lookups fail, warn and continue with normal source drift mode.
|
|
46
53
|
|
|
47
54
|
**If `--allow-workspace-drift` was provided:** set `allow_workspace_drift: true` in workflow context. This flag is consumed by step 3 §0.a's pre-flight drift guard (gap-driven mode only) and has no effect in normal source-drift mode.
|
|
48
55
|
|