bmad-module-skill-forge 1.5.0 → 1.5.1
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-detect-tools.py +50 -8
- package/src/shared/scripts/skf-manifest-ops.py +22 -6
- package/src/skf-analyze-source/references/identify-units.md +8 -4
- package/src/skf-analyze-source/references/init.md +6 -4
- package/src/skf-analyze-source/references/scan-project.md +6 -2
- package/src/skf-audit-skill/references/init.md +9 -3
- package/src/skf-audit-skill/references/structural-diff.md +9 -3
- package/src/skf-brief-skill/SKILL.md +1 -1
- package/src/skf-brief-skill/references/analyze-target.md +22 -10
- package/src/skf-brief-skill/references/gather-intent.md +11 -5
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/qmd-collection-registration.md +1 -1
- package/src/skf-brief-skill/references/scope-definition.md +8 -4
- package/src/skf-brief-skill/references/write-brief.md +19 -7
- package/src/skf-create-skill/references/extraction-patterns.md +2 -0
- package/src/skf-create-skill/references/validate.md +2 -2
- package/src/skf-create-stack-skill/references/detect-integrations.md +7 -2
- package/src/skf-create-stack-skill/references/detect-manifests.md +6 -2
- package/src/skf-create-stack-skill/references/parallel-extract.md +6 -2
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +6 -1
- package/src/skf-export-skill/references/summary.md +4 -4
- package/src/skf-export-skill/references/update-context.md +2 -2
- package/src/skf-test-skill/references/coherence-check.md +14 -7
- package/src/skf-verify-stack/references/coverage.md +6 -2
- package/src/skf-verify-stack/references/init.md +6 -2
- package/src/skf-verify-stack/references/integrations.md +6 -2
- package/src/skf-verify-stack/references/report.md +6 -2
- package/src/skf-verify-stack/references/requirements.md +7 -3
- package/src/skf-verify-stack/references/synthesize.md +6 -2
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.5.
|
|
29
|
+
skf_version: "1.5.1"
|
|
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.5.
|
|
4
|
+
"version": "1.5.1",
|
|
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",
|
|
@@ -88,13 +88,26 @@ from __future__ import annotations
|
|
|
88
88
|
import argparse
|
|
89
89
|
import json
|
|
90
90
|
import os
|
|
91
|
+
import shutil
|
|
91
92
|
import subprocess
|
|
92
93
|
import sys
|
|
94
|
+
import tempfile
|
|
93
95
|
from concurrent.futures import ThreadPoolExecutor
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
VALID_TIERS = ("Quick", "Forge", "Forge+", "Deep")
|
|
97
99
|
PROBE_TIMEOUT_SEC = 8 # per-tool subprocess.run timeout
|
|
100
|
+
# Prefer the OS `timeout(1)` utility to bound each probe. A daemon-backed
|
|
101
|
+
# tool (e.g. `qmd status`) can block in an uninterruptible syscall against
|
|
102
|
+
# its daemon; `subprocess.run`'s own post-timeout `process.wait()` is
|
|
103
|
+
# unbounded and never returns in that case, hanging the whole detector.
|
|
104
|
+
# `timeout --kill-after` reaps the child (and its session) at the OS level,
|
|
105
|
+
# so Python's wait always returns. POSIX-only: Windows' `timeout.exe` is an
|
|
106
|
+
# unrelated builtin (waits for input; cannot run a command), and the
|
|
107
|
+
# uninterruptible-wait hang is itself POSIX-specific (Windows uses a
|
|
108
|
+
# forcible TerminateProcess), so on Windows we fall back to subprocess.run's
|
|
109
|
+
# own timeout. None when unavailable.
|
|
110
|
+
_TIMEOUT_BIN = shutil.which("timeout") if os.name == "posix" else None
|
|
98
111
|
CCC_IDENTITY_MARKER = "cocoindex code" # case-insensitive substring
|
|
99
112
|
|
|
100
113
|
|
|
@@ -115,16 +128,45 @@ def _run(cmd: list[str], timeout: int = PROBE_TIMEOUT_SEC) -> tuple[int, str, st
|
|
|
115
128
|
Treats every failure mode (FileNotFoundError, TimeoutExpired, OSError,
|
|
116
129
|
CalledProcessError) as a failed probe — returns rc=127 and an empty
|
|
117
130
|
stdout/stderr. Tool detection should never crash the workflow.
|
|
131
|
+
|
|
132
|
+
The child is wrapped in the OS `timeout(1)` utility (when available) so a
|
|
133
|
+
daemon-backed probe blocked in an uninterruptible syscall (e.g.
|
|
134
|
+
`qmd status` waiting on its daemon socket) is reaped at the OS level —
|
|
135
|
+
`subprocess.run`'s own post-timeout `process.wait()` is unbounded and
|
|
136
|
+
would otherwise never return, hanging the whole detector. Child
|
|
137
|
+
stdout/stderr are redirected to temp files (no pipe to drain), and
|
|
138
|
+
`start_new_session=True` isolates the child's process group; the
|
|
139
|
+
Python-level timeout is a secondary net set slightly above the OS one.
|
|
118
140
|
"""
|
|
141
|
+
if _TIMEOUT_BIN:
|
|
142
|
+
run_cmd = [_TIMEOUT_BIN, "--kill-after=2", str(timeout), *cmd]
|
|
143
|
+
py_timeout = timeout + 5
|
|
144
|
+
else:
|
|
145
|
+
run_cmd = cmd
|
|
146
|
+
py_timeout = timeout
|
|
119
147
|
try:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
148
|
+
with tempfile.TemporaryFile() as out_f, tempfile.TemporaryFile() as err_f:
|
|
149
|
+
result = subprocess.run(
|
|
150
|
+
run_cmd,
|
|
151
|
+
stdin=subprocess.DEVNULL,
|
|
152
|
+
stdout=out_f,
|
|
153
|
+
stderr=err_f,
|
|
154
|
+
timeout=py_timeout,
|
|
155
|
+
check=False,
|
|
156
|
+
start_new_session=True,
|
|
157
|
+
)
|
|
158
|
+
# Mocked unit tests patch `subprocess.run` to return a fake with
|
|
159
|
+
# `.stdout`/`.stderr` strings set; real runs redirect to the temp
|
|
160
|
+
# files (so `result.stdout` is None — read the files instead).
|
|
161
|
+
stdout = result.stdout
|
|
162
|
+
if stdout is None:
|
|
163
|
+
out_f.seek(0)
|
|
164
|
+
stdout = out_f.read().decode("utf-8", "replace")
|
|
165
|
+
stderr = result.stderr
|
|
166
|
+
if stderr is None:
|
|
167
|
+
err_f.seek(0)
|
|
168
|
+
stderr = err_f.read().decode("utf-8", "replace")
|
|
169
|
+
return result.returncode, stdout or "", stderr or ""
|
|
128
170
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
129
171
|
return 127, "", ""
|
|
130
172
|
|
|
@@ -12,7 +12,10 @@ CLI: python3 skf-manifest-ops.py <skills-folder> <command> [args]
|
|
|
12
12
|
Commands:
|
|
13
13
|
read — Read entire manifest
|
|
14
14
|
get <skill-name> — Get a single skill entry
|
|
15
|
-
set <skill-name> <version>
|
|
15
|
+
set <skill-name> <version> [--ides a,b]
|
|
16
|
+
— Add/update skill with active version;
|
|
17
|
+
--ides unions a comma-separated IDE list
|
|
18
|
+
into the version entry (deduped, sorted)
|
|
16
19
|
remove <skill-name> — Remove skill from manifest
|
|
17
20
|
deprecate <skill-name> [ver] — Mark skill or version as deprecated
|
|
18
21
|
rename <old-name> <new-name> — Rename a skill entry
|
|
@@ -124,7 +127,7 @@ def cmd_get(manifest_path, skill_name):
|
|
|
124
127
|
return {"status": "ok", "skill": skill_name, "entry": exports[skill_name]}
|
|
125
128
|
|
|
126
129
|
|
|
127
|
-
def cmd_set(manifest_path, skill_name, version):
|
|
130
|
+
def cmd_set(manifest_path, skill_name, version, ides=None):
|
|
128
131
|
data, err = read_manifest(manifest_path)
|
|
129
132
|
if err:
|
|
130
133
|
return {"status": "error", "error": err}
|
|
@@ -140,10 +143,18 @@ def cmd_set(manifest_path, skill_name, version):
|
|
|
140
143
|
if old_active and old_active in versions and old_active != version:
|
|
141
144
|
versions[old_active]["status"] = "archived"
|
|
142
145
|
|
|
143
|
-
# Add or update the new active version
|
|
146
|
+
# Add or update the new active version. Absent --ides preserves the
|
|
147
|
+
# existing ides/platforms list verbatim (backward-compatible). When
|
|
148
|
+
# --ides is supplied, union it in (dedupe, keep sorted).
|
|
144
149
|
existing_version = versions.get(version, {})
|
|
150
|
+
merged_ides = list(existing_version.get("ides", existing_version.get("platforms", [])))
|
|
151
|
+
if ides:
|
|
152
|
+
for ide in ides:
|
|
153
|
+
if ide not in merged_ides:
|
|
154
|
+
merged_ides.append(ide)
|
|
155
|
+
merged_ides = sorted(merged_ides)
|
|
145
156
|
versions[version] = {
|
|
146
|
-
"ides":
|
|
157
|
+
"ides": merged_ides,
|
|
147
158
|
"last_exported": today,
|
|
148
159
|
"status": "active",
|
|
149
160
|
}
|
|
@@ -205,7 +216,7 @@ def cmd_rename(manifest_path, old_name, new_name):
|
|
|
205
216
|
def main():
|
|
206
217
|
if len(sys.argv) < 3:
|
|
207
218
|
print("Usage: python3 skf-manifest-ops.py <skills-folder> <command> [args]", file=sys.stderr)
|
|
208
|
-
print("Commands: read, get <name>, set <name> <version
|
|
219
|
+
print("Commands: read, get <name>, set <name> <version> [--ides a,b], remove <name>, deprecate <name> [version], rename <old> <new>", file=sys.stderr)
|
|
209
220
|
sys.exit(1)
|
|
210
221
|
|
|
211
222
|
skills_folder = Path(sys.argv[1])
|
|
@@ -217,7 +228,12 @@ def main():
|
|
|
217
228
|
elif command == "get" and len(sys.argv) >= 4:
|
|
218
229
|
result = cmd_get(manifest_path, sys.argv[3])
|
|
219
230
|
elif command == "set" and len(sys.argv) >= 5:
|
|
220
|
-
|
|
231
|
+
ides_arg = None
|
|
232
|
+
if "--ides" in sys.argv:
|
|
233
|
+
idx = sys.argv.index("--ides")
|
|
234
|
+
if idx + 1 < len(sys.argv):
|
|
235
|
+
ides_arg = [s for s in sys.argv[idx + 1].split(",") if s]
|
|
236
|
+
result = cmd_set(manifest_path, sys.argv[3], sys.argv[4], ides_arg)
|
|
221
237
|
elif command == "remove" and len(sys.argv) >= 4:
|
|
222
238
|
result = cmd_remove(manifest_path, sys.argv[3])
|
|
223
239
|
elif command == "deprecate" and len(sys.argv) >= 4:
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
nextStepFile: 'map-and-detect.md'
|
|
3
3
|
outputFile: '{forge_data_folder}/analyze-source-report-{project_name}.md'
|
|
4
4
|
heuristicsFile: 'references/unit-detection-heuristics.md'
|
|
5
|
-
|
|
5
|
+
disqualifyCandidatesProbeOrder:
|
|
6
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-disqualify-candidates.py'
|
|
7
|
+
- '{project-root}/src/shared/scripts/skf-disqualify-candidates.py'
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -32,6 +34,8 @@ Load {heuristicsFile} for classification rules.
|
|
|
32
34
|
|
|
33
35
|
### 2. Apply Detection Heuristics
|
|
34
36
|
|
|
37
|
+
**Resolve `{disqualifyCandidatesHelper}`** from `{disqualifyCandidatesProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
38
|
+
|
|
35
39
|
For EACH detected boundary from the scan:
|
|
36
40
|
|
|
37
41
|
**Step A — Count detection signals:**
|
|
@@ -58,16 +62,16 @@ Run the shared disqualification helper to apply the deterministic subset of the
|
|
|
58
62
|
```json
|
|
59
63
|
[
|
|
60
64
|
{"name": "<unit-name>",
|
|
61
|
-
"path": "<rel-from-
|
|
65
|
+
"path": "<rel-from-analyzed-source-root (project_paths[0])>",
|
|
62
66
|
"files": ["<rel-path>", ...]},
|
|
63
67
|
...
|
|
64
68
|
]
|
|
65
69
|
```
|
|
66
70
|
2. **Invoke the script** via stdin:
|
|
67
71
|
```bash
|
|
68
|
-
uv run {
|
|
72
|
+
uv run {disqualifyCandidatesHelper} filter --boundaries - --source-root {project_paths[0]}
|
|
69
73
|
```
|
|
70
|
-
piping the boundaries JSON on stdin. The script emits:
|
|
74
|
+
piping the boundaries JSON on stdin. `--source-root` is the analyzed-source root (`project_paths[0]`) — the directory the boundaries/manifest scan ran against — not `{project-root}` (the forge workspace), which differs whenever the analyzed target lives outside the forge workspace. The script emits:
|
|
71
75
|
```json
|
|
72
76
|
{
|
|
73
77
|
"kept": [{"name": "...", "path": "...", "files_count": N, "loc_total": L}, ...],
|
|
@@ -26,9 +26,11 @@ To initialize the analyze-source workflow by loading configuration, detecting co
|
|
|
26
26
|
Look for {outputFile}.
|
|
27
27
|
|
|
28
28
|
**IF the file exists AND has `stepsCompleted` with entries:**
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
29
|
+
- The report filename is keyed to `{project_name}` (the forge workspace), not the analyzed target — so a report from a *different* target can collide here. Before resuming, establish the requested target and compare it to the existing report:
|
|
30
|
+
- Determine the requested target now: if `--project-path <path>` was passed at invocation, set `project_paths[]` from it (comma-split if multiple); otherwise collect the path(s) using the section-3 "Collect Project Path" prompt and store as `project_paths[]`. (Section 3 must NOT re-prompt when `project_paths[]` is already populated here.)
|
|
31
|
+
- Read the existing report's frontmatter `project_paths`.
|
|
32
|
+
- **IF the existing report's `project_paths` matches the requested target:** "**Found an existing analysis report. Resuming previous session...**" — Load, read entirely, then execute {continueFile}. **STOP HERE** — do not continue this sequence.
|
|
33
|
+
- **ELSE (different target — stale collision):** the existing report belongs to another analysis. Archive it by renaming to `{forge_data_folder}/analyze-source-report-{project_name}-<UTC-timestamp>.md`, announce "**Existing report belongs to a different target — archived as <name>; starting a fresh analysis.**", then continue to section 2 (skip re-collecting the path in section 3 — it is already set).
|
|
32
34
|
|
|
33
35
|
**IF the file does not exist OR stepsCompleted is empty:**
|
|
34
36
|
- Continue to section 2
|
|
@@ -46,7 +48,7 @@ Look for {outputFile}.
|
|
|
46
48
|
|
|
47
49
|
### 3. Collect Project Path
|
|
48
50
|
|
|
49
|
-
**Headless flag consumption:** If `--project-path <path>` was passed at invocation, set `project_paths[]`
|
|
51
|
+
**Headless flag consumption:** If `project_paths[]` is already populated (e.g. collected by the section-1 stale-collision guard) OR `--project-path <path>` was passed at invocation, set/keep `project_paths[]` (comma-split the flag value if multiple paths were supplied), skip the prompt below, and proceed to validation. Otherwise prompt as today.
|
|
50
52
|
|
|
51
53
|
"**Welcome to Analyze Source — the SKF decomposition engine.**
|
|
52
54
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
nextStepFile: 'identify-units.md'
|
|
3
3
|
outputFile: '{forge_data_folder}/analyze-source-report-{project_name}.md'
|
|
4
4
|
heuristicsFile: 'references/unit-detection-heuristics.md'
|
|
5
|
-
|
|
5
|
+
scanManifestsProbeOrder:
|
|
6
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-scan-manifests.py'
|
|
7
|
+
- '{project-root}/src/shared/scripts/skf-scan-manifests.py'
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -33,11 +35,13 @@ Load {heuristicsFile} for reference on detection signals.
|
|
|
33
35
|
|
|
34
36
|
### 2. Scan Directory Structure
|
|
35
37
|
|
|
38
|
+
**Resolve `{scanManifestsHelper}`** from `{scanManifestsProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
39
|
+
|
|
36
40
|
**For each path in `project_paths[]`**, launch a subprocess that scans the project directory structure (aggregate results across all repos with clear repo-level grouping):
|
|
37
41
|
|
|
38
42
|
1. Map the top-level directory tree (2-3 levels deep)
|
|
39
43
|
2. Identify workspace configuration files (pnpm-workspace.yaml, lerna.json, Cargo.toml [workspace], go.work, etc.)
|
|
40
|
-
3. Enumerate package manifests deterministically — invoke `uv run {
|
|
44
|
+
3. Enumerate package manifests deterministically — invoke `uv run {scanManifestsHelper} scan {path}` and parse the JSON envelope. The script returns `{manifests[], total_unique, monorepo, warnings?}` covering npm/python/rust/go/maven/gradle/ruby/composer/swift; record each `{path, ecosystem}` for the manifests catalog in §4 and capture `monorepo` for the boundary-signal pass in §3
|
|
41
45
|
4. Locate entry point files (index.ts, main.ts, app.ts, main.go, main.rs, __init__.py, etc.)
|
|
42
46
|
5. Detect service configuration (Dockerfile, docker-compose.yml, kubernetes manifests, serverless.yml) — keep this step LLM-driven; file glob + presence check is sufficient, no parsing required
|
|
43
47
|
6. Return structured findings — file paths and types only, not contents
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
nextStepFile: 're-index.md'
|
|
3
3
|
outputFile: '{forge_version}/drift-report-{timestamp}.md'
|
|
4
4
|
templateFile: 'assets/drift-report-template.md'
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
loadProvenanceProbeOrder:
|
|
6
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-load-provenance.py'
|
|
7
|
+
- '{project-root}/src/shared/scripts/skf-load-provenance.py'
|
|
8
|
+
compareFileHashesProbeOrder:
|
|
9
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-compare-file-hashes.py'
|
|
10
|
+
- '{project-root}/src/shared/scripts/skf-compare-file-hashes.py'
|
|
7
11
|
---
|
|
8
12
|
|
|
9
13
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -98,12 +102,14 @@ Load the following from the skill directory:
|
|
|
98
102
|
|
|
99
103
|
Search for provenance map at `{forge_data_folder}/{skill_name}/{active_version}/provenance-map.json` (i.e., `{forge_version}/provenance-map.json`). If not found at the versioned path, fall back to `{forge_data_folder}/{skill_name}/provenance-map.json`:
|
|
100
104
|
|
|
105
|
+
**Resolve `{loadProvenanceHelper}`** from `{loadProvenanceProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
106
|
+
|
|
101
107
|
**If found:**
|
|
102
108
|
- Record provenance map age (days since last extraction) from file mtime
|
|
103
109
|
- Normalize the map's deterministic projections in one subprocess call:
|
|
104
110
|
|
|
105
111
|
```bash
|
|
106
|
-
uv run {
|
|
112
|
+
uv run {loadProvenanceHelper} normalize {provenanceMap}
|
|
107
113
|
```
|
|
108
114
|
|
|
109
115
|
Parse the emitted JSON and stash these fields in workflow context (downstream steps read them directly — no re-walk):
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'semantic-diff.md'
|
|
3
3
|
outputFile: '{forge_version}/drift-report-{timestamp}.md'
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
loadProvenanceProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-load-provenance.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-load-provenance.py'
|
|
7
|
+
compareFileHashesProbeOrder:
|
|
8
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-compare-file-hashes.py'
|
|
9
|
+
- '{project-root}/src/shared/scripts/skf-compare-file-hashes.py'
|
|
6
10
|
---
|
|
7
11
|
|
|
8
12
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -89,10 +93,12 @@ For each changed export, record:
|
|
|
89
93
|
|
|
90
94
|
**Only execute if provenance-map.json contains `file_entries`.**
|
|
91
95
|
|
|
96
|
+
**Resolve `{compareFileHashesHelper}`** from `{compareFileHashesProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
97
|
+
|
|
92
98
|
Run one deterministic comparison subprocess — it walks tracked file_entries[] AND the inverse direction (source-tree → candidate set in standard script/asset/doc directories) so the LLM does not orchestrate per-file hashing:
|
|
93
99
|
|
|
94
100
|
```bash
|
|
95
|
-
uv run {
|
|
101
|
+
uv run {compareFileHashesHelper} compare {provenanceMap} {sourceRoot}
|
|
96
102
|
```
|
|
97
103
|
|
|
98
104
|
Parse the emitted JSON:
|
|
@@ -30,7 +30,7 @@ You are a skill scoping architect collaborating with a developer who wants to cr
|
|
|
30
30
|
These rules apply to every step in this workflow:
|
|
31
31
|
|
|
32
32
|
- Only load one step file at a time — never preload future steps
|
|
33
|
-
- **Lazy-load references and assets:** `references/*.md` and `assets/*.md` files are loaded inside the section that needs them, not at step entry. If a section is skipped (e.g. `version-resolution.md` when `{
|
|
33
|
+
- **Lazy-load references and assets:** `references/*.md` and `assets/*.md` files are loaded inside the section that needs them, not at step entry. If a section is skipped (e.g. `version-resolution.md` when `{extractPublicApiHelper}` already returned a version, `scope-templates.md` for the `docs-only` branch that bypasses §2c), do not load that file. Each unnecessary load costs context (~5-10 KB per reference) and biases the LLM toward consulting material the current path does not need.
|
|
34
34
|
- Always communicate in `{communication_language}` (the language for user-facing prose). Written artifact text — the `description`, `notes`, and other free-form fields persisted into `skill-brief.yaml` — is in `{document_output_language}`; per-step rules call this out where it applies (see step 5). The two values may be the same.
|
|
35
35
|
- If `{headless_mode}` is true, auto-proceed through confirmation gates with their default action and log each auto-decision
|
|
36
36
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'scope-definition.md'
|
|
3
3
|
versionResolutionFile: 'references/version-resolution.md'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
extractPublicApiProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-extract-public-api.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-extract-public-api.py'
|
|
7
|
+
detectWorkspacesProbeOrder:
|
|
8
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-detect-workspaces.py'
|
|
9
|
+
- '{project-root}/src/shared/scripts/skf-detect-workspaces.py'
|
|
10
|
+
detectLanguageProbeOrder:
|
|
11
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-detect-language.py'
|
|
12
|
+
- '{project-root}/src/shared/scripts/skf-detect-language.py'
|
|
7
13
|
---
|
|
8
14
|
|
|
9
15
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -58,11 +64,13 @@ Display: "**Resolving target...**"
|
|
|
58
64
|
|
|
59
65
|
### 1b. Detect Monorepo / Workspace Layout
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
**Resolve `{detectWorkspacesHelper}`** from `{detectWorkspacesProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
68
|
+
|
|
69
|
+
Delegate workspace detection to `{detectWorkspacesHelper}` instead of reasoning through manifest rules in prose. Build a payload from the tree fetched in §1 plus the small set of root manifests the detector needs, then invoke the script:
|
|
62
70
|
|
|
63
71
|
```bash
|
|
64
72
|
echo '{"tree": [<flat list of repo-relative file paths>], "manifests": {"package.json": "<raw text>", "Cargo.toml": "<raw text>", "pnpm-workspace.yaml": "<raw text>", "lerna.json": "<raw text>"}}' | \
|
|
65
|
-
uv run {
|
|
73
|
+
uv run {detectWorkspacesHelper}
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
- **`tree`** — pass the flat list of repo-relative file paths already fetched in §1 (for GitHub: the `path` values from the `gh api .../git/trees/HEAD?recursive=1` response; for local: the equivalent listing).
|
|
@@ -104,10 +112,12 @@ List the top-level directory structure:
|
|
|
104
112
|
|
|
105
113
|
### 3. Detect Primary Language
|
|
106
114
|
|
|
107
|
-
|
|
115
|
+
**Resolve `{detectLanguageHelper}`** from `{detectLanguageProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
116
|
+
|
|
117
|
+
Delegate the rule walk to `{detectLanguageHelper}` instead of evaluating manifest presence and extension frequency in prose:
|
|
108
118
|
|
|
109
119
|
```bash
|
|
110
|
-
echo '{"tree": [<flat list of repo-relative file paths from §1>]}' | uv run {
|
|
120
|
+
echo '{"tree": [<flat list of repo-relative file paths from §1>]}' | uv run {detectLanguageHelper}
|
|
111
121
|
```
|
|
112
122
|
|
|
113
123
|
The script returns `{language, confidence, detection_source, fallback_to_extension_frequency}` after walking the documented rule table (manifest presence first — package.json with tsconfig.json disambiguation, Cargo.toml, pyproject.toml/setup.py/setup.cfg, go.mod, pom.xml, build.gradle.kts, build.gradle Groovy with Java/Kotlin disambiguation, *.csproj/*.sln, Gemfile — then extension-frequency fallback over recognized source extensions). Use the returned values directly:
|
|
@@ -120,7 +130,9 @@ If `confidence` is `low` (or `unknown` is returned for `language`): flag for use
|
|
|
120
130
|
|
|
121
131
|
### 4. List Top-Level Modules and Exports
|
|
122
132
|
|
|
123
|
-
|
|
133
|
+
**Resolve `{extractPublicApiHelper}`** from `{extractPublicApiProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
134
|
+
|
|
135
|
+
Identify the public API surface. **Delegate the parsing to `{extractPublicApiHelper}` whenever the detected language is supported** — the script is the single source of truth for manifest parsing, export discovery, and version detection across the whole SKF pipeline. Hand-rolling these in prose creates drift seams the LLM cannot fully close.
|
|
124
136
|
|
|
125
137
|
**Script-supported languages** (use the script): `js`, `ts`, `javascript`, `typescript`, `python`, `rust`, `go`, `java`, `kotlin`.
|
|
126
138
|
|
|
@@ -153,7 +165,7 @@ This section runs exactly one of §4.1 (script path) or §4.2 (fallback path) ba
|
|
|
153
165
|
3. Invoke the script and parse its JSON stdout:
|
|
154
166
|
|
|
155
167
|
```bash
|
|
156
|
-
echo '<payload-json>' | uv run {
|
|
168
|
+
echo '<payload-json>' | uv run {extractPublicApiHelper} --mode quick
|
|
157
169
|
```
|
|
158
170
|
|
|
159
171
|
On a non-zero exit (codes 1 or 2 per the script's docstring), capture stderr, log it, and fall through to §4.2 (the prose-fallback path) — never HALT just because the script choked on an unusual manifest.
|
|
@@ -200,7 +212,7 @@ If CCC is unavailable or returns no results: skip this subsection silently.
|
|
|
200
212
|
|
|
201
213
|
### 4b. Detect Source Version
|
|
202
214
|
|
|
203
|
-
**When the language was script-supported (§4 took the script path):** the `version` field returned by `{
|
|
215
|
+
**When the language was script-supported (§4 took the script path):** the `version` field returned by `{extractPublicApiHelper}` IS the detected version — do not re-derive it and do not load `{versionResolutionFile}`. The script already implements the language-specific lookups documented in that reference, so loading the reference here only burns context.
|
|
204
216
|
|
|
205
217
|
**When the language was not script-supported:** load `{versionResolutionFile}` and follow the prose Detection Algorithm directly (Ruby / C# / Swift / etc. fall outside the script's coverage).
|
|
206
218
|
|
|
@@ -6,8 +6,12 @@ headlessArgsFile: 'references/headless-args.md'
|
|
|
6
6
|
headlessSourceAuthorityDetectionFile: 'references/headless-source-authority-detection.md'
|
|
7
7
|
portfolioSimilarityCheckFile: 'references/portfolio-similarity-check.md'
|
|
8
8
|
draftCheckpointFile: 'references/draft-checkpoint.md'
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
validateBriefInputsProbeOrder:
|
|
10
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-validate-brief-inputs.py'
|
|
11
|
+
- '{project-root}/src/shared/scripts/skf-validate-brief-inputs.py'
|
|
12
|
+
emitBriefEnvelopeProbeOrder:
|
|
13
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
14
|
+
- '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -302,14 +306,16 @@ Display: "**Select:** [C] Continue to Target Analysis · [X] Cancel and exit"
|
|
|
302
306
|
#### EXECUTION RULES:
|
|
303
307
|
|
|
304
308
|
- ALWAYS halt and wait for user input after presenting menu
|
|
305
|
-
- **
|
|
309
|
+
- **Resolve `{validateBriefInputsHelper}`** from `{validateBriefInputsProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
310
|
+
|
|
311
|
+
- **GATE [default: use args]** — If `{headless_mode}`, consume pre-supplied arguments and auto-proceed. The full argument set (required/optional, defaults, halt codes, enum values) is documented in `{headlessArgsFile}` — load it now if you need to look up a specific argument. Validation is delegated to `{validateBriefInputsHelper}`; the table is the canonical operator-facing documentation, the script enforces it.
|
|
306
312
|
|
|
307
313
|
**Preset merge (before validation).** If the headless args include a `preset` field, load `{sidecar_path}/brief-presets/{preset}.yaml` and merge its contents as defaults — explicit args override preset values, key by key. The preset file is YAML; if it does not exist, log `"warn: preset '{name}' not found at {path} — proceeding without preset"` and continue (do not HALT). If it parses but contains unknown fields, log per-field warnings and pass through unchanged (the validator's KNOWN_FIELDS check will catch any that survive). Drop the `preset` key itself from the merged dict before passing to the validator (it is consumed at this level and is not a brief field).
|
|
308
314
|
|
|
309
|
-
**Delegate validation to `{
|
|
315
|
+
**Delegate validation to `{validateBriefInputsHelper}`** instead of reasoning through the table rules in prose:
|
|
310
316
|
|
|
311
317
|
```bash
|
|
312
|
-
echo '<headless-args-as-json>' | uv run {
|
|
318
|
+
echo '<headless-args-as-json>' | uv run {validateBriefInputsHelper}
|
|
313
319
|
```
|
|
314
320
|
|
|
315
321
|
The script returns a JSON envelope: `{valid, errors[], warnings[], normalized, halt_reason}`. Apply the result deterministically:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Headless Argument Table
|
|
2
2
|
|
|
3
|
-
Loaded by step 1 §8 only when `{headless_mode}` is true. Canonical operator-facing documentation for the argument set consumed at step 1's GATE; the `{
|
|
3
|
+
Loaded by step 1 §8 only when `{headless_mode}` is true. Canonical operator-facing documentation for the argument set consumed at step 1's GATE; the `{validateBriefInputsHelper}` enforces these rules deterministically (its `KNOWN_FIELDS` set must stay in sync with this table).
|
|
4
4
|
|
|
5
5
|
| Argument | Required | Default | Notes |
|
|
6
6
|
|----------|----------|---------|-------|
|
|
@@ -41,7 +41,7 @@ echo '{
|
|
|
41
41
|
"skill_name": "{skill-name}",
|
|
42
42
|
"created_at": "{current ISO date}"
|
|
43
43
|
// include "status": "pending" only when embed verification failed
|
|
44
|
-
}' | uv run {
|
|
44
|
+
}' | uv run {forgeTierRwHelper} register-qmd-collection --target {forgeTierFile}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
The script handles the upsert deterministically (replace existing entry with same `name`, else append) and preserves all other forge-tier state (tools, tier, ccc_index, ccc_index_registry, other qmd_collections entries) — no need to reason about YAML re-rendering or section comments.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'confirm-brief.md'
|
|
3
3
|
scopeTemplatesFile: 'assets/scope-templates.md'
|
|
4
|
-
|
|
4
|
+
recommendScopeTypeProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-recommend-scope-type.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-recommend-scope-type.py'
|
|
5
7
|
advancedElicitationSkill: '/bmad-advanced-elicitation'
|
|
6
8
|
partyModeSkill: '/bmad-party-mode'
|
|
7
9
|
---
|
|
@@ -82,7 +84,9 @@ Load `{scopeTemplatesPath}` for the scope type options ([F], [M], [P], [C], [R])
|
|
|
82
84
|
|
|
83
85
|
**Recommend a scope type — don't present the five options as equal weight.** SKILL.md states this workflow "steers toward the smaller, sharper version when scope is unclear" — surface that opinion at decision time. Use the analysis from step 2 and the user's intent from step 1 to pick the best-fit recommendation, then present the menu with that option marked as the suggested default.
|
|
84
86
|
|
|
85
|
-
**
|
|
87
|
+
**Resolve `{recommendScopeTypeHelper}`** from `{recommendScopeTypeProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
88
|
+
|
|
89
|
+
**Delegate the recommendation to `{recommendScopeTypeHelper}`** instead of walking the heuristic ladder in prose. The script is the single source of truth for the five-rule ladder (component-registry → reference-app keywords → specific-modules naming/count → narrow-public-api → default full-library) plus the docs-only short-circuit. Both the interactive recommendation and the §6 headless GATE invoke the same script — same inputs, same outputs, no drift.
|
|
86
90
|
|
|
87
91
|
**Fetch registry-file contents before building the payload.** Step-02 §4.1 fetches `package.json` plus the entry-point files but does not fetch `registry.ts` / `components.ts` — the deep-match branch of the component-registry rule needs those contents. Scan the tree for any of `registry.ts` / `registry.tsx` / `components.ts` / `components.tsx` (any depth). For each match, fetch its contents in **one message with N parallel Bash calls** (`gh api repos/{owner}/{repo}/contents/{path}` for GitHub, file reads for local), then base64-decode the responses together. Skip the fetch if the tree contains no registry files.
|
|
88
92
|
|
|
@@ -97,7 +101,7 @@ echo '{
|
|
|
97
101
|
"entry_files": [{"path": "<registry path>", "content": "<contents>"}, ...],
|
|
98
102
|
"source_type": "source",
|
|
99
103
|
"mode": "interactive"
|
|
100
|
-
}' | uv run {
|
|
104
|
+
}' | uv run {recommendScopeTypeHelper}
|
|
101
105
|
```
|
|
102
106
|
|
|
103
107
|
`entry_files` carries the registry contents fetched above; omit when no registry files exist in the tree. `mode: "interactive"` activates the content-inspection branch of the component-registry rule (10+ entries or `Component[]` annotation); the headless GATE in §6 uses `mode: "headless"` which falls back to presence-only matching. `source_type: "docs-only"` short-circuits to `docs-only` regardless of the other signals.
|
|
@@ -182,7 +186,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
|
|
|
182
186
|
- ALWAYS halt and wait for user input after presenting menu
|
|
183
187
|
- **GATE [default: C]** — If `{headless_mode}`: consume the headless inputs from step 1 in priority order:
|
|
184
188
|
- If `scope_type` was supplied, use it (must match one of the six valid types) and skip the §2c template menu.
|
|
185
|
-
- Otherwise auto-select via `{
|
|
189
|
+
- Otherwise auto-select via `{recommendScopeTypeHelper}` — invoke the script with the **same payload shape** documented in §2c but with `mode: "headless"` (presence-only matching for the component-registry rule, since `entry_files` may not be available without an interactive context). Use the returned `scope_type` and log `"headless: scope_type={value} from heuristic={matched_heuristic}"`. The script's docs-only short-circuit handles `source_type=docs-only` automatically.
|
|
186
190
|
- If `include`/`exclude` were supplied, use them verbatim (split on comma) instead of running the boundary prompts in §3.
|
|
187
191
|
- If `scripts_intent`/`assets_intent` were supplied, record them and skip §5b; otherwise default to `detect`.
|
|
188
192
|
- Set `scope.rationale`: `recommended`/`heuristic` from the script (or `recommended = scope_type` arg, `heuristic = "user-supplied-arg"` when `scope_type` was passed); `chosen = <resolved type>`; `accepted_recommendation = (no scope_type arg)`; `reason = "<script rationale>"` (auto path) or `"headless: scope_type supplied as argument"` (arg path); `recorded = {date}`. No prompt — headless never asks "why".
|
|
@@ -3,9 +3,15 @@ briefSchemaFile: 'assets/skill-brief-schema.md'
|
|
|
3
3
|
versionResolutionFile: 'references/version-resolution.md'
|
|
4
4
|
qmdRegistrationFile: 'references/qmd-collection-registration.md'
|
|
5
5
|
nextStepFile: 'health-check.md'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
writeSkillBriefProbeOrder:
|
|
7
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-write-skill-brief.py'
|
|
8
|
+
- '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
|
|
9
|
+
emitBriefEnvelopeProbeOrder:
|
|
10
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
11
|
+
- '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
|
|
12
|
+
forgeTierRwProbeOrder:
|
|
13
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-forge-tier-rw.py'
|
|
14
|
+
- '{project-root}/src/shared/scripts/skf-forge-tier-rw.py'
|
|
9
15
|
forgeTierFile: '{sidecar_path}/forge-tier.yaml'
|
|
10
16
|
---
|
|
11
17
|
|
|
@@ -30,7 +36,9 @@ To generate the complete skill-brief.yaml from the approved brief data and write
|
|
|
30
36
|
|
|
31
37
|
### 1. Reference the Schema (LLM context only)
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
**Resolve `{writeSkillBriefHelper}`** from `{writeSkillBriefProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
40
|
+
|
|
41
|
+
`{briefSchemaPath}` and `{versionResolutionFile}` document the brief contract for human readers. The deterministic enforcement of that contract lives in `{writeSkillBriefHelper}` and its JSON Schema artifact at `src/shared/scripts/schemas/skill-brief.v1.json`. Load `{briefSchemaPath}` only if you need to explain a specific field to the user during inline adjustments — otherwise skip the read; the script is the source of truth.
|
|
34
42
|
|
|
35
43
|
### 2. Resolve Output Path
|
|
36
44
|
|
|
@@ -94,7 +102,7 @@ Assemble the brief context as a **flat** JSON object — every approved value is
|
|
|
94
102
|
Pipe it into the writer script with the `--from-flat` flag:
|
|
95
103
|
|
|
96
104
|
```bash
|
|
97
|
-
echo '<context-json>' | uv run {
|
|
105
|
+
echo '<context-json>' | uv run {writeSkillBriefHelper} write --target {resolved-target-path} --from-flat
|
|
98
106
|
```
|
|
99
107
|
|
|
100
108
|
The script translates flat → nested internally, drops the null optional fields, and runs the same schema validation and atomic write as before — pass every key always, the writer decides what reaches the YAML.
|
|
@@ -121,20 +129,22 @@ The script:
|
|
|
121
129
|
|
|
122
130
|
### 4b. Headless Result Envelope (Canonical)
|
|
123
131
|
|
|
132
|
+
**Resolve `{emitBriefEnvelopeHelper}`** from `{emitBriefEnvelopeProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
133
|
+
|
|
124
134
|
This section is the canonical envelope-emission reference for the workflow. Every headless emission — the success terminal here and every HARD HALT in step 1/02/05 — uses this contract. Remote sites point here instead of restating it.
|
|
125
135
|
|
|
126
136
|
**Success (this call site only — emitted from §3 directly):**
|
|
127
137
|
|
|
128
138
|
```bash
|
|
129
139
|
echo '{"status":"success","brief_path":"<from §3 response>","skill_name":"<name>","version":"<from §3 response>","language":"<language>","scope_type":"<scope.type>","halt_reason":null}' | \
|
|
130
|
-
uv run {
|
|
140
|
+
uv run {emitBriefEnvelopeHelper} emit
|
|
131
141
|
```
|
|
132
142
|
|
|
133
143
|
**Error (used by every HARD HALT site):**
|
|
134
144
|
|
|
135
145
|
```bash
|
|
136
146
|
echo '{"status":"error","skill_name":"<name>","halt_reason":"<reason>"}' | \
|
|
137
|
-
uv run {
|
|
147
|
+
uv run {emitBriefEnvelopeHelper} emit --target stderr
|
|
138
148
|
```
|
|
139
149
|
|
|
140
150
|
When the HALT fires before `skill_name` has been resolved (step 1 §1 pre-flight write probe, step 1 §8 input-missing on a malformed args bundle), pass the partially-gathered value or the literal `"unknown"` — the script accepts any non-empty string at this position.
|
|
@@ -149,6 +159,8 @@ When `{headless_mode}` is false, skip this section silently — no envelope is e
|
|
|
149
159
|
|
|
150
160
|
### 5. QMD Collection Registration (Deep Tier Only)
|
|
151
161
|
|
|
162
|
+
**Resolve `{forgeTierRwHelper}`** from `{forgeTierRwProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
163
|
+
|
|
152
164
|
**IF forge tier is Deep AND QMD tool is available:** load `{qmdRegistrationFile}` and follow the procedure there to index the brief into a QMD collection and update the forge-tier registry.
|
|
153
165
|
|
|
154
166
|
**IF forge tier is NOT Deep OR QMD is not available:** skip this section silently — do not load `{qmdRegistrationFile}`. No messaging.
|
|
@@ -400,6 +400,8 @@ When using ast-grep for extraction, be aware of these documented limitations:
|
|
|
400
400
|
|
|
401
401
|
7. **Python class patterns with bases/colon return zero (ast-grep 0.42.x):** The patterns `class $NAME($$$BASES)` and `class $NAME($$$BASES):` return zero matches on real Python sources with ast-grep 0.42.0, even on files containing dozens of subclassed public classes. `find_code_by_rule` also rejects the bare inline rule without `kind` as `Rule must specify a set of AST kinds to match. Try adding \`kind\` rule.` **Workaround:** Use the minimal `class $NAME` pattern with `kind: class_definition` (YAML) or `ast-grep run -p 'class $NAME' -l python --json=stream` (CLI), then post-filter names via the `^[^_]` regex. The `^[^_]` constraint enforces the "public" filter since ast-grep's base-match rule is what's broken, not the name-match rule. See the Python — public classes recipe above.
|
|
402
402
|
|
|
403
|
+
8. **Rust `pub fn` any-pattern returns zero; bare `pub fn $NAME` over-captures (ast-grep 0.42.x):** The `rust-public-functions` recipe's `any:` of `pub fn $NAME($$$PARAMS) -> $RET` / `pub fn $NAME($$$PARAMS)` returns "No matches found" on real Rust sources with ast-grep 0.42.2, even on crates containing 200+ public functions. Dropping to the bare `pub fn $NAME` pattern matches, but over-captures restricted-visibility functions such as `pub(crate) fn` / `pub(super) fn`, which are **not** public API. **Workaround:** Prefer a visibility-constrained source grep — `rg '^\s*pub fn ' <src>` filtered to exclude lines beginning `pub(` — cross-checked against the AN-verified public surface, at T1-low confidence. Never silently accept zero results for Rust public functions, and never treat a bare `pub fn $NAME` match set as the public API without stripping `pub(...)`-restricted items. See the Rust — public functions recipe above.
|
|
404
|
+
|
|
403
405
|
### Component Library Demo/Example Auto-Exclusion
|
|
404
406
|
|
|
405
407
|
When `scope.type: "component-library"`, auto-detect and propose demo/example exclusions before extraction begins. **User confirmation is required before applying** — some `examples/` directories contain API-level code.
|
|
@@ -61,11 +61,11 @@ Run the external skill-check tool against the compiled skill staging directory.
|
|
|
61
61
|
**Flag probe (run once, cache the result for §4 and §5 re-invocations):**
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
npx skill-check --help 2>/dev/null | grep -- --no-security-scan
|
|
64
|
+
npx skill-check check --help 2>/dev/null | grep -- --no-security-scan
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
- If the probe matches `--no-security-scan`: set `{security_scan_flag} = "--no-security-scan"`.
|
|
68
|
-
- Else run a second probe — `npx skill-check --help 2>/dev/null | grep -- --skip-security` — and if it matches, set `{security_scan_flag} = "--skip-security"`.
|
|
68
|
+
- Else run a second probe — `npx skill-check check --help 2>/dev/null | grep -- --skip-security` — and if it matches, set `{security_scan_flag} = "--skip-security"`.
|
|
69
69
|
- If neither flag exists: set `{security_scan_flag} = ""` (empty) AND set `{skill_check_flag_fallback} = true`. Skip §2 and §4 automated flows entirely — fall through to §3 manual frontmatter validation. Record in evidence-report: `skill_check_flag_probe: neither --no-security-scan nor --skip-security supported by installed skill-check; validation performed manually`.
|
|
70
70
|
|
|
71
71
|
**If a security-scan-disable flag was resolved (probe succeeded):**
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
nextStepFile: 'compile-stack.md'
|
|
3
3
|
integrationPatterns: 'references/integration-patterns.md'
|
|
4
4
|
composeModeRules: 'references/compose-mode-rules.md'
|
|
5
|
-
|
|
5
|
+
pairIntersectProbeOrder:
|
|
6
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-pair-intersect.py'
|
|
7
|
+
- '{project-root}/src/shared/scripts/skf-pair-intersect.py'
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -35,8 +37,11 @@ From `confirmed_dependencies`, conceptually you have N*(N-1)/2 unordered pairs.
|
|
|
35
37
|
]
|
|
36
38
|
```
|
|
37
39
|
2. **Invoke the script** via stdin (or a temp file under `{forge_data_folder}/` if stdin piping is unavailable):
|
|
40
|
+
|
|
41
|
+
**Resolve `{pairIntersectHelper}`** from `{pairIntersectProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
42
|
+
|
|
38
43
|
```bash
|
|
39
|
-
uv run {
|
|
44
|
+
uv run {pairIntersectHelper} intersect --libraries -
|
|
40
45
|
```
|
|
41
46
|
piping the libraries JSON on stdin. The script emits:
|
|
42
47
|
```json
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'rank-and-confirm.md'
|
|
3
3
|
manifestPatterns: 'references/manifest-patterns.md'
|
|
4
|
-
|
|
4
|
+
scanManifestsProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-scan-manifests.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-scan-manifests.py'
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -90,8 +92,10 @@ Store the explicit list as `raw_dependencies` and skip to [Display Detection Sum
|
|
|
90
92
|
|
|
91
93
|
Invoke the deterministic manifest scanner — it walks the project root, parses every recognised manifest, dedupes the production dep set, and flags monorepo layout:
|
|
92
94
|
|
|
95
|
+
**Resolve `{scanManifestsHelper}`** from `{scanManifestsProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
96
|
+
|
|
93
97
|
```bash
|
|
94
|
-
uv run {
|
|
98
|
+
uv run {scanManifestsHelper} scan {scan_root}
|
|
95
99
|
```
|
|
96
100
|
|
|
97
101
|
Where `{scan_root}` is the project root path. Load `{manifestPatterns}` for the ecosystem reference table that documents supported filenames, dependency keys, and normalisation rules; the script implements exactly that table (npm/pnpm/yarn, python pip/poetry/pdm, rust cargo, go modules, java/kotlin maven + gradle, ruby bundler, composer, swift package manager). Exclusion patterns (`node_modules/`, `.venv/`, `vendor/`, `dist/`, `build/`, `target/`, `.git/`, hidden dirs) are applied internally.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'detect-integrations.md'
|
|
3
|
-
|
|
3
|
+
enumerateStackSkillsProbeOrder:
|
|
4
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-enumerate-stack-skills.py'
|
|
5
|
+
- '{project-root}/src/shared/scripts/skf-enumerate-stack-skills.py'
|
|
4
6
|
---
|
|
5
7
|
|
|
6
8
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -33,8 +35,10 @@ Use `skill_package_path` (stored in step 2 and optionally refreshed above) direc
|
|
|
33
35
|
|
|
34
36
|
**Exports resolution order (H1) — script-driven:** Do NOT walk per-skill `metadata.json` → `references/` → SKILL.md by hand. Invoke the helper once at step entry to compute the full inventory for every confirmed skill in one deterministic call:
|
|
35
37
|
|
|
38
|
+
**Resolve `{enumerateStackSkillsHelper}`** from `{enumerateStackSkillsProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
39
|
+
|
|
36
40
|
```bash
|
|
37
|
-
uv run {
|
|
41
|
+
uv run {enumerateStackSkillsHelper} enumerate {skills_output_folder}
|
|
38
42
|
```
|
|
39
43
|
|
|
40
44
|
The script emits JSON of the form:
|
|
@@ -48,8 +48,9 @@ Emit a single warning (once, not per snippet) and present resolution options bef
|
|
|
48
48
|
> - **(a) Set override** — add `snippet_skill_root_override: {observed_prefix}` to `config.yaml`. Snippets keep their on-disk prefix; the managed section references the real location.
|
|
49
49
|
> - **(b) Proceed with IDE mapping** — step 4 will rewrite every snippet's root path to the IDE's skill_root. Use this only if the IDE's skill directory actually contains the skill files.
|
|
50
50
|
> - **(c) Cancel** — abort export and investigate.
|
|
51
|
+
> - **(d) Use observed prefix for this run only** — set the effective snippet root to the single observed on-disk prefix for this export run, without writing to `config.yaml`. The managed section references the real on-disk location. (Only offered when exactly one prefix was observed.)
|
|
51
52
|
>
|
|
52
|
-
> If multiple distinct prefixes were observed, the snippets disagree with each other — investigate before choosing (a).
|
|
53
|
+
> If multiple distinct prefixes were observed, the snippets disagree with each other — investigate before choosing (a) or (d).
|
|
53
54
|
|
|
54
55
|
Wait for user choice.
|
|
55
56
|
|
|
@@ -69,6 +70,10 @@ Continue to §2. Step 4's root-rewrite algorithm will rewrite every snippet's pr
|
|
|
69
70
|
|
|
70
71
|
HALT. Exit code 6, `halt_reason: "user-cancelled"`. No files written.
|
|
71
72
|
|
|
73
|
+
### (d) Use observed prefix for this run only
|
|
74
|
+
|
|
75
|
+
**Only available when `observed_prefixes` contains exactly one value.** Set the effective snippet root for this run to that single observed prefix — do NOT mutate `config.yaml`. Step 4's root-rewrite algorithm uses this value as the reference instead of `target_context_files[0].skill_root`, so snippets keep their on-disk prefix and the managed section resolves to the real location. Print a one-line hint: "Persist `snippet_skill_root_override: {observed_prefix}` in config.yaml to skip this prompt on future exports." Continue to §2.
|
|
76
|
+
|
|
72
77
|
## No-mismatch fast path
|
|
73
78
|
|
|
74
79
|
**If all observed prefixes match the reference `skill_root` (or no existing snippets were found):** no warning, no gate. Return control to §1b and proceed silently to §2.
|
|
@@ -89,18 +89,18 @@ Your skill is generated from an external dependency for local use.
|
|
|
89
89
|
|
|
90
90
|
### 3. Use It In This Project
|
|
91
91
|
|
|
92
|
-
Always show this section (regardless of `source_authority`) so the author can install the skill they just forged into the current project and try it immediately
|
|
92
|
+
Always show this section (regardless of `source_authority`) so the author can install the skill they just forged into the current project and try it immediately.
|
|
93
|
+
|
|
94
|
+
**Rendering rule for the command below — must be obeyed:** substitute `{resolved_skill_package}` with the actual resolved path before printing (do not leave the placeholder for the user to replace). The substituted path must be either absolute (starts with `/`) or `./`-prefixed. A bare relative path like `skills/foo/1.0.0/foo` is parsed by the `skills` CLI as `https://github.com/skills/foo.git` (org/repo GitHub lookup) and will fail with an authentication error. If `{resolved_skill_package}` resolves to a relative path, prefix it with `./` before emitting.
|
|
93
95
|
|
|
94
96
|
"### Use It In This Project
|
|
95
97
|
|
|
96
|
-
To install this skill into the current project right now:
|
|
98
|
+
To install this skill into the current project right now, run:
|
|
97
99
|
|
|
98
100
|
```
|
|
99
101
|
npx skills add {resolved_skill_package}
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
Replace `{resolved_skill_package}` with the absolute path to the package folder shown above (from step 2), e.g. `{skills_output_folder}/{skill-name}/{version}/{skill-name}/`.
|
|
103
|
-
|
|
104
104
|
For other source formats (registry name, git URL, tarball, etc.), see the **Installation → Source Formats** section at <https://www.npmjs.com/package/skills>.
|
|
105
105
|
|
|
106
106
|
If the installed skill's commands are not available in the same IDE session, reload plugins (e.g. for Claude Code users, run `/reload-plugins`) or restart your AI editor. Next time you will see the command `/{skill-name}` (e.g. `/{skill-name}`)."
|
|
@@ -335,10 +335,10 @@ On success per file, report: "**{target-file} updated successfully.** Verified b
|
|
|
335
335
|
5. Write the updated manifest atomically via `{manifestOpsHelper}` after all skills in the batch have been applied. For each skill / version pair, invoke:
|
|
336
336
|
|
|
337
337
|
```bash
|
|
338
|
-
python3 {manifestOpsHelper} {skills_output_folder} set {skill-name} {version}
|
|
338
|
+
python3 {manifestOpsHelper} {skills_output_folder} set {skill-name} {version} --ides {ides_written}
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
The helper handles v2-schema validation, v1→v2 migration, and `platforms`→`ides` rename internally — no in-prompt JSON manipulation needed. Each `set` invocation
|
|
341
|
+
`{ides_written}` is the comma-joined sorted IDE set computed in step 3. The helper handles v2-schema validation, v1→v2 migration, and `platforms`→`ides` rename internally — no in-prompt JSON manipulation needed. Each `set` invocation unions the supplied `--ides` into the version entry's existing `ides` (deduplicated, sorted) server-side and refreshes `last_exported`. After all skills have been set, re-read the manifest via `{manifestOpsHelper} {skills_output_folder} read` to confirm the final state matches expectations.
|
|
342
342
|
|
|
343
343
|
**Dry-run mode:** Do NOT update the manifest. Display: "**[DRY RUN] Export manifest would be updated for {skill-name-list} — ides: {ides_written}.**" (list every skill in `skill_batch`)
|
|
344
344
|
|
|
@@ -4,7 +4,9 @@ outputFile: '{forge_version}/test-report-{skill_name}-{run_id}.md'
|
|
|
4
4
|
outputFormatsFile: 'assets/output-section-formats.md'
|
|
5
5
|
scoringRulesFile: 'references/scoring-rules.md'
|
|
6
6
|
migrationSectionRules: 'references/migration-section-rules.md'
|
|
7
|
-
|
|
7
|
+
scanSkillMdStructureProbeOrder:
|
|
8
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-scan-skill-md-structure.py'
|
|
9
|
+
- '{project-root}/src/shared/scripts/skf-scan-skill-md-structure.py'
|
|
8
10
|
---
|
|
9
11
|
|
|
10
12
|
<!-- Config: communicate in {communication_language}. -->
|
|
@@ -26,11 +28,13 @@ Read `testMode` from `{outputFile}` frontmatter.
|
|
|
26
28
|
|
|
27
29
|
Perform the following explicit checks (no hand-waving — most use a single deterministic script; severity assignments are binding; do not relax them).
|
|
28
30
|
|
|
29
|
-
**
|
|
31
|
+
**Resolve `{scanSkillMdStructureHelper}`** from `{scanSkillMdStructureProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
32
|
+
|
|
33
|
+
**2.0 Run the structural scan.** Invoke `{scanSkillMdStructureHelper}` twice and parse the JSON outputs. These results back §§2.1, 2.2, 2.3, and 2.6 — do not re-implement those checks with grep/sed/awk loops.
|
|
30
34
|
|
|
31
35
|
```bash
|
|
32
|
-
uv run {
|
|
33
|
-
uv run {
|
|
36
|
+
uv run {scanSkillMdStructureHelper} scan {skill-md} --required-sections
|
|
37
|
+
uv run {scanSkillMdStructureHelper} scan {skill-md}
|
|
34
38
|
```
|
|
35
39
|
|
|
36
40
|
The first call returns `{ description: {satisfied, matched_synonym, tried[]}, usage: {...}, api_surface: {...} }`. The second returns `{ unbalanced_fences, fence_count, bare_opening_fences[{line,text}], table_drift[{line,section,expected_cols,actual_cols,row}] }`. Hold both JSON blobs for the checks below.
|
|
@@ -47,9 +51,12 @@ The script matches case-insensitively and tolerates `##`/`###` heading levels. S
|
|
|
47
51
|
|
|
48
52
|
**2.3 Language tags on opening fences.** Read `bare_opening_fences[]` from the second JSON blob. The script already runs the stateful open/close scan — closing fences are never reported. For each entry, emit a **Medium severity** finding: `naive-coherence — opening code fence at line {entry.line} missing language tag`.
|
|
49
53
|
|
|
50
|
-
**2.4 Exports cross-used in
|
|
51
|
-
-
|
|
52
|
-
- **
|
|
54
|
+
**2.4 Exports cross-used in a usage-family section.** For each function name reported in the step 3 subagent inventory (`exports[].name` where `kind == "function"` or `kind == "method"`):
|
|
55
|
+
- Determine the usage-family search scope:
|
|
56
|
+
- **Single-body skill** (no `references/` directory, or `## Full*` sections carry real content): the span from §2.1's `matched_synonym` anchor to the next `^## ` anchor.
|
|
57
|
+
- **Split-body skill** (a `references/` directory exists alongside SKILL.md AND the SKILL.md `## Full*` sections are stubs/pointers): the union of EVERY usage-family heading present in SKILL.md (`Usage`/`Examples`/`How to use`/`Quickstart`/`Quick Start`/`Getting Started`/`Common Workflows`/`Key API Summary`), each from its anchor to the next `^## ` anchor, PLUS the full text of every file under `references/`.
|
|
58
|
+
- `grep -c "{export.name}"` across that scope and sum the counts.
|
|
59
|
+
- **Zero occurrences across the entire scope → High severity** finding: `naive-coherence — exported {kind} \`{name}\` is not referenced in any usage-family section or reference file`. This catches the "documented but unused" failure mode that trivially fails discovery testing. A method referenced in any usage-family section OR any `references/` file satisfies the check.
|
|
53
60
|
|
|
54
61
|
**2.5 Async/sync consistency.** For every export with `async` in its description prose (grep for `\basync\b` in the description segment), check the corresponding code example segment for `await` / `async` keywords:
|
|
55
62
|
- Description says async + example shows no `await` → **High severity** finding: `naive-coherence — \`{name}\` described as async but example lacks \`await\``
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
nextStepFile: 'integrations.md'
|
|
3
3
|
coveragePatternsData: '{coveragePatternsPath}'
|
|
4
4
|
feasibilitySchemaRef: 'src/shared/references/feasibility-report-schema.md'
|
|
5
|
-
|
|
5
|
+
atomicWriteProbeOrder:
|
|
6
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
7
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
6
8
|
outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
|
|
7
9
|
outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
|
|
8
10
|
---
|
|
@@ -107,13 +109,15 @@ Extra and Orphan skills are informational only. They do not affect the coverage
|
|
|
107
109
|
|
|
108
110
|
### 6. Append to Report
|
|
109
111
|
|
|
112
|
+
**Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
113
|
+
|
|
110
114
|
Write the **Coverage Analysis** section to `{outputFile}` (see `{feasibilitySchemaRef}` — section headings are fixed and ordered: `## Executive Summary`, `## Coverage Analysis`, `## Integration Verdicts`, `## Recommendations`, `## Evidence Sources`):
|
|
111
115
|
- Include the full coverage table
|
|
112
116
|
- Include coverage percentage
|
|
113
117
|
- Include missing skill recommendations
|
|
114
118
|
- Include the Extra (unreferenced) and Orphan (source_repo unresolvable) subdivisions from section 4
|
|
115
119
|
- Update frontmatter: append `'coverage'` to `stepsCompleted`; set `coveragePercentage` (integer 0..100)
|
|
116
|
-
- Pipe the updated full content through `python3 {
|
|
120
|
+
- Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
|
|
117
121
|
|
|
118
122
|
### 7. Auto-Proceed to Next Step
|
|
119
123
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'coverage.md'
|
|
3
3
|
feasibilitySchemaRef: 'src/shared/references/feasibility-report-schema.md'
|
|
4
|
-
|
|
4
|
+
atomicWriteProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
5
7
|
# {outputFile} and {outputFileLatest} resolve from the activation-stored
|
|
6
8
|
# {project_slug}, {timestamp}, and {outputFolderPath} variables (set in
|
|
7
9
|
# SKILL.md On Activation §2 + §4). The activation-stored values are
|
|
@@ -109,6 +111,8 @@ Each helper-emitted entry includes: `skill_name`, `version`, `language`, `confid
|
|
|
109
111
|
|
|
110
112
|
### 4. Create Feasibility Report
|
|
111
113
|
|
|
114
|
+
**Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
115
|
+
|
|
112
116
|
This skill is the PRODUCER of the feasibility report schema defined in `{feasibilitySchemaRef}`. All outputs MUST conform to that schema — in particular: `schemaVersion: "1.0"`, the defined verdict token set (`Verified|Plausible|Risky|Blocked`; overall `FEASIBLE|CONDITIONALLY_FEASIBLE|NOT_FEASIBLE`), the filename pattern, and the section-heading order.
|
|
113
117
|
|
|
114
118
|
**Filename variables** (already computed at activation — do not re-derive):
|
|
@@ -137,7 +141,7 @@ This skill is the PRODUCER of the feasibility report schema defined in `{feasibi
|
|
|
137
141
|
- `skillsAnalyzed: {count}`
|
|
138
142
|
- `stepsCompleted: ['init']`
|
|
139
143
|
|
|
140
|
-
**Atomic write:** Pipe the staged content through `python3 {
|
|
144
|
+
**Atomic write:** Pipe the staged content through `python3 {atomicWriteHelper} write --target {outputFile}` and then again with `--target {outputFileLatest}`. Both writes use the same staged content. Do NOT use `rm`+rewrite; do NOT create a symlink for the `-latest` copy.
|
|
141
145
|
|
|
142
146
|
On any non-zero exit from either write: HALT (exit code 4, `halt_reason: "write-failed"`) and emit the error envelope per SKILL.md "Result Contract (Headless)" with `report_path: null`, `report_latest_path: null`, `overall_verdict: null`.
|
|
143
147
|
|
|
@@ -3,7 +3,9 @@ nextStepFile: 'requirements.md'
|
|
|
3
3
|
integrationRulesData: '{integrationRulesPath}'
|
|
4
4
|
coveragePatternsData: '{coveragePatternsPath}'
|
|
5
5
|
feasibilitySchemaRef: 'src/shared/references/feasibility-report-schema.md'
|
|
6
|
-
|
|
6
|
+
atomicWriteProbeOrder:
|
|
7
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
8
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
7
9
|
outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
|
|
8
10
|
outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
|
|
9
11
|
---
|
|
@@ -160,12 +162,14 @@ For each integration pair `{library_a, library_b}`, apply the verification proto
|
|
|
160
162
|
|
|
161
163
|
### 6. Append to Report
|
|
162
164
|
|
|
165
|
+
**Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
166
|
+
|
|
163
167
|
Write the **Integration Verdicts** section to `{outputFile}` (heading is fixed — consumers grep for `## Integration Verdicts`; the table header MUST be the canonical `| lib_a | lib_b | verdict | rationale |` per `{feasibilitySchemaRef}`; the skill-local display table with the extra Context/Source/Evidence columns can be rendered beneath it for human readers):
|
|
164
168
|
- Emit the canonical `| lib_a | lib_b | verdict | rationale |` table first (verdict tokens MUST be one of `Verified`, `Plausible`, `Risky`, `Blocked` — case-sensitive)
|
|
165
169
|
- Include the extended table with Context, Source, and Evidence columns below it
|
|
166
170
|
- Include recommendations for Risky and Blocked pairs (Blocked recommendations MUST cite a named candidate per step 5 H6, or the explicit no-candidate notice)
|
|
167
171
|
- Update frontmatter: append `'integrations'` to `stepsCompleted`; set `pairsVerified`, `pairsPlausible`, `pairsRisky`, `pairsBlocked` counts
|
|
168
|
-
- Pipe the updated full content through `python3 {
|
|
172
|
+
- Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
|
|
169
173
|
|
|
170
174
|
### 7. Auto-Proceed to Next Step
|
|
171
175
|
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
|
|
7
7
|
outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
|
|
8
8
|
feasibilitySchemaRef: 'src/shared/references/feasibility-report-schema.md'
|
|
9
|
-
|
|
9
|
+
atomicWriteProbeOrder:
|
|
10
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
11
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
10
12
|
nextStepFile: 'health-check.md'
|
|
11
13
|
---
|
|
12
14
|
|
|
@@ -98,9 +100,11 @@ Based on the overall verdict, present the appropriate recommendation:
|
|
|
98
100
|
|
|
99
101
|
### 4b. Result Contract
|
|
100
102
|
|
|
103
|
+
**Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
104
|
+
|
|
101
105
|
Write the result contract per `shared/references/output-contract-schema.md`: the per-run record at `{forge_data_folder}/verify-stack-result-{YYYYMMDD-HHmmss}.json` (UTC timestamp, resolution to seconds) and a copy at `{forge_data_folder}/verify-stack-result-latest.json` (stable path for pipeline consumers — copy, not symlink). Include the feasibility report path (both `{outputFile}` and `{outputFileLatest}`) in `outputs`; include `overallVerdict` (`FEASIBLE` / `CONDITIONALLY_FEASIBLE` / `NOT_FEASIBLE`), `coveragePercentage`, and `recommendationCount` in `summary` — use the case-sensitive schema tokens.
|
|
102
106
|
|
|
103
|
-
Write both JSON files through `python3 {
|
|
107
|
+
Write both JSON files through `python3 {atomicWriteHelper} write --target ...` to avoid partial-write corruption. On any non-zero exit: HALT (exit code 4, `halt_reason: "write-failed"`) and emit the error envelope.
|
|
104
108
|
|
|
105
109
|
When `{headless_mode}` is true, also emit the single-line envelope on **stdout** before chaining to step 7 (matches the SKILL.md "Result Contract (Headless)" shape):
|
|
106
110
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'synthesize.md'
|
|
3
3
|
feasibilitySchemaRef: 'src/shared/references/feasibility-report-schema.md'
|
|
4
|
-
|
|
4
|
+
atomicWriteProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
5
7
|
outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
|
|
6
8
|
outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
|
|
7
9
|
---
|
|
@@ -24,6 +26,8 @@ If a PRD or vision document was provided in Step 01, verify that the combined ca
|
|
|
24
26
|
|
|
25
27
|
### 1. Check PRD Availability
|
|
26
28
|
|
|
29
|
+
**Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
30
|
+
|
|
27
31
|
**Read `prdAvailable` from `{outputFile}` frontmatter (set in Step 01). If `prdAvailable` is false (no PRD/vision document was provided):**
|
|
28
32
|
|
|
29
33
|
"**Pass 3: Requirements Coverage — Skipped**
|
|
@@ -34,7 +38,7 @@ To include this pass, re-run **[VS]** with a PRD or vision document path.
|
|
|
34
38
|
|
|
35
39
|
**Proceeding to synthesis...**"
|
|
36
40
|
|
|
37
|
-
Update `{outputFile}` frontmatter: append `'requirements'` to `stepsCompleted`; set `requirementsPass: "skipped"`. Pipe the updated content through `python3 {
|
|
41
|
+
Update `{outputFile}` frontmatter: append `'requirements'` to `stepsCompleted`; set `requirementsPass: "skipped"`. Pipe the updated content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`.
|
|
38
42
|
|
|
39
43
|
Load, read the full file and then execute `{nextStepFile}`. **STOP HERE — do not execute sections 2-6.**
|
|
40
44
|
|
|
@@ -105,7 +109,7 @@ Write the Requirements Coverage content under the `## Recommendations` section (
|
|
|
105
109
|
- Update frontmatter: append `'requirements'` to `stepsCompleted`
|
|
106
110
|
- Set `requirementsPass: "completed"`
|
|
107
111
|
- Set `requirementsFulfilled`, `requirementsPartial`, `requirementsNotAddressed` counts
|
|
108
|
-
- Pipe the updated full content through `python3 {
|
|
112
|
+
- Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
|
|
109
113
|
|
|
110
114
|
### 6. Auto-Proceed to Next Step
|
|
111
115
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
nextStepFile: 'report.md'
|
|
3
3
|
feasibilitySchemaRef: 'src/shared/references/feasibility-report-schema.md'
|
|
4
|
-
|
|
4
|
+
atomicWriteProbeOrder:
|
|
5
|
+
- '{project-root}/_bmad/skf/shared/scripts/skf-atomic-write.py'
|
|
6
|
+
- '{project-root}/src/shared/scripts/skf-atomic-write.py'
|
|
5
7
|
outputFile: '{outputFolderPath}/feasibility-report-{project_slug}-{timestamp}.md'
|
|
6
8
|
outputFileLatest: '{outputFolderPath}/feasibility-report-{project_slug}-latest.md'
|
|
7
9
|
---
|
|
@@ -121,6 +123,8 @@ Assemble the following for the report:
|
|
|
121
123
|
|
|
122
124
|
### 5. Append to Report
|
|
123
125
|
|
|
126
|
+
**Resolve `{atomicWriteHelper}`** from `{atomicWriteProbeOrder}`; first existing path wins. HALT if no candidate exists.
|
|
127
|
+
|
|
124
128
|
Write the **Recommendations** and **Evidence Sources** sections to `{outputFile}` (per the fixed heading order in `{feasibilitySchemaRef}`):
|
|
125
129
|
- Include overall verdict with rationale in the `## Executive Summary` section (replace the placeholder text from the template)
|
|
126
130
|
- Include prioritized recommendation list under `## Recommendations`
|
|
@@ -137,7 +141,7 @@ Write the **Recommendations** and **Evidence Sources** sections to `{outputFile}
|
|
|
137
141
|
- If any pair has Check 4 missing/weak AND was capped at `Plausible`, that alone does NOT force `NOT_FEASIBLE`, but `FEASIBLE` requires zero such pairs
|
|
138
142
|
- `FEASIBLE` requires 100% coverage AND zero Blocked pairs AND zero Check-4-missing pairs — otherwise downgrade to `CONDITIONALLY_FEASIBLE`
|
|
139
143
|
- `coveragePercentage == 0` forces `NOT_FEASIBLE` (per section 1 short-circuit)
|
|
140
|
-
- Pipe the updated full content through `python3 {
|
|
144
|
+
- Pipe the updated full content through `python3 {atomicWriteHelper} write --target {outputFile}` and again with `--target {outputFileLatest}`
|
|
141
145
|
|
|
142
146
|
### 6. Auto-Proceed to Next Step
|
|
143
147
|
|