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,29 +1,40 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = ["pyyaml"]
4
4
  # ///
5
5
  """SKF QMD Classify Collections — Set arithmetic over QMD collection names.
6
6
 
7
7
  Replaces the prose-driven classification logic in `src/skf-setup/references/
8
- auto-index.md` §3 with one Python invocation. Compares the live
8
+ auto-index.md` §2 with one Python invocation. Compares the live
9
9
  QMD collections (from `qmd collection list`) against the forge registry
10
10
  (`qmd_collections` array in forge-tier.yaml) and classifies each name as
11
11
  Healthy / Orphaned / Stale, applying the forge-namespace suffix filter
12
12
  added in PR #244 to silently exclude collections owned by unrelated
13
13
  tools sharing the QMD daemon.
14
14
 
15
- Classification rules (per step 3 §3):
15
+ Classification rules (per step 3 §2):
16
16
 
17
- Healthy — name in {forge-suffix-matched live} AND in registry.
17
+ Healthy: name in {forge-suffix-matched live} AND in registry.
18
18
  No action needed.
19
- Orphaned — name in {forge-suffix-matched live} but NOT in registry.
20
- Flagged for user-prompted removal in step 3 §4.
21
- Stale — name in registry but NOT in {all live}. Registry entry
19
+ Orphaned: name in {forge-suffix-matched live}, NOT in registry, and
20
+ `qmd collection show <name>` reports a Path inside
21
+ --project-root. Offered for removal in step 3 §3.
22
+ Stale: name in registry but NOT in {all live}. Registry entry
22
23
  should be removed.
23
- Foreign — name in live but does NOT match a forge suffix. Silently
24
- excluded from every classification — never displayed,
25
- never proposed for removal. Reported as a count for
26
- telemetry only.
24
+ Foreign: name in live that does NOT match a forge suffix, or a
25
+ forge-suffixed name missing from the registry whose Path
26
+ lies outside --project-root (or that qmd cannot show).
27
+ Silently excluded from every classification: never
28
+ displayed, never proposed for removal. Reported as a count
29
+ for telemetry only.
30
+
31
+ QMD's index is shared by every project on the machine, and every SKF
32
+ project names its collections with the same suffixes, so a suffix alone
33
+ cannot tell this project's orphan from another project's healthy
34
+ collection. SKF adds each collection from a folder inside its project
35
+ (the forge data folder, the skills folder or `_bmad-output/`), so the
36
+ collection's Path names its owner. A Path that resolves outside the
37
+ project root, or none at all, never makes a collection removable here.
27
38
 
28
39
  Forge suffixes (the only suffixes a forge-managed collection can have,
29
40
  set by producers `skf-brief-skill` and `skf-create-skill` per
@@ -31,33 +42,66 @@ src/knowledge/qmd-registry.md § Collection Types):
31
42
 
32
43
  -brief, -temporal, -docs, -extraction
33
44
 
34
- Inputs:
45
+ Subcommands:
35
46
 
36
- --live-names Comma-separated list of collection names currently
37
- in QMD (caller obtains this from `qmd collection list`
38
- before invoking the script). Empty string → no live
39
- collections, which is a valid first-run state.
47
+ classify Classify the live collections against the registry.
40
48
 
41
- --registry-from-yaml <path>
49
+ --registry-from-yaml <path>
42
50
  Path to forge-tier.yaml. The script reads the file's
43
51
  `qmd_collections` array and extracts the `name` field
44
52
  from each entry. Missing file or missing array → empty
45
53
  registry, which is a valid first-run state.
46
54
 
47
- Output (single JSON document on stdout):
55
+ --project-root <path>
56
+ The project whose orphans may be offered for removal.
57
+
58
+ --live-names Comma-separated list of collection names currently
59
+ in QMD. If omitted, the script invokes `qmd collection
60
+ list` itself. Empty string → no live collections, which
61
+ is a valid first-run state.
62
+
63
+ remove-orphans Remove the orphans a classification found.
64
+
65
+ --classification-from <path>
66
+ The classify output step 3 staged (`qmd-classify.json`);
67
+ its `orphaned` list names the collections to remove.
68
+
69
+ --project-root <path>
70
+ Each collection's Path is read again with `qmd
71
+ collection show` just before `qmd collection remove`
72
+ runs, and a collection whose Path no longer lies inside
73
+ this root is not removed (it is listed under `failed`).
74
+
75
+ Output of classify (single JSON document on stdout):
48
76
 
49
77
  {
50
78
  "status": "ok",
51
79
  "version": "v1",
80
+ "live_names": ["bar-extraction", "foo-brief", "memory-root-1"],
52
81
  "healthy": ["foo-brief", "foo-extraction"],
53
82
  "orphaned": ["bar-extraction"],
83
+ "orphaned_paths": {"bar-extraction": "/abs/project/skills/bar"},
54
84
  "stale": ["baz-docs"],
55
85
  "foreign_filtered_count": 4,
56
86
  "foreign_filtered_sample": ["memory-root-1", "sessions-2"]
57
87
  }
58
88
 
89
+ Output of remove-orphans (single JSON document on stdout):
90
+
91
+ {
92
+ "status": "ok",
93
+ "version": "v1",
94
+ "removed": ["bar-extraction"],
95
+ "failed": ["old-docs"],
96
+ "errors": {"old-docs": "its Path no longer lies inside the project root"}
97
+ }
98
+
99
+ A collection that could not be removed is a hygiene note, not an error:
100
+ remove-orphans exits 0 whatever it removed.
101
+
59
102
  `foreign_filtered_sample` is capped at 5 names (telemetry; the full list
60
- is never useful — if it were forge-relevant it would have a forge suffix).
103
+ is never useful: if it were forge-relevant it would have a forge suffix
104
+ and a Path inside the project).
61
105
 
62
106
  CLI — invoke via `uv run` so the PEP 723 PyYAML dependency declared
63
107
  above is auto-resolved on first call and cached. `docs/getting-started.md`
@@ -65,14 +109,15 @@ documents uv as the runtime prerequisite for exactly this. Bare
65
109
  `python3` will fail with `ModuleNotFoundError: No module named 'yaml'`
66
110
  on a fresh interpreter:
67
111
 
68
- uv run skf-qmd-classify-collections.py \\
69
- --live-names foo-brief,foo-extraction,memory-root-1 \\
70
- --registry-from-yaml /path/forge-tier.yaml
112
+ uv run skf-qmd-classify-collections.py classify \\
113
+ --registry-from-yaml /path/forge-tier.yaml --project-root /path/project
114
+ uv run skf-qmd-classify-collections.py remove-orphans \\
115
+ --classification-from /run/qmd-classify.json --project-root /path/project
71
116
 
72
117
  Exit codes:
73
118
  0 success
74
- 1 user error (bad args, malformed registry file)
75
- 2 internal error
119
+ 1 user error (bad args, malformed registry or classification file)
120
+ 2 internal error (qmd collection list failed)
76
121
  """
77
122
 
78
123
  from __future__ import annotations
@@ -163,11 +208,36 @@ def load_registry_names(path: Path) -> list[str]:
163
208
  return names
164
209
 
165
210
 
166
- def classify(live: list[str], registry: list[str]) -> dict:
211
+ def is_inside(path: str | None, root) -> bool:
212
+ """True when `path` resolves to `root` or a folder below it.
213
+
214
+ Both sides go through realpath and normcase, so a symlinked project
215
+ root or a Windows drive letter in another case still matches. A path
216
+ on another drive, or none at all, is never inside.
217
+ """
218
+ if not path or not root:
219
+ return False
220
+ try:
221
+ target = os.path.normcase(os.path.realpath(path))
222
+ base = os.path.normcase(os.path.realpath(str(root)))
223
+ return os.path.commonpath([target, base]) == base
224
+ except (OSError, ValueError):
225
+ return False
226
+
227
+
228
+ def classify(live: list[str], registry: list[str], project_root=None,
229
+ paths: dict | None = None) -> dict:
167
230
  """Pure function: classify live vs registry into healthy/orphaned/stale/foreign.
168
231
 
232
+ With `project_root`, a forge-suffixed live name missing from the
233
+ registry is orphaned only when `paths` maps it to a Path inside that
234
+ root; the rest count as foreign. Without it every such name is
235
+ orphaned: only a caller that already knows they all belong to this
236
+ project may leave it out (the CLI always passes it).
237
+
169
238
  Returns the classification payload (without status/version envelope).
170
239
  """
240
+ paths = paths or {}
171
241
  forge_live = [n for n in live if is_forge_owned(n)]
172
242
  foreign_live = [n for n in live if not is_forge_owned(n)]
173
243
 
@@ -176,7 +246,14 @@ def classify(live: list[str], registry: list[str]) -> dict:
176
246
  all_live_set = set(live)
177
247
 
178
248
  healthy = sorted(forge_live_set & registry_set)
179
- orphaned = sorted(forge_live_set - registry_set)
249
+ candidates = sorted(forge_live_set - registry_set)
250
+ if project_root is None:
251
+ orphaned = candidates
252
+ else:
253
+ orphaned = [n for n in candidates if is_inside(paths.get(n), project_root)]
254
+ # Another project's collection, or one qmd could not show, is no
255
+ # orphan of this one: it counts the way a name with no forge suffix does.
256
+ foreign_live += [n for n in candidates if n not in orphaned]
180
257
  # Stale uses the full live set, NOT just the forge-filtered set, so a
181
258
  # registry entry whose name happens to match a non-forge live collection
182
259
  # would still count as stale. Registry entries always have forge suffixes
@@ -187,6 +264,7 @@ def classify(live: list[str], registry: list[str]) -> dict:
187
264
  "live_names": sorted(all_live_set),
188
265
  "healthy": healthy,
189
266
  "orphaned": orphaned,
267
+ "orphaned_paths": {n: paths[n] for n in orphaned if paths.get(n)},
190
268
  "stale": stale,
191
269
  "foreign_filtered_count": len(foreign_live),
192
270
  "foreign_filtered_sample": foreign_live[:FOREIGN_SAMPLE_CAP],
@@ -231,7 +309,10 @@ def _resolve_outside_cwd(command: str) -> str | None:
231
309
  into CWD would execute a repo-planted shim (e.g. qmd.cmd). Such a
232
310
  resolution is treated as not-found. Explicit paths supplied by callers
233
311
  (containing a separator) are honored as-is. Keep identical to the
234
- sibling guard in skf-detect-tools.py.
312
+ sibling guards in skf-detect-tools.py,
313
+ skf-merge-ccc-exclusions.py, skf-ccc-git-hygiene.py,
314
+ skf-source-tree.py, skf-tessl-review.py and
315
+ skf-verify-provenance-completeness.py.
235
316
  """
236
317
  resolved = shutil.which(command)
237
318
  if resolved is None:
@@ -246,25 +327,19 @@ def _resolve_outside_cwd(command: str) -> str | None:
246
327
  return resolved
247
328
 
248
329
 
249
- def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
250
- """Invoke `qmd collection list` and return (names, error).
251
-
252
- Owns the CLI parsing so callers (and prompts) don't reinvent it.
253
- Empty stdout / non-zero exit → ([], error_message). The classifier's
254
- same-process invocation here is the single source of truth for what
255
- counts as a "live collection name".
256
- """
330
+ def _run_qmd(*args: str) -> tuple[int | None, str, str]:
331
+ """Run `qmd <args>`; return (exit code, stdout, stderr), exit code None when it could not run."""
257
332
  import subprocess
258
333
  # Resolve before spawning: on Windows qmd ships from npm as a .CMD
259
334
  # shim, which a bare-name subprocess.run cannot launch (WinError 2).
260
335
  qmd = _resolve_outside_cwd("qmd")
261
336
  if qmd is None:
262
- return [], "qmd not found on PATH"
337
+ return None, "", "qmd not found on PATH"
263
338
  try:
264
339
  # qmd emits UTF-8; a locale-default decode (cp1252 on Windows)
265
340
  # mojibakes names or raises UnicodeDecodeError on unmapped bytes.
266
341
  result = subprocess.run(
267
- [qmd, "collection", "list"],
342
+ [qmd, *args],
268
343
  capture_output=True,
269
344
  encoding="utf-8",
270
345
  errors="replace",
@@ -272,33 +347,124 @@ def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
272
347
  check=False,
273
348
  )
274
349
  except (FileNotFoundError, subprocess.TimeoutExpired) as e:
275
- return [], f"qmd collection list failed: {e}"
276
- if result.returncode != 0:
277
- return [], f"qmd collection list exited {result.returncode}: {result.stderr.strip() or '<no stderr>'}"
278
- return parse_collection_list_output(result.stdout), None
350
+ return None, "", f"qmd {' '.join(args[:2])} failed: {e}"
351
+ return result.returncode, result.stdout or "", result.stderr or ""
352
+
353
+
354
+ def fetch_live_names_from_qmd() -> tuple[list[str], str | None]:
355
+ """Invoke `qmd collection list` and return (names, error).
356
+
357
+ Owns the CLI parsing so callers (and prompts) don't reinvent it.
358
+ Empty stdout / non-zero exit → ([], error_message). The classifier's
359
+ same-process invocation here is the single source of truth for what
360
+ counts as a "live collection name".
361
+ """
362
+ code, out, err = _run_qmd("collection", "list")
363
+ if code is None:
364
+ return [], err
365
+ if code != 0:
366
+ return [], f"qmd collection list exited {code}: {err.strip() or '<no stderr>'}"
367
+ return parse_collection_list_output(out), None
368
+
369
+
370
+ def parse_collection_path(raw: str) -> str | None:
371
+ """The folder a `qmd collection show` output names on its `Path:` line, or None."""
372
+ for line in raw.splitlines():
373
+ key, sep, value = line.strip().partition(":")
374
+ if sep and key == "Path" and value.strip():
375
+ return value.strip()
376
+ return None
377
+
378
+
379
+ def fetch_collection_path(name: str) -> tuple[str | None, str | None]:
380
+ """Run `qmd collection show <name>`; return (its Path, error)."""
381
+ code, out, err = _run_qmd("collection", "show", name)
382
+ if code is None:
383
+ return None, err
384
+ if code != 0:
385
+ return None, f"qmd collection show exited {code}: {(err or out).strip() or '<no output>'}"
386
+ path = parse_collection_path(out)
387
+ return (path, None) if path else (None, "qmd collection show printed no Path line")
388
+
389
+
390
+ def remove_orphans(names: list[str], project_root) -> dict:
391
+ """Remove each orphan whose Path still lies inside `project_root`.
392
+
393
+ The Path is read again just before each removal, so a collection that
394
+ another project re-created under the same name since the classification
395
+ is left alone. Returns {"removed", "failed", "errors"}.
396
+ """
397
+ removed: list[str] = []
398
+ failed: list[str] = []
399
+ errors: dict[str, str] = {}
400
+ for name in names:
401
+ path, error = fetch_collection_path(name)
402
+ if error is None and not is_inside(path, project_root):
403
+ error = "its Path no longer lies inside the project root"
404
+ if error is None:
405
+ code, out, err = _run_qmd("collection", "remove", name)
406
+ if code is None:
407
+ error = err
408
+ elif code != 0:
409
+ error = f"qmd collection remove exited {code}: {(err or out).strip() or '<no output>'}"
410
+ if error is None:
411
+ removed.append(name)
412
+ else:
413
+ failed.append(name)
414
+ errors[name] = " ".join(error.split())
415
+ return {"removed": removed, "failed": failed, "errors": errors}
416
+
417
+
418
+ def load_orphaned_names(path: Path) -> list[str]:
419
+ """The `orphaned` list of a staged classification, or exit 1 when there is none."""
420
+ try:
421
+ data = json.loads(path.read_text(encoding="utf-8-sig"))
422
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e:
423
+ _die(1, f"remove-orphans: cannot read the classification {path}: {e}")
424
+ orphaned = data.get("orphaned") if isinstance(data, dict) else None
425
+ if not isinstance(orphaned, list) or not all(isinstance(n, str) for n in orphaned):
426
+ _die(1, f"remove-orphans: {path} holds no `orphaned` list of names")
427
+ return orphaned
279
428
 
280
429
 
281
430
  def main() -> None:
282
431
  parser = argparse.ArgumentParser(
283
- description="Classify QMD collections vs forge registry.",
432
+ description="Classify QMD collections vs forge registry, and remove the orphans.",
284
433
  formatter_class=argparse.RawDescriptionHelpFormatter,
285
434
  )
286
- parser.add_argument(
435
+ sub = parser.add_subparsers(dest="cmd", required=True)
436
+ p_classify = sub.add_parser("classify", help="Classify live collections against the registry")
437
+ p_classify.add_argument(
287
438
  "--live-names",
288
439
  default=None,
289
440
  help="Comma-separated list of collection names currently in QMD. "
290
441
  "If omitted, the script invokes `qmd collection list` itself. "
291
442
  "Empty string → no live collections.",
292
443
  )
293
- parser.add_argument(
444
+ p_classify.add_argument(
294
445
  "--registry-from-yaml",
295
446
  type=Path,
296
447
  required=True,
297
448
  help="Path to forge-tier.yaml. Script reads qmd_collections array. "
298
449
  "Missing file → empty registry (first-run state).",
299
450
  )
451
+ p_classify.add_argument(
452
+ "--project-root",
453
+ required=True,
454
+ help="The project whose orphans may be offered for removal: a collection "
455
+ "outside the registry is orphaned only when its Path lies inside it.",
456
+ )
457
+ p_remove = sub.add_parser("remove-orphans",
458
+ help="Remove the orphans a staged classification lists")
459
+ p_remove.add_argument("--classification-from", type=Path, required=True,
460
+ help="The classify output (qmd-classify.json).")
461
+ p_remove.add_argument("--project-root", required=True,
462
+ help="Each Path is checked against this root again before its removal.")
300
463
  args = parser.parse_args()
301
464
 
465
+ if args.cmd == "remove-orphans":
466
+ _ok(remove_orphans(load_orphaned_names(args.classification_from), args.project_root))
467
+ return
302
468
  if args.live_names is None:
303
469
  names, error = fetch_live_names_from_qmd()
304
470
  if error is not None:
@@ -307,8 +473,26 @@ def main() -> None:
307
473
  else:
308
474
  live = parse_live_names(args.live_names)
309
475
  registry = load_registry_names(args.registry_from_yaml)
310
- _ok(classify(live, registry))
476
+ candidates = sorted({n for n in live if is_forge_owned(n)} - set(registry))
477
+ paths = {name: fetch_collection_path(name)[0] for name in candidates}
478
+ _ok(classify(live, registry, args.project_root, paths))
479
+
480
+
481
+ def _force_utf8(*streams) -> None:
482
+ """Reconfigure stdout and stderr to UTF-8, keeping each stream's error handler.
483
+
484
+ A Windows console pipes them as cp1252, which cannot print every character
485
+ of the --help text, so --help would stop with UnicodeEncodeError.
486
+ """
487
+ for stream in streams:
488
+ if hasattr(stream, "reconfigure"):
489
+ errors = getattr(stream, "errors", None)
490
+ if errors is None:
491
+ stream.reconfigure(encoding="utf-8")
492
+ else:
493
+ stream.reconfigure(encoding="utf-8", errors=errors)
311
494
 
312
495
 
313
496
  if __name__ == "__main__":
497
+ _force_utf8(sys.stdout, sys.stderr)
314
498
  main()