bmad-module-skill-forge 2.2.0 → 4.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 (437) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +49 -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 +143 -18
  18. package/docs/verifying-a-skill.md +87 -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 +3 -3
  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 +31 -59
  40. package/src/shared/references/description-guard-protocol.md +12 -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 +233 -64
  64. package/src/shared/scripts/skf-build-change-manifest.py +2753 -28
  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 +772 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +267 -105
  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 +21 -3
  74. package/src/shared/scripts/skf-description-guard.py +99 -11
  75. package/src/shared/scripts/skf-detect-docs.py +694 -2
  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 +197 -55
  79. package/src/shared/scripts/skf-detect-tools.py +209 -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 +5247 -96
  86. package/src/shared/scripts/skf-extraction-inventory.py +1280 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +664 -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 +469 -205
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +700 -0
  94. package/src/shared/scripts/skf-hash-content.py +180 -20
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +161 -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 +143 -20
  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 +176 -28
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +64 -8
  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 +155 -14
  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 +787 -121
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +120 -67
  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 +23 -2
  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 +3945 -152
  138. package/src/shared/scripts/skf-write-skill-brief.py +406 -25
  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 +28 -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/health-check.md +2 -0
  168. package/src/skf-audit-skill/references/init.md +149 -210
  169. package/src/skf-audit-skill/references/re-index.md +103 -110
  170. package/src/skf-audit-skill/references/report.md +96 -33
  171. package/src/skf-audit-skill/references/semantic-diff.md +34 -25
  172. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  173. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  174. package/src/skf-audit-skill/references/structural-diff.md +80 -90
  175. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  176. package/src/skf-audit-skill/scripts/render-drift-tables.py +712 -0
  177. package/src/skf-brief-skill/SKILL.md +37 -26
  178. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  179. package/src/skf-brief-skill/assets/scope-templates.md +18 -25
  180. package/src/skf-brief-skill/assets/skill-brief-schema.md +30 -23
  181. package/src/skf-brief-skill/customize.toml +48 -20
  182. package/src/skf-brief-skill/references/analyze-target.md +144 -127
  183. package/src/skf-brief-skill/references/confirm-brief.md +11 -9
  184. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  185. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  186. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  187. package/src/skf-brief-skill/references/headless-args.md +84 -16
  188. package/src/skf-brief-skill/references/health-check.md +1 -1
  189. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  190. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  191. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  192. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  193. package/src/skf-brief-skill/references/step-auto-brief.md +26 -100
  194. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  195. package/src/skf-brief-skill/references/version-resolution.md +7 -10
  196. package/src/skf-brief-skill/references/write-brief.md +66 -57
  197. package/src/skf-campaign/SKILL.md +31 -34
  198. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  199. package/src/skf-campaign/customize.toml +47 -16
  200. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  201. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  202. package/src/skf-campaign/references/health-check.md +7 -16
  203. package/src/skf-campaign/references/step-01-setup.md +46 -84
  204. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  205. package/src/skf-campaign/references/step-03-pins.md +26 -18
  206. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  207. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  208. package/src/skf-campaign/references/step-06-batch.md +41 -34
  209. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  210. package/src/skf-campaign/references/step-08-verify.md +28 -29
  211. package/src/skf-campaign/references/step-09-refine.md +29 -30
  212. package/src/skf-campaign/references/step-10-export.md +48 -36
  213. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  214. package/src/skf-campaign/references/step-resume.md +37 -68
  215. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  216. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  217. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  218. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  219. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  220. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  221. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  222. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  223. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  224. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  225. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  226. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  227. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  228. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  229. package/src/skf-create-skill/SKILL.md +27 -20
  230. package/src/skf-create-skill/assets/compile-assembly-rules.md +11 -8
  231. package/src/skf-create-skill/assets/skill-sections.md +21 -11
  232. package/src/skf-create-skill/customize.toml +20 -8
  233. package/src/skf-create-skill/references/authoritative-files-protocol.md +36 -58
  234. package/src/skf-create-skill/references/batch-mode.md +81 -0
  235. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  236. package/src/skf-create-skill/references/compile.md +119 -70
  237. package/src/skf-create-skill/references/component-extraction.md +113 -91
  238. package/src/skf-create-skill/references/enrich.md +41 -23
  239. package/src/skf-create-skill/references/entry-points-by-hand.md +37 -0
  240. package/src/skf-create-skill/references/extract.md +204 -162
  241. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  242. package/src/skf-create-skill/references/extraction-patterns-tracing.md +11 -11
  243. package/src/skf-create-skill/references/extraction-patterns.md +588 -201
  244. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  245. package/src/skf-create-skill/references/health-check.md +1 -1
  246. package/src/skf-create-skill/references/load-brief.md +53 -36
  247. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  248. package/src/skf-create-skill/references/report.md +37 -57
  249. package/src/skf-create-skill/references/source-resolution-protocols.md +58 -169
  250. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  251. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  252. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  253. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  254. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  255. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  256. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  257. package/src/skf-create-skill/references/validate.md +173 -99
  258. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  259. package/src/skf-create-stack-skill/SKILL.md +29 -56
  260. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  261. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  262. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  263. package/src/skf-create-stack-skill/customize.toml +23 -12
  264. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  265. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  266. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  267. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  268. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  269. package/src/skf-create-stack-skill/references/init.md +90 -32
  270. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  271. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  272. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  273. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  274. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  275. package/src/skf-create-stack-skill/references/report.md +39 -33
  276. package/src/skf-create-stack-skill/references/validate.md +20 -39
  277. package/src/skf-drop-skill/SKILL.md +28 -51
  278. package/src/skf-drop-skill/customize.toml +44 -30
  279. package/src/skf-drop-skill/references/execute.md +83 -146
  280. package/src/skf-drop-skill/references/health-check.md +2 -0
  281. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  282. package/src/skf-drop-skill/references/report.md +46 -30
  283. package/src/skf-drop-skill/references/select.md +122 -124
  284. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  285. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  286. package/src/skf-export-skill/SKILL.md +40 -62
  287. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  288. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  289. package/src/skf-export-skill/customize.toml +42 -13
  290. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  291. package/src/skf-export-skill/references/health-check.md +2 -0
  292. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  293. package/src/skf-export-skill/references/load-skill.md +88 -62
  294. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  295. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  296. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  297. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  298. package/src/skf-export-skill/references/package.md +15 -73
  299. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  300. package/src/skf-export-skill/references/result-envelope.md +31 -16
  301. package/src/skf-export-skill/references/summary.md +41 -17
  302. package/src/skf-export-skill/references/token-report.md +4 -12
  303. package/src/skf-export-skill/references/update-context.md +118 -237
  304. package/src/skf-forger/SKILL.md +61 -37
  305. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  306. package/src/skf-forger/customize.toml +43 -0
  307. package/src/skf-forger/references/pipeline-mode.md +73 -18
  308. package/src/skf-forger/scripts/forge-status.py +299 -0
  309. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  310. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  311. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  312. package/src/skf-quick-skill/SKILL.md +54 -53
  313. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  314. package/src/skf-quick-skill/customize.toml +51 -20
  315. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  316. package/src/skf-quick-skill/references/compile.md +30 -31
  317. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  318. package/src/skf-quick-skill/references/finalize.md +69 -18
  319. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  320. package/src/skf-quick-skill/references/health-check.md +5 -3
  321. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  322. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  323. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  324. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  325. package/src/skf-refine-architecture/SKILL.md +43 -53
  326. package/src/skf-refine-architecture/customize.toml +49 -20
  327. package/src/skf-refine-architecture/references/compile.md +97 -54
  328. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  329. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  330. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  331. package/src/skf-refine-architecture/references/improvements.md +14 -20
  332. package/src/skf-refine-architecture/references/init.md +98 -57
  333. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  334. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  335. package/src/skf-refine-architecture/references/report.md +65 -54
  336. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  337. package/src/skf-rename-skill/SKILL.md +19 -54
  338. package/src/skf-rename-skill/customize.toml +32 -26
  339. package/src/skf-rename-skill/references/execute.md +230 -184
  340. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  341. package/src/skf-rename-skill/references/health-check.md +1 -7
  342. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  343. package/src/skf-rename-skill/references/report.md +63 -52
  344. package/src/skf-rename-skill/references/select.md +197 -115
  345. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  346. package/src/skf-setup/SKILL.md +26 -26
  347. package/src/skf-setup/customize.toml +14 -10
  348. package/src/skf-setup/references/auto-index.md +50 -63
  349. package/src/skf-setup/references/ccc-index.md +29 -90
  350. package/src/skf-setup/references/detect-and-tier.md +58 -67
  351. package/src/skf-setup/references/health-check.md +2 -0
  352. package/src/skf-setup/references/invocation-contract.md +14 -0
  353. package/src/skf-setup/references/report.md +43 -166
  354. package/src/skf-setup/references/tier-rules.md +5 -5
  355. package/src/skf-setup/references/write-config.md +34 -57
  356. package/src/skf-test-skill/SKILL.md +17 -57
  357. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  358. package/src/skf-test-skill/customize.toml +27 -13
  359. package/src/skf-test-skill/references/coherence-check.md +134 -62
  360. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  361. package/src/skf-test-skill/references/coverage-check.md +209 -264
  362. package/src/skf-test-skill/references/detect-mode.md +5 -5
  363. package/src/skf-test-skill/references/external-validators.md +94 -82
  364. package/src/skf-test-skill/references/health-check.md +6 -7
  365. package/src/skf-test-skill/references/init.md +164 -91
  366. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  367. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  368. package/src/skf-test-skill/references/report.md +164 -142
  369. package/src/skf-test-skill/references/score.md +80 -107
  370. package/src/skf-test-skill/references/scoring-rules.md +42 -155
  371. package/src/skf-test-skill/references/source-access-protocol.md +18 -94
  372. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  373. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  374. package/src/skf-test-skill/scripts/build-result-context.py +380 -0
  375. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  376. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  377. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  378. package/src/skf-test-skill/scripts/gap-ledger.py +1666 -0
  379. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  380. package/src/skf-test-skill/scripts/load-coverage-inputs.py +1198 -0
  381. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  382. package/src/skf-test-skill/scripts/reconcile-coverage.py +547 -79
  383. package/src/skf-test-skill/scripts/score-signatures.py +479 -0
  384. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  385. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  386. package/src/skf-test-skill/scripts/verify-declared-numerator.py +133 -56
  387. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  388. package/src/skf-update-skill/SKILL.md +38 -25
  389. package/src/skf-update-skill/customize.toml +27 -12
  390. package/src/skf-update-skill/references/detect-changes.md +253 -223
  391. package/src/skf-update-skill/references/gap-driven.md +336 -0
  392. package/src/skf-update-skill/references/health-check.md +31 -5
  393. package/src/skf-update-skill/references/init.md +254 -84
  394. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  395. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  396. package/src/skf-update-skill/references/merge.md +182 -49
  397. package/src/skf-update-skill/references/re-extract.md +53 -262
  398. package/src/skf-update-skill/references/report.md +102 -35
  399. package/src/skf-update-skill/references/write.md +357 -155
  400. package/src/skf-update-skill/scripts/skf-new-file-diff.py +183 -46
  401. package/src/skf-verify-stack/SKILL.md +23 -42
  402. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  403. package/src/skf-verify-stack/customize.toml +55 -22
  404. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  405. package/src/skf-verify-stack/references/coverage.md +67 -64
  406. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  407. package/src/skf-verify-stack/references/init.md +107 -62
  408. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  409. package/src/skf-verify-stack/references/integrations.md +106 -65
  410. package/src/skf-verify-stack/references/report.md +59 -42
  411. package/src/skf-verify-stack/references/requirements.md +32 -9
  412. package/src/skf-verify-stack/references/synthesize.md +53 -39
  413. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  414. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  415. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  416. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  417. package/tools/cli/commands/install.js +11 -4
  418. package/tools/cli/commands/status.js +15 -3
  419. package/tools/cli/commands/update.js +11 -4
  420. package/tools/cli/lib/tool-check.js +320 -0
  421. package/tools/cli/lib/version-check.js +22 -9
  422. package/tools/cli/skf-cli.js +4 -1
  423. package/.gitattributes +0 -16
  424. package/.nvmrc +0 -1
  425. package/docs/_internal/RELEASING.md +0 -679
  426. package/release-audits/v1.0.0-launch-audit.md +0 -728
  427. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  428. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  429. package/src/skf-campaign/manifest.yaml +0 -15
  430. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  431. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  432. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  433. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  434. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  435. package/src/skf-update-skill/references/validate.md +0 -124
  436. package/tools/validate-docs-drift.js +0 -233
  437. package/tools/validate-docs-links.js +0 -665
@@ -1,45 +1,52 @@
1
1
  ---
2
2
  nextStepFile: 're-extract.md'
3
3
  noChangeReportFile: 'report.md'
4
- # Resolve `{hashContentHelper}` to the first existing path; HALT if neither
5
- # candidate exists — §1b and §Category D rely on the helper for deterministic
6
- # SHA-256 hashing (file-read + size + line-count) and provenance comparison
7
- # (UNCHANGED / MODIFIED_FILE / DELETED_FILE classification). Falling back to
8
- # prose-driven hashing would lose hash stability across runs.
4
+ extractionPatternsData: 'skf-create-skill/references/extraction-patterns.md'
5
+ # `{hashContentHelper}`: Category D's hash comparison. HALT if neither exists.
9
6
  hashContentProbeOrder:
10
7
  - '{project-root}/_bmad/skf/shared/scripts/skf-hash-content.py'
11
8
  - '{project-root}/src/shared/scripts/skf-hash-content.py'
12
- # Resolve `{buildChangeManifestHelper}` to the first existing path; HALT if
13
- # neither candidate exists. §3 uses `build` to aggregate Category A/B/C/D
14
- # results into the unified manifest; §2.2 uses `deletion-ratio` to compute
15
- # the major-version trigger. Falling back to prose-driven count rollups
16
- # would let the LLM drift on manifest shape across runs.
9
+ # `{buildChangeManifestHelper}`: §3's `build` (and §1's for a docs-only
10
+ # skill), §2.2's `deletion-ratio`, Category B's `baseline-gaps` and Category
11
+ # C's `rename-candidates`. HALT if neither exists.
17
12
  buildChangeManifestProbeOrder:
18
13
  - '{project-root}/_bmad/skf/shared/scripts/skf-build-change-manifest.py'
19
14
  - '{project-root}/src/shared/scripts/skf-build-change-manifest.py'
20
- # Resolve `{provenanceGapDispatchHelper}` to the first existing path; HALT
21
- # if neither exists. §1c uses it to discover the latest drift report, parse
22
- # Out-of-Scope candidates, and classify them against brief.scope.amendments[]
23
- # in one call. Falling back to prose-driven markdown parsing would let
24
- # report-format drift produce silent skips of out-of-scope candidates.
15
+ # `{provenanceGapDispatchHelper}`: §1c's drift-report candidates. HALT if
16
+ # neither exists.
25
17
  provenanceGapDispatchProbeOrder:
26
18
  - '{project-root}/_bmad/skf/shared/scripts/skf-provenance-gap-dispatch.py'
27
19
  - '{project-root}/src/shared/scripts/skf-provenance-gap-dispatch.py'
28
- # Resolve `{detectScriptsAssetsHelper}` to the first existing path; HALT if
29
- # neither candidate exists. §Category D uses `detect` to walk the current
30
- # source tree for scripts/assets (directory conventions, shebang signals,
31
- # `package.json` `bin` entry-points, asset filename patterns) so NEW_FILE
32
- # detection mirrors create-skill §4c exactly. Falling back to prose-driven
33
- # file walking would let the LLM drift on the heuristic list and miss new
34
- # scripts/assets at deeper directory depths — and on installed modules
35
- # (no `src/` tree) the LLM would otherwise guess a non-existent path.
20
+ # `{detectScriptsAssetsHelper}`: Category D's script and asset walk, the
21
+ # detector create-skill runs, over the whole source. HALT if neither exists.
36
22
  detectScriptsAssetsProbeOrder:
37
23
  - '{project-root}/_bmad/skf/shared/scripts/skf-detect-scripts-assets.py'
38
24
  - '{project-root}/src/shared/scripts/skf-detect-scripts-assets.py'
39
- # `{newFileDiffHelper}` derives NEW_FILE (inventory source_files not in the
40
- # provenance map, minus [MANUAL] paths) — the set-difference §Category D would
41
- # otherwise ask the model to compute by hand. Per-skill helper, always bundled.
25
+ # `{newFileDiffHelper}`: Category D's NEW_FILE set difference, kept to the
26
+ # skill brief's scope, bundled with this skill.
42
27
  newFileDiffHelper: 'scripts/skf-new-file-diff.py'
28
+ # `{resolveAuthoritativeFilesHelper}`: §1b's mirror. HALT if neither exists.
29
+ resolveAuthoritativeFilesProbeOrder:
30
+ - '{project-root}/_bmad/skf/shared/scripts/skf-resolve-authoritative-files.py'
31
+ - '{project-root}/src/shared/scripts/skf-resolve-authoritative-files.py'
32
+ # `{classifyChangedFilesHelper}`: §2.1 Category A. HALT if neither exists.
33
+ classifyChangedFilesProbeOrder:
34
+ - '{project-root}/_bmad/skf/shared/scripts/skf-classify-changed-files.py'
35
+ - '{project-root}/src/shared/scripts/skf-classify-changed-files.py'
36
+ # `{extractPublicApiHelper}`: §2.1 Category B's recipe runner. If neither
37
+ # exists, read the files by eye.
38
+ extractPublicApiProbeOrder:
39
+ - '{project-root}/_bmad/skf/shared/scripts/skf-extract-public-api.py'
40
+ - '{project-root}/src/shared/scripts/skf-extract-public-api.py'
41
+ # `{structuralDiffHelper}`: §2.1 Category B's diff. HALT if neither exists.
42
+ structuralDiffProbeOrder:
43
+ - '{project-root}/_bmad/skf/shared/scripts/skf-structural-diff.py'
44
+ - '{project-root}/src/shared/scripts/skf-structural-diff.py'
45
+ # `{compareDocHashesHelper}`: §1's docs-only branch, the doc-hash comparison
46
+ # audit-skill's doc-drift step runs. HALT if neither exists.
47
+ compareDocHashesProbeOrder:
48
+ - '{project-root}/_bmad/skf/shared/scripts/skf-detect-docs.py'
49
+ - '{project-root}/src/shared/scripts/skf-detect-docs.py'
43
50
  ---
44
51
 
45
52
  <!-- Config: communicate in {communication_language}. -->
@@ -52,60 +59,51 @@ Compare current source code state against the provenance map to produce a comple
52
59
 
53
60
  ## Rules
54
61
 
55
- - Focus only on detecting and classifying changes — do not extract or merge
56
- - Use subprocess Pattern 4 (parallel) when available; if unavailable, compare sequentially
62
+ - Focus only on detecting and classifying changes: extract only what Category B's diff reads (step 3 reuses it), and merge nothing
63
+ - Run §2.1's categories in order, A, then B, then C: each reads the one before. Fan Category B's reading out in parallel batches (Pattern 4) only when its file list is long
57
64
 
58
65
  ## Steps
59
66
 
60
- ### 0. Check for Test Report Input (Gap-Driven Mode)
67
+ The helpers below and step 3 pass their JSON to each other through `{run_dir}`, the run folder SKILL.md On Activation created.
61
68
 
62
- **If `update_mode == "gap-driven"` (set in step 1 via `--from-test-report`):**
69
+ **Halt procedure.** Every HALT in this step names its payload (`status`, `phase`, `path` when it has one, and `reason`), displays its message, and then runs, from `{project-root}`, the halt helper SKILL.md On Activation resolved. Nothing this step wrote is undone: it writes the skill brief only with a person's decision or a headless deferral (§1b, §1c), both of which stand, and the read-only modes write a copy in `{run_dir}` instead.
63
70
 
64
- Load the test report at `{test_report_path}` and extract findings:
71
+ ```bash
72
+ uv run {runStateHelper} halt --run-dir "{run_dir}" \
73
+ [--tree "{source_tree}"] \
74
+ [--lock "{forge_data_folder}/{skill_name}/.skf-update.lock" --owner "{lock_owner}"] \
75
+ [--emit] <<'SKF_JSON'
76
+ {"status": "<status>", "phase": "<phase>", "path": "<path; leave the key out when the halt names none>", "reason": "<reason>", "skill_name": "{skill_name}", "version": "<the metadata.json version>", "previous_version": "<the same>", "update_mode": "<normal or degraded>"}
77
+ SKF_JSON
78
+ ```
65
79
 
66
- 1. Read the **Gap Report** section — each gap entry has severity, category, and description
67
- 2. Read the **Coverage Analysis** section — each per-export row has documented/missing/mismatch status
68
- 3. Translate findings into change manifest format:
80
+ Pass `--tree` when init.md §6b bound `{source_tree}`, `--lock` and `--owner` when init.md §1b bound `{lock_owner}` (the read-only modes take no lock), and `--emit` in `{headless_mode}`. It removes the private source tree, releases the run lock (never one another run holds) and, with `--emit`, prints the halt's `SKF_UPDATE_RESULT_JSON:` line through the shared emitter, which adds the decisions recorded so far, the `error` object and a warning for each step the helper could not finish (`source-tree-not-removed`, `run-lock-not-released`); it never stops on a result. Write each payload value as a JSON string: escape `"` and `\`, and write every path with `/`. Display the line it prints verbatim. When it exits 1 and its message names the payload, fix the payload once and run it again, which redoes nothing already done; when it still fails or prints no line, display the halt message alone. An interactive HALT displays its message and emits nothing. A HALT that names no payload (a helper the frontmatter says to HALT without, resolving to no path) takes `status: "blocked"`, `phase: "detect-changes:<the helper's file name>"` and `reason: "<the helper's file name> is missing; re-install SKF"`.
69
81
 
70
- | Gap Severity | Gap Type | Change Category |
71
- |-------------|----------|-----------------|
72
- | Critical | Missing export documentation | NEW_EXPORT (undocumented public API) — unless the remediation says the export is internal / out of scope; then DELETED_EXPORT (rescope), see rule R1 |
73
- | High | Signature mismatch | MODIFIED_EXPORT (signature needs update) |
74
- | Medium | Missing type/interface docs | NEW_EXPORT (undocumented type) |
75
- | Medium | Stale documentation | MODIFIED_EXPORT (docs reference removed export) |
76
- | Critical/Medium | Coverage gap whose remediation is **removal** (export is internal, `#[doc(hidden)]`, or explicitly out of scope) | DELETED_EXPORT (rescope) — see rule R1 |
77
- | Medium/High | Structural / coherence drift (output file only, no source change) | STRUCTURAL_FIX — see rule R2 |
78
- | Medium | Export documented in SKILL.md/references but **missing from the provenance-map** | NEW_EXPORT (provenance-completeness) — see rule R3 |
79
- | Low | Missing metadata/examples | metadata update — see rule R4 |
82
+ The halt leaves `{run_dir}` in place, with the decisions the gates below recorded before it.
80
83
 
81
- **Translation rules (referenced by the table above):**
84
+ **Warnings go to the run log.** Record each warning this step adds to `warnings[]` the moment it is raised: write its text to `{run_dir}/warning.txt` with a file write (a warning can hold quotes, `$` or backticks), then, from `{project-root}`, run `uv run {emitEnvelopeHelper} record --run-dir "{run_dir}" --warning "$(cat "{run_dir}/warning.txt")"`. The halt line and the result line read warnings only from `{run_dir}/warnings.jsonl`.
85
+
86
+ ### 1. Scan Current Source State
82
87
 
83
- - **R1 — DELETED_EXPORT (rescope).** A coverage gap is a rescope only when its remediation text names removal (`rescope`, `remove from surface`, `out of scope`) or the export is upstream `#[doc(hidden)]` / internal. Default a bare "missing export documentation" gap to NEW_EXPORT (document it); choose rescope only on that explicit signal. **Interactive:** prompt per qualifying gap — "[D] Document the export / [R] Rescope (remove from the public surface)". **Headless:** default to **document** (NEW_EXPORT); choose rescope only when the remediation explicitly says removal *and* the export is internal/`#[doc(hidden)]`. A rescope is honest only if the reduction is expressed in the brief's scope: append a `scope.amendments[]` entry (`category: "scope-expansion"`, `action: "excluded"`) **and** add the export's source path to `brief.scope.exclude`, then route the entry to merge Priority 1 (removal) so step 4/6 remove it and recompute stats from the amended scope. Never close a coverage gap by editing `metadata.stats` to equal the documented count — that is denominator deflation and `skf-test-skill` will reject it.
84
- - **R2 — STRUCTURAL_FIX.** A coherence finding from `skf-scan-skill-md-structure.py` (e.g., `table_drift`, `unbalanced_fences`, a broken intra-skill anchor) that touches the generated output file only, with no source change and no provenance entry change. Carries `remediation` text describing the surgical markdown edit. Routes to merge Priority 8 (generated-markdown edit only); it never adds, modifies, or removes a provenance `entries[]` row.
85
- - **R3 — provenance-completeness.** An export documented in SKILL.md/`references/` but absent from the provenance-map. These are documented-and-known, so `unknown` is never correct: route to re-extract §0a source resolution **regardless of severity**, gated only on `source_root` being pinned and readable (see re-extract.md §0).
86
- - **R4 — metadata update.** A metadata-coherence patch that changes no export's source (e.g., reconcile a divergent `stats` count). Routes to merge Priority 8b and is applied by write.md §2 *before* the automatic stat recount (see merge.md §3 / write.md §2).
88
+ **A docs-only skill** (`source_type: "docs-only"` in the brief or metadata.json) has no source tree for §2's categories to read: its drift is in the documents `doc_sources` records with their hashes. Compare them with the helper audit-skill's doc-drift step runs: resolve `{compareDocHashesHelper}` ← first existing path in `{compareDocHashesProbeOrder}` and, from `{project-root}`, run:
89
+
90
+ ```bash
91
+ uv run {compareDocHashesHelper} compare-hashes "{skill_package}/metadata.json" > "{run_dir}/doc-hashes.json"
92
+ ```
87
93
 
88
- 4. Build the change manifest from translated gaps — no file-level timestamp comparison needed since source hasn't changed. For each manifest entry, propagate these fields from the test report finding so step 3 can resolve the export against live source:
94
+ It fetches each URL with a recorded hash and buckets it `changed`, `unchanged`, `fetch_failed` or `skipped_null_hash`, with `stats`: never fetch or hash a URL by hand. On exit 2, no JSON, or no candidate resolves, HALT with status `blocked` (halt procedure: `phase: "detect-changes:doc-hashes"`, `path: "{skill_package}/metadata.json"`, `reason: "<its error, or skf-detect-docs.py is missing; re-install SKF>"`). Add `doc-fetch-failed: {url} ({reason})` to `warnings[]` for each `fetch_failed` entry (a document that could not be read is not drift), `doc-not-hashed: {url}` for each `skipped_null_hash` entry, and `doc-drift-not-checked: metadata.json records no doc_sources` when `stats.total_tracked` is 0. Then skip §1b to §2.2 and §3's build, and have the helper write the change manifest from that output, never by hand:
89
95
 
90
- - **`severity`** — the Gap Report severity (`Critical`, `High`, `Medium`, `Low`, `Info`). Step-03 §0 and step 6 §3 gate the null-citation fallback on severity: Critical/High gaps must produce AST provenance, Medium/Low/Info gaps may degrade to `unknown`.
91
- - **`source_citation: {file, line}`** — populated only when the finding's `Source:` field is a `file:line` pair (e.g., a Gap Report row that cites `packages/utils/src/builder-utils.ts:33`). Step-03 §0 uses this field to perform a live spot-check against source rather than flagging the export as `unknown`. Omit when the `Source:` field is a region reference (e.g., `@storybook/addon-docs control primitives`) or missing.
92
- - **`remediation_paths: [path, ...]`** — path-like tokens extracted from the finding's `Remediation:` text: any substring matching a recognized source file extension (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.py`, `.rs`, `.go`, `.java`, `.rb`, `.c`, `.h`, `.cpp`), or a directory/glob fragment under the project's source root. Include every matching path verbatim. Step-03 §0a uses this list as the source set for its Targeted Re-Extraction Branch when `source_citation` is absent and severity is Critical/High. Omit the field when the Remediation text names no paths — the entry then falls through to `unknown` or to §0a's halt, depending on severity.
93
- - **`change_category`** — the Change Category resolved from the table above (`NEW_EXPORT`, `MODIFIED_EXPORT`, `DELETED_EXPORT`, `STRUCTURAL_FIX`, or `metadata update`). Step 3 §1a partitions on this field; merge.md §3 dispatches on it.
94
- - **`remediation`** — the finding's full `Remediation:` text, verbatim. Required for `STRUCTURAL_FIX` (the surgical markdown edit), `metadata update` (the patch description), and `DELETED_EXPORT` rescope (the removal rationale recorded in the `scope.amendments[]` entry). For `NEW_EXPORT` / `MODIFIED_EXPORT` it is informational.
95
- - **`provenance_completeness: true`** — set on a `NEW_EXPORT` entry whose gap is rule R3 (documented in SKILL.md/`references/` but absent from the provenance-map). Step 3 §0 routes these to §0a regardless of severity.
96
- 5. Set `gap_count` from the total number of translated entries
97
- 6. **Skip to section 5** (Display Change Summary) with the gap-derived manifest
96
+ ```bash
97
+ uv run {buildChangeManifestHelper} build --doc-hashes "{run_dir}/doc-hashes.json" > "{run_dir}/change-manifest.json"
98
+ ```
98
99
 
99
- "**Gap-driven update mode.** Translating {gap_count} test report findings into change manifest — source drift detection skipped."
100
+ It writes `{"mode": "docs-only", "no_changes", "changed_urls", "fetch_failed", "counts": {"docs_changed", "docs_fetch_failed"}}`. On exit 1 or no JSON: HALT with status `blocked` (halt procedure: `phase: "detect-changes:change-manifest"`, its stderr as `reason`). §4 then reports no change only when every hashed document matches (`changed_urls` is empty); otherwise §5 routes to step 3, whose §1 re-fetches the changed URLs, and step 5 records their new hashes from `{run_dir}/doc-hashes.json`.
100
101
 
101
- **If normal mode:** Continue with source drift detection below.
102
+ `{source_root}` is the tree init.md §6b prepared at `{target_commit}` when `{source_tree_status}` is `ready` or `offline`, and otherwise the skill's local source. **If `{source_tree_status}` is `ready` or `offline` and `{source_root}` no longer exists**, HALT with status `blocked` per SKILL.md's source-tree rule (halt procedure: `phase: "detect-changes:source-tree-missing"`, `path: "{source_root}"`, `reason: "source tree {source_root} disappeared mid-run"`): reading a missing tree would report every file deleted. §2.1's helpers walk `{source_root}` themselves: build no file inventory here.
102
103
 
103
- ### 1. Scan Current Source State
104
+ **The brief this run reads.** Bind `{brief_path}` ← `{forge_data_folder}/{skill_name}/skill-brief.yaml`. When `detect_only_mode` or `dry_run_mode` is true and that file exists, copy it to `{run_dir}/skill-brief.yaml` and bind `{brief_path}` to the copy: §1b and §1c write each decision there, Category A reads the scope from there, and the brief itself stays as it was. Each amendment a read-only run writes to the copy is a proposed one: add `proposed-amendment: {action} {path} ({category}); not written: {--dry-run or --detect-only}` to `warnings[]` as you write it, and step 6 lists them with the change manifest.
104
105
 
105
- Read the source directory at `{source_root}` and build a current file inventory:
106
- - For each source file: record path, file size, last modified timestamp
107
- - Focus on file types relevant to the skill (from provenance map file patterns)
108
- - Exclude non-source files (node_modules, build artifacts, etc.)
106
+ **The scope type this run reads.** Bind `{scope_type}` ← the `scope_type` metadata.json records (init.md §2), else the `scope.type` of `{brief_path}` when that file exists and names one: a skill whose metadata.json records no scope type updates under its brief's. Category B's runner takes it as `--scope-type` and writes it to `{run_dir}/extraction.json` as `scope.type`, where step 3's `records` and step 5's `apply` read it: under `public-api`, a normal-mode update neither documents nor adds to the provenance map a new export off the runner's public surface (re-extract.md §4, write.md §3).
109
107
 
110
108
  ### 1b. Discovered Authoritative Files Protocol (Mirror)
111
109
 
@@ -113,26 +111,30 @@ Read the source directory at `{source_root}` and build a current file inventory:
113
111
 
114
112
  **Skip this section entirely if:**
115
113
 
116
- - `update_mode == "gap-driven"` (source hasn't drifted — we're verifying test report findings, not discovering new files), OR
117
- - `metadata.json.source_type == "docs-only"` (no source tree to scan)
114
+ - `metadata.json.source_type == "docs-only"` (no source tree to scan), OR
115
+ - `{brief_path}` does not exist (a skill built without a brief, such as a quick skill): there is no scope or amendment to match a document against, and no brief to amend. Display `"Authoritative files mirror: skipped (no skill brief)."`
118
116
 
119
- **Procedure (identical heuristics to create-skill §2a):**
117
+ **Procedure (the helper and buckets of create-skill §2a):**
120
118
 
121
- 1. **Walk the source tree.** Match file basenames against the heuristic list case-insensitively:
122
- - `llms.txt`, `llms-full.txt`
123
- - `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `COPILOT.md`
124
- - `.cursorrules`, `.windsurfrules`, `.clinerules`
119
+ 1. **Scan, classify and hash in one call.** Resolve `{resolveAuthoritativeFilesHelper}` ← first existing path in `{resolveAuthoritativeFilesProbeOrder}` and, from `{project-root}`, run:
125
120
 
126
- 2. **Cross-reference with provenance map.** For each match:
127
- - **Already in provenance map** (`entries[].source_file` or `file_entries[].source_file` contains this path): the file is already tracked. §2 will detect any drift in the normal flow. No action in §1b.
128
- - **Not in provenance map:** continue to amendment check.
121
+ ```bash
122
+ uv run {resolveAuthoritativeFilesHelper} resolve \
123
+ --source-root "{source_root}" \
124
+ --brief "{brief_path}" \
125
+ [--provenance-map "{provenance_map_path}"]
126
+ ```
127
+
128
+ Pass `--provenance-map` whenever init.md §4 found one. It finds the files create-skill §2a looks for (`llms.txt`, `AGENTS.md`, `.cursorrules` and the like), sorts each into a bucket below, and hashes and previews each one a decision needs. Never walk, match or hash these files by hand. On exit 1 (a source root, brief or provenance map it cannot read), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:authoritative-files"`, `reason: "<its stderr>"`), showing its stderr.
129
129
 
130
- 3. **Check brief amendments.** Load `brief.scope.amendments[]` from `{forge_data_folder}/{skill_name}/skill-brief.yaml`. For each candidate not in the provenance map:
131
- - **`action: "promoted"` for this path exists:** the brief says this file should be in scope, but it's missing from the provenance map. This means the file was promoted by a prior run but its `file_entries[]` row is missing (e.g. provenance-map was regenerated from source without re-reading amendments). Add the path to `promoted_docs_new[]` (see step 6 below) with its content hash so §4 merge writes a new `file_entries[]` row. No user prompt — the decision was already made. Display: `"Honoring prior amendment: promoted {path} scheduled for file_entries write."`
132
- - **`action: "skipped"` for this path exists:** user previously declined promotion. Honor the skip silently. No prompt, no action.
133
- - **No amendment for this path:** continue to user prompt.
130
+ 2. **Apply each bucket:**
131
+ - **`already_tracked[]`:** the provenance map names the file (`entries[]` or `file_entries[]`), so §2 detects its drift. No action here.
132
+ - **`already_in_scope[]`:** the brief's scope already takes the file (a `scope.include` glob matches it, or its latest amendment promoted it), but the map has no row for it. Add the helper's `{path, heuristic, size_bytes, line_count, content_hash}` to `promoted_docs_new[]` (the list [P] Promote below fills), with no prompt, as create-skill §2a does. Display: `"In scope: {path} scheduled for file_entries write."`
133
+ - **`pre_decided[]` with `prior_action: "promoted"`:** a prior run promoted it, but its `file_entries[]` row is missing (e.g. provenance-map was regenerated from source without re-reading amendments). Add it to `promoted_docs_new[]` with the helper's hash, size and line count so §4 merge writes a new `file_entries[]` row. No user prompt: the decision was already made. Display: `"Honoring prior amendment: promoted {path} scheduled for file_entries write."`
134
+ - **`pre_decided[]` with `prior_action: "skipped"`:** a person declined promotion before. Honor the skip silently. No prompt, no action.
135
+ - **`unresolved[]`:** no one has decided the file yet (a `prior_action` of `deferred-headless` marks one a headless run met and left for a person, the legacy `skipped` amendment with reason `headless: no user to prompt` included): continue to the prompt.
134
136
 
135
- 4. **Prompt.** For each unresolved candidate, present the same prompt as create-skill §2a:
137
+ 3. **Prompt.** For each `unresolved[]` candidate, present the same prompt as create-skill §2a, with the helper's `path`, `line_count`, `size_bytes`, `heuristic` and `preview` as it reported them (never recomputed):
136
138
 
137
139
  ```
138
140
  **New authoritative file discovered since skill creation**
@@ -152,36 +154,28 @@ Read the source directory at `{source_root}` and build a current file inventory:
152
154
  [U] Update — halt this run and return to skf-brief-skill to refine scope
153
155
  ```
154
156
 
155
- 5. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate — record `action: "skipped"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently add files to scope. **Also append one entry per candidate to in-context `headless_decisions[]`** (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "detect-changes.promoted-doc-prompt", default_action: "S", taken_action: "S", reason: "headless: no user to prompt", evidence: {path: "<candidate.path>"}}`.
157
+ Steps 4 and 5 are the decision protocol for a scope candidate, which §1c follows too, with the differences it names. Every amendment they append goes to `brief.scope.amendments[]` with `path: candidate.path`, `heuristic: {basename}`, `date: {today ISO}` and `workflow: "skf-update-skill"`, and every brief write goes to `{brief_path}` (the run folder's copy in a read-only mode), preserving all other fields.
156
158
 
157
- 6. **Apply decision:**
159
+ 4. **GATE [default: defer]**: in headless mode (`{headless_mode}` is true), promote nothing and decide nothing: whether a path belongs in scope needs a person. Leave every candidate out of this run and record it as deferred, so the next interactive run asks: append an amendment `action: "deferred-headless"`, `reason: "headless: no user to prompt; left for the next interactive run"`, except for a candidate whose `prior_action` is `deferred-headless`, which a headless run already recorded: write no second amendment for it. A non-interactive update run must never silently add files to scope, and never records a skip no person chose. **Also record one decision per candidate path**, from `{project-root}`:
158
160
 
159
- - **[P] Promote:**
160
- 1. Append `candidate.path` to `brief.scope.include` as a literal glob.
161
- 2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `path: candidate.path`, `reason: {user-provided or auto: "discovered post-creation — matched heuristic {basename}"}`, `heuristic: {basename}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
162
- 3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
163
- 4. **Hash the candidate** via `uv run {hashContentHelper} hash {candidate.path}` — emits `{content_hash, size_bytes, line_count}` as JSON. Combine with the existing context fields and append to the in-context `promoted_docs_new[]` list: `{path, heuristic, size_bytes, line_count, content_hash}`. This list is consumed by §4 merge Priority 7 to write new `file_entries[]` rows — promoted docs do NOT go through §3 code re-extraction, which would produce ghost entries on non-code files.
164
- 5. Display: `"Promoted {path} — brief amended, scheduled as new file_entries row for file_type doc."`
161
+ ```bash
162
+ uv run {emitEnvelopeHelper} record --workflow skf-update-skill --run-dir "{run_dir}" --decision <<'SKF_JSON'
163
+ {"gate": "detect-changes.promoted-doc-prompt", "default_action": "S", "taken_action": "deferred-headless", "reason": "headless: no user to prompt; left for the next interactive run", "evidence": {"path": "<candidate.path>"}}
164
+ SKF_JSON
165
+ ```
165
166
 
166
- - **[S] Skip:**
167
- 1. Do NOT modify `scope.include`.
168
- 2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1b"}`, `heuristic: {basename}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
169
- 3. **Write the amended brief back to disk** so neither update-skill nor create-skill will re-prompt in future runs.
170
- 4. Display: `"Skipped {path} — decision recorded in amendments."`
167
+ 5. **Apply decision:**
171
168
 
172
- - **[U] Update:**
173
- 1. Halt the workflow immediately.
174
- 2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
175
- 3. Exit with status `halted-for-brief-refinement`. Change manifest is discarded — no partial writes.
169
+ - **[P] Promote:** append `candidate.path` to `brief.scope.include` as a literal glob, append an amendment `action: "promoted"`, `reason: {user-provided or auto: "discovered post-creation: matched heuristic {basename}"}`, and **write the amended brief back to disk immediately**. Then append the candidate's `{path, heuristic, size_bytes, line_count, content_hash}`, as the helper reported them, to `promoted_docs_new[]`, which this section keeps as the JSON array `{run_dir}/promoted-docs.json` (each record as the helper printed it; `[]` when none): step 5's `apply` writes a new `file_entries[]` row for each (merge Priority 7). Promoted docs do NOT go through step 3's code re-extraction, which would produce ghost entries on non-code files. Display: `"Promoted {path}: brief amended, scheduled as new file_entries row for file_type doc."`
170
+ - **[S] Skip:** leave `scope.include` as it is, append an amendment `action: "skipped"`, `reason: {user-provided or auto: "user declined promotion at update-skill §1b"}`, and write the amended brief back to disk, so neither update-skill nor create-skill re-prompts in a later run. Display: `"Skipped {path}: decision recorded in amendments."`
171
+ - **[U] Update:** HALT with status `halted-for-brief-refinement` (halt procedure: `phase: "detect-changes:authoritative-files"`, `path: "{brief_path}"`, `reason: "the user chose to refine the brief's scope for {path}"`) and display `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`: no partial writes.
176
172
 
177
- 7. **Summary.** After all candidates are resolved (or none were found):
173
+ 6. **Summary.** After all candidates are resolved (or none were found):
178
174
 
179
- - `"Authoritative files mirror: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments, {T} already tracked in provenance."`
175
+ - `"Authoritative files mirror: {N} candidates, {P} promoted, {S} skipped, {D} deferred to an interactive run, {A} decided by the brief (in scope or amended), {T} already tracked in provenance."`, with N the helper's `summary.candidates_total`, A its `already_in_scope_count` plus `pre_decided_count` and T its `already_tracked_count`.
180
176
  - If N = 0: `"Authoritative files mirror: no candidates."`
181
177
 
182
- **Record for evidence report:** the update-skill evidence report appends `authoritative_files_mirror: {candidates: N, promoted: P, skipped: S, pre_decided: A, already_tracked: T, decisions: [{path, action, heuristic, reason}]}`.
183
-
184
- **Interaction with §2 change detection:** promoted docs live in `promoted_docs_new[]`, not in the change manifest. §2.0's `change_detection_excludes` set (built below) is what stops §2 Category A from re-classifying them as ADDED — it is the only coordination that survives across the parallel Category A subprocesses.
178
+ **Record for evidence report:** append the record `authoritative_files_mirror: {candidates: N, promoted: P, skipped: S, deferred: D, pre_decided: A, already_tracked: T, decisions: [{path, action, heuristic, reason}]}` to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `authoritative_files_mirror`: step 5 §4 reads it there, never from context.
185
179
 
186
180
  ### 1c. Major-Version Scope Reconciliation (Pre-Detection)
187
181
 
@@ -189,8 +183,8 @@ Read the source directory at `{source_root}` and build a current file inventory:
189
183
 
190
184
  **Skip this section entirely if:**
191
185
 
192
- - `update_mode == "gap-driven"` (test-report mode — source hasn't drifted), OR
193
186
  - `metadata.json.source_type == "docs-only"` (no source tree to scope), OR
187
+ - `{brief_path}` does not exist (a skill built without a brief, such as a quick skill): there is no scope to expand and no brief to amend. Display `"Scope reconciliation: skipped (no skill brief)."`, OR
194
188
  - No audit drift report is available at the path computed in step 1 below.
195
189
 
196
190
  **Procedure:**
@@ -199,12 +193,14 @@ Read the source directory at `{source_root}` and build a current file inventory:
199
193
 
200
194
  ```bash
201
195
  uv run {provenanceGapDispatchHelper} dispatch \
202
- --skill-name {skill_name} \
203
- --baseline-version {baseline_version} \
204
- --forge-data-folder {forge_data_folder} \
205
- --brief {forge_data_folder}/{skill_name}/skill-brief.yaml
196
+ --skill-name "{skill_name}" \
197
+ --baseline-version "{baseline_version}" \
198
+ --forge-data-folder "{forge_data_folder}" \
199
+ --brief "{brief_path}"
206
200
  ```
207
201
 
202
+ On exit 1 (a forge folder it cannot read, or a brief that exists but cannot be read), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:scope-reconciliation"`, `path: "{brief_path}"`, `reason: "<its stderr>"`).
203
+
208
204
  Output envelope:
209
205
 
210
206
  ```json
@@ -220,7 +216,7 @@ Read the source directory at `{source_root}` and build a current file inventory:
220
216
  | "pre-decided-demoted" | "unresolved",
221
217
  "prior_action": "promoted" | "skipped"
222
218
  | "demoted-include" | "demoted-exclude"
223
- | null
219
+ | "deferred-headless" | null
224
220
  }, ...
225
221
  ],
226
222
  "summary": {"pre_decided_count": N, "unresolved_count": N}
@@ -235,7 +231,7 @@ Read the source directory at `{source_root}` and build a current file inventory:
235
231
  - `status: "already-in-scope"` → skip silently (`prior_action: "promoted"`).
236
232
  - `status: "pre-decided-skipped"` → honor silently (`prior_action: "skipped"`).
237
233
  - `status: "pre-decided-demoted"` → record as `pre_decided`; do not re-prompt (`prior_action` ∈ `demoted-include`, `demoted-exclude`).
238
- - `status: "unresolved"` → continue to step 4 (user prompt).
234
+ - `status: "unresolved"` → continue to step 3 (user prompt). A `prior_action` of `deferred-headless` marks a path a headless run deferred (the legacy `skipped` amendment with reason `headless: no user to prompt` reads the same): a person decides it now.
239
235
 
240
236
  **Note:** the Out-of-Scope section is an optional audit-skill output and is often absent or empty — new-file detection is update-skill's job (§1b/§2.2), not audit-skill's — so the `no-report` / `no-candidates` paths above are the common case.
241
237
 
@@ -255,95 +251,125 @@ Read the source directory at `{source_root}` and build a current file inventory:
255
251
  [U] Update — halt this run and return to skf-brief-skill to refine scope
256
252
  ```
257
253
 
258
- 4. **Headless mode (`{headless_mode}` is true):** auto-select `[S] Skip` for every candidate — record `action: "skipped"`, `category: "scope-expansion"`, `reason: "headless: no user to prompt"`, `workflow: "skf-update-skill"`. A non-interactive update run must never silently expand scope. **Also append one entry per candidate to in-context `headless_decisions[]`** (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "detect-changes.scope-expansion", default_action: "S", taken_action: "S", reason: "headless: no user to prompt", evidence: {path: "<candidate.path>"}}`.
259
-
260
- 5. **Apply decision:**
261
-
262
- - **[P] Promote:**
263
- 1. Append `candidate.path` to `brief.scope.include` as a literal glob (preserve any wildcards from the drift report).
264
- 2. Append a `brief.scope.amendments[]` entry: `action: "promoted"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "out-of-scope new public API — drift report {report basename}"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
265
- 3. **Write the amended brief back to disk immediately** at `{forge_data_folder}/{skill_name}/skill-brief.yaml`. Preserve all other fields.
266
- 4. Display: `"Promoted {path} — brief amended; §2 Category A will pick up matching files as ADDED."`
267
- 5. **No `promoted_docs_new[]` entry and no `change_detection_excludes` write** — promoted code globs flow through the standard §2 Category A → §3 extraction path, unlike §1b's promoted docs which bypass extraction.
254
+ 4. **GATE [default: defer]**: in headless mode (`{headless_mode}` is true), defer each candidate as §1b step 4 does, with this section's amendment fields (step 5) and the gate `detect-changes.scope-expansion` in its decision record: a non-interactive update run must never silently expand scope.
268
255
 
269
- - **[S] Skip:**
270
- 1. Do NOT modify `scope.include` or `scope.exclude`.
271
- 2. Append a `brief.scope.amendments[]` entry: `action: "skipped"`, `category: "scope-expansion"`, `path: candidate.path`, `reason: {user-provided or auto: "user declined promotion at update-skill §1c"}`, `evidence: {evidence string}`, `date: {today ISO}`, `workflow: "skf-update-skill"`.
272
- 3. **Write the amended brief back to disk** so neither §1c nor a future run will re-prompt.
273
- 4. Display: `"Skipped {path} — decision recorded in amendments."`
256
+ 5. **Apply decision** as §1b step 5 does, with these differences:
274
257
 
275
- - **[U] Update:**
276
- 1. Halt the workflow immediately.
277
- 2. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."`
278
- 3. Exit with status `halted-for-brief-refinement`. Change manifest is not yet built — no partial writes to provenance.
258
+ - **Amendment fields:** every amendment carries `category: "scope-expansion"` and the candidate's `evidence: {evidence string}` in place of `heuristic`. [P]'s auto reason is "out-of-scope new public API: drift report {report basename}", and [S]'s "user declined promotion at update-skill §1c".
259
+ - **Promotion target:** [P] appends the path to `scope.include` as the drift report gives it, wildcards kept, and adds no `promoted_docs_new[]` entry and no `--exclude`: §2 Category A lists the files a promoted code glob matches as ADDED, and step 3 extracts them, unlike §1b's documents. Display: `"Promoted {path}: brief amended; §2 Category A will pick up matching files as ADDED."`
260
+ - **[S]** leaves `scope.exclude` as it is too, and **[U]** halts with `phase: "detect-changes:scope-reconciliation"`.
279
261
 
280
262
  6. **Summary:** After all candidates are resolved (or none were found):
281
263
 
282
- - `"Scope reconciliation: {N} candidates, {P} promoted, {S} skipped, {A} pre-decided from amendments."`
264
+ - `"Scope reconciliation: {N} candidates, {P} promoted, {S} skipped, {D} deferred to an interactive run, {A} pre-decided from amendments."`
283
265
  - If N = 0 (section absent or empty): `"Scope reconciliation: no out-of-scope observations in drift report."`
284
- - If §1c was skipped entirely (no drift report): omit this line; §2.2 will still run.
266
+ - If §1c was skipped entirely (a docs-only skill, no skill brief or no drift report): omit this line; §2.2 will still run.
285
267
 
286
- **Record for evidence report:** the update-skill evidence report appends `scope_reconciliation_pre: {drift_report: path, candidates: N, promoted: P, skipped: S, pre_decided: A, decisions: [{path, action, evidence}]}` (omit when §1c was skipped).
268
+ **Record for evidence report:** append the record `scope_reconciliation_pre: {drift_report: path, candidates: N, promoted: P, skipped: S, deferred: D, pre_decided: A, decisions: [{path, action, evidence}]}` to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `scope_reconciliation_pre` (none when §1c was skipped).
287
269
 
288
270
  ### 2. Compare Against Provenance Map
289
271
 
290
- **If normal mode (provenance map available):**
272
+ **In degraded mode (no provenance map),** run Category A without `--provenance-map`: its `full` mode lists every in-scope file as MODIFIED (with no brief, `--language` scopes the walk), so step 3 re-extracts every export. Run Category B's steps 1 and 2 only (re-extract.md §4's records come from them), and skip its step 3, Category C, Category D and §2.2: there is no map to compare against.
291
273
 
292
- #### 2.0 — Build Pre-filter Exclusion Set
274
+ #### 2.0 Change-Detection Excludes
293
275
 
294
- Before launching parallel subprocesses, build a `change_detection_excludes` set in context that Category A subprocess workers must honor. Parallel subprocesses cannot see each other's in-memory state, so any coordination between §1b's decisions and §2's scan results must be pre-materialized into an explicit input the subprocesses receive.
276
+ `promoted_docs_new[]` (§1b) holds documents that reach the provenance map as `file_entries[]` rows through step 4 Priority 7, never through code extraction. Category A leaves each of them out with `--exclude`: each is in scope and the map does not name it yet, so it would read as ADDED, and step 3 would send it to AST extraction, producing ghost entries. A path a `file_entries[]` row already names (a tracked document, script or asset) needs no exclude: Category A never lists one, and Category D compares its hash.
295
277
 
296
- The exclusion set includes:
278
+ #### 2.1 Categories A, B and C, in Order
297
279
 
298
- - Every path in `promoted_docs_new[]` (populated by §1b). These files are tracked as `file_entries[]` via step 4 Priority 7, not through Category A code extraction. Without this exclusion, Category A would classify them as ADDED (because they're in source but not yet in the provenance map) and §3 re-extract would send them to AST extraction, producing ghost entries.
299
- - Every source path in `file_entries[].source_file` where `file_type == "doc"` in the existing provenance map. These are already-tracked authoritative docs; any drift in them is handled by Category D (script/asset file changes), not Category A.
280
+ Category B reads the files Category A lists, and Category C pairs what A and B leave deleted and added. Category D (below) needs neither.
300
281
 
301
- Record the set size: "**Change-detection excludes:** {count} paths ({promoted_docs_new count} new promotions + {existing doc file_entries count} already tracked)."
282
+ **Category A: file-level changes.** Resolve `{classifyChangedFilesHelper}` ← first existing path in `{classifyChangedFilesProbeOrder}` and, from `{project-root}`, run:
302
283
 
303
- #### 2.1 — Launch Category Subprocesses
284
+ ```bash
285
+ uv run {classifyChangedFilesHelper} classify \
286
+ --source-root "{source_root}" \
287
+ [--provenance-map "{provenance_map_path}"] \
288
+ [--brief "{brief_path}"] \
289
+ [--language "{language}"] \
290
+ --tree-status "{source_tree_status}" \
291
+ --diff-status "{source_diff_status}" \
292
+ --changed-files "{source_changed_files}" \
293
+ [--exclude "<promoted document path>"] \
294
+ --lists-dir "{run_dir}" \
295
+ > "{run_dir}/category-a.json"
296
+ ```
304
297
 
305
- Launch subprocesses in parallel that compare source state against provenance map across these categories, returning change findings per category. **Every subprocess receives `change_detection_excludes` as an explicit input** and applies it to its file-path iteration loop.
298
+ Pass `--provenance-map` whenever init.md §4 found one; `--brief` (as §1c left it) when that file exists (a skill built without a brief, such as a quick skill after its degraded update, has none); `--language` when metadata.json records one; one `--exclude` for each `promoted_docs_new[]` path; and the three init.md §6b values as bound (an empty or `null` value counts as not given). In a tree it takes the changed files from git's list at `{source_changed_files}`, never from file times or sizes, which a checkout rewrites. Add each of its `warnings[]` to `warnings[]`. It writes `{run_dir}/modified-files.json` (the files Category B diffs) and `{run_dir}/extract-files.json` (the modified and added files and each moved file's new path, which Category B and step 3 read): never retype or re-derive a list. On exit 1 (an input it cannot read, named on stderr), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-a"`, the helper's message as `reason`).
306
299
 
307
- **Category A — File-level changes:**
308
- - Files in provenance map but missing from source → DELETED
309
- - Files in source but not in provenance map AND not in `change_detection_excludes` → ADDED
310
- - Files in `change_detection_excludes`: skip entirely (routed to file_entries via §1b → step 4 Priority 7, never through Category A)
311
- - Files in both but with different timestamps/sizes → MODIFIED
312
- - Files with same content at different paths → MOVED
300
+ **Category B: export-level changes.** Skip it when `{run_dir}/extract-files.json` lists no file.
313
301
 
314
- **Category B — Export-level changes (for MODIFIED files only):**
315
- - For each modified file, compare export list against provenance map exports
316
- - Exports in provenance but not in source → DELETED_EXPORT
317
- - Exports in source but not in provenance → NEW_EXPORT
318
- - Exports with changed signatures/types → MODIFIED_EXPORT
319
- - Exports at different line numbers but same content → MOVED_EXPORT
302
+ 1. **The recipe runner.** At Forge tier and above, resolve `{extractPublicApiHelper}` ← first existing path in `{extractPublicApiProbeOrder}` and, from `{project-root}`, run:
320
303
 
321
- **Category C — Rename detection:**
322
- - Cross-reference deleted files/exports with added files/exports
323
- - If content similarity > 80% (fixed bundled threshold — not configurable): classify as RENAMED instead of deleted+added. **Similarity mechanism by tier:** Quick: compare file size ratio (within 20%) and export name overlap (>70% of exports match by name). Forge and above: use ast-grep to compare export signatures between the deleted and added files. Forge+/Deep: use CCC semantic similarity when available
304
+ ```bash
305
+ uv run {extractPublicApiHelper} --mode full \
306
+ --source-root "{source_root}" \
307
+ --files-from "{run_dir}/extract-files.json" \
308
+ [--language "{language}"] \
309
+ [--scope-type "{scope_type}"] \
310
+ --head-cap 0 \
311
+ -o "{run_dir}/extraction.json"
312
+ ```
324
313
 
325
- **Subprocess return contract.** Hand each Category worker its exact output slice so it returns parse-ready JSON, not prose the parent must re-read. Each worker returns ONLY its own object — no prose, no commentary, no markdown fences (parent strips wrapping fences before parsing). The slice each worker fills is exactly the key §3's `build` helper consumes (the `category_a/b/c` shape below; Category D is helper-driven, not a worker):
314
+ Pass `--language` when metadata.json records it, and `--scope-type` when §1 bound `{scope_type}`. It takes no `--brief`: Category A applied the scope, and a tracked file the brief's scope leaves out (gap-driven.md §1's rule R1 excludes a rescoped export's file) keeps its exports. `--head-cap 0` keeps every match, since a dropped one would read as a deleted export. Step 3 reads this file and never runs the runner again. **Exit 1** (`incomplete`, its JSON written): run it once more; when still incomplete, step 2 below reads by eye each listed file with no export in `exports[]` or that an `errors[]` item lists in `unread`, and tells the user which files were read by eye. **Exit 2** (an input error, such as a file list it cannot read, or a failure it did not foresee, named in one stderr line with no JSON; `uv` failing to start it exits 2 too): HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, the stderr line that holds `error:` as `reason`). No by-eye read stands in for it: the skill is not written yet, and reading every file by eye would hide the error. **Exit 3 (`no-ast-grep`), no JSON on another exit, no candidate resolves, or Quick tier:** write `{"exports": []}` to `{run_dir}/extraction.json`, with `"scope": {"type": "{scope_type}"}` when §1 bound `{scope_type}`, so step 5's `apply` can warn that a `public-api` surface was not applied; step 2 reads every listed file by eye (at Quick tier, by text pattern). Tell the user which files were read by eye.
326
315
 
327
- ```json
328
- // Category A worker returns ONLY:
329
- {"category_a": {"modified": [...], "added": [...], "deleted": [...]}}
330
- // Category B worker returns ONLY:
331
- {"category_b": {"modified_exports": [...], "new_exports": [...], "deleted_exports": [...], "moved_exports": [...]}}
332
- // Category C worker returns ONLY:
333
- {"category_c": {"renamed_files": [...], "renamed_exports": [...]}}
316
+ Then, at every tier (in degraded mode there is no map: skip it), list the map's entries the runner left out that their file still declares: the runner never reports a module, an alias that renames, a dunder such as `__version__` or another underscore name, and step 3's diff would read each as a deleted export. From `{project-root}`, run:
317
+
318
+ ```bash
319
+ uv run {buildChangeManifestHelper} baseline-gaps \
320
+ --provenance-map "{provenance_map_path}" \
321
+ --extraction "{run_dir}/extraction.json" \
322
+ --files "{run_dir}/modified-files.json" \
323
+ --source-root "{source_root}" \
324
+ -o "{run_dir}/baseline-gaps.json"
325
+ ```
326
+
327
+ It writes `{"gaps": [{name, file, line, entry, export_type}], "unchecked": [{name, file, export_type}]}`: in `gaps[]` each entry of a modified file that no export of `{run_dir}/extraction.json` holds and whose file still declares it, at the line that declares it, by the verifier's rule (audit-skill's extraction snapshot applies the same one), and in `unchecked[]` each such entry no rule can check (its file neither Python nor TS/JS, a file the rule will not read, such as a test file or one with a symlink on its path, or a dotted module name), which step 2 reads by eye. Never add a gap or drop one by eye. On exit 1 (an input it cannot read, or a `skf-verify-provenance-completeness.py` beside it that cannot be loaded), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, its stderr as `reason`): without it, every name the runner leaves out would read as deleted.
328
+
329
+ 2. **What the recipes do not record.** Workers, in parallel batches (Pattern 4) when the list is long, read the modified and added files and each return ONLY `{"exports": [...]}`, with no prose and no fences. For each function export the runner found whose `params` it left null (it could not read the signature): `export_name` and `source_file` as it wrote them, with `params` (each parameter as the source writes it, `name: type`, the form the provenance map's `params` hold) and `return_type` (null when there is none), read at its `source_line`. For each export it could not find (a name `entry_point_diff.extraction_gaps[]` lists, a name `{run_dir}/baseline-gaps.json` lists in `gaps[]`, a form Known Limitation #11 in `{extractionPatternsData}` lists, an export of a file `file_issues[]` names or that was read by eye): the same, plus `export_type`, `source_line`, `confidence: T1-low` and `extraction_method: source-read`. Record a baseline gap at its `file` and `line` (`source_line`), with, as `export_type`, its `export_type` while the declaration there still has that kind, else the kind it has now (a `variable` now declared by a `def` is a `function`, which step 3 reports as a changed kind), and the declaration's kind when the gap gives none; `params` and `return_type` as the declaration writes them, null when it has none (a module, an alias or a variable). For each `unchecked[]` entry, read its file by eye and record it the same way, at the line that declares it, only when the file still declares it. Record each name and file once: when a file read by eye whole (at Quick tier, or when the runner could not run or left the file unread) holds a name a gap or an `unchecked[]` entry lists, that read's record stands and the gap adds none. Write the union of their `exports` arrays to `{run_dir}/export-details.json`.
330
+
331
+ 3. **The diff.** Resolve `{structuralDiffHelper}` ← first existing path in `{structuralDiffProbeOrder}` and, from `{project-root}`, run:
332
+
333
+ ```bash
334
+ uv run {structuralDiffHelper} "{provenance_map_path}" "{run_dir}/extraction.json" \
335
+ --current-extra "{run_dir}/export-details.json" \
336
+ --files "{run_dir}/modified-files.json" \
337
+ -o "{run_dir}/category-b-diff.json"
338
+ ```
339
+
340
+ Exit 0 (no difference) and exit 1 (differences) both wrote the diff, which §3's helper maps onto Category B: never re-diff by eye. On exit 2, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-b"`, the helper's `error` as `reason`).
341
+
342
+ **Category C: rename detection.** The fixed rules pair Category A's `deleted` files with its `added` ones, and the diff's `removed[]` exports with its `added[]` ones and with the added files' exports, by script, never by eye. From `{project-root}`, run:
343
+
344
+ ```bash
345
+ uv run {buildChangeManifestHelper} rename-candidates \
346
+ --category-a "{run_dir}/category-a.json" \
347
+ [--category-b-diff "{run_dir}/category-b-diff.json"] \
348
+ --provenance-map "{provenance_map_path}" \
349
+ [--extraction "{run_dir}/extraction.json"] \
350
+ [--export-details "{run_dir}/export-details.json"] \
351
+ --tier "{forge_tier}" \
352
+ --source-root "{source_root}" \
353
+ [--sizes-commit "{source_commit}"] \
354
+ -o "{run_dir}/category-c.json"
334
355
  ```
335
356
 
336
- The parent merges the three slices (plus Category D and the `degraded_mode`/`update_mode` flags) into the single category-JSON object §3 pipes to the `build` helper.
357
+ Pass `--category-b-diff`, `--extraction` and `--export-details` when Category B wrote them, and `--sizes-commit` only when `{source_tree_status}` is `ready` or `offline`: in that tree the helper reads a deleted file's size at the pinned commit (`git cat-file -s`), while in a local source the file is gone and the size test is skipped. Skip Category C in degraded mode (there is no map). Content similarity above 80% (fixed, not configurable) is a rename: at Quick tier a file size within 20% and export names that overlap above 70% (export names above 80% alike), at Forge and above equal export signatures. The helper keeps a pair only when it is each side's best match, so a tie pairs nothing, and writes `category_c` (`renamed_files: [{old_path, new_path}]`, `renamed_exports: [{old_name, new_name, file, old_file?}]`, `old_file` only when it differs from `file`), the `evidence` of each pair and what it left `unpaired`. On exit 1, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-c"`, its stderr as `reason`). Category A's `moved_files[]` (same-content moves) are not Category C's to pair, and §3's helper takes each pair out of the lists it was found in.
337
358
 
338
- **Category D — Script/asset file changes:**
359
+ **CCC check (Forge+ and Deep, a local source only).** When `tools.ccc` is true and `{source_tree_status}` is neither `ready` nor `offline` (the tree init.md §6b prepared has no ccc index, and a search there could start indexing a folder step 7 deletes), you may pair what the rules left in `unpaired`: a deleted file with an added one that CCC ranks as the same code (`ccc_bridge.search` over the deleted file's export names; **Tool resolution:** `/ccc` skill search, ccc MCP or `ccc search`). This is the one judgment in Category C: add a pair only on CCC's evidence, name it in the report as a CCC pairing, and never undo a pair the rules made. Write only these pairs, with each path as `unpaired` gives it, to `{run_dir}/ccc-pairs.json` as `{"renamed_files": [{"old_path": "<deleted>", "new_path": "<added>"}]}`; the helpers below read `{run_dir}/category-c.json` themselves, so never copy its pairs.
339
360
 
340
- Run the bulk comparison once via:
361
+ **Category D: script, asset and tracked-document file changes.**
362
+
363
+ Run the bulk comparison once, keeping its output in the run folder (step 5's `apply` reads it):
341
364
 
342
365
  ```bash
343
- uv run {hashContentHelper} compare <source-root> \
344
- --provenance-map <provenance-map-path>
366
+ uv run {hashContentHelper} compare "{source_root}" \
367
+ --provenance-map "{provenance_map_path}" \
368
+ > "{run_dir}/category-d-compare.json"
345
369
  ```
346
370
 
371
+ On exit 1, no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-d"`, its stderr as `reason`).
372
+
347
373
  The helper emits:
348
374
 
349
375
  ```json
@@ -357,43 +383,51 @@ The helper emits:
357
383
  }
358
384
  ```
359
385
 
360
- Translate the helper's output into the change manifest:
361
- - `MODIFIED_FILE` rows → add to manifest as MODIFIED_FILE
362
- - `DELETED_FILE` rows → add to manifest as DELETED_FILE
363
- - `UNCHANGED` rows → omit from the manifest (no action needed)
386
+ A map with no `file_entries` field, or a null one (a skill with no script, asset or promoted document, or a map written before the field existed), tracks no file: the comparison lists no row and the set difference below subtracts nothing.
364
387
 
365
- The compare helper reports only tracked files; NEW_FILE detection (a file present in source but absent from the provenance map) is a set-difference, so it runs through a script rather than the prompt. Pipe the same deterministic detector create-skill step 3 §4c uses (resolved via `detectScriptsAssetsProbeOrder`) into `{newFileDiffHelper}`, which subtracts the provenance map's `file_entries[].source_file` and sets aside user-authored `[MANUAL]` paths:
388
+ The compare helper reports only tracked files; NEW_FILE detection (a file present in source but absent from the provenance map) is a set-difference, so it runs through a script rather than the prompt. **When `{brief_path}` does not exist** (a skill built without a brief, such as a quick skill), skip it: with no scope to keep to, the walk would read every script and asset of the repository as new. Add `new-files-not-checked: no skill brief, so no new script or asset was looked for` to `warnings[]`, write no `{run_dir}/new-files.json`, and leave `--new-files` out of §3's `build` and step 5's `apply`. Otherwise pipe the detector create-skill step 3 §4c runs (resolved via `detectScriptsAssetsProbeOrder`), which walks the whole source, into `{newFileDiffHelper}`, which keeps what the brief takes and subtracts what the map tracks:
366
389
 
367
390
  ```bash
368
- uv run {detectScriptsAssetsHelper} detect <source-root> \
369
- | uv run {newFileDiffHelper} {forge_version}/provenance-map.json
391
+ uv run {detectScriptsAssetsHelper} detect "{source_root}" \
392
+ | uv run {newFileDiffHelper} "{provenance_map_path}" [--brief "{brief_path}"] \
393
+ > "{run_dir}/new-files.json"
370
394
  ```
371
395
 
372
- It emits `{"new_files":[{source_file, kind}], "skipped_manual":[...], "already_tracked":[...], "stats":{...}}`. Add each `new_files[]` entry to the manifest as NEW_FILE — `kind` (`script`/`asset`) selects the target array. `skipped_manual[]` are user-authored files under `scripts/[MANUAL]/` or `assets/[MANUAL]/`, preserved and not touched; `already_tracked[]` were handled by the compare above.
396
+ Pass `--brief` as Category A does, `{brief_path}` as §1c left it (in a read-only run, its copy in `{run_dir}`). On exit 2 (the detector's JSON, the map or the brief it cannot read, named in its JSON `error` on stderr, which also catches a detector that printed nothing), no JSON, or no candidate resolves: HALT with status `blocked` (halt procedure: `phase: "detect-changes:category-d"`, its `error` as `reason`).
373
397
 
374
- Aggregate all subprocess results into a unified change manifest.
398
+ It writes `{"new_files":[{source_file, kind}], "skipped_manual":[...], "already_tracked":[...], "tracked_code":[...], "out_of_scope":[...], "intent_none":[...], "stats":{...}}`. §3's `build` reads both files, typing each compare row by its `file_entries[]` row's `file_type` and each new file by its `kind`: sort no row by hand. `skipped_manual[]` are user-authored files under `scripts/[MANUAL]/` or `assets/[MANUAL]/`, preserved and not touched; `already_tracked[]` were handled by the compare above; `tracked_code[]` are code files the map's `entries[]` cite, which Categories A and B compare; `out_of_scope[]` are paths the brief's scope leaves out, by Category A's in-scope test; `intent_none[]` are paths of a kind whose `scripts_intent` or `assets_intent` is `none` (an absent or free-text intent detects). A path in these lists is no new file and no `stats` count, so step 6's report counts each of the three lists from this file, where it shows Category D: a brief scoped wrongly shows as many paths out of scope. A new file Category A lists too stays a new file: Category A lists every changed in-scope file, a genuine script included.
375
399
 
376
- **If degraded mode (no provenance map):**
377
- - All source files are treated as MODIFIED
378
- - All exports will be fully re-extracted in step 03
379
- - Skip export-level comparison
400
+ **Write the category JSON** to `{run_dir}/categories.json`, where §2.2 and §3 read it beside the helper files: the two flags, `degraded_mode` and `update_mode: "normal"`. Category C stays in the files its steps wrote, which §2.2, §3 and step 5's `apply` take as `--category-c` and `--ccc-pairs`:
401
+
402
+ ```bash
403
+ cat > "{run_dir}/categories.json" <<'SKF_JSON'
404
+ {"degraded_mode": <bool>, "update_mode": "normal"}
405
+ SKF_JSON
406
+ ```
380
407
 
381
408
  #### 2.2 — Major-Version Scope Reconciliation (Post-Detection)
382
409
 
383
410
  **Purpose:** §1c catches the major-version case when an audit drift report supplies explicit candidates. §2.2 is the safety net that fires when no audit was run (or audit emitted no out-of-scope section): it inspects the just-built Category A/B results for the deletion-ratio signature of a major-version restructure and gives the user an off-ramp before §3 commits the change manifest.
384
411
 
385
- **Trigger computation:** invoke the helper with the same Category A/B/C/D JSON used for §3, plus the provenance map:
412
+ **Trigger computation:** skip §2.2 in degraded mode (there is no map). Otherwise invoke the helper with the files §3 reads, plus the provenance map:
386
413
 
387
414
  ```bash
388
- echo "{category JSON}" | uv run {buildChangeManifestHelper} deletion-ratio \
389
- --provenance-map {forge_version}/provenance-map.json
415
+ uv run {buildChangeManifestHelper} deletion-ratio \
416
+ --provenance-map "{provenance_map_path}" \
417
+ --category-a "{run_dir}/category-a.json" \
418
+ [--category-b-diff "{run_dir}/category-b-diff.json"] \
419
+ [--category-c "{run_dir}/category-c.json"] \
420
+ [--ccc-pairs "{run_dir}/ccc-pairs.json"] \
421
+ --input "{run_dir}/categories.json"
390
422
  ```
391
423
 
392
- The helper handles the three skip conditions internally — when the input has `update_mode: "gap-driven"`, `degraded_mode: true`, or the provenance has zero entries, it returns `skip_reason` set and `should_trigger: false`. `should_trigger` fires when the deletion ratio reaches the fixed bundled threshold of 50% (`ratio >= 0.50`, baked into the helper — not configurable). The output envelope:
424
+ Pass `--category-b-diff`, `--category-c` and `--ccc-pairs` when Category B, Category C and the CCC check wrote them.
425
+
426
+ The helper handles the two skip conditions internally: when the input has `degraded_mode: true`, or the provenance has zero entries, it returns `skip_reason` set and `should_trigger: false`. `should_trigger` fires when the deletion ratio reaches the fixed bundled threshold of 50% (`ratio >= 0.50`, baked into the helper, not configurable). The output envelope:
393
427
 
394
428
  ```json
395
429
  {
396
- "skip_reason": "gap-driven" | "degraded-mode" | "zero-provenance-exports" | null,
430
+ "skip_reason": "degraded-mode" | "zero-provenance-exports" | null,
397
431
  "deleted_export_count": N,
398
432
  "total_provenance_exports": N,
399
433
  "deletion_ratio": 0.X,
@@ -424,37 +458,37 @@ The upstream surface appears to have been substantially replaced. The brief's
424
458
  [A] Audit — halt and run skf-audit-skill to map the new surface, then re-run update-skill
425
459
  ```
426
460
 
427
- **Headless mode (`{headless_mode}` is true):** auto-select `[C] Continue`, log a WARN-level entry to the evidence report (`scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "headless-continue"}`), and surface the warning in step 7's report. A non-interactive run must not silently halt, but the user must be able to see the signal post-hoc. **Also append to in-context `headless_decisions[]`** (surfaced via `SKF_UPDATE_RESULT_JSON` by step 7): `{gate: "detect-changes.deletion-ratio", default_action: "C", taken_action: "C", reason: "headless: deletion-ratio threshold exceeded but no user to halt", evidence: {deletion_ratio: <ratio>, deleted_export_count: <N>, total_provenance_exports: <T>}}`.
461
+ **GATE [default: C]**: in headless mode (`{headless_mode}` is true), auto-select `[C] Continue`, append the record `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "headless-continue"}` to `{run_dir}/evidence-records.jsonl` as one JSON line keyed `scope_reconciliation_post` (step 5 §4 lists it in the evidence report), and surface the warning in step 6's report. A non-interactive run must not silently halt, but the user must be able to see the signal post-hoc. **Also record the decision**, from `{project-root}`:
462
+
463
+ ```bash
464
+ uv run {emitEnvelopeHelper} record --workflow skf-update-skill --run-dir "{run_dir}" --decision <<'SKF_JSON'
465
+ {"gate": "detect-changes.deletion-ratio", "default_action": "C", "taken_action": "C", "reason": "headless: deletion-ratio threshold exceeded but no user to halt", "evidence": {"deletion_ratio": <ratio>, "deleted_export_count": <N>, "total_provenance_exports": <T>}}
466
+ SKF_JSON
467
+ ```
428
468
 
429
469
  **Apply decision:**
430
470
 
431
- - **[C] Continue:** record `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "continue"}` and proceed to §3.
432
- - **[B] Brief:** halt with status `halted-for-brief-refinement`. Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` Change manifest discarded — no partial writes.
433
- - **[A] Audit:** halt with status `halted-for-audit`. Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface — its drift report will feed §1c on the next update-skill run."` Change manifest discarded.
471
+ - **[C] Continue:** unless the headless branch above appended its record, append `scope_reconciliation_post: {trigger: "deletion-ratio", ratio: X, decision: "continue"}` to `{run_dir}/evidence-records.jsonl` the same way, and proceed to §3.
472
+ - **[B] Brief:** HALT with status `halted-for-brief-refinement` (halt procedure: `phase: "detect-changes:deletion-ratio"`, `reason: "the user chose to refine the brief's scope: {deleted_export_count} of {total_provenance_exports} exports deleted"`). Display: `"Halting update-skill. Re-run skf-brief-skill to refine scope for {skill_name}, then re-run skf-update-skill."` The change manifest is discarded: no partial writes.
473
+ - **[A] Audit:** HALT with status `halted-for-audit` (halt procedure: `phase: "detect-changes:deletion-ratio"`, `reason: "the user chose to audit the new surface first"`). Display: `"Halting update-skill. Run skf-audit-skill against {skill_name} to map the new surface: its drift report will feed §1c on the next update-skill run."` The change manifest is discarded.
434
474
 
435
475
  ### 3. Build Change Manifest
436
476
 
437
- Hand the assembled Category A/B/C/D JSON to the helper:
477
+ Hand the helper files and the category JSON to the helper, which takes Category A from the classify output, maps the diff onto Category B, and takes each Category C pair out of the lists it was found in:
438
478
 
439
479
  ```bash
440
- echo "{category JSON}" | uv run {buildChangeManifestHelper} build
480
+ uv run {buildChangeManifestHelper} build \
481
+ [--category-a "{run_dir}/category-a.json"] \
482
+ [--category-b-diff "{run_dir}/category-b-diff.json"] \
483
+ [--category-c "{run_dir}/category-c.json"] \
484
+ [--ccc-pairs "{run_dir}/ccc-pairs.json"] \
485
+ [--file-compare "{run_dir}/category-d-compare.json" --provenance-map "{provenance_map_path}"] \
486
+ [--new-files "{run_dir}/new-files.json"] \
487
+ --input "{run_dir}/categories.json" \
488
+ > "{run_dir}/change-manifest.json"
441
489
  ```
442
490
 
443
- The category JSON shape is:
444
-
445
- ```json
446
- {
447
- "category_a": {"modified": [...], "added": [...], "deleted": [...]},
448
- "category_b": {"modified_exports": [...], "new_exports": [...],
449
- "deleted_exports": [...], "moved_exports": [...]},
450
- "category_c": {"renamed_files": [...], "renamed_exports": [...]},
451
- "category_d": {"scripts_modified": [...], "scripts_added": [...],
452
- "scripts_deleted": [...], "assets_modified": [...],
453
- "assets_added": [...], "assets_deleted": [...]},
454
- "degraded_mode": <bool>,
455
- "update_mode": "normal" | "gap-driven"
456
- }
457
- ```
491
+ Pass each helper file its category wrote. On exit 1 or no JSON: HALT with status `blocked` (halt procedure: `phase: "detect-changes:change-manifest"`, its stderr as `reason`).
458
492
 
459
493
  The helper emits the unified manifest envelope:
460
494
 
@@ -467,21 +501,23 @@ The helper emits the unified manifest envelope:
467
501
  "exports_modified": N, "exports_new": N, "exports_deleted": N,
468
502
  "exports_renamed": N, "exports_moved": N,
469
503
  "scripts_modified": N, "scripts_added": N, "scripts_deleted": N,
470
- "assets_modified": N, "assets_added": N, "assets_deleted": N
504
+ "assets_modified": N, "assets_added": N, "assets_deleted": N,
505
+ "docs_modified": N, "docs_deleted": N
471
506
  },
472
507
  "total_export_changes": N,
473
508
  "per_file": [
474
509
  {"file_path": "...", "status": "MODIFIED|ADDED|DELETED|MOVED",
475
- "exports_affected": [{name, change_type, old_line, new_line}, ...]}
476
- ]
510
+ "exports_affected": [{name, change_type, old_line, new_line, old_file?}, ...]}
511
+ ],
512
+ "category_d": {"scripts_modified": [...], ..., "docs_deleted": [...]}
477
513
  }
478
514
  ```
479
515
 
480
- `per_file` entries are sorted MODIFIED → ADDED → DELETED → MOVED, then alphabetically within each status group, so downstream stages can rely on stable ordering. MOVED entries include an extra `old_path` field. Stash the envelope as the change manifest in workflow context.
516
+ `per_file` entries are sorted MODIFIED → ADDED → DELETED → MOVED, then alphabetically within each status group, so downstream stages can rely on stable ordering. MOVED entries include an extra `old_path` field, and a `MOVED_EXPORT` across files an `old_file`, the file it left, where `apply` finds its entry. `category_d` lists the paths behind the `scripts_*`, `assets_*` and `docs_*` counts: a `docs_*` path is a tracked document, which only Category D detects, so a change to one alone is a change. `{run_dir}/change-manifest.json` is the change manifest: step 3, step 6 and step 5's `apply` read it there, so it survives a compacted context.
481
517
 
482
518
  ### 4. Check for No-Change Shortcut
483
519
 
484
- **If zero changes detected across all categories:**
520
+ **If zero changes detected across all categories** (`no_changes` is true in `{run_dir}/change-manifest.json`; for a docs-only skill, when every hashed document matched):
485
521
 
486
522
  "**No changes detected.** Source code matches provenance map exactly.
487
523
 
@@ -489,23 +525,17 @@ The skill `{skill_name}` is current — no update needed.
489
525
 
490
526
  **Skipping to report step...**"
491
527
 
492
- → Skip steps 03-06, immediately load {noChangeReportFile} with "no changes" status.
528
+ → Skip steps 3-5, immediately load {noChangeReportFile} with "no changes" status. A `--detect-only` or `--dry-run` run takes this route too and stays read-only there: report.md §1 prints its line with no result file and never runs `{onCompleteCommand}`.
493
529
 
494
530
  ### 5. Display Change Summary and Route
495
531
 
496
- "**Change Detection Complete:**
532
+ Display one line from the change manifest: "**Detected:** {modified} modified, {added} added, {deleted} deleted and {moved} moved or renamed files; {total_export_changes} exports affected." The report (step 6) shows the full counts.
497
533
 
498
- | Category | Count |
499
- |----------|-------|
500
- | Files modified | {count} |
501
- | Files added | {count} |
502
- | Files deleted | {count} |
503
- | Files moved/renamed | {count} |
504
- | Exports affected | {total_export_changes} |"
534
+ For a docs-only skill, show instead the documents that changed (`changed_urls`) and the ones that could not be fetched.
505
535
 
506
536
  This step auto-proceeds — no user choices. Once the change manifest is fully built, load and fully read the next file, then execute it, per the branch that applies:
507
537
 
508
- - **`detect_only_mode == true`** → display "**Detect-only mode — skipping re-extract/merge/validate/write.** Loading report..." and load `{noChangeReportFile}` (report.md), which emits status `detect-only`. Do not load `{nextStepFile}`.
538
+ - **`detect_only_mode == true`** → display "**Detect-only mode: skipping re-extract, merge and write.** Loading report..." and load `{noChangeReportFile}` (report.md), which emits status `detect-only`. Do not load `{nextStepFile}`.
509
539
  - **No changes detected** (section 4) → load `{noChangeReportFile}` (report.md), which emits status `no-changes`.
510
- - **Otherwise** → display "**Proceeding to re-extraction of {affected_file_count if normal mode, or gap_count if gap-driven mode} changes...**" and load `{nextStepFile}` (re-extract.md) to begin re-extraction.
540
+ - **Otherwise** → display "**Proceeding to re-extraction of {affected_file_count} changes...**" and load `{nextStepFile}` (re-extract.md) to begin re-extraction.
511
541