bmad-module-skill-forge 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +51 -49
  3. package/docs/404.md +10 -8
  4. package/docs/_data/pinned.yaml +22 -13
  5. package/docs/_internal/STABILITY.md +58 -35
  6. package/docs/agents.md +39 -53
  7. package/docs/architecture.md +65 -51
  8. package/docs/bmad-synergy.md +53 -51
  9. package/docs/campaign.md +127 -75
  10. package/docs/concepts.md +34 -28
  11. package/docs/examples.md +70 -68
  12. package/docs/forge-auto.md +34 -31
  13. package/docs/getting-started.md +118 -57
  14. package/docs/how-it-works.md +25 -21
  15. package/docs/index.md +15 -15
  16. package/docs/skill-model.md +70 -59
  17. package/docs/troubleshooting.md +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +14 -9
  41. package/src/shared/references/feasibility-report-schema.md +19 -13
  42. package/src/shared/references/headless-gate-convention.md +55 -3
  43. package/src/shared/references/output-contract-schema.md +19 -8
  44. package/src/shared/references/pipeline-contracts.md +94 -29
  45. package/src/shared/references/tessl-review.md +77 -0
  46. package/src/shared/scripts/schemas/skf-analyze-result-envelope.v1.json +194 -0
  47. package/src/shared/scripts/schemas/skf-audit-result-envelope.v1.json +192 -0
  48. package/src/shared/scripts/schemas/skf-brief-result-envelope.v1.json +32 -4
  49. package/src/shared/scripts/schemas/skf-campaign-result-envelope.v1.json +174 -0
  50. package/src/shared/scripts/schemas/skf-create-skill-result-envelope.v1.json +284 -0
  51. package/src/shared/scripts/schemas/skf-drop-skill-result-envelope.v1.json +164 -0
  52. package/src/shared/scripts/schemas/skf-export-result-envelope.v1.json +160 -0
  53. package/src/shared/scripts/schemas/skf-quick-skill-result-envelope.v1.json +261 -0
  54. package/src/shared/scripts/schemas/skf-refine-architecture-result-envelope.v1.json +192 -0
  55. package/src/shared/scripts/schemas/skf-rename-skill-result-envelope.v1.json +179 -0
  56. package/src/shared/scripts/schemas/skf-setup-result-envelope.v1.json +27 -11
  57. package/src/shared/scripts/schemas/skf-stack-result-envelope.v1.json +176 -0
  58. package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +173 -0
  59. package/src/shared/scripts/schemas/skf-update-result-envelope.v1.json +19 -5
  60. package/src/shared/scripts/schemas/skf-verify-stack-result-envelope.v1.json +180 -0
  61. package/src/shared/scripts/schemas/skill-brief.v1.json +34 -0
  62. package/src/shared/scripts/schemas/workspace-detection.v1.json +1 -1
  63. package/src/shared/scripts/skf-atomic-write.py +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +117 -9
  75. package/src/shared/scripts/skf-detect-docs.py +892 -4
  76. package/src/shared/scripts/skf-detect-language.py +486 -107
  77. package/src/shared/scripts/skf-detect-registry.py +887 -0
  78. package/src/shared/scripts/skf-detect-scripts-assets.py +1 -1
  79. package/src/shared/scripts/skf-detect-tools.py +215 -27
  80. package/src/shared/scripts/skf-detect-workspaces.py +506 -30
  81. package/src/shared/scripts/skf-disqualify-candidates.py +821 -182
  82. package/src/shared/scripts/skf-emit-brief-result-envelope.py +69 -191
  83. package/src/shared/scripts/skf-emit-result-envelope.py +1604 -106
  84. package/src/shared/scripts/skf-enumerate-stack-skills.py +986 -117
  85. package/src/shared/scripts/skf-extract-public-api.py +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +17 -6
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +55 -18
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -57
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -89
  348. package/src/skf-setup/references/detect-and-tier.md +58 -66
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -645
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # /// script
3
- # requires-python = ">=3.10"
3
+ # requires-python = ">=3.11"
4
4
  # ///
5
5
  """SKF Validate Rename Name — deterministic new-name gate for skf-rename-skill.
6
6
 
@@ -12,9 +12,11 @@ with one correct answer for a given (name, filesystem, manifest) state:
12
12
  skf-validate-output.py / skf-validate-brief-inputs.py)
13
13
  length — 1..64 characters (agentskills.io spec)
14
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
15
+ collision — does NOT already exist (a folder, a file or a link, even a
16
+ broken one) as a manifest `exports` key, a top-level entry
17
+ under the skills output folder or the forge data folder, and
18
+ is not `improvement-queue`, SKF's own forge folder (location
19
+ kind `reserved`)
18
20
 
19
21
  Doing this in the prompt is a set-membership computation across three sources
20
22
  plus a regex match — deterministic, so it lives here and the prompt keeps only
@@ -23,14 +25,34 @@ the interactive re-ask / halt decision. Checks run in the select.md §5 order an
23
25
  halt (`input-invalid`/exit 2 for format/length/identity, `name-collision`/exit 5
24
26
  for collision).
25
27
 
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.
28
+ `interrupted_rename` fires the select.md §5 recovery fingerprint. A rename's
29
+ copy creates the new skill folder first, and a new forge folder only from an
30
+ old one. So the new name collides only on disk (not in the manifest, not on a
31
+ reserved name), at least under the skills output folder, while the old skill
32
+ folder still exists, and under the forge data folder only when the old skill
33
+ has a forge folder too — the signature of a rename interrupted between copy
34
+ and delete-old, which the caller surfaces as a named cleanup path instead of a
35
+ dead-end collision. The new skill folder must also hold nothing a copy of the
36
+ old one could not: every entry in it exists in the old folder, of the same
37
+ kind, with only each version's package renamed. A forge-only collision
38
+ (another tool's folder with the new name), or a module's own skill or another
39
+ tool's folder at the new name, is a genuine clash.
40
+
41
+ `interrupted_after_rekey` fires the recovery for a rename that stopped after
42
+ it re-keyed the export manifest, before it deleted the old folders: the new
43
+ name is a manifest key and the old name no longer is, both skill folders are
44
+ plain folders, and the old one holds nothing the new one lacks (every entry
45
+ in it exists in the new folder, of the same kind, with only each version's
46
+ package renamed). The new name is then the skill, and the old folders are
47
+ what that run left. `leftover_folders` lists them for the caller to name:
48
+ the old skill folder and, when that rename moved the forge folder (the old
49
+ and new forge folders are plain folders and the old one holds nothing the
50
+ new one lacks), the old forge folder. It is empty when the fingerprint does
51
+ not fire.
30
52
 
31
53
  Output (stdout, always JSON):
32
54
  {status, valid, old_name, new_name, first_failure, checks{...},
33
- interrupted_rename, regex}
55
+ interrupted_rename, interrupted_after_rekey, leftover_folders, regex}
34
56
 
35
57
  Exit codes:
36
58
  0 valid (all checks pass)
@@ -46,13 +68,19 @@ from __future__ import annotations
46
68
 
47
69
  import argparse
48
70
  import json
71
+ import os
49
72
  import re
50
73
  import sys
51
74
  from pathlib import Path
52
75
 
53
76
  NAME_REGEX = r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"
77
+ # Keep identical to FORGE_GROUP_DIRS in skf-skill-inventory.py
78
+ # (test/test-skf-skill-inventory.py pins the copies).
79
+ FORGE_GROUP_DIRS = frozenset({"_campaign", "improvement-queue"})
54
80
  MIN_LEN = 1
55
81
  MAX_LEN = 64
82
+ # OS clutter that a copy can gain without the rename writing it.
83
+ CLUTTER = frozenset({".DS_Store", "Thumbs.db", "desktop.ini"})
56
84
 
57
85
 
58
86
  def load_exports(manifest_path: Path) -> tuple[set[str], str | None]:
@@ -69,6 +97,107 @@ def load_exports(manifest_path: Path) -> tuple[set[str], str | None]:
69
97
  return set(exports.keys()) if isinstance(exports, dict) else set(), None
70
98
 
71
99
 
100
+ # Keep identical to _is_link_or_junction in skf-skill-inventory.py
101
+ # (test/test-skf-skill-inventory.py pins the copies).
102
+ def _is_link_or_junction(p: Path) -> bool:
103
+ """True for POSIX symlinks AND Windows junctions/symlinks.
104
+
105
+ `Path.is_symlink()` is False for Windows junctions; os.readlink succeeds
106
+ for both symlinks and junctions (since CPython 3.8 on Windows). A regular
107
+ directory raises OSError on readlink, which is the signal we want to
108
+ refuse replacement. On Windows, any other reparse point (a cloud-sync
109
+ placeholder, a deduplicated file, an app execution alias) raises
110
+ ValueError: it does not redirect to another path, so it is not a link.
111
+ """
112
+ if p.is_symlink():
113
+ return True
114
+ if not p.exists() and not p.is_symlink():
115
+ return False
116
+ try:
117
+ os.readlink(p)
118
+ return True
119
+ except (OSError, ValueError):
120
+ return False
121
+
122
+
123
+ def _within(new: Path, old: Path, depth: int, old_name: str, new_name: str) -> bool:
124
+ """True when every entry under `new` also exists under `old`, of the same kind.
125
+
126
+ Depth 0 is the skill folder, depth 1 a version folder: there the renamed
127
+ package `{new_name}` stands for `{old_name}`. OS clutter and `.skf-`
128
+ names (the rename's own staging and link swaps) are ignored. A link or
129
+ junction matches only a link or junction.
130
+ """
131
+ try:
132
+ children = list(new.iterdir())
133
+ except OSError:
134
+ return False
135
+ for child in children:
136
+ name = child.name
137
+ if name in CLUTTER or ".skf-" in name:
138
+ continue
139
+ counterpart = old / (old_name if depth == 1 and name == new_name else name)
140
+ child_link, counterpart_link = _is_link_or_junction(child), _is_link_or_junction(counterpart)
141
+ if child_link or counterpart_link:
142
+ if not (child_link and counterpart_link):
143
+ return False
144
+ elif child.is_dir():
145
+ if not counterpart.is_dir() or not _within(child, counterpart, depth + 1, old_name, new_name):
146
+ return False
147
+ elif not counterpart.is_file():
148
+ return False
149
+ return True
150
+
151
+
152
+ def _looks_like_a_copy(skills_dir: Path, old_name: str, new_name: str) -> bool:
153
+ """True when `{skills_dir}/{new_name}` could be a rename's copy of `{old_name}`.
154
+
155
+ The copy takes the old skill folder as it is, then renames each version's
156
+ `{old_name}/` package to `{new_name}/` and rewrites files in place, so it
157
+ never holds an entry the old folder lacks. A module's own skill (a root
158
+ `SKILL.md`) or another tool's folder with the new name does.
159
+ """
160
+ new, old = skills_dir / new_name, skills_dir / old_name
161
+ if (_is_link_or_junction(new) or not new.is_dir()
162
+ or _is_link_or_junction(old) or not old.is_dir()):
163
+ return False
164
+ return _within(new, old, 0, old_name, new_name)
165
+
166
+
167
+ def _copied_whole(old: Path, new: Path, old_name: str, new_name: str) -> bool:
168
+ """True when both are plain folders and `old` holds nothing `new` lacks.
169
+
170
+ _within with the two folders swapped: every entry under the old folder
171
+ exists under the new one, of the same kind, where a version's
172
+ `{old_name}/` package stands for its renamed `{new_name}/`.
173
+ """
174
+ if (_is_link_or_junction(old) or not old.is_dir()
175
+ or _is_link_or_junction(new) or not new.is_dir()):
176
+ return False
177
+ return _within(old, new, 0, new_name, old_name)
178
+
179
+
180
+ def _rekeyed_leftovers(skills_dir: Path, forge_dir: Path, old_name: str, new_name: str,
181
+ export_keys: set[str]) -> list[str]:
182
+ """The old folders a rename left when it stopped after re-keying the manifest.
183
+
184
+ Empty unless the manifest names the new skill and no longer the old one,
185
+ and the old skill folder holds nothing the new one lacks. The old forge
186
+ folder is listed only when the rename moved it: its copy under the new
187
+ name exists, and it holds nothing that copy lacks. When both settings
188
+ name one folder, the skill folder is the forge folder, listed once.
189
+ """
190
+ if new_name not in export_keys or old_name in export_keys:
191
+ return []
192
+ if not _copied_whole(skills_dir / old_name, skills_dir / new_name, old_name, new_name):
193
+ return []
194
+ leftovers = [str(skills_dir / old_name)]
195
+ if (os.path.realpath(forge_dir) != os.path.realpath(skills_dir)
196
+ and _copied_whole(forge_dir / old_name, forge_dir / new_name, old_name, new_name)):
197
+ leftovers.append(str(forge_dir / old_name))
198
+ return leftovers
199
+
200
+
72
201
  def validate_name(
73
202
  old_name: str,
74
203
  new_name: str,
@@ -90,24 +219,33 @@ def validate_name(
90
219
  locations: list[dict] = []
91
220
  if new_name in export_keys:
92
221
  locations.append({"kind": "manifest.exports", "path": str(manifest)})
93
- if (skills_dir / new_name).is_dir():
222
+ if os.path.lexists(skills_dir / new_name):
94
223
  locations.append({"kind": "skills_output_folder", "path": str(skills_dir / new_name)})
95
- if (forge_dir / new_name).is_dir():
224
+ if os.path.lexists(forge_dir / new_name):
96
225
  locations.append({"kind": "forge_data_folder", "path": str(forge_dir / new_name)})
226
+ if new_name in FORGE_GROUP_DIRS:
227
+ locations.append({"kind": "reserved", "path": str(forge_dir / new_name)})
97
228
  collision_ok = len(locations) == 0
98
229
 
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
- )
230
+ # Recovery fingerprint: a rename's copy creates the new skill folder
231
+ # first, and a new forge folder only from an old one. So a stranded
232
+ # partial rename collides on disk only (not in the manifest, not on a
233
+ # reserved name), at least in the skills folder, where the new folder
234
+ # holds nothing a copy of the old skill folder could not, and at the
235
+ # forge folder only when the old skill has one.
236
+ kinds = {loc["kind"] for loc in locations}
106
237
  interrupted_rename = bool(
107
- collides_on_disk_only
108
- and (skills_dir / old_name).is_dir()
109
- and (forge_dir / old_name).is_dir()
238
+ not collision_ok
239
+ and not kinds & {"manifest.exports", "reserved"}
240
+ and "skills_output_folder" in kinds
241
+ and _looks_like_a_copy(skills_dir, old_name, new_name)
242
+ and ("forge_data_folder" not in kinds or (forge_dir / old_name).is_dir())
110
243
  )
244
+ # Recovery fingerprint after the commit point: the manifest already names
245
+ # the new skill, so the new name is taken for good, and the old folders are
246
+ # what the rename left before it deleted them.
247
+ leftover_folders = [] if collision_ok or "reserved" in kinds else _rekeyed_leftovers(
248
+ skills_dir, forge_dir, old_name, new_name, export_keys)
111
249
 
112
250
  checks = {
113
251
  "format": {"ok": fmt_ok},
@@ -130,6 +268,8 @@ def validate_name(
130
268
  "first_failure": first_failure,
131
269
  "checks": checks,
132
270
  "interrupted_rename": interrupted_rename,
271
+ "interrupted_after_rekey": bool(leftover_folders),
272
+ "leftover_folders": leftover_folders,
133
273
  "regex": NAME_REGEX,
134
274
  }
135
275
  if manifest_error:
@@ -1,32 +1,37 @@
1
1
  ---
2
2
  name: skf-setup
3
- description: Initialize forge environment, detect tools, and set capability tier (Quick/Forge/Forge+/Deep). Use when the user requests to "set up" or "initialize the forge".
3
+ description: Initialize forge environment, detect tools, and set capability tier (Quick/Forge/Forge+/Deep). Use when the user requests to "set up the forge" or "initialize the forge".
4
4
  ---
5
5
 
6
6
  # Setup Forge
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.
10
+ Detects the available tools, sets the capability tier (Quick/Forge/Forge+/Deep) and writes the forge configuration to `{sidecar_path}/`. With `ccc` it also keeps `.cocoindex_code/settings.yml` and the semantic-search index current, and it reconciles the QMD and CCC index registries.
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.
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.
15
+ - **Module-level path exception:** bare paths beginning with `knowledge/` or `shared/` resolve from the SKF module root (`{project-root}/_bmad/skf/` installed, `src/` in dev), not the skill root.
16
+ - **Sibling skills:** a path that names another SKF skill's folder (`skf-<name>/...`) resolves from the SKF module root, and that skill must be installed with this one.
17
+ - `references/` holds the workflow stages (chained by frontmatter `nextStepFile`), the invocation contract and `tier-rules.md`, the tier table `report.md` passes to the emitter's banner.
18
+ - Shared Python helpers do the deterministic work, with no prose fallback: each step's frontmatter `*ProbeOrder` array lists a helper's installed path (`{project-root}/_bmad/skf/shared/scripts/<name>`) then its dev path (`{project-root}/src/shared/scripts/<name>`). Run the first that exists; if none does, halt through the On Activation halt contract with phase `step <N>:helper-missing`.
17
19
  - `{skill-root}` resolves to this skill's installed directory (where `customize.toml` lives, if present).
18
20
  - `{project-root}`-prefixed paths resolve from the project working directory.
19
21
  - `{skill-name}` resolves to the skill directory's basename.
20
22
 
21
23
  ## Role
22
24
 
23
- You are a system executor performing environment resolution. Run each step in sequence, write configuration files, and report results at completion.
25
+ You are a system executor: run each step in sequence, write the configuration files, and report at completion.
24
26
 
25
27
  ## Workflow Rules
26
28
 
27
29
  - Only load one step file at a time — never preload future steps.
28
30
  - Communicate in `{communication_language}`.
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.
31
+ - If `{orphan_action}` is non-null or `{quiet_mode}` is true, resolve the step 3 orphan-removal gate without prompting; step 3 records the decision for the envelope's `warnings`.
32
+ - When `{quiet_mode}` is true, the one line setup displays is the `SKF_SETUP_RESULT_JSON: {…}` envelope that step 4 builds, or the blocked envelope from a halt. In a standalone run that line is the run's final message, so it is all `claude -p` prints (step 4 holds it until then).
33
+ - When `{quiet_mode}` is true, write no assistant text at all between tool calls: no status, progress or step-transition notes, however brief.
34
+ - When `{pipeline_mode}` is true, the forger runs setup as one step of a pipeline: display the same envelope line, then return control to the forger, which keeps chaining. The final-message rule above is for a standalone run.
30
35
 
31
36
  ## Stages
32
37
 
@@ -41,38 +46,33 @@ You are a system executor performing environment resolution. Run each step in se
41
46
 
42
47
  ## Invocation Contract
43
48
 
44
- | Aspect | Detail |
45
- |--------|--------|
46
- | **Inputs** | (none) |
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) |
48
- | **Gates** | One optional: orphaned QMD collection removal (step 3, Deep tier only; default: Keep, or whatever `--orphan-action` set) |
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 |
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. |
49
+ `references/invocation-contract.md` holds the flags, gate, outputs, `SKF_SETUP_RESULT_JSON` contract and failure modes for pipeline authors; no run loads it.
52
50
 
53
51
  ## On Activation
54
52
 
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.
53
+ > **Halt contract.** Every halt in this workflow that names a phase, below and in the step files, follows this contract; the step 4 tier-miss halt names none and displays its `tier_failure` envelope instead. When `{quiet_mode}` is true, run `<runner> <helper> emit-blocked --phase '<phase>' --reason '<reason>' --path "<path>"` (`<path>` written with `/`; no `--path` for a halt without one): `<helper>` is `skf-emit-result-envelope.py`, at its installed path, else its dev path (Conventions), and `<runner>` is `uv run` in the step files. The halts in this list can fire before `uv` is proven present, so for them try the runners `uv run`, `python3`, `python` and `py -3` in that order and stop at the first that exits 0 and prints a line starting `SKF_SETUP_RESULT_JSON:` (the helper is stdlib-only). Display that line verbatim, nothing else. If neither path exists, or no runner exits 0 and prints that line, display the reason alone as that one line. Interactive runs display the reason as their diagnostic and emit no envelope. Text from the user or a helper never goes into the single-quoted `--reason`: write `<message>` there and pass the text on stdin with `--stderr-from -` and a quoted `<<'SKF_TEXT'` heredoc, one per runner tried, for the emitter to fold to one line and escape. Once item 6 binds `{customization_resolver_unavailable}` to a reason, every halt also passes `--customization-resolver-unavailable "<reason>"`, escaping each `\`, `"`, `$` and backtick in it with a backslash. `--headless` and `--quiet` are known at every halt, `headless_mode` from `preferences.yaml` only after item 5.
56
54
 
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`).
55
+ 1. **Parse invocation flags first**, so every halt below knows whether to emit: `{headless_mode}` (true on `--headless` / `-H`, and when `{pipeline_mode}` is true), `{quiet_mode}` (true on `--quiet`, the alias of `--headless`, and whenever `{headless_mode}` is true), `{require_tier}` and `{orphan_action}` (each flag's raw value exactly as given, after `=` or a space; an empty string when nothing, or another flag, follows it; null only when the flag is absent), `{ccc_skip_index}` (true on `--ccc-skip-index`, false otherwise). `{pipeline_mode}` is not a flag: it is true only when the forger invokes setup as one step of a pipeline and passes it, and false otherwise, including every direct `/skf-setup` run. Step 1's detector rejects a `{require_tier}` that names no tier. When `{orphan_action}` is non-null and neither `keep` nor `remove`, halt with phase `on-activation:orphan-action-invalid`, no `path`, and reason `Setup cannot proceed: --orphan-action takes keep or remove, not <message>. Re-run with --orphan-action=keep or --orphan-action=remove.`, where `<message>` is the value as given, or `an empty value` when there is none, passed as the halt contract says.
58
56
 
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:
57
+ 2. **Check for the config.** If `{project-root}/_bmad/skf/config.yaml` does not exist, halt with phase `on-activation:config-missing`, `path` `{project-root}/_bmad/skf/config.yaml`, and reason `Setup cannot proceed: the SKF config file was not found. SKF is not installed in this project: from the project root, run npx bmad-module-skill-forge install (or npx bmad-method install and add SKF), then re-run /skf-setup.`
60
58
 
61
- "**Setup cannot proceed: `uv` is not installed.** SKF helpers depend on `uv` to auto-resolve their Python dependencies. Install it from <https://docs.astral.sh/uv/getting-started/installation/> and re-run `/skf-setup`."
59
+ 3. **Probe `uv` runtime.** Run `uv --version`. If `uv` is missing, halt with phase `on-activation:uv-missing`, no `path`, and reason `Setup cannot proceed: uv is not installed. Install it from https://docs.astral.sh/uv/getting-started/installation/ and re-run /skf-setup.`
62
60
 
63
- 3. **Load config** from `{project-root}/_bmad/skf/config.yaml` and resolve `project_name`, `output_folder`, `user_name`, `communication_language`, `document_output_language`, `skills_output_folder`, `forge_data_folder`, `sidecar_path`. Halt with phase `on-activation:config-missing` if the file does not exist, or `on-activation:config-malformed` if the YAML is invalid, with the matching human diagnostic.
61
+ 4. **Load config and preferences.** `<preflight>` is `skf-preflight.py`, at its installed path, else its dev path (neither: halt with phase `on-activation:helper-missing`, `path` the installed one, reason `Setup cannot proceed: skf-preflight.py was not found. Reinstall SKF, then re-run /skf-setup.`). Run `uv run "<preflight>" "{project-root}" --allow-missing-sidecar`, which parses config.yaml and `preferences.yaml` into one JSON object:
64
62
 
65
- 4. **Reconcile `{headless_mode}`** with `preferences.yaml`: OR the parsed flag with `headless_mode: true` from the YAML.
63
+ - `status` `ok`: bind `{project_name}`, `{user_name}`, `{communication_language}` and `{document_output_language}` from `config`, each folder from its absolute path (`{output_folder}`, `{skills_output_folder}`, `{forge_data_folder}` and `{sidecar_path}` from `config.<name>_resolved`), and `{tier_override}` from `sidecar.preferences.tier_override` (null when absent). With `sidecar.preferences_error`, go on with the defaults and, unless `{quiet_mode}` is true, display that error in one line.
64
+ - `code` `CONFIG_MISSING`: the item 2 halt. Any other `hard-halt`, or no JSON `status`: halt with phase `on-activation:config-malformed`, `path` `{project-root}/_bmad/skf/config.yaml`, and reason `Setup cannot proceed: <message>`: its `error` without the leading `Cannot initialize. ` (else the first line the call printed), passed as the halt contract says, never preflight's whole JSON.
66
65
 
67
- 5. **Resolve workflow customization.** Run:
66
+ 5. **Reconcile `{headless_mode}`**: OR it with `derived.headless_mode` (`headless_mode: true` in `preferences.yaml`). Then, if `{headless_mode}` is true, set `{quiet_mode}` to true.
67
+
68
+ 6. **Resolve workflow customization.** Run:
68
69
 
69
70
  ```bash
70
- python3 {project-root}/_bmad/scripts/resolve_customization.py \
71
- --skill {skill-root} --key workflow
71
+ uv run {project-root}/_bmad/scripts/resolve_customization.py --skill {skill-root} --project-root {project-root} --key workflow
72
72
  ```
73
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.
74
+ It merges the bundled `{skill-root}/customize.toml` with `{project-root}/_bmad/custom/skf-setup.toml` (team overrides, committed) and `.user.toml` (personal overrides, gitignored). When it exits non-zero, prints no JSON or is missing, print one line, `[activation/warn] customization_resolver_unavailable: <reason>` (`<reason>`: its first stderr line, `not found` when the script is missing, `no JSON` when it printed none). If the resolver cannot run, read `{skill-root}/customize.toml` alone and use its bundled defaults: the `{project-root}/_bmad/custom/` overrides do not apply to this run. Bind `{customization_resolver_unavailable}` ← that reason (null when the resolver ran), which `references/report.md` and every later halt hand to the emitter; print the warning only when `{quiet_mode}` is false.
75
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.
76
+ Apply the resolved values, with no narration of your own when `{quiet_mode}` is true: run each `workflow.activation_steps_prepend` entry now; treat each `workflow.persistent_facts` entry as standing context (a `file:` entry loads its file or glob contents); bind `{onCompleteCommand}` ← `workflow.on_complete` (empty: no hook) for `references/report.md` §5; then run each `workflow.activation_steps_append` entry, before the first stage.
77
77
 
78
- 6. Execute `references/detect-and-tier.md`.
78
+ 7. Execute `references/detect-and-tier.md`.
@@ -1,8 +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
+ # Team overrides: {project-root}/_bmad/custom/skf-setup.toml
5
+ # Personal overrides: {project-root}/_bmad/custom/skf-setup.user.toml
6
6
 
7
7
  [workflow]
8
8
 
@@ -10,9 +10,12 @@
10
10
  # scalars: override wins • arrays (persistent_facts, activation_steps_*): append
11
11
  # arrays-of-tables with `code`/`id`: replace matching items, append new ones.
12
12
 
13
- # Steps to run before the standard activation (uv probe, config load).
14
- # Overrides append. Use for org-wide pre-flight checks (auth, network,
15
- # compliance) that must precede any forge setup work.
13
+ # Steps run at activation after flag parsing, the config check, the uv
14
+ # probe and the config load (SKILL.md On Activation items 1-5), and
15
+ # before tool detection (step 1). Overrides append. Use for org-wide
16
+ # pre-flight checks (auth, network, compliance) that must precede any
17
+ # forge setup work. A prepend step cannot install uv or create
18
+ # config.yaml: those halts fire before it runs.
16
19
 
17
20
  activation_steps_prepend = []
18
21
 
@@ -34,10 +37,11 @@ activation_steps_append = []
34
37
 
35
38
  persistent_facts = []
36
39
 
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.
40
+ # An instruction the agent carries out (not a shell command run with
41
+ # --result-path, as in most other SKF workflows) once the forge is fully
42
+ # configured: after the status report (step 4), before chaining to the
43
+ # shared health check, and never on a required-tier miss. Override wins. Use
44
+ # for a post-setup action an org wants, such as notifying an onboarding
45
+ # channel. Empty string = no terminal hook.
42
46
 
43
47
  on_complete = ""
@@ -2,125 +2,112 @@
2
2
  nextStepFile: 'report.md'
3
3
  # `{qmdClassifyHelper}` and `{forgeTierRwHelper}` = first existing path in
4
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.
5
+ # actually invokes.
7
6
  qmdClassifyProbeOrder:
8
7
  - '{project-root}/_bmad/skf/shared/scripts/skf-qmd-classify-collections.py'
9
8
  - '{project-root}/src/shared/scripts/skf-qmd-classify-collections.py'
10
9
  forgeTierRwProbeOrder:
11
10
  - '{project-root}/_bmad/skf/shared/scripts/skf-forge-tier-rw.py'
12
11
  - '{project-root}/src/shared/scripts/skf-forge-tier-rw.py'
12
+ # `{emitEnvelopeHelper}` = first existing path in `{emitEnvelopeProbeOrder}`,
13
+ # for the blocked envelope a halt in this step emits under headless or quiet.
14
+ emitEnvelopeProbeOrder:
15
+ - '{project-root}/_bmad/skf/shared/scripts/skf-emit-result-envelope.py'
16
+ - '{project-root}/src/shared/scripts/skf-emit-result-envelope.py'
13
17
  ---
14
18
 
15
- <!-- Config: communicate in {communication_language}. The orphan-removal prompt and the headless-resolution log message render in the user's language. -->
19
+ <!-- Config: communicate in {communication_language}. The orphan-removal prompt and the `--orphan-action` auto-decision message (interactive runs only) render in the user's language. -->
16
20
 
17
21
  # Step 3: QMD + CCC Registry Hygiene
18
22
 
19
23
  ## STEP GOAL:
20
24
 
21
- When the detected tier is Deep, classify live QMD collections against the `qmd_collections` registry and prompt the user before removing orphans. Whenever ccc is available (Forge+ or Deep), prune `ccc_index_registry` entries whose source paths no longer exist. All set-arithmetic and YAML mutation goes through scripts (`{qmdClassifyHelper}` and `{forgeTierRwHelper}`); the workflow only orchestrates external CLI calls (`qmd collection list`, `qmd collection remove`) and the user-prompt branch.
22
-
23
- For Quick and Forge tiers, skip silently and proceed (QMD is not available; ccc registry cleanup only runs when ccc is available regardless of tier).
25
+ Reconcile the `qmd_collections` registry with the live QMD collections, and prune `ccc_index_registry` entries whose source paths no longer exist. Classification, removal and every registry edit go through `{qmdClassifyHelper}` and `{forgeTierRwHelper}`, which stage their results in the run folder for step 4; this step runs them and makes the orphan-removal decision.
24
26
 
25
27
  ## Rules
26
28
 
27
- - Focus only on registry hygiene — no new collection creation (that belongs to create-skill)
28
- - Never reimplement the forge-namespace suffix filter in prose — the classifier owns it
29
- - Never silently delete collections — always prompt before `qmd collection remove`
30
- - Headless runs must auto-resolve the orphan prompt to the documented default (Keep)
31
- - Do not fail the workflow if hygiene encounters errors
29
+ - Never reimplement the forge-namespace suffix filter or the path check in prose: the classifier owns them
30
+ - Run `qmd collection remove`, through the classifier's `remove-orphans`, only on the interactive **[R]** or an explicit `--orphan-action=remove`: that flag is the consent, and `{orphan_auto_resolution}` records the decision, so the envelope's `warnings` name each collection the removal deleted
31
+ - Display messages only when `{quiet_mode}` is false; the one exception is the envelope line a halt displays
32
+ - When `{quiet_mode}` is true, write no assistant text at all between tool calls: no status, progress or step-transition notes, however brief
33
+ - If a section needs a helper whose probe order has no existing path, halt (an install fault, not a hygiene error) with phase `step 3:helper-missing`, `path` set to that array's first entry, and reason `Setup cannot proceed: <script file name> was not found. Reinstall SKF, then re-run /skf-setup.`
34
+ - Every halt follows the SKILL.md halt contract: when `{quiet_mode}` is true, run `uv run {emitEnvelopeHelper} emit-blocked --phase '<phase>' --reason '<reason>' --path "<path>"`, which builds the payload itself (no `--path` for a halt without one, `--stderr-from` where the halt names it, and `--customization-resolver-unavailable "<reason>"` once SKILL.md On Activation item 6 bound `{customization_resolver_unavailable}` to a reason, escaped as the halt contract says), and display its stdout line verbatim and nothing else (the reason alone if `{emitEnvelopeHelper}` resolves to no path, or the helper exits non-zero or prints no line); otherwise display the reason
35
+ - Do not fail the workflow if hygiene encounters errors; a missing helper is not a hygiene error
32
36
 
33
37
  ## MANDATORY SEQUENCE
34
38
 
35
39
  ### 1. Check Tier
36
40
 
37
- Read `{calculated_tier}` and `{ccc}` from context (set by step 1).
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
-
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.
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.
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.
46
-
47
- **If `{calculated_tier}` is Deep:** Continue to section 2.
41
+ Set `{orphan_auto_resolution}` to null. When `{require_tier_satisfied}` (from step 1) is `false`, go to section 5 with no output: a run that ends `tier_failure` runs no hygiene, so it removes no collection and prunes no registry entry, and step 4 reports none. Otherwise the QMD sections (2 and 3) run only when `{calculated_tier}` is Deep, and section 4 runs when `{calculated_tier}` is Deep or `{ccc}` is true. Go to section 2 at Deep tier, else to section 4 when `{ccc}` is true, else to section 5 with no output.
48
42
 
49
43
  ### 2. Classify Live QMD Collections vs Registry
50
44
 
51
- Run the classifier — it owns the `qmd collection list` invocation and stdout parsing. Invoke via `uv run`:
52
-
53
45
  ```bash
54
- uv run {qmdClassifyHelper} \
55
- --registry-from-yaml "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml"
46
+ uv run {qmdClassifyHelper} classify \
47
+ --registry-from-yaml "{sidecar_path}/forge-tier.yaml" \
48
+ --project-root "{project-root}" \
49
+ > "{run_dir}/qmd-classify.json" && cat "{run_dir}/qmd-classify.json"
56
50
  ```
57
51
 
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.
52
+ **If the script exits non-zero** (qmd is not running), go to section 4: step 4 reports QMD hygiene as skipped.
59
53
 
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).
54
+ Otherwise bind `{orphaned_collections}` ← `orphaned` and `{orphan_paths}` ← `orphaned_paths`.
61
55
 
62
- **Parse the JSON output and set context flags:**
63
-
64
- - `{hygiene_healthy}` ← `len(healthy)`
65
- - `{orphaned_collections}` ← `orphaned` (the list — used in section 3)
66
- - `{stale_collections}` ← `stale` (the list — used in section 4)
67
- - `{live_collections}` ← comma-join of `live_names` (used in §4's clean-stale invocation; the script owns the raw set, the prompt only forwards it)
68
- - `{foreign_filtered_count}` ← `foreign_filtered_count`
56
+ ### 3. Handle Orphaned Collections
69
57
 
70
- Set `{hygiene_result: "completed"}`.
58
+ **If `{orphaned_collections}` is empty:** go to section 4.
71
59
 
72
- ### 3. Handle Orphaned Collections
60
+ **Non-interactive resolution.** Resolve the gate without prompting in the first case that applies:
73
61
 
74
- **If `{orphaned_collections}` is empty:** Set `{hygiene_orphaned_removed: 0, hygiene_orphaned_kept: 0}` and skip to section 4.
62
+ - `{orphan_action}` is non-null (activation lets through only `keep` or `remove`) → act on that value, with `source: "orphan-action-flag"`
63
+ - `{quiet_mode}` is true → the default **Keep**, with `source: "headless-default"` when `{headless_mode}` is true and `source: "quiet-default"` otherwise (a `--quiet` run)
75
64
 
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.)
65
+ Set `{orphan_auto_resolution: {action: <keep|remove>, source: <as above>}}`: step 4 adds the count and, after a removal, the collections removed and not removed, so the envelope's `warnings` audit a decision nobody confirmed. On keep, unless `{quiet_mode}` is true, display `"Auto-decision (--orphan-action=keep): kept {count} orphaned forge collection(s)"`, where `{count}` is the number of names in `{orphaned_collections}`, then go to section 4. On remove, run the removal below, still with no prompt.
77
66
 
78
- **If `{headless_mode}` is false**, display to the user:
67
+ **GATE [default: K]**: reached only when none of the cases above applies, so `{orphan_action}` is null and `{quiet_mode}` is false. Display to the user:
79
68
 
80
- "**QMD Hygiene: Found {count} orphaned collection(s) not tracked in the forge registry:**
69
+ "**QMD Hygiene: Found {count} orphaned collection(s) in this project that its forge registry does not list:**
81
70
 
82
- {list orphaned collection names}
71
+ {each name in `{orphaned_collections}`, with its path from `{orphan_paths}`}
83
72
 
84
- These collections exist in QMD but are not managed by any skill workflow. They may be from a previous auto-index run or manual creation.
73
+ QMD indexes them from folders inside this project, but no skill workflow recorded them: a run may have stopped before it registered one, or one was added by hand.
85
74
 
86
75
  **[R]emove** orphaned collections — clean up QMD
87
76
  **[K]eep** orphaned collections — leave them as-is (default)"
88
77
 
89
- **If user selects R (Remove):** For each name in `{orphaned_collections}`:
78
+ On the user's **[K]**, go to section 4.
79
+
80
+ **On remove** (the user's **[R]**, or the non-interactive remove):
90
81
 
91
82
  ```bash
92
- qmd collection remove <name>
83
+ uv run {qmdClassifyHelper} remove-orphans \
84
+ --classification-from "{run_dir}/qmd-classify.json" \
85
+ --project-root "{project-root}" \
86
+ > "{run_dir}/qmd-remove.json" && cat "{run_dir}/qmd-remove.json"
93
87
  ```
94
88
 
95
- Track the count of successful removals as `{hygiene_orphaned_removed}`. Set `{hygiene_orphaned_kept: 0}`.
96
-
97
- **If user selects K (Keep) or no orphans:** Set `{hygiene_orphaned_removed: 0, hygiene_orphaned_kept: len(orphaned_collections)}`.
89
+ It reads each orphan's path again just before removing it, removes only a collection that still lies inside this project, and lists the names under `removed` and `failed` (each failure's reason under `errors`). After a non-interactive remove, unless `{quiet_mode}` is true, display `"Auto-decision (--orphan-action=remove): removed {removed, comma-separated}"`, followed by `"; could not remove: {failed, comma-separated}"` when `failed` is non-empty.
98
90
 
99
91
  ### 4. Stale Registry Cleanup
100
92
 
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.
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.
93
+ Pass `--prune-missing-ccc-paths` when `{ccc}` is true, and leave it out otherwise:
104
94
 
105
95
  ```bash
106
96
  uv run {forgeTierRwHelper} clean-stale \
107
- --target "{project-root}/_bmad/_memory/forger-sidecar/forge-tier.yaml" \
108
- [--qmd-live-names "{live_collections}"] \
109
- [--prune-missing-ccc-paths]
97
+ --target "{sidecar_path}/forge-tier.yaml" \
98
+ --qmd-live-from "{run_dir}/qmd-classify.json" \
99
+ [--prune-missing-ccc-paths] \
100
+ > "{run_dir}/clean-stale.json" && cat "{run_dir}/clean-stale.json"
110
101
  ```
111
102
 
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.
113
-
114
- **Parse the JSON output and set context flags for step 4:**
103
+ The script removes the `qmd_collections` entries no live collection matches, reading the live names from section 2's output; with no such output (below Deep tier, or a classifier that failed) it leaves `qmd_collections` alone.
115
104
 
116
- - `{hygiene_stale_cleaned}` ← `len(qmd_removed)`
117
- - `{ccc_registry_stale_cleaned}` ← `len(ccc_removed)`
118
- - `{ccc_registry_stale_removed_paths}` ← `ccc_removed` (the list — step 4 folds individual paths into envelope warnings)
105
+ **If the script exits non-zero:** the registry stays as it was. Unless `{quiet_mode}` is true, display one line: "Registry cleanup skipped: {message}.", where `{message}` is the `message` of the stderr JSON `{"status":"error","message":...}`. Then continue to section 5: hygiene errors never fail the workflow.
119
106
 
120
- If `{hygiene_stale_cleaned}` > 0, display: "**Cleaned {hygiene_stale_cleaned} stale QMD registry entry/entries** (collection no longer exists in QMD)."
107
+ **Otherwise**, when `qmd_removed` is non-empty and `{quiet_mode}` is false, display: "**Cleaned {n} stale QMD registry entry/entries** (collection no longer exists in QMD).", where `{n}` is the number of names in `qmd_removed`.
121
108
 
122
- (The script already logs each ccc removal as a WARNING line; no additional display needed.)
109
+ Removed ccc registry paths are not displayed here: the step 4 report's CCC Registry line counts them, and the envelope's `warnings` lists each one.
123
110
 
124
111
  ### 5. Auto-Proceed
125
112
 
126
- After hygiene completes (or is skipped for non-Deep tiers without ccc), display "**Proceeding to forge status report...**", then load `{nextStepFile}`, read it fully, and execute it.
113
+ Load `{nextStepFile}`, read it fully, and execute it.