bmad-module-skill-forge 1.9.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 (281) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.nvmrc +1 -1
  3. package/README.md +10 -5
  4. package/docs/_data/pinned.yaml +2 -2
  5. package/docs/agents.md +11 -2
  6. package/docs/architecture.md +5 -4
  7. package/docs/bmad-synergy.md +30 -7
  8. package/docs/campaign.md +172 -0
  9. package/docs/examples.md +7 -3
  10. package/docs/forge-auto.md +90 -0
  11. package/docs/getting-started.md +9 -0
  12. package/docs/how-it-works.md +6 -4
  13. package/docs/index.md +4 -3
  14. package/docs/skill-model.md +1 -1
  15. package/docs/troubleshooting.md +17 -1
  16. package/docs/verifying-a-skill.md +2 -2
  17. package/docs/why-skf.md +1 -1
  18. package/docs/workflows.md +64 -24
  19. package/package.json +2 -2
  20. package/src/knowledge/skill-lifecycle.md +1 -1
  21. package/src/module-help.csv +4 -3
  22. package/src/shared/data/language-corpora.json +32 -0
  23. package/src/shared/references/pipeline-contracts.md +6 -3
  24. package/src/shared/scripts/schemas/skill-brief.v1.json +6 -1
  25. package/src/shared/scripts/skf-comention-pairs.py +350 -0
  26. package/src/shared/scripts/skf-count-tokens.py +222 -0
  27. package/src/shared/scripts/skf-derive-assembly-shape.py +107 -0
  28. package/src/shared/scripts/skf-detect-docs.py +240 -4
  29. package/src/shared/scripts/skf-detect-language.py +82 -3
  30. package/src/shared/scripts/skf-detect-tools.py +105 -2
  31. package/src/shared/scripts/skf-emit-result-envelope.py +17 -0
  32. package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
  33. package/src/shared/scripts/skf-extract-public-api.py +53 -0
  34. package/src/shared/scripts/skf-find-cycles.py +246 -0
  35. package/src/shared/scripts/skf-hash-content.py +487 -2
  36. package/src/shared/scripts/skf-language-corpora.py +100 -0
  37. package/src/shared/scripts/skf-manifest-ops.py +67 -1
  38. package/src/shared/scripts/skf-merge-doc-urls.py +244 -0
  39. package/src/shared/scripts/skf-preapply.py +1 -1
  40. package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
  41. package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
  42. package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
  43. package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
  44. package/src/shared/scripts/skf-shape-detect.py +576 -25
  45. package/src/shared/scripts/skf-shard-body.py +461 -0
  46. package/src/shared/scripts/skf-skill-inventory.py +139 -3
  47. package/src/shared/scripts/skf-structural-diff.py +307 -67
  48. package/src/shared/scripts/skf-validate-brief-schema.py +41 -6
  49. package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
  51. package/src/shared/scripts/skf-validate-output.py +443 -10
  52. package/src/shared/scripts/skf-validate-pins.py +3 -3
  53. package/src/shared/scripts/skf-verify-no-trace.py +214 -0
  54. package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
  55. package/src/shared/scripts/skf-write-skill-brief.py +21 -5
  56. package/src/skf-analyze-source/SKILL.md +17 -32
  57. package/src/skf-analyze-source/assets/skill-brief-schema.md +1 -1
  58. package/src/skf-analyze-source/customize.toml +2 -0
  59. package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
  60. package/src/skf-analyze-source/references/continue.md +8 -13
  61. package/src/skf-analyze-source/references/generate-briefs.md +28 -12
  62. package/src/skf-analyze-source/references/headless-contract.md +33 -0
  63. package/src/skf-analyze-source/references/health-check.md +1 -2
  64. package/src/skf-analyze-source/references/identify-units.md +25 -36
  65. package/src/skf-analyze-source/references/init.md +12 -25
  66. package/src/skf-analyze-source/references/map-and-detect.md +8 -20
  67. package/src/skf-analyze-source/references/recommend.md +4 -14
  68. package/src/skf-analyze-source/references/scan-project.md +3 -11
  69. package/src/skf-analyze-source/references/step-auto-scope.md +211 -197
  70. package/src/skf-analyze-source/references/step-shape-detect.md +20 -13
  71. package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
  72. package/src/skf-audit-skill/SKILL.md +9 -6
  73. package/src/skf-audit-skill/customize.toml +2 -0
  74. package/src/skf-audit-skill/references/init.md +58 -32
  75. package/src/skf-audit-skill/references/re-index.md +15 -34
  76. package/src/skf-audit-skill/references/report.md +11 -58
  77. package/src/skf-audit-skill/references/semantic-diff.md +4 -22
  78. package/src/skf-audit-skill/references/severity-classify.md +53 -87
  79. package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
  80. package/src/skf-audit-skill/references/structural-diff.md +43 -68
  81. package/src/skf-brief-skill/SKILL.md +7 -36
  82. package/src/skf-brief-skill/assets/scope-templates.md +1 -1
  83. package/src/skf-brief-skill/assets/skill-brief-schema.md +1 -1
  84. package/src/skf-brief-skill/customize.toml +14 -0
  85. package/src/skf-brief-skill/references/analyze-target.md +13 -18
  86. package/src/skf-brief-skill/references/confirm-brief.md +2 -15
  87. package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
  88. package/src/skf-brief-skill/references/gather-intent.md +27 -24
  89. package/src/skf-brief-skill/references/headless-args.md +1 -1
  90. package/src/skf-brief-skill/references/health-check.md +1 -22
  91. package/src/skf-brief-skill/references/invocation-contract.md +37 -0
  92. package/src/skf-brief-skill/references/scope-definition.md +6 -15
  93. package/src/skf-brief-skill/references/step-auto-brief.md +22 -9
  94. package/src/skf-brief-skill/references/step-auto-validate.md +3 -7
  95. package/src/skf-brief-skill/references/version-resolution.md +1 -1
  96. package/src/skf-brief-skill/references/write-brief.md +19 -13
  97. package/src/skf-campaign/SKILL.md +121 -0
  98. package/src/skf-campaign/assets/campaign-state-schema.json +191 -0
  99. package/src/skf-campaign/customize.toml +75 -0
  100. package/src/skf-campaign/manifest.yaml +15 -0
  101. package/src/skf-campaign/references/campaign-contracts.md +67 -0
  102. package/src/skf-campaign/references/campaign-directive-spec.md +32 -0
  103. package/src/skf-campaign/references/health-check.md +35 -0
  104. package/src/skf-campaign/references/step-01-setup.md +121 -0
  105. package/src/skf-campaign/references/step-02-strategy.md +102 -0
  106. package/src/skf-campaign/references/step-03-pins.md +56 -0
  107. package/src/skf-campaign/references/step-04-provenance.md +63 -0
  108. package/src/skf-campaign/references/step-05-skill-loop.md +103 -0
  109. package/src/skf-campaign/references/step-06-batch.md +88 -0
  110. package/src/skf-campaign/references/step-07-capstone.md +63 -0
  111. package/src/skf-campaign/references/step-08-verify.md +75 -0
  112. package/src/skf-campaign/references/step-09-refine.md +83 -0
  113. package/src/skf-campaign/references/step-10-export.md +106 -0
  114. package/src/skf-campaign/references/step-11-maintenance.md +84 -0
  115. package/src/skf-campaign/references/step-resume.md +115 -0
  116. package/src/skf-campaign/scripts/.gitkeep +0 -0
  117. package/src/skf-campaign/scripts/campaign-deps.py +244 -0
  118. package/src/skf-campaign/scripts/campaign-parse-manifest.py +119 -0
  119. package/src/skf-campaign/scripts/campaign-provenance.py +247 -0
  120. package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
  121. package/src/skf-campaign/scripts/campaign-render-kickoff.py +158 -0
  122. package/src/skf-campaign/scripts/campaign-report.py +249 -0
  123. package/src/skf-campaign/scripts/campaign-status.py +162 -0
  124. package/src/skf-campaign/scripts/campaign-validate-pins.py +174 -0
  125. package/src/skf-campaign/scripts/campaign-validate-state.py +207 -0
  126. package/src/skf-campaign/templates/campaign-brief-template.yaml +34 -0
  127. package/src/skf-campaign/templates/campaign-report-template.md +54 -0
  128. package/src/skf-campaign/templates/kickoff-template.md +48 -0
  129. package/src/skf-create-skill/SKILL.md +15 -4
  130. package/src/skf-create-skill/assets/compile-assembly-rules.md +35 -34
  131. package/src/skf-create-skill/assets/skill-sections.md +9 -100
  132. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
  133. package/src/skf-create-skill/customize.toml +13 -0
  134. package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
  135. package/src/skf-create-skill/references/compile.md +37 -47
  136. package/src/skf-create-skill/references/component-extraction.md +8 -35
  137. package/src/skf-create-skill/references/ecosystem-check.md +9 -25
  138. package/src/skf-create-skill/references/enrich.md +4 -17
  139. package/src/skf-create-skill/references/extract.md +20 -27
  140. package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
  141. package/src/skf-create-skill/references/extraction-patterns.md +10 -8
  142. package/src/skf-create-skill/references/generate-artifacts.md +20 -25
  143. package/src/skf-create-skill/references/health-check.md +2 -2
  144. package/src/skf-create-skill/references/load-brief.md +16 -18
  145. package/src/skf-create-skill/references/report.md +27 -14
  146. package/src/skf-create-skill/references/source-resolution-protocols.md +4 -4
  147. package/src/skf-create-skill/references/step-auto-shard.md +52 -25
  148. package/src/skf-create-skill/references/step-doc-rot.md +33 -15
  149. package/src/skf-create-skill/references/step-doc-sources.md +13 -4
  150. package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
  151. package/src/skf-create-skill/references/sub/fetch-docs.md +33 -46
  152. package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
  153. package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
  154. package/src/skf-create-skill/references/validate.md +69 -29
  155. package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
  156. package/src/skf-create-stack-skill/SKILL.md +15 -9
  157. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
  158. package/src/skf-create-stack-skill/customize.toml +10 -0
  159. package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
  160. package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
  161. package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
  162. package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
  163. package/src/skf-create-stack-skill/references/generate-output.md +24 -75
  164. package/src/skf-create-stack-skill/references/init.md +9 -81
  165. package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
  166. package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
  167. package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
  168. package/src/skf-create-stack-skill/references/report.md +27 -62
  169. package/src/skf-create-stack-skill/references/validate.md +35 -31
  170. package/src/skf-drop-skill/SKILL.md +20 -23
  171. package/src/skf-drop-skill/customize.toml +10 -0
  172. package/src/skf-drop-skill/references/execute.md +67 -36
  173. package/src/skf-drop-skill/references/headless-contract.md +18 -0
  174. package/src/skf-drop-skill/references/health-check.md +1 -13
  175. package/src/skf-drop-skill/references/report.md +44 -10
  176. package/src/skf-drop-skill/references/select.md +93 -48
  177. package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
  178. package/src/skf-export-skill/SKILL.md +17 -14
  179. package/src/skf-export-skill/assets/managed-section-format.md +2 -5
  180. package/src/skf-export-skill/customize.toml +10 -6
  181. package/src/skf-export-skill/references/generate-snippet.md +21 -35
  182. package/src/skf-export-skill/references/load-skill.md +44 -42
  183. package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
  184. package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
  185. package/src/skf-export-skill/references/package.md +27 -33
  186. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
  187. package/src/skf-export-skill/references/result-envelope.md +28 -0
  188. package/src/skf-export-skill/references/summary.md +11 -5
  189. package/src/skf-export-skill/references/token-report.md +42 -24
  190. package/src/skf-export-skill/references/update-context.md +32 -33
  191. package/src/skf-forger/SKILL.md +32 -39
  192. package/src/skf-forger/references/pipeline-mode.md +34 -0
  193. package/src/skf-forger/scripts/parse-pipeline.py +270 -0
  194. package/src/skf-quick-skill/SKILL.md +10 -60
  195. package/src/skf-quick-skill/customize.toml +9 -0
  196. package/src/skf-quick-skill/references/batch-mode.md +6 -4
  197. package/src/skf-quick-skill/references/compile.md +15 -52
  198. package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
  199. package/src/skf-quick-skill/references/finalize.md +12 -6
  200. package/src/skf-quick-skill/references/halt-contract.md +54 -0
  201. package/src/skf-quick-skill/references/health-check.md +3 -4
  202. package/src/skf-quick-skill/references/quick-extract.md +7 -18
  203. package/src/skf-quick-skill/references/registry-resolution.md +1 -23
  204. package/src/skf-quick-skill/references/resolve-target.md +27 -34
  205. package/src/skf-quick-skill/references/write-and-validate.md +10 -25
  206. package/src/skf-refine-architecture/SKILL.md +16 -8
  207. package/src/skf-refine-architecture/customize.toml +9 -0
  208. package/src/skf-refine-architecture/references/compile.md +10 -8
  209. package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
  210. package/src/skf-refine-architecture/references/health-check.md +1 -1
  211. package/src/skf-refine-architecture/references/improvements.md +9 -25
  212. package/src/skf-refine-architecture/references/init.md +9 -16
  213. package/src/skf-refine-architecture/references/issue-detection.md +10 -25
  214. package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
  215. package/src/skf-refine-architecture/references/report.md +14 -11
  216. package/src/skf-rename-skill/SKILL.md +11 -21
  217. package/src/skf-rename-skill/customize.toml +10 -0
  218. package/src/skf-rename-skill/references/execute.md +112 -82
  219. package/src/skf-rename-skill/references/exit-codes.md +12 -0
  220. package/src/skf-rename-skill/references/report.md +16 -8
  221. package/src/skf-rename-skill/references/select.md +69 -42
  222. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
  223. package/src/skf-setup/SKILL.md +21 -13
  224. package/src/skf-setup/customize.toml +10 -0
  225. package/src/skf-setup/references/auto-index.md +11 -10
  226. package/src/skf-setup/references/ccc-index.md +14 -15
  227. package/src/skf-setup/references/detect-and-tier.md +10 -8
  228. package/src/skf-setup/references/report.md +26 -32
  229. package/src/skf-setup/references/tier-rules.md +2 -32
  230. package/src/skf-setup/references/write-config.md +9 -10
  231. package/src/skf-test-skill/SKILL.md +10 -8
  232. package/src/skf-test-skill/customize.toml +2 -0
  233. package/src/skf-test-skill/references/coherence-check.md +19 -31
  234. package/src/skf-test-skill/references/coverage-check.md +111 -67
  235. package/src/skf-test-skill/references/detect-mode.md +10 -41
  236. package/src/skf-test-skill/references/external-validators.md +11 -19
  237. package/src/skf-test-skill/references/init.md +39 -21
  238. package/src/skf-test-skill/references/migration-section-rules.md +4 -4
  239. package/src/skf-test-skill/references/report.md +60 -40
  240. package/src/skf-test-skill/references/score.md +32 -64
  241. package/src/skf-test-skill/references/scoring-rules.md +6 -62
  242. package/src/skf-test-skill/references/source-access-protocol.md +15 -5
  243. package/src/skf-test-skill/references/step-hard-gate.md +1 -3
  244. package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
  245. package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
  246. package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
  247. package/src/skf-test-skill/scripts/compute-score.py +135 -4
  248. package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
  249. package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
  250. package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
  251. package/src/skf-update-skill/SKILL.md +17 -5
  252. package/src/skf-update-skill/customize.toml +11 -6
  253. package/src/skf-update-skill/references/detect-changes.md +48 -36
  254. package/src/skf-update-skill/references/health-check.md +5 -11
  255. package/src/skf-update-skill/references/init.md +40 -32
  256. package/src/skf-update-skill/references/manual-section-rules.md +1 -2
  257. package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
  258. package/src/skf-update-skill/references/merge.md +13 -57
  259. package/src/skf-update-skill/references/re-extract.md +27 -25
  260. package/src/skf-update-skill/references/report.md +14 -7
  261. package/src/skf-update-skill/references/validate.md +32 -30
  262. package/src/skf-update-skill/references/write.md +86 -51
  263. package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
  264. package/src/skf-verify-stack/SKILL.md +11 -21
  265. package/src/skf-verify-stack/customize.toml +12 -3
  266. package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
  267. package/src/skf-verify-stack/references/coverage.md +21 -10
  268. package/src/skf-verify-stack/references/exit-codes.md +14 -0
  269. package/src/skf-verify-stack/references/health-check.md +1 -1
  270. package/src/skf-verify-stack/references/init.md +20 -25
  271. package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
  272. package/src/skf-verify-stack/references/integrations.md +44 -49
  273. package/src/skf-verify-stack/references/report.md +28 -34
  274. package/src/skf-verify-stack/references/requirements.md +3 -3
  275. package/src/skf-verify-stack/references/synthesize.md +36 -39
  276. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
  277. package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
  278. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
  279. package/tools/cli/lib/ui.js +3 -2
  280. package/docs/deepwiki.md +0 -89
  281. 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 |
@@ -46,18 +44,17 @@ All stages auto-proceed except the optional orphan-removal gate in step 3.
46
44
  | Aspect | Detail |
47
45
  |--------|--------|
48
46
  | **Inputs** | (none) |
49
- | **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"`); `--quiet` (suppress the human-readable FORGE STATUS banner — pipelines and expert re-runners get the envelope only) |
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'
@@ -37,13 +36,15 @@ For Quick and Forge tiers, skip silently and proceed (QMD is not available; ccc
37
36
 
38
37
  Read `{calculated_tier}` and `{ccc}` from context (set by step 1).
39
38
 
39
+ Default `{orphan_auto_resolution}` to null at the top of this step; only the non-interactive branch in section 3 overrides it.
40
+
40
41
  **If `{calculated_tier}` is Quick or Forge AND `{ccc}` is false:** No registry hygiene needed. Set `{hygiene_result: "skipped", hygiene_healthy: 0, hygiene_orphaned_removed: 0, hygiene_orphaned_kept: 0, hygiene_stale_cleaned: 0, ccc_registry_stale_cleaned: 0, ccc_registry_stale_removed_paths: []}`. Proceed directly to section 5 (Auto-Proceed) — no output, no messaging.
41
42
 
42
43
  **If `{calculated_tier}` is Quick or Forge AND `{ccc}` is true:** No QMD work, but ccc registry needs pruning. Set QMD-related flags to defaults (`hygiene_result: "skipped"`, all hygiene_* counts = 0). Skip directly to section 4 (Stale Registry Cleanup), running it with the ccc-prune flag only.
43
44
 
44
45
  **If `{calculated_tier}` is Forge+:** Same as Quick/Forge with ccc — no QMD work (qmd unavailable at Forge+), but ccc registry hygiene runs.
45
46
 
46
- **If `{calculated_tier}` IS Deep:** Continue to section 2.
47
+ **If `{calculated_tier}` is Deep:** Continue to section 2.
47
48
 
48
49
  ### 2. Classify Live QMD Collections vs Registry
49
50
 
@@ -54,7 +55,7 @@ uv run {qmdClassifyHelper} \
54
55
  --registry-from-yaml "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml"
55
56
  ```
56
57
 
57
- 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.
58
59
 
59
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).
60
61
 
@@ -72,7 +73,7 @@ Set `{hygiene_result: "completed"}`.
72
73
 
73
74
  **If `{orphaned_collections}` is empty:** Set `{hygiene_orphaned_removed: 0, hygiene_orphaned_kept: 0}` and skip to section 4.
74
75
 
75
- **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)"`. 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 and skip to section 4. The two paths compose: `--orphan-action` overrides the headless default; `--headless` without `--orphan-action` keeps backward-compatible behavior.
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.)
76
77
 
77
78
  **If `{headless_mode}` is false**, display to the user:
78
79
 
@@ -97,7 +98,7 @@ Track the count of successful removals as `{hygiene_orphaned_removed}`. Set `{hy
97
98
 
98
99
  ### 4. Stale Registry Cleanup
99
100
 
100
- 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.
101
102
 
102
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.
103
104
 
@@ -108,7 +109,7 @@ uv run {forgeTierRwHelper} clean-stale \
108
109
  [--prune-missing-ccc-paths]
109
110
  ```
110
111
 
111
- 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.
112
113
 
113
114
  **Parse the JSON output and set context flags for step 4:**
114
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'
@@ -48,11 +47,15 @@ Prior state lives in two files; the detector helper (§2) reads forge-tier.yaml
48
47
 
49
48
  Press Esc or Ctrl+C now if this isn't the right project — no files have been written yet."
50
49
 
50
+ **Re-run notice** — when the prior-state read (§2 below, `prior.previous_tier`) returns a non-null value AND `{headless_mode}` is `false`, display this notice instead of the first-run preamble, before continuing, so a user who re-ran in the wrong/sibling repo can abort before any config rewrite or re-index. (When `{headless_mode}` is true, skip it — pipelines re-run intentionally.)
51
+
52
+ "**Forge already set up here:** {previous_tier} tier, detected {previous_detection_date}. Re-running re-probes the tools and refreshes config/index in this project. To refresh the tier without paying the ccc re-index cost, re-run with `--ccc-skip-index`. Press Esc or Ctrl+C now if this isn't the project you meant — nothing has been rewritten yet."
53
+
51
54
  ### 2. Run Detection Helper
52
55
 
53
- 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.)
54
57
 
55
- 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.
56
59
 
57
60
  ### 3. Parse Output and Set Context Flags
58
61
 
@@ -97,6 +100,7 @@ From `prior` (populated by `--prior-state-from`; all fields null/empty on first
97
100
  - `{previous_ccc_indexed_path}` ← `prior.previous_ccc_indexed_path`
98
101
  - `{previous_ccc_last_indexed}` ← `prior.previous_ccc_last_indexed`
99
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.)
100
104
 
101
105
  From `deltas` (computed by the script from current tools + prior; eliminates LLM-side set arithmetic in the report banner):
102
106
 
@@ -104,8 +108,6 @@ From `deltas` (computed by the script from current tools + prior; eliminates LLM
104
108
  - `{tools_removed}` ← `deltas.tools_removed` (the list)
105
109
  - `{tier_changed}` ← `deltas.tier_changed` (boolean)
106
110
 
107
- **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.
108
-
109
111
  ### 4. Auto-Proceed
110
112
 
111
113
  After context flags are populated, display "**Proceeding to CCC index check...**", then load `{nextStepFile}`, read it fully, and execute it.