bmad-module-skill-forge 2.0.0 → 2.0.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 (235) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/docs/_data/pinned.yaml +1 -1
  4. package/package.json +2 -2
  5. package/src/knowledge/skill-lifecycle.md +1 -1
  6. package/src/module-help.csv +2 -2
  7. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  8. package/src/shared/scripts/skf-count-tokens.py +222 -0
  9. package/src/shared/scripts/skf-detect-docs.py +232 -1
  10. package/src/shared/scripts/skf-detect-language.py +77 -0
  11. package/src/shared/scripts/skf-detect-tools.py +105 -2
  12. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  13. package/src/shared/scripts/skf-find-cycles.py +246 -0
  14. package/src/shared/scripts/skf-hash-content.py +487 -2
  15. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  16. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  17. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  18. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  19. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  20. package/src/shared/scripts/skf-shard-body.py +461 -0
  21. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  22. package/src/shared/scripts/skf-structural-diff.py +307 -67
  23. package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
  24. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  25. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  26. package/src/shared/scripts/skf-validate-output.py +443 -10
  27. package/src/shared/scripts/skf-validate-pins.py +1 -1
  28. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  29. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  30. package/src/skf-analyze-source/SKILL.md +17 -32
  31. package/src/skf-analyze-source/customize.toml +2 -0
  32. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  33. package/src/skf-analyze-source/references/continue.md +8 -13
  34. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  35. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  36. package/src/skf-analyze-source/references/health-check.md +1 -2
  37. package/src/skf-analyze-source/references/identify-units.md +25 -36
  38. package/src/skf-analyze-source/references/init.md +12 -25
  39. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  40. package/src/skf-analyze-source/references/recommend.md +4 -14
  41. package/src/skf-analyze-source/references/scan-project.md +3 -11
  42. package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
  43. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  44. package/src/skf-audit-skill/SKILL.md +9 -6
  45. package/src/skf-audit-skill/customize.toml +2 -0
  46. package/src/skf-audit-skill/references/init.md +58 -32
  47. package/src/skf-audit-skill/references/re-index.md +15 -34
  48. package/src/skf-audit-skill/references/report.md +11 -58
  49. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  50. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  51. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  52. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  53. package/src/skf-brief-skill/SKILL.md +7 -36
  54. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  55. package/src/skf-brief-skill/customize.toml +14 -0
  56. package/src/skf-brief-skill/references/analyze-target.md +6 -14
  57. package/src/skf-brief-skill/references/confirm-brief.md +2 -14
  58. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  59. package/src/skf-brief-skill/references/gather-intent.md +19 -22
  60. package/src/skf-brief-skill/references/headless-args.md +1 -1
  61. package/src/skf-brief-skill/references/health-check.md +1 -22
  62. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  63. package/src/skf-brief-skill/references/scope-definition.md +4 -14
  64. package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
  65. package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
  66. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  67. package/src/skf-brief-skill/references/write-brief.md +17 -10
  68. package/src/skf-campaign/SKILL.md +15 -84
  69. package/src/skf-campaign/customize.toml +2 -0
  70. package/src/skf-campaign/manifest.yaml +4 -0
  71. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  72. package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
  73. package/src/skf-campaign/references/step-01-setup.md +4 -5
  74. package/src/skf-campaign/references/step-02-strategy.md +8 -3
  75. package/src/skf-campaign/references/step-03-pins.md +1 -1
  76. package/src/skf-campaign/references/step-04-provenance.md +1 -1
  77. package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
  78. package/src/skf-campaign/references/step-06-batch.md +7 -6
  79. package/src/skf-campaign/references/step-11-maintenance.md +2 -2
  80. package/src/skf-campaign/references/step-resume.md +9 -8
  81. package/src/skf-campaign/scripts/campaign-deps.py +10 -1
  82. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  83. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  84. package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
  85. package/src/skf-create-skill/SKILL.md +14 -3
  86. package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
  87. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  88. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  89. package/src/skf-create-skill/customize.toml +13 -0
  90. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  91. package/src/skf-create-skill/references/compile.md +34 -47
  92. package/src/skf-create-skill/references/component-extraction.md +8 -35
  93. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  94. package/src/skf-create-skill/references/enrich.md +4 -17
  95. package/src/skf-create-skill/references/extract.md +14 -29
  96. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  97. package/src/skf-create-skill/references/extraction-patterns.md +7 -7
  98. package/src/skf-create-skill/references/generate-artifacts.md +9 -23
  99. package/src/skf-create-skill/references/health-check.md +1 -1
  100. package/src/skf-create-skill/references/load-brief.md +16 -18
  101. package/src/skf-create-skill/references/report.md +12 -15
  102. package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
  103. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  104. package/src/skf-create-skill/references/step-doc-rot.md +30 -12
  105. package/src/skf-create-skill/references/step-doc-sources.md +2 -2
  106. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  107. package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
  108. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  109. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  110. package/src/skf-create-skill/references/validate.md +59 -28
  111. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  112. package/src/skf-create-stack-skill/SKILL.md +15 -9
  113. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  114. package/src/skf-create-stack-skill/customize.toml +10 -0
  115. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  116. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  117. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  118. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  119. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  120. package/src/skf-create-stack-skill/references/init.md +9 -81
  121. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  122. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  123. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  124. package/src/skf-create-stack-skill/references/report.md +27 -62
  125. package/src/skf-create-stack-skill/references/validate.md +35 -31
  126. package/src/skf-drop-skill/SKILL.md +20 -23
  127. package/src/skf-drop-skill/customize.toml +10 -0
  128. package/src/skf-drop-skill/references/execute.md +51 -31
  129. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  130. package/src/skf-drop-skill/references/health-check.md +1 -13
  131. package/src/skf-drop-skill/references/report.md +42 -10
  132. package/src/skf-drop-skill/references/select.md +88 -48
  133. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  134. package/src/skf-export-skill/SKILL.md +17 -14
  135. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  136. package/src/skf-export-skill/customize.toml +10 -6
  137. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  138. package/src/skf-export-skill/references/load-skill.md +44 -42
  139. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  140. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  141. package/src/skf-export-skill/references/package.md +27 -33
  142. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  143. package/src/skf-export-skill/references/result-envelope.md +28 -0
  144. package/src/skf-export-skill/references/summary.md +11 -5
  145. package/src/skf-export-skill/references/token-report.md +42 -24
  146. package/src/skf-export-skill/references/update-context.md +32 -33
  147. package/src/skf-forger/SKILL.md +27 -39
  148. package/src/skf-forger/references/pipeline-mode.md +34 -0
  149. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  150. package/src/skf-quick-skill/SKILL.md +10 -60
  151. package/src/skf-quick-skill/customize.toml +9 -0
  152. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  153. package/src/skf-quick-skill/references/compile.md +15 -52
  154. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  155. package/src/skf-quick-skill/references/finalize.md +12 -6
  156. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  157. package/src/skf-quick-skill/references/health-check.md +3 -4
  158. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  159. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  160. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  161. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  162. package/src/skf-refine-architecture/SKILL.md +14 -7
  163. package/src/skf-refine-architecture/customize.toml +9 -0
  164. package/src/skf-refine-architecture/references/compile.md +9 -7
  165. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  166. package/src/skf-refine-architecture/references/health-check.md +1 -1
  167. package/src/skf-refine-architecture/references/improvements.md +9 -25
  168. package/src/skf-refine-architecture/references/init.md +9 -16
  169. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  170. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  171. package/src/skf-refine-architecture/references/report.md +13 -10
  172. package/src/skf-rename-skill/SKILL.md +11 -21
  173. package/src/skf-rename-skill/customize.toml +10 -0
  174. package/src/skf-rename-skill/references/execute.md +107 -78
  175. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  176. package/src/skf-rename-skill/references/report.md +16 -8
  177. package/src/skf-rename-skill/references/select.md +69 -42
  178. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  179. package/src/skf-setup/SKILL.md +20 -12
  180. package/src/skf-setup/customize.toml +10 -0
  181. package/src/skf-setup/references/auto-index.md +9 -10
  182. package/src/skf-setup/references/ccc-index.md +14 -15
  183. package/src/skf-setup/references/detect-and-tier.md +6 -8
  184. package/src/skf-setup/references/report.md +22 -31
  185. package/src/skf-setup/references/tier-rules.md +2 -32
  186. package/src/skf-setup/references/write-config.md +9 -10
  187. package/src/skf-test-skill/SKILL.md +10 -8
  188. package/src/skf-test-skill/customize.toml +2 -0
  189. package/src/skf-test-skill/references/coherence-check.md +19 -31
  190. package/src/skf-test-skill/references/coverage-check.md +109 -75
  191. package/src/skf-test-skill/references/detect-mode.md +10 -41
  192. package/src/skf-test-skill/references/external-validators.md +11 -19
  193. package/src/skf-test-skill/references/init.md +38 -20
  194. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  195. package/src/skf-test-skill/references/report.md +60 -40
  196. package/src/skf-test-skill/references/score.md +32 -64
  197. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  198. package/src/skf-test-skill/references/source-access-protocol.md +6 -6
  199. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  200. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  201. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  202. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  203. package/src/skf-test-skill/scripts/compute-score.py +131 -1
  204. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  205. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  206. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  207. package/src/skf-update-skill/SKILL.md +17 -5
  208. package/src/skf-update-skill/customize.toml +14 -0
  209. package/src/skf-update-skill/references/detect-changes.md +19 -37
  210. package/src/skf-update-skill/references/health-check.md +3 -9
  211. package/src/skf-update-skill/references/init.md +39 -32
  212. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  213. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  214. package/src/skf-update-skill/references/merge.md +13 -57
  215. package/src/skf-update-skill/references/re-extract.md +12 -24
  216. package/src/skf-update-skill/references/report.md +14 -7
  217. package/src/skf-update-skill/references/validate.md +32 -30
  218. package/src/skf-update-skill/references/write.md +86 -51
  219. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  220. package/src/skf-verify-stack/SKILL.md +11 -21
  221. package/src/skf-verify-stack/customize.toml +12 -3
  222. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  223. package/src/skf-verify-stack/references/coverage.md +21 -10
  224. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  225. package/src/skf-verify-stack/references/health-check.md +1 -1
  226. package/src/skf-verify-stack/references/init.md +20 -25
  227. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  228. package/src/skf-verify-stack/references/integrations.md +44 -49
  229. package/src/skf-verify-stack/references/report.md +28 -34
  230. package/src/skf-verify-stack/references/requirements.md +3 -3
  231. package/src/skf-verify-stack/references/synthesize.md +36 -39
  232. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  233. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  234. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  235. package/src/skf-rename-skill/references/rebuild-context.md +0 -110
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # requires-python = ">=3.10"
4
+ # ///
5
+ """SKF Validate Rename Name — deterministic new-name gate for skf-rename-skill.
6
+
7
+ select.md §5 accepts a proposed new skill name and must decide four things, each
8
+ with one correct answer for a given (name, filesystem, manifest) state:
9
+
10
+ format — matches the module's canonical kebab-case rule
11
+ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ (same regex as
12
+ skf-validate-output.py / skf-validate-brief-inputs.py)
13
+ length — 1..64 characters (agentskills.io spec)
14
+ identity — differs from the current name (nothing to rename otherwise)
15
+ collision — does NOT already exist as a manifest `exports` key, a top-level
16
+ directory under the skills output folder, or a top-level
17
+ directory under the forge data folder
18
+
19
+ Doing this in the prompt is a set-membership computation across three sources
20
+ plus a regex match — deterministic, so it lives here and the prompt keeps only
21
+ the interactive re-ask / halt decision. Checks run in the select.md §5 order and
22
+ `first_failure` names the first failing one so the caller maps it to the right
23
+ halt (`input-invalid`/exit 2 for format/length/identity, `name-collision`/exit 5
24
+ for collision).
25
+
26
+ `interrupted_rename` fires the select.md §5 recovery fingerprint: the new name
27
+ collides only on disk (not in the manifest) AND both old-name directories still
28
+ exist — the signature of a rename interrupted between copy and delete-old, which
29
+ the caller surfaces as a named cleanup path instead of a dead-end collision.
30
+
31
+ Output (stdout, always JSON):
32
+ {status, valid, old_name, new_name, first_failure, checks{...},
33
+ interrupted_rename, regex}
34
+
35
+ Exit codes:
36
+ 0 valid (all checks pass)
37
+ 2 invalid (at least one check failed; see first_failure / checks)
38
+ 1 operation error (bad args)
39
+
40
+ CLI example:
41
+ python3 skf-validate-rename-name.py --old-name rename --new-name rename-skill \
42
+ --skills-output-folder /out --forge-data-folder /forge
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import json
49
+ import re
50
+ import sys
51
+ from pathlib import Path
52
+
53
+ NAME_REGEX = r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
54
+ MIN_LEN = 1
55
+ MAX_LEN = 64
56
+
57
+
58
+ def load_exports(manifest_path: Path) -> tuple[set[str], str | None]:
59
+ """Return (export skill-name keys, error). Missing manifest -> empty set."""
60
+ if not manifest_path.is_file():
61
+ return set(), None
62
+ try:
63
+ data = json.loads(manifest_path.read_text(encoding="utf-8"))
64
+ except (json.JSONDecodeError, OSError) as e:
65
+ # select.md §2 halts before §5 on a malformed manifest, so treat an
66
+ # unreadable file here as "no exports" rather than crashing the gate.
67
+ return set(), f"{type(e).__name__}: {e}"
68
+ exports = data.get("exports", {}) if isinstance(data, dict) else {}
69
+ return set(exports.keys()) if isinstance(exports, dict) else set(), None
70
+
71
+
72
+ def validate_name(
73
+ old_name: str,
74
+ new_name: str,
75
+ skills_output_folder: str,
76
+ forge_data_folder: str,
77
+ manifest_path: str | None = None,
78
+ ) -> dict:
79
+ skills_dir = Path(skills_output_folder)
80
+ forge_dir = Path(forge_data_folder)
81
+ manifest = Path(manifest_path) if manifest_path else skills_dir / ".export-manifest.json"
82
+
83
+ export_keys, manifest_error = load_exports(manifest)
84
+
85
+ fmt_ok = re.match(NAME_REGEX, new_name) is not None
86
+ length = len(new_name)
87
+ len_ok = MIN_LEN <= length <= MAX_LEN
88
+ identity_ok = new_name != old_name
89
+
90
+ locations: list[dict] = []
91
+ if new_name in export_keys:
92
+ locations.append({"kind": "manifest.exports", "path": str(manifest)})
93
+ if (skills_dir / new_name).is_dir():
94
+ locations.append({"kind": "skills_output_folder", "path": str(skills_dir / new_name)})
95
+ if (forge_dir / new_name).is_dir():
96
+ locations.append({"kind": "forge_data_folder", "path": str(forge_dir / new_name)})
97
+ collision_ok = len(locations) == 0
98
+
99
+ # Recovery fingerprint: collides on disk only (not manifest) and both
100
+ # old-name directories still exist -> stranded partial rename, not a genuine
101
+ # clash.
102
+ collides_on_disk_only = (
103
+ not collision_ok
104
+ and all(loc["kind"] != "manifest.exports" for loc in locations)
105
+ )
106
+ interrupted_rename = bool(
107
+ collides_on_disk_only
108
+ and (skills_dir / old_name).is_dir()
109
+ and (forge_dir / old_name).is_dir()
110
+ )
111
+
112
+ checks = {
113
+ "format": {"ok": fmt_ok},
114
+ "length": {"ok": len_ok, "length": length},
115
+ "identity": {"ok": identity_ok},
116
+ "collision": {"ok": collision_ok, "locations": locations},
117
+ }
118
+
119
+ first_failure = None
120
+ for name in ("format", "length", "identity", "collision"):
121
+ if not checks[name]["ok"]:
122
+ first_failure = name
123
+ break
124
+
125
+ result = {
126
+ "status": "ok",
127
+ "valid": first_failure is None,
128
+ "old_name": old_name,
129
+ "new_name": new_name,
130
+ "first_failure": first_failure,
131
+ "checks": checks,
132
+ "interrupted_rename": interrupted_rename,
133
+ "regex": NAME_REGEX,
134
+ }
135
+ if manifest_error:
136
+ result["manifest_error"] = manifest_error
137
+ return result
138
+
139
+
140
+ def main(argv: list[str] | None = None) -> int:
141
+ parser = argparse.ArgumentParser(description="Validate a proposed rename target name.")
142
+ parser.add_argument("--old-name", required=True)
143
+ parser.add_argument("--new-name", required=True)
144
+ parser.add_argument("--skills-output-folder", required=True)
145
+ parser.add_argument("--forge-data-folder", required=True)
146
+ parser.add_argument("--manifest", default=None)
147
+ args = parser.parse_args(argv)
148
+
149
+ result = validate_name(
150
+ args.old_name,
151
+ args.new_name,
152
+ args.skills_output_folder,
153
+ args.forge_data_folder,
154
+ args.manifest,
155
+ )
156
+ print(json.dumps(result, indent=2))
157
+ return 0 if result["valid"] else 2
158
+
159
+
160
+ if __name__ == "__main__":
161
+ sys.exit(main())
@@ -7,12 +7,13 @@ description: Initialize forge environment, detect tools, and set capability tier
7
7
 
8
8
  ## Overview
9
9
 
10
- Initializes the forge environment by detecting available tools, determining the capability tier (Quick/Forge/Forge+/Deep), and writing persistent configuration to `{project-root}/_bmad/_memory/forger-sidecar/`. When `ccc` (cocoindex-code) is available, also augments `.cocoindex_code/settings.yml` with SKF exclusion patterns and creates or refreshes the project's semantic-search index. On Deep tier, reconciles the QMD collection registry; whenever ccc is available, reconciles the CCC index registry as well. The workflow is autonomous with one optional gate — orphaned QMD collection removal in step 3 (Deep tier only; default action: Keep) — which auto-resolves non-interactively when `{headless_mode}` is true or when `--orphan-action=<keep|remove>` is passed.
10
+ Initializes the forge environment by detecting available tools, determining the capability tier (Quick/Forge/Forge+/Deep), and writing persistent configuration to `{project-root}/_bmad/_memory/forger-sidecar/`. When `ccc` (cocoindex-code) is available, also augments `.cocoindex_code/settings.yml` with SKF exclusion patterns and creates or refreshes the project's semantic-search index. On Deep tier, reconciles the QMD collection registry; whenever ccc is available, reconciles the CCC index registry as well.
11
11
 
12
12
  ## Conventions
13
13
 
14
14
  - Bare paths (e.g. `references/<name>.md`) resolve from the skill root.
15
- - `references/` holds prompt content carved out of SKILL.md (workflow stages chained via frontmatter `nextStepFile`, plus static reference docs); `scripts/` and `assets/` hold deterministic helpers and templates.
15
+ - `references/` holds prompt content carved out of SKILL.md workflow stages chained via frontmatter `nextStepFile`, plus static reference docs.
16
+ - Deterministic work is delegated to shared Python helpers under `src/shared/scripts/` (installed as `_bmad/skf/shared/scripts/`). Each step's frontmatter declares a `*ProbeOrder` array for the helpers it needs: run the first path in the array that exists, and halt if none resolve — the script owns that logic, with no prose fallback.
16
17
  - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
17
18
  - `{project-root}`-prefixed paths resolve from the project working directory.
18
19
  - `{skill-name}` resolves to the skill directory's basename.
@@ -23,15 +24,12 @@ You are a system executor performing environment resolution. Run each step in se
23
24
 
24
25
  ## Workflow Rules
25
26
 
26
- - One optional gate (step 3 orphan-removal prompt; default: Keep). Every other step auto-proceeds.
27
27
  - Only load one step file at a time — never preload future steps.
28
28
  - Communicate in `{communication_language}`.
29
- - If `{headless_mode}` is true, or if `{orphan_action}` is non-null, auto-resolve the gate non-interactively and log the decision.
29
+ - If `{headless_mode}` is true, or if `{orphan_action}` is non-null, auto-resolve the step 3 orphan-removal gate non-interactively and log the decision.
30
30
 
31
31
  ## Stages
32
32
 
33
- All stages auto-proceed except the optional orphan-removal gate in step 3.
34
-
35
33
  | # | Step | File |
36
34
  |---|------|------|
37
35
  | 1 | Detect Tools & Set Tier | references/detect-and-tier.md |
@@ -49,15 +47,14 @@ All stages auto-proceed except the optional orphan-removal gate in step 3.
49
47
  | **Flags** | `--headless` / `-H` (skip prompts, auto-resolve gates to defaults); `--require-tier=<Quick\|Forge\|Forge+\|Deep>` (halt with failure if calculated tier does not satisfy the requirement); `--orphan-action=<keep\|remove>` (resolve the orphan-removal gate non-interactively, even outside `--headless`); `--ccc-skip-index` (skip CCC indexing; envelope `ccc_index.status` becomes `"skipped"` — the fast re-probe lane for an expert re-running only to refresh the detected tier without paying the full ccc re-index cost); `--quiet` (suppress the human-readable FORGE STATUS banner — pipelines and expert re-runners get the envelope only) |
50
48
  | **Gates** | One optional: orphaned QMD collection removal (step 3, Deep tier only; default: Keep, or whatever `--orphan-action` set) |
51
49
  | **Outputs** | `forger-sidecar/forge-tier.yaml`, `forger-sidecar/preferences.yaml`, `{forge_data_folder}/`; when ccc is available, `.cocoindex_code/settings.yml` (exclusion patterns merged) and the project ccc index |
52
- | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Under `--headless` or `--quiet`, step 4 emits a single-line `SKF_SETUP_RESULT_JSON: {…}` envelope as the only stdout line — the FORGE STATUS banner is suppressed. Branch on the envelope's top-level `status` field (one of `success`, `tier_failure`, `write_failure`, `blocked`) — `skf-emit-result-envelope.py` derives it from the payload, so pipelines never compose it from `require_tier_satisfied` + `error`. Schema in `references/report.md` §4. |
53
- | **Failure modes** | `--require-tier` not satisfied → status becomes `tier_failure`, the envelope sets `"require_tier_satisfied": false`, and the workflow halts before step 5 (interactive runs also print a "REQUIRED TIER NOT MET" block). |
54
- | **Exit codes** | The workflow runs as an LLM-driven sequence rather than a CLI process, so "exit code" describes the agent's terminal state for a calling pipeline that reads `SKF_SETUP_RESULT_JSON`. **0 — success** (no JSON `error` field, all writes completed, `require_tier_satisfied` is `true` or `null`). **1 — required-tier failure** (`require_tier_satisfied` is `false`; envelope still emitted with full state for diagnosis). **2 — write failure** (forge-tier.yaml or preferences.yaml could not be written; envelope `error` field names the path and reason). Pipelines should branch on the JSON `require_tier_satisfied` and `error` fields rather than process exit codes. |
50
+ | **Headless** | All gates auto-resolve with default action when `{headless_mode}` is true. Under `--headless` or `--quiet`, step 4 emits a single-line `SKF_SETUP_RESULT_JSON: {…}` envelope as the only stdout line — the FORGE STATUS banner is suppressed. Branch on the envelope's top-level `status` field: `success`, `tier_failure` (require-tier miss), or `blocked` (any pre-report halt the `error.phase` names the cause). `skf-emit-result-envelope.py` derives `status` from the payload, so pipelines never compose it from `require_tier_satisfied` + `error`. Schema in `references/report.md` §4. |
51
+ | **Failure modes** | `--require-tier` not satisfied → status `tier_failure`, the envelope sets `"require_tier_satisfied": false`, and the workflow halts before step 5 (interactive runs also print a "REQUIRED TIER NOT MET" block). A write failure (forge-tier.yaml or preferences.yaml could not be written) halts step 2 with a `blocked` envelope whose `error.phase` (`step 2:write-tools` or `step 2:init-prefs`) and `error` name the path and reason. |
55
52
 
56
53
  ## On Activation
57
54
 
58
- > **Halt contract for headless/quiet runs.** Any halt below MUST first emit a blocked envelope when `{headless_mode}` or `{quiet_mode}` is true, before printing the human diagnostic. Pipe `{"phase":"<phase>","reason":"<reason>","path":"<path>"}` to `python3 <helper> emit-blocked` where `<helper>` is the first existing path of `{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py` then `{project-root}/src/shared/scripts/skf-emit-result-envelope.py`. The `emit-blocked` subcommand declares zero dependencies (no `uv`, no `pyyaml`), so it works even when `uv` itself is the thing that's missing.
55
+ > **Halt contract for headless/quiet runs.** Any halt below must first emit a blocked envelope when `{headless_mode}` or `{quiet_mode}` is true, before printing the human diagnostic — a pipeline observer that sees no envelope treats the run as not-completed-cleanly. Pipe `{"phase":"<phase>","reason":"<reason>","path":"<path>"}` to `python3 <helper> emit-blocked` where `<helper>` is the first existing path of `{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py` then `{project-root}/src/shared/scripts/skf-emit-result-envelope.py`. The `emit-blocked` subcommand declares zero dependencies (no `uv`, no `pyyaml`), so it works even when `uv` itself is the thing that's missing.
59
56
 
60
- 1. **Parse invocation flags FIRST** (so every halt below knows whether to emit an envelope): `{headless_mode}` (true on `--headless` / `-H`), `{require_tier}` (`--require-tier=<Quick|Forge|Forge+|Deep>`, case-sensitive; null if absent or unparseable), `{orphan_action}` (`--orphan-action=<keep|remove>`; null if absent), `{ccc_skip_index}` (true on `--ccc-skip-index`), `{quiet_mode}` (true on `--quiet`).
57
+ 1. **Parse invocation flags first** (so every halt below knows whether to emit an envelope): `{headless_mode}` (true on `--headless` / `-H`), `{require_tier}` (`--require-tier=<Quick|Forge|Forge+|Deep>`, case-sensitive; null if absent or unparseable), `{orphan_action}` (`--orphan-action=<keep|remove>`; null if absent), `{ccc_skip_index}` (true on `--ccc-skip-index`), `{quiet_mode}` (true on `--quiet`).
61
58
 
62
59
  2. **Probe `uv` runtime.** Run `uv --version`. Every step invokes shared Python helpers via `uv run` (PEP 723 inline metadata auto-resolves `pyyaml`). If `uv` is missing, halt with phase `on-activation:uv-missing` and the human diagnostic:
63
60
 
@@ -67,4 +64,15 @@ All stages auto-proceed except the optional orphan-removal gate in step 3.
67
64
 
68
65
  4. **Reconcile `{headless_mode}`** with `preferences.yaml`: OR the parsed flag with `headless_mode: true` from the YAML.
69
66
 
70
- 5. Execute `references/detect-and-tier.md`.
67
+ 5. **Resolve workflow customization.** Run:
68
+
69
+ ```bash
70
+ python3 {project-root}/_bmad/scripts/resolve_customization.py \
71
+ --skill {skill-root} --key workflow
72
+ ```
73
+
74
+ The script merges the three customization layers per `bmad-customize`'s structural merge rules (scalars override, arrays append): `{skill-root}/customize.toml` (bundled defaults), `_bmad/custom/<skill-name>.toml` under `{project-root}` (team overrides, committed), and `_bmad/custom/<skill-name>.user.toml` under `{project-root}` (personal overrides, gitignored). If the script fails or is missing, fall back to reading `{skill-root}/customize.toml` directly.
75
+
76
+ Apply the resolved values so the surface is not a silent no-op: execute each entry in `workflow.activation_steps_prepend` in order now (org-wide pre-flight checks such as auth, network, or compliance); treat every entry in `workflow.persistent_facts` as standing context for the whole run (`file:`-prefixed entries are paths or globs whose contents load as facts); stash `{onCompleteCommand}` ← `workflow.on_complete` (empty string = no-op) for `references/report.md` §5 to invoke at the terminal stage. After activation completes, execute each entry in `workflow.activation_steps_append` in order, before the first stage runs.
77
+
78
+ 6. Execute `references/detect-and-tier.md`.
@@ -1,6 +1,8 @@
1
1
  # DO NOT EDIT -- overwritten on every update.
2
2
  #
3
3
  # Workflow customization surface for skf-setup.
4
+ # Team overrides: _bmad/custom/skf-setup.toml (under {project-root})
5
+ # Personal overrides: _bmad/custom/skf-setup.user.toml (under {project-root})
4
6
 
5
7
  [workflow]
6
8
 
@@ -31,3 +33,11 @@ activation_steps_append = []
31
33
  # contents are loaded and treated as facts).
32
34
 
33
35
  persistent_facts = []
36
+
37
+ # Instruction run once the forge is fully configured — after the status report
38
+ # (step 4) emits, before chaining to the shared health check. Override wins. Use
39
+ # for a post-setup action an org wants after the forge is configured — e.g.
40
+ # trigger the first index build or notify an onboarding channel. Empty string =
41
+ # no terminal hook.
42
+
43
+ on_complete = ""
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  nextStepFile: 'report.md'
3
- # Resolve `{qmdClassifyHelper}` and `{forgeTierRwHelper}` by probing the
4
- # corresponding `*ProbeOrder` arrays (installed SKF module path first, src/
5
- # dev-checkout fallback); first existing path wins. HALT if neither resolves
6
- # for the helper a section actually invokes — both scripts own classification
7
- # / registry-cleanup contracts that have no prose fallback.
3
+ # `{qmdClassifyHelper}` and `{forgeTierRwHelper}` = first existing path in
4
+ # their `*ProbeOrder` arrays; halt if neither exists for a helper a section
5
+ # actually invokes. Both scripts own classification / registry-cleanup
6
+ # contracts with no prose fallback.
8
7
  qmdClassifyProbeOrder:
9
8
  - '{project-root}/_bmad/skf/shared/scripts/skf-qmd-classify-collections.py'
10
9
  - '{project-root}/src/shared/scripts/skf-qmd-classify-collections.py'
@@ -45,7 +44,7 @@ Default `{orphan_auto_resolution}` to null at the top of this step; only the non
45
44
 
46
45
  **If `{calculated_tier}` is Forge+:** Same as Quick/Forge with ccc — no QMD work (qmd unavailable at Forge+), but ccc registry hygiene runs.
47
46
 
48
- **If `{calculated_tier}` IS Deep:** Continue to section 2.
47
+ **If `{calculated_tier}` is Deep:** Continue to section 2.
49
48
 
50
49
  ### 2. Classify Live QMD Collections vs Registry
51
50
 
@@ -56,7 +55,7 @@ uv run {qmdClassifyHelper} \
56
55
  --registry-from-yaml "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml"
57
56
  ```
58
57
 
59
- The script (see `src/shared/scripts/skf-qmd-classify-collections.py` docstring for the full schema) invokes `qmd collection list` itself, applies the forge-namespace suffix filter (`-brief | -temporal | -docs | -extraction`) before classifying, and exits non-zero with an error message on stderr if the daemon is down. Collections owned by unrelated tools are silently excluded from the orphan / healthy / stale sets and counted under `foreign_filtered_count` for telemetry only. This is the PR #244 incident protection: foreign collections never enter any classification that could lead to data loss.
58
+ The script (see `src/shared/scripts/skf-qmd-classify-collections.py` docstring for the full schema) invokes `qmd collection list` itself, applies the forge-namespace suffix filter (`-brief | -temporal | -docs | -extraction`) before classifying, and exits non-zero with an error message on stderr if the daemon is down. Collections owned by unrelated tools are silently excluded from the orphan / healthy / stale sets and counted under `foreign_filtered_count` for telemetry only foreign collections never enter any classification that could lead to data loss.
60
59
 
61
60
  **Error handling:** If the script exits non-zero, set `{hygiene_result: "qmd_unavailable", hygiene_healthy: 0, hygiene_orphaned_removed: 0, hygiene_orphaned_kept: 0, hygiene_stale_cleaned: 0}` and skip directly to section 4 (which will still run the ccc-prune branch if `{ccc}` is true).
62
61
 
@@ -74,7 +73,7 @@ Set `{hygiene_result: "completed"}`.
74
73
 
75
74
  **If `{orphaned_collections}` is empty:** Set `{hygiene_orphaned_removed: 0, hygiene_orphaned_kept: 0}` and skip to section 4.
76
75
 
77
- **Non-interactive resolution.** If `{orphan_action}` is non-null, resolve the gate without prompting using that value: log `"Auto-decision (--orphan-action={value}): kept|removed {len(orphaned_collections)} orphaned forge collection(s)"` and set `{orphan_auto_resolution: {action: "{orphan_action}", count: len(orphaned_collections), source: "orphan-action-flag"}}` so step 4 can fold it into the envelope warnings (the audit trail matters most when `remove` deletes collections headlessly). If `{orphan_action}` is `"keep"`, set `{hygiene_orphaned_removed: 0, hygiene_orphaned_kept: len(orphaned_collections)}` and skip to section 4. If `"remove"`, fall through to the removal block below (still no user prompt). Independently, if `{headless_mode}` is true and `{orphan_action}` is null, auto-resolve to the default **Keep** with the equivalent log line, set `{orphan_auto_resolution: {action: "keep", count: len(orphaned_collections), source: "headless-default"}}`, and skip to section 4. The two paths compose: `--orphan-action` overrides the headless default; `--headless` without `--orphan-action` keeps backward-compatible behavior. (On an interactive resolution — section 3's prompt below — leave `{orphan_auto_resolution}` null; the human chose, so there is no auto-decision to audit.)
76
+ **Non-interactive resolution.** If `{orphan_action}` is non-null, resolve the gate without prompting using that value: log `"Auto-decision (--orphan-action={value}): kept|removed {len(orphaned_collections)} orphaned forge collection(s)"` and set `{orphan_auto_resolution: {action: "{orphan_action}", count: len(orphaned_collections), source: "orphan-action-flag"}}` so step 4 can fold it into the envelope warnings (the audit trail matters most when `remove` deletes collections headlessly). If `{orphan_action}` is `"keep"`, set `{hygiene_orphaned_removed: 0, hygiene_orphaned_kept: len(orphaned_collections)}` and skip to section 4. If `"remove"`, fall through to the removal block below (still no user prompt). Independently, if `{headless_mode}` is true and `{orphan_action}` is null, auto-resolve to the default **Keep** with the equivalent log line, set `{orphan_auto_resolution: {action: "keep", count: len(orphaned_collections), source: "headless-default"}}`, and skip to section 4. The two paths compose: `--orphan-action` overrides the headless default; `--headless` alone falls back to the default Keep. (On an interactive resolution — section 3's prompt below — leave `{orphan_auto_resolution}` null; the human chose, so there is no auto-decision to audit.)
78
77
 
79
78
  **If `{headless_mode}` is false**, display to the user:
80
79
 
@@ -99,7 +98,7 @@ Track the count of successful removals as `{hygiene_orphaned_removed}`. Set `{hy
99
98
 
100
99
  ### 4. Stale Registry Cleanup
101
100
 
102
- This section ALWAYS runs when reachable — it handles both `qmd_collections` stale entries (Deep tier) and `ccc_index_registry` stale entries (whenever ccc is true). The script's flags are mutually independent.
101
+ This section runs whenever reachable — it handles both `qmd_collections` stale entries (Deep tier) and `ccc_index_registry` stale entries (whenever ccc is true). The script's flags are mutually independent.
103
102
 
104
103
  Build the invocation. Always include `--target` for the forge-tier.yaml path. Include `--qmd-live-names "{live_collections}"` ONLY when section 2 ran successfully (i.e. `{hygiene_result}` is `"completed"`); omit the flag entirely otherwise so the script skips QMD cleanup. Include `--prune-missing-ccc-paths` ONLY when `{ccc}` is true; omit it otherwise.
105
104
 
@@ -110,7 +109,7 @@ uv run {forgeTierRwHelper} clean-stale \
110
109
  [--prune-missing-ccc-paths]
111
110
  ```
112
111
 
113
- The script reads the registry, computes set-difference operations (qmd: registry − live; ccc: filter where `path` does not exist on disk), and atomically rewrites forge-tier.yaml only when something actually changed (mtime preserved on idempotent re-runs). The CI ephemeral-mount caveat for ccc-registry pruning is logged in the script's WARNING message (per PR #248).
112
+ The script reads the registry, computes set-difference operations (qmd: registry − live; ccc: filter where `path` does not exist on disk), and atomically rewrites forge-tier.yaml only when something actually changed (mtime preserved on idempotent re-runs). The CI ephemeral-mount caveat for ccc-registry pruning is logged in the script's warning message.
114
113
 
115
114
  **Parse the JSON output and set context flags for step 4:**
116
115
 
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  nextStepFile: 'write-config.md'
3
- # Resolve `{mergeCccExclusionsHelper}` by probing `{mergeCccExclusionsProbeOrder}`
4
- # in order (installed SKF module path first, src/ dev-checkout fallback); first
5
- # existing path wins. HALT if neither resolves — the script owns config-value
6
- # validation and the set-union merge into .cocoindex_code/settings.yml; no
7
- # fallback to prose-driven validation.
3
+ # `{mergeCccExclusionsHelper}` = first existing path in
4
+ # `{mergeCccExclusionsProbeOrder}`; halt if neither exists. The script owns
5
+ # config-value validation and the set-union merge into
6
+ # .cocoindex_code/settings.yml no prose fallback.
8
7
  mergeCccExclusionsProbeOrder:
9
8
  - '{project-root}/_bmad/skf/shared/scripts/skf-merge-ccc-exclusions.py'
10
9
  - '{project-root}/src/shared/scripts/skf-merge-ccc-exclusions.py'
@@ -25,7 +24,7 @@ For Quick and Forge tiers, or when ccc is unavailable, skip silently and proceed
25
24
  - Focus only on ccc index verification, exclusion-pattern merge, and (re-)indexing
26
25
  - Do not display skip messages for Quick/Forge tiers
27
26
  - Do not fail the workflow if ccc indexing fails
28
- - Never reimplement the exclusion-pattern validation in prose the script owns the PR #248 rules
27
+ - The script owns exclusion-pattern validation — do not reimplement it in prose
29
28
 
30
29
  ## MANDATORY SEQUENCE
31
30
 
@@ -35,24 +34,24 @@ Read `{ccc}` and `{ccc_skip_index}` from context.
35
34
 
36
35
  **If `{ccc}` is false:** Set `{ccc_index_result: "none", ccc_indexed_path: null, ccc_last_indexed: null, ccc_exclude_patterns: [], ccc_exclusion_warnings: [], settings_yml_written: false, settings_yml_patterns_added: 0}`. Proceed directly to section 4 (Auto-Proceed) — no output, no messaging.
37
36
 
38
- **If `{ccc}` is true AND `{ccc_skip_index}` is true:** Run the exclusion-merge (section 3) so settings.yml stays current, then set `{ccc_index_result: "skipped", ccc_indexed_path: null, ccc_last_indexed: null}` and proceed to section 4 — do NOT run `ccc init` or `ccc index`. The envelope's `ccc_index.status` will be `"skipped"` so pipelines that plan an out-of-band re-index can distinguish "the operator opted out" from "indexing failed".
37
+ **If `{ccc}` is true AND `{ccc_skip_index}` is true:** Run the exclusion-merge (section 3) so settings.yml stays current, then set `{ccc_index_result: "skipped", ccc_indexed_path: null, ccc_last_indexed: null}` and proceed to section 4 — do not run `ccc init` or `ccc index`. The envelope's `ccc_index.status` will be `"skipped"` so pipelines that plan an out-of-band re-index can distinguish "the operator opted out" from "indexing failed".
39
38
 
40
39
  **If `{ccc}` is true AND `{ccc_skip_index}` is false:** Continue to section 2.
41
40
 
42
41
  ### 2. Check Existing Index State
43
42
 
44
- Consume prior CCC state from stage 1's detector output (`prior.previous_ccc_*` context flags) — no YAML re-parse here, the detector already read forge-tier.yaml. Use `prior.previous_ccc_staleness_threshold_hours` if set, else default 24 hours.
43
+ Consume prior CCC state from stage 1's detector output (`prior.*` context flags) — no YAML re-parse and no timestamp math here. The detector already read forge-tier.yaml and computed the freshness verdict `{ccc_index_fresh}` against `{project-root}` and the current time (same-project path match AND `"fresh"`/`"created"` status AND `last_indexed` within the staleness threshold, defaulting to 24 hours).
45
44
 
46
- Decide `{needs_reindex}` and `{ccc_index_result}` from those flags:
45
+ Decide `{needs_reindex}` and `{ccc_index_result}` from `{ccc_index_fresh}`:
47
46
 
48
- - If `previous_ccc_indexed_path` matches `{project-root}` AND `previous_ccc_index_status` is `"fresh"` or `"created"` AND `previous_ccc_last_indexed` is within `staleness_threshold_hours` of now → index is fresh. Set `{needs_reindex: false}`, `{ccc_index_result: "fresh", ccc_indexed_path: {project-root}, ccc_last_indexed: <existing timestamp>}`. Exclusions still merge in section 3 (which may force a re-index).
49
- - If path matches but the timestamp is older than threshold, or if the path doesn't match, or if any prior CCC field is null → `{needs_reindex: true}`.
47
+ - If `{ccc_index_fresh}` is true → index is fresh. Set `{needs_reindex: false}`, `{ccc_index_result: "fresh", ccc_indexed_path: {project-root}, ccc_last_indexed: {previous_ccc_last_indexed}}`. Exclusions still merge in section 3 (which may force a re-index).
48
+ - If `{ccc_index_fresh}` is false (path mismatch, non-fresh status, stale timestamp, or any required prior CCC field null) → `{needs_reindex: true}`.
50
49
 
51
50
  ### 3. Merge SKF Exclusion Patterns
52
51
 
53
52
  SKF infrastructure and output directories must be excluded from the CCC index — they contain workflow instructions, build artifacts, and generated skills that pollute semantic search results with zero extraction value.
54
53
 
55
- Forward `skills_output_folder` and `forge_data_folder` from `{project-root}/_bmad/skf/config.yaml` **verbatim** — the script resolves `{project-root}/...` template strings and rejects absolute paths / placeholders / glob meta-chars internally. The step does no string surgery; that work moved into the helper. Invoke via `uv run` so PEP 723 inline metadata resolves the script's PyYAML dependency automatically (per `docs/getting-started.md`'s prereq list — uv exists for this exact purpose). Bare `python3` will fail on a fresh Python with `ModuleNotFoundError: No module named 'yaml'`.
54
+ Forward `skills_output_folder` and `forge_data_folder` from `{project-root}/_bmad/skf/config.yaml` **verbatim** — the script resolves `{project-root}/...` template strings and rejects absolute paths / placeholders / glob meta-chars internally. The step does no string surgery; that work moved into the helper. Invoke via `uv run`.
56
55
 
57
56
  ```bash
58
57
  uv run {mergeCccExclusionsHelper} \
@@ -61,14 +60,14 @@ uv run {mergeCccExclusionsHelper} \
61
60
  --forge-data-folder "{forge_data_folder}"
62
61
  ```
63
62
 
64
- The script (see `src/shared/scripts/skf-merge-ccc-exclusions.py` docstring for the full schema) builds the SKF exclusion list (4 always-include hardcoded patterns + 2 conditional from validated config), applies the PR #248 validation rules to reject empty / absolute / glob-meta config values with actionable warnings, and performs an idempotent set-union merge into `{project-root}/.cocoindex_code/settings.yml`. User customizations are preserved. When the file does not exist yet (first-time setup before `ccc init`) the script creates it; when nothing new needs adding the script skips the write entirely (mtime preserved).
63
+ The script (see `src/shared/scripts/skf-merge-ccc-exclusions.py` docstring for the full schema) builds the SKF exclusion list (4 always-include hardcoded patterns + 2 conditional from validated config), applies the validation rules to reject empty / absolute / glob-meta config values with actionable warnings, and performs an idempotent set-union merge into `{project-root}/.cocoindex_code/settings.yml`. User customizations are preserved. When the file does not exist yet (first-time setup before `ccc init`) the script creates it; when nothing new needs adding the script skips the write entirely (mtime preserved).
65
64
 
66
65
  **Parse the JSON output and set context flags:**
67
66
 
68
67
  - `{settings_yml_existed}` ← `settings_yml_existed`
69
68
  - `{settings_yml_written}` ← `written`
70
69
  - `{settings_yml_patterns_added}` ← `patterns_added`
71
- - `{ccc_exclude_patterns}` ← `effective_patterns` (the script returns the final, sorted, deduplicated SKF pattern set after validation — consume verbatim; do NOT re-derive in prose).
70
+ - `{ccc_exclude_patterns}` ← `effective_patterns` (the script returns the final, sorted, deduplicated SKF pattern set after validation — consume verbatim; do not re-derive in prose).
72
71
  - `{ccc_exclusion_warnings}` ← `warnings` (a list — step 4 folds them into the envelope's warnings array)
73
72
 
74
73
  **If `{settings_yml_written}` is true** (new patterns merged into settings.yml): set `{needs_reindex: true}` — new exclusions require re-indexing for the index to reflect them. Display: "**CCC exclusions configured:** {patterns_added} SKF patterns applied to .cocoindex_code/settings.yml"
@@ -114,7 +113,7 @@ ccc index
114
113
 
115
114
  - Store `{ccc_index_result: "failed", ccc_indexed_path: null, ccc_last_indexed: null, ccc_indexing_failed_reason: {error}}` (the failed-reason flag flows into step 4's envelope warnings)
116
115
  - Display: "CCC indexing failed: {error}. Extraction will use direct AST scanning — semantic pre-ranking unavailable this session."
117
- - Continue — this is NOT a workflow error
116
+ - Continue — this is not a workflow error
118
117
 
119
118
  ### 5. Auto-Proceed
120
119
 
@@ -1,9 +1,8 @@
1
1
  ---
2
2
  nextStepFile: 'ccc-index.md'
3
- # Resolve `{detectToolsHelper}` by probing `{detectToolsProbeOrder}` in order
4
- # (installed SKF module path first, src/ dev-checkout fallback); first existing
5
- # path wins. HALT if neither resolves the script is the source of truth for
6
- # tool detection and tier calculation; no fallback to prose-driven probes.
3
+ # `{detectToolsHelper}` = first existing path in `{detectToolsProbeOrder}`;
4
+ # halt if neither exists. This script is the source of truth for tool
5
+ # detection and tier calculationno prose-driven probes.
7
6
  detectToolsProbeOrder:
8
7
  - '{project-root}/_bmad/skf/shared/scripts/skf-detect-tools.py'
9
8
  - '{project-root}/src/shared/scripts/skf-detect-tools.py'
@@ -54,9 +53,9 @@ Press Esc or Ctrl+C now if this isn't the right project — no files have been w
54
53
 
55
54
  ### 2. Run Detection Helper
56
55
 
57
- Build the Bash invocation: `uv run {detectToolsHelper} --prior-state-from "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml"`. If `{tier_override}` is non-null, append `--tier-override "{tier_override}"`. If `{require_tier}` is non-null, append `--require-tier "{require_tier}"`. Then execute. (The script's PEP 723 metadata is why `uv run` is required bare `python3` skips dependency resolution.)
56
+ Build the Bash invocation: `uv run {detectToolsHelper} --project-root "{project-root}" --prior-state-from "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml"`. If `{tier_override}` is non-null, append `--tier-override "{tier_override}"`. If `{require_tier}` is non-null, append `--require-tier "{require_tier}"`. Then execute. (`--project-root` lets the script compute the CCC-index freshness verdict `prior.ccc_index_fresh` so step 1b branches on a boolean instead of doing timestamp math.)
58
57
 
59
- The script (see `src/shared/scripts/skf-detect-tools.py` docstring for the full `DETECT_OUTPUT_SCHEMA`) probes ast-grep / gh / qmd / ccc concurrently with two-step verification for qmd and ccc (binary-identity check + daemon-health check, including the `CocoIndex Code` identity-marker substring check that rejects PATH-shadowing aliases). It applies the 4-rule tier table, performs the tier-override sanity check (override is honored but flagged unsafe when underlying tools are missing), and evaluates `--require-tier` using a tool-prerequisite check (Deep does NOT subsume Forge+ — Deep does not require ccc). Output is one JSON document on stdout.
58
+ The script (see `src/shared/scripts/skf-detect-tools.py` docstring for the full `DETECT_OUTPUT_SCHEMA`) probes ast-grep / gh / qmd / ccc concurrently with two-step verification for qmd and ccc (binary-identity check + daemon-health check, including the `CocoIndex Code` identity-marker substring check that rejects PATH-shadowing aliases). It applies the 4-rule tier table, performs the tier-override sanity check (override is honored but flagged unsafe when underlying tools are missing), and evaluates `--require-tier` using a tool-prerequisite check (Deep does not subsume Forge+ — Deep does not require ccc). Output is one JSON document on stdout.
60
59
 
61
60
  ### 3. Parse Output and Set Context Flags
62
61
 
@@ -101,6 +100,7 @@ From `prior` (populated by `--prior-state-from`; all fields null/empty on first
101
100
  - `{previous_ccc_indexed_path}` ← `prior.previous_ccc_indexed_path`
102
101
  - `{previous_ccc_last_indexed}` ← `prior.previous_ccc_last_indexed`
103
102
  - `{previous_ccc_staleness_threshold_hours}` ← `prior.previous_ccc_staleness_threshold_hours`
103
+ - `{ccc_index_fresh}` ← `prior.ccc_index_fresh` (boolean; the script's deterministic freshness verdict — prior index covers this project AND status was fresh/created AND `last_indexed` is within the staleness threshold of now. Step 1b branches on this directly instead of doing timestamp math.)
104
104
 
105
105
  From `deltas` (computed by the script from current tools + prior; eliminates LLM-side set arithmetic in the report banner):
106
106
 
@@ -108,8 +108,6 @@ From `deltas` (computed by the script from current tools + prior; eliminates LLM
108
108
  - `{tools_removed}` ← `deltas.tools_removed` (the list)
109
109
  - `{tier_changed}` ← `deltas.tier_changed` (boolean)
110
110
 
111
- **The script is the source of truth.** Every tier-rules edge case (override-honored-but-unsafe, Deep-doesn't-subsume-Forge+, qmd-binary-vs-daemon distinction, ccc-identity-marker rejection of foreign binaries) is locked into the test suite at `test/test-skf-detect-tools.py`. Substituting prose-driven logic for the script's output here will cause subtle re-run regressions that the prompt's prose is no longer detailed enough to catch.
112
-
113
111
  ### 4. Auto-Proceed
114
112
 
115
113
  After context flags are populated, display "**Proceeding to CCC index check...**", then load `{nextStepFile}`, read it fully, and execute it.
@@ -1,12 +1,11 @@
1
1
  ---
2
2
  tierRulesData: 'references/tier-rules.md'
3
3
  nextStepFile: 'health-check.md'
4
- # Resolve `{emitEnvelopeHelper}` by probing `{emitEnvelopeProbeOrder}` in order
5
- # (installed SKF module path first, src/ dev-checkout fallback); first existing
6
- # path wins. HALT if neither resolves when the headless envelope must be emitted
7
- # the script is the source of truth for the SKF_SETUP_RESULT_JSON contract;
8
- # never inline-render the envelope from prose (LLM schema drift is the bug
9
- # this script exists to prevent).
4
+ # `{emitEnvelopeHelper}` = first existing path in `{emitEnvelopeProbeOrder}`;
5
+ # halt if neither exists when the headless envelope must be emitted. The script
6
+ # is the source of truth for the SKF_SETUP_RESULT_JSON contract do not
7
+ # inline-render the envelope from prose (LLM schema drift is the bug this
8
+ # script exists to prevent).
10
9
  emitEnvelopeProbeOrder:
11
10
  - '{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py'
12
11
  - '{project-root}/src/shared/scripts/skf-emit-result-envelope.py'
@@ -27,19 +26,13 @@ Display the forge status report with positive capability framing, surface tier c
27
26
  - Do not list tools that are not available
28
27
  - Use tier capability descriptions from tier-rules.md
29
28
  - Never inline-render the envelope JSON — the script owns the schema; drift breaks pipelines
30
- - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing status report is NOT the terminal step
31
-
32
- ## Headless Mode Display Rule
33
-
34
- When `{headless_mode}` is `true` OR `{quiet_mode}` is `true`, sections 2 (the human-readable banner) and 3 (the REQUIRED TIER NOT MET human prose block) are **skipped entirely**. The single line of stdout in those modes is the `SKF_SETUP_RESULT_JSON: {…}` envelope from section 4 — every signal the human banner would have surfaced is already in the envelope (`tier`, `previous_tier`, `tier_changed`, `tools`, `tools_added`, `tools_removed`, `files_written`, `tier_override_*`, `require_tier_satisfied`, `warnings`, `error`). Parent skills, CI pipelines, and expert re-runners consume the envelope alone; they should not be forced to scroll past 30 lines of ASCII-art they cannot use.
35
-
36
- When both `{headless_mode}` and `{quiet_mode}` are `false`, sections 2 and 3 display normally and section 4 is skipped (interactive runs read the human banner; envelope emission would just be log noise).
29
+ - Chains to the local health-check step via `{nextStepFile}` after completion — the user-facing status report is not the terminal step
37
30
 
38
31
  ## MANDATORY SEQUENCE
39
32
 
40
33
  ### 1. Load Capability Descriptions
41
34
 
42
- Load and read {tierRulesData} for the tier capability descriptions and re-run messages. Needed by section 2; safe to load unconditionally — the data load itself is silent.
35
+ Load and read {tierRulesData} for the tier capability descriptions and re-run messages. Needed by section 2.
43
36
 
44
37
  ### 2. Display Forge Status Report (skip when `{headless_mode}` or `{quiet_mode}` is true)
45
38
 
@@ -54,7 +47,7 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
54
47
  {tier capability description from tier-rules.md}
55
48
 
56
49
  Tools Detected:
57
- {for each tool that is available, show: tool name — version}
50
+ {for each tool that is available, show: tool name — version. ccc exposes no version string, so for ccc show its daemon health instead: ccc — daemon {ccc_daemon} (e.g. "ccc — daemon healthy")}
58
51
  {if no tools are available: (none yet — see "Climb to next tier" below)}
59
52
 
60
53
  {if calculated_tier is not Deep:}
@@ -64,7 +57,7 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
64
57
  {if tools.ast_grep and not tools.gh_cli: - Install GitHub CLI (https://cli.github.com) — required for Deep tier (cross-repository synthesis)}
65
58
  {if tools.ast_grep and not tools.qmd and qmd_status is "absent": - Install qmd (https://github.com/tobi/qmd) — required for Deep tier (knowledge search)}
66
59
  {if tools.ast_grep and not tools.qmd and qmd_status is "daemon_stopped": - Start the qmd daemon (already installed) — run `qmd start` (or your distribution's qmd service command) to unlock Deep tier (knowledge search)}
67
- {if tools.ccc and ccc_daemon is "error": - The ccc daemon is reporting errors — run `ccc doctor` to diagnose. CCC index will fail until resolved (mirrors the qmd daemon-stopped pattern above for parity)}
60
+ {if tools.ccc and ccc_daemon is "error": - The ccc daemon is reporting errors — run `ccc doctor` to diagnose. CCC index will fail until resolved}
68
61
  {end if}
69
62
 
70
63
  {if hygiene_result is "completed":}
@@ -122,6 +115,9 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
122
115
  rely on the missing tool(s) will fail at runtime. Install the missing tool(s) or remove
123
116
  the override from preferences.yaml.
124
117
 
118
+ {if {previous_tier} is null:}
119
+ Initial detection — {calculated_tier} tier established.
120
+
125
121
  {if {tier_changed} is true:}
126
122
  {appropriate upgrade/downgrade message from tier-rules.md}
127
123
 
@@ -129,7 +125,7 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
129
125
  {same-tier message from tier-rules.md}
130
126
  {if preferences_yaml_created is false and (ccc_index_result is "fresh" or ccc_index_result is "none" or ccc_index_result is "skipped"): Nothing changed — your preferences were left untouched and the index was already current. You're good.}
131
127
 
132
- {if {tier_changed} is false and ({tools_added} or {tools_removed} is non-empty):}
128
+ {if {tier_changed} is false and ({tools_added} or {tools_removed} is non-empty) and {previous_tier} is non-null:}
133
129
  Tier unchanged: {calculated_tier}.
134
130
  {if {tools_added} non-empty:} Newly detected: {comma-separated tool names from tools_added}{if ccc was added and tier is Deep: " — ccc enhances Deep tier transparently."}
135
131
  {if {tools_removed} non-empty:} No longer detected: {comma-separated tool names from tools_removed} — re-install to restore those capabilities.
@@ -144,15 +140,9 @@ Load and read {tierRulesData} for the tier capability descriptions and re-run me
144
140
 
145
141
  All re-run-delta context flags (`{tools_added}`, `{tools_removed}`, `{tier_changed}`) come from the detector's `deltas` block bound in stage 1 — no LLM-side recomputation, no set arithmetic in prose.
146
142
 
147
- **Tool display rules:**
148
-
149
- - Only show tools that ARE available with their version strings
150
- - Do NOT list unavailable tools
151
- - Do NOT show a "missing" column or section
152
-
153
143
  ### 3. Display Required-Tier Failure Block (when applicable; skip when `{headless_mode}` or `{quiet_mode}` is true)
154
144
 
155
- If `{require_tier_satisfied}` is `false` AND both `{headless_mode}` and `{quiet_mode}` are `false`, display this block immediately after the status report. In headless or quiet modes the failure surfaces via the envelope's `require_tier_satisfied: false` and the synthesized `warnings` entry — the human prose block would be log noise.
145
+ If `{require_tier_satisfied}` is `false`, display this block immediately after the status report (the heading gate already handles the headless/quiet skip).
156
146
 
157
147
  When the block does fire (interactive run with require-tier failure):
158
148
 
@@ -170,11 +160,9 @@ When the block does fire (interactive run with require-tier failure):
170
160
  ═══════════════════════════════════════
171
161
  ```
172
162
 
173
- This block exists to make pipeline failures visible without the operator parsing the JSON envelope.
174
-
175
163
  ### 4. Emit Headless JSON Envelope
176
164
 
177
- When `{headless_mode}` is `true` OR `{quiet_mode}` is `true`, build the context payload from this step's accumulated flags and forward it to `{emitEnvelopeHelper}` on stdin. Invoke via `uv run` so the script's PEP 723 dependency declarations are honored (the canonical runtime invocation per `docs/getting-started.md`'s uv prereq — bare `python3` skips the metadata block and breaks on a fresh interpreter). The script computes derived fields (`tools_added`, `tools_removed`, `tier_changed`, `warnings`), validates the assembled envelope against the JSON Schema at `src/shared/scripts/schemas/skf-setup-result-envelope.v1.json`, and emits the single prefixed line `SKF_SETUP_RESULT_JSON: {…}` on stdout.
165
+ When `{headless_mode}` is `true` OR `{quiet_mode}` is `true`, build the context payload from this step's accumulated flags and forward it to `{emitEnvelopeHelper}` on stdin. Invoke via `uv run`. The script computes derived fields (`tools_added`, `tools_removed`, `tier_changed`, `warnings`), validates the assembled envelope against the JSON Schema at `src/shared/scripts/schemas/skf-setup-result-envelope.v1.json`, and emits the single prefixed line `SKF_SETUP_RESULT_JSON: {…}` on stdout.
178
166
 
179
167
  ```bash
180
168
  echo '{
@@ -212,10 +200,13 @@ echo '{
212
200
 
213
201
  The script's documented context-payload shape (see `src/shared/scripts/skf-emit-result-envelope.py` docstring) tolerates two `tools` shapes — bare booleans OR `skf-detect-tools.py`'s `{key: {available: bool, ...}}` output — so either step 1's normalized booleans OR the raw detect-tools output forwarded as-is will produce the correct envelope.
214
202
 
215
- **If the script exits non-zero:** the assembled envelope failed schema validation, which means a context flag from an earlier step is malformed. Surface the error to stderr and continue (the human-readable banner is already displayed; missing JSON envelope on a headless run is a degraded but non-fatal state — the pipeline observer will see no envelope and treat that as "agent did not complete cleanly").
216
-
217
- **When both `{headless_mode}` and `{quiet_mode}` are `false`, do NOT invoke the script** — interactive runs read the human-readable banner.
203
+ **If the script exits non-zero:** the assembled envelope failed schema validation, which means a context flag from an earlier step is malformed. Surface the error to stderr and continue (missing JSON envelope on a headless run is a degraded but non-fatal state — the pipeline observer will see no envelope and treat that as "agent did not complete cleanly").
218
204
 
219
205
  ### 5. Chain to Health Check
220
206
 
221
- After the forge status report (and any failure block + JSON envelope) has been displayed, load `{nextStepFile}`, read it fully, and execute it — UNLESS `{require_tier_satisfied}` is `false`, in which case halt the workflow here without chaining to step 5. The health-check step is the true terminal step on success — do not stop after the report on a passing run even though it reads as final. step 5 in turn delegates to `shared/health-check.md`; after that returns, the setup workflow is fully done.
207
+ After the forge status report (and any failure block + JSON envelope) has been displayed:
208
+
209
+ - If `{require_tier_satisfied}` is `false`, halt the workflow here without chaining to step 5. The tier miss is terminal; `{onCompleteCommand}` does not fire on a failed run.
210
+ - Otherwise the forge is fully configured. If `{onCompleteCommand}` (resolved from `workflow.on_complete` at activation) is non-empty, execute it now — this is the workflow's terminal skill-specific action (e.g. trigger the first index build or notify an onboarding channel); in headless, log the action. Then load `{nextStepFile}`, read it fully, and execute it.
211
+
212
+ The health-check step is the true terminal step on success — do not stop after the report on a passing run even though it reads as final. Step 5 in turn delegates to `shared/health-check.md`; after that returns, the setup workflow is fully done.