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.
Files changed (32) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/docs/_data/pinned.yaml +1 -1
  3. package/package.json +1 -1
  4. package/src/shared/scripts/skf-detect-tools.py +50 -8
  5. package/src/shared/scripts/skf-manifest-ops.py +22 -6
  6. package/src/skf-analyze-source/references/identify-units.md +8 -4
  7. package/src/skf-analyze-source/references/init.md +6 -4
  8. package/src/skf-analyze-source/references/scan-project.md +6 -2
  9. package/src/skf-audit-skill/references/init.md +9 -3
  10. package/src/skf-audit-skill/references/structural-diff.md +9 -3
  11. package/src/skf-brief-skill/SKILL.md +1 -1
  12. package/src/skf-brief-skill/references/analyze-target.md +22 -10
  13. package/src/skf-brief-skill/references/gather-intent.md +11 -5
  14. package/src/skf-brief-skill/references/headless-args.md +1 -1
  15. package/src/skf-brief-skill/references/qmd-collection-registration.md +1 -1
  16. package/src/skf-brief-skill/references/scope-definition.md +8 -4
  17. package/src/skf-brief-skill/references/write-brief.md +19 -7
  18. package/src/skf-create-skill/references/extraction-patterns.md +2 -0
  19. package/src/skf-create-skill/references/validate.md +2 -2
  20. package/src/skf-create-stack-skill/references/detect-integrations.md +7 -2
  21. package/src/skf-create-stack-skill/references/detect-manifests.md +6 -2
  22. package/src/skf-create-stack-skill/references/parallel-extract.md +6 -2
  23. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +6 -1
  24. package/src/skf-export-skill/references/summary.md +4 -4
  25. package/src/skf-export-skill/references/update-context.md +2 -2
  26. package/src/skf-test-skill/references/coherence-check.md +14 -7
  27. package/src/skf-verify-stack/references/coverage.md +6 -2
  28. package/src/skf-verify-stack/references/init.md +6 -2
  29. package/src/skf-verify-stack/references/integrations.md +6 -2
  30. package/src/skf-verify-stack/references/report.md +6 -2
  31. package/src/skf-verify-stack/references/requirements.md +7 -3
  32. package/src/skf-verify-stack/references/synthesize.md +6 -2
@@ -30,7 +30,7 @@
30
30
  "name": "skill-forge",
31
31
  "source": "./",
32
32
  "description": "Evidence-based agent skills compiler with progressive capability tiers (Quick/Forge/Forge+/Deep).",
33
- "version": "1.5.0",
33
+ "version": "1.5.1",
34
34
  "author": {
35
35
  "name": "Armel"
36
36
  },
@@ -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.0"
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.0",
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
- result = subprocess.run(
121
- cmd,
122
- capture_output=True,
123
- text=True,
124
- timeout=timeout,
125
- check=False,
126
- )
127
- return result.returncode, result.stdout or "", result.stderr or ""
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> Add/update skill with active 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": existing_version.get("ides", existing_version.get("platforms", [])),
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>, remove <name>, deprecate <name> [version], rename <old> <new>", file=sys.stderr)
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
- result = cmd_set(manifest_path, sys.argv[3], sys.argv[4])
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
- disqualifyCandidatesScript: '{project-root}/src/shared/scripts/skf-disqualify-candidates.py'
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-project-root>",
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 {disqualifyCandidatesScript} filter --boundaries - --source-root {project-root}
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
- - "**Found an existing analysis report. Resuming previous session...**"
30
- - Load, read entirely, then execute {continueFile}
31
- - **STOP HERE** do not continue this sequence
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[]` directly from the flag value (comma-split if multiple paths were supplied), skip the prompt below, and proceed to validation. Otherwise prompt as today.
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
- scanManifestsScript: '{project-root}/src/shared/scripts/skf-scan-manifests.py'
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 {scanManifestsScript} 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
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
- loadProvenanceScript: '{project-root}/src/shared/scripts/skf-load-provenance.py'
6
- compareFileHashesScript: '{project-root}/src/shared/scripts/skf-compare-file-hashes.py'
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 {loadProvenanceScript} normalize {provenanceMap}
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
- loadProvenanceScript: '{project-root}/src/shared/scripts/skf-load-provenance.py'
5
- compareFileHashesScript: '{project-root}/src/shared/scripts/skf-compare-file-hashes.py'
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 {compareFileHashesScript} compare {provenanceMap} {sourceRoot}
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 `{extractPublicApiScript}` 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.
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
- extractPublicApiScript: '{project-root}/src/shared/scripts/skf-extract-public-api.py'
5
- detectWorkspacesScript: '{project-root}/src/shared/scripts/skf-detect-workspaces.py'
6
- detectLanguageScript: '{project-root}/src/shared/scripts/skf-detect-language.py'
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
- Delegate workspace detection to `{detectWorkspacesScript}` 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:
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 {detectWorkspacesScript}
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
- Delegate the rule walk to `{detectLanguageScript}` instead of evaluating manifest presence and extension frequency in prose:
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 {detectLanguageScript}
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
- Identify the public API surface. **Delegate the parsing to `{extractPublicApiScript}` 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.
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 {extractPublicApiScript} --language <lang> --mode quick
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 `{extractPublicApiScript}` 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.
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
- validateBriefInputsScript: '{project-root}/src/shared/scripts/skf-validate-brief-inputs.py'
10
- emitBriefEnvelopeScript: '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
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
- - **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 `{validateBriefInputsScript}`; the table is the canonical operator-facing documentation, the script enforces it.
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 `{validateBriefInputsScript}`** instead of reasoning through the table rules in prose:
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 {validateBriefInputsScript}
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 `{validateBriefInputsScript}` enforces these rules deterministically (its `KNOWN_FIELDS` set must stay in sync with this table).
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 {forgeTierRwScript} register-qmd-collection --target {forgeTierFile}
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
- recommendScopeTypeScript: '{project-root}/src/shared/scripts/skf-recommend-scope-type.py'
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
- **Delegate the recommendation to `{recommendScopeTypeScript}`** 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.
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 {recommendScopeTypeScript}
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 `{recommendScopeTypeScript}` — 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.
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
- writeSkillBriefScript: '{project-root}/src/shared/scripts/skf-write-skill-brief.py'
7
- emitBriefEnvelopeScript: '{project-root}/src/shared/scripts/skf-emit-brief-result-envelope.py'
8
- forgeTierRwScript: '{project-root}/src/shared/scripts/skf-forge-tier-rw.py'
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
- `{briefSchemaPath}` and `{versionResolutionFile}` document the brief contract for human readers. The deterministic enforcement of that contract lives in `{writeSkillBriefScript}` 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.
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 {writeSkillBriefScript} write --target {resolved-target-path} --from-flat
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 {emitBriefEnvelopeScript} emit
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 {emitBriefEnvelopeScript} emit --target stderr
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
- pairIntersectScript: '{project-root}/src/shared/scripts/skf-pair-intersect.py'
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 {pairIntersectScript} intersect --libraries -
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
- scanManifestsScript: '{project-root}/src/shared/scripts/skf-scan-manifests.py'
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 {scanManifestsScript} scan {scan_root}
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
- enumerateStackSkillsScript: '{project-root}/src/shared/scripts/skf-enumerate-stack-skills.py'
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 {enumerateStackSkillsScript} enumerate {skills_output_folder}
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 merges the per-version `ides` and `last_exported` fields into the existing entry. After all skills have been set, re-read the manifest via `{manifestOpsHelper} {skills_output_folder} read` to confirm the final state matches expectations.
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
- scanSkillMdStructureScript: '{project-root}/src/shared/scripts/skf-scan-skill-md-structure.py'
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
- **2.0 Run the structural scan.** Invoke `{scanSkillMdStructureScript}` 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.
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 {scanSkillMdStructureScript} scan {skill-md} --required-sections
33
- uv run {scanSkillMdStructureScript} scan {skill-md}
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 Usage section.** For each function name reported in the step 3 subagent inventory (`exports[].name` where `kind == "function"` or `kind == "method"`):
51
- - `grep -c "{export.name}" SKILL.md` restricted to the Usage section (find the `## Usage` anchor from §2.1 and the next `^## ` anchor; count within that span).
52
- - **Zero occurrences → High severity** finding: `naive-coherence exported {kind} \`{name}\` is not referenced in the Usage section`. This catches the "documented but unused" failure mode that trivially fails discovery testing.
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
- atomicWriteScript: '{project-root}/src/shared/scripts/skf-atomic-write.py'
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 {atomicWriteScript} write --target {outputFile}` and again with `--target {outputFileLatest}`
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
- atomicWriteScript: '{project-root}/src/shared/scripts/skf-atomic-write.py'
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 {atomicWriteScript} 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.
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
- atomicWriteScript: '{project-root}/src/shared/scripts/skf-atomic-write.py'
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 {atomicWriteScript} write --target {outputFile}` and again with `--target {outputFileLatest}`
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
- atomicWriteScript: '{project-root}/src/shared/scripts/skf-atomic-write.py'
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 {atomicWriteScript} 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.
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
- atomicWriteScript: '{project-root}/src/shared/scripts/skf-atomic-write.py'
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 {atomicWriteScript} write --target {outputFile}` and again with `--target {outputFileLatest}`.
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 {atomicWriteScript} write --target {outputFile}` and again with `--target {outputFileLatest}`
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
- atomicWriteScript: '{project-root}/src/shared/scripts/skf-atomic-write.py'
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 {atomicWriteScript} write --target {outputFile}` and again with `--target {outputFileLatest}`
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