bmad-module-skill-forge 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +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 +135 -18
  18. package/docs/verifying-a-skill.md +83 -57
  19. package/docs/why-skf.md +24 -22
  20. package/docs/workflows.md +274 -110
  21. package/package.json +18 -10
  22. package/src/forger/preferences.yaml +8 -1
  23. package/src/knowledge/agentskills-spec.md +2 -0
  24. package/src/knowledge/ccc-bridge.md +40 -24
  25. package/src/knowledge/confidence-tiers.md +9 -5
  26. package/src/knowledge/overview.md +3 -3
  27. package/src/knowledge/progressive-capability.md +8 -8
  28. package/src/knowledge/provenance-tracking.md +9 -5
  29. package/src/knowledge/qmd-registry.md +2 -2
  30. package/src/knowledge/skf-knowledge-index.csv +2 -2
  31. package/src/knowledge/skill-lifecycle.md +18 -11
  32. package/src/knowledge/split-body-strategy.md +0 -3
  33. package/src/knowledge/tool-resolution.md +4 -2
  34. package/src/knowledge/version-paths.md +101 -15
  35. package/src/module-help.csv +3 -3
  36. package/src/module.yaml +10 -0
  37. package/src/shared/data/ast-grep-recipes.yaml +608 -0
  38. package/src/shared/data/ide-context-files.json +30 -0
  39. package/src/shared/health-check.md +32 -12
  40. package/src/shared/references/description-guard-protocol.md +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 +30 -14
  64. package/src/shared/scripts/skf-build-change-manifest.py +2203 -24
  65. package/src/shared/scripts/skf-ccc-git-hygiene.py +532 -0
  66. package/src/shared/scripts/skf-check-unit-records.py +184 -0
  67. package/src/shared/scripts/skf-check-workspace-drift.py +538 -9
  68. package/src/shared/scripts/skf-classify-changed-files.py +694 -0
  69. package/src/shared/scripts/skf-comention-pairs.py +1333 -123
  70. package/src/shared/scripts/skf-compare-file-hashes.py +1 -1
  71. package/src/shared/scripts/skf-count-imports.py +1867 -0
  72. package/src/shared/scripts/skf-count-tokens.py +1 -1
  73. package/src/shared/scripts/skf-derive-assembly-shape.py +3 -2
  74. package/src/shared/scripts/skf-description-guard.py +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 +1 -1
  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 +4084 -85
  86. package/src/shared/scripts/skf-extraction-inventory.py +746 -0
  87. package/src/shared/scripts/skf-extraction-snapshot.py +539 -0
  88. package/src/shared/scripts/skf-fetch-temporal.py +674 -0
  89. package/src/shared/scripts/skf-find-cycles.py +97 -29
  90. package/src/shared/scripts/skf-find-test-report.py +476 -0
  91. package/src/shared/scripts/skf-forge-tier-rw.py +444 -203
  92. package/src/shared/scripts/skf-github-fetch.py +365 -0
  93. package/src/shared/scripts/skf-github-probe.py +661 -0
  94. package/src/shared/scripts/skf-hash-content.py +166 -12
  95. package/src/shared/scripts/skf-language-corpora.py +1 -1
  96. package/src/shared/scripts/skf-load-provenance.py +159 -27
  97. package/src/shared/scripts/skf-manifest-ops.py +1 -1
  98. package/src/shared/scripts/skf-merge-ccc-exclusions.py +2307 -223
  99. package/src/shared/scripts/skf-merge-doc-urls.py +1 -1
  100. package/src/shared/scripts/skf-names-present.py +221 -0
  101. package/src/shared/scripts/skf-pair-intersect.py +119 -28
  102. package/src/shared/scripts/skf-parse-gaps.py +1384 -0
  103. package/src/shared/scripts/skf-preapply.py +1 -1
  104. package/src/shared/scripts/skf-preflight.py +161 -27
  105. package/src/shared/scripts/skf-promote-staged.py +430 -0
  106. package/src/shared/scripts/skf-provenance-gap-dispatch.py +39 -7
  107. package/src/shared/scripts/skf-qmd-classify-collections.py +229 -45
  108. package/src/shared/scripts/skf-quick-batch.py +774 -0
  109. package/src/shared/scripts/skf-rebuild-managed-sections.py +972 -88
  110. package/src/shared/scripts/skf-recommend-scope-type.py +323 -156
  111. package/src/shared/scripts/skf-render-metadata-stats.py +156 -12
  112. package/src/shared/scripts/skf-render-quick-metadata.py +132 -28
  113. package/src/shared/scripts/skf-render-stack-metadata.py +976 -0
  114. package/src/shared/scripts/skf-resolve-authoritative-files.py +128 -13
  115. package/src/shared/scripts/skf-resolve-package.py +523 -106
  116. package/src/shared/scripts/skf-rewrite-skill-name.py +433 -47
  117. package/src/shared/scripts/skf-run-lock.py +577 -0
  118. package/src/shared/scripts/skf-scan-manifests.py +998 -57
  119. package/src/shared/scripts/skf-scan-skill-md-structure.py +1341 -37
  120. package/src/shared/scripts/skf-severity-classify.py +712 -108
  121. package/src/shared/scripts/skf-shape-detect.py +298 -204
  122. package/src/shared/scripts/skf-shard-body.py +9 -8
  123. package/src/shared/scripts/skf-skill-inventory.py +2412 -89
  124. package/src/shared/scripts/skf-skills-module.py +472 -0
  125. package/src/shared/scripts/skf-source-tree.py +1796 -0
  126. package/src/shared/scripts/skf-structural-diff.py +670 -119
  127. package/src/shared/scripts/skf-tessl-review.py +706 -0
  128. package/src/shared/scripts/skf-update-active-symlink.py +5 -5
  129. package/src/shared/scripts/skf-update-run-state.py +624 -0
  130. package/src/shared/scripts/skf-validate-brief-inputs.py +92 -29
  131. package/src/shared/scripts/skf-validate-brief-schema.py +4 -1
  132. package/src/shared/scripts/skf-validate-feasibility-report.py +577 -66
  133. package/src/shared/scripts/skf-validate-frontmatter.py +103 -26
  134. package/src/shared/scripts/skf-validate-output.py +712 -64
  135. package/src/shared/scripts/skf-validate-pins.py +1 -1
  136. package/src/shared/scripts/skf-verify-no-trace.py +116 -10
  137. package/src/shared/scripts/skf-verify-provenance-completeness.py +2919 -54
  138. package/src/shared/scripts/skf-write-skill-brief.py +367 -19
  139. package/src/shared/tool-requirements.yaml +201 -0
  140. package/src/skf-analyze-source/SKILL.md +41 -36
  141. package/src/skf-analyze-source/assets/skill-brief-schema.md +7 -8
  142. package/src/skf-analyze-source/customize.toml +36 -24
  143. package/src/skf-analyze-source/references/auto-docs-only.md +58 -20
  144. package/src/skf-analyze-source/references/continue.md +2 -13
  145. package/src/skf-analyze-source/references/discover-additional-source.md +147 -0
  146. package/src/skf-analyze-source/references/generate-briefs.md +121 -55
  147. package/src/skf-analyze-source/references/headless-contract.md +42 -15
  148. package/src/skf-analyze-source/references/identify-units.md +96 -110
  149. package/src/skf-analyze-source/references/init.md +73 -71
  150. package/src/skf-analyze-source/references/map-and-detect.md +123 -105
  151. package/src/skf-analyze-source/references/map-unit-exports.md +68 -0
  152. package/src/skf-analyze-source/references/recommend.md +51 -70
  153. package/src/skf-analyze-source/references/scan-project.md +60 -84
  154. package/src/skf-analyze-source/references/scan-root.md +39 -0
  155. package/src/skf-analyze-source/references/step-auto-scope-coexistence.md +47 -0
  156. package/src/skf-analyze-source/references/step-auto-scope-corpora.md +53 -0
  157. package/src/skf-analyze-source/references/step-auto-scope-split.md +91 -0
  158. package/src/skf-analyze-source/references/step-auto-scope.md +208 -293
  159. package/src/skf-analyze-source/references/step-shape-detect.md +14 -12
  160. package/src/skf-analyze-source/references/unit-detection-heuristics.md +37 -42
  161. package/src/skf-audit-skill/SKILL.md +43 -51
  162. package/src/skf-audit-skill/assets/drift-report-template.md +27 -3
  163. package/src/skf-audit-skill/customize.toml +41 -20
  164. package/src/skf-audit-skill/references/constituent-freshness.md +91 -0
  165. package/src/skf-audit-skill/references/doc-drift.md +141 -0
  166. package/src/skf-audit-skill/references/headless-contract.md +29 -0
  167. package/src/skf-audit-skill/references/init.md +148 -210
  168. package/src/skf-audit-skill/references/re-index.md +96 -108
  169. package/src/skf-audit-skill/references/report.md +96 -33
  170. package/src/skf-audit-skill/references/semantic-diff.md +27 -18
  171. package/src/skf-audit-skill/references/severity-classify.md +79 -65
  172. package/src/skf-audit-skill/references/severity-rules.md +15 -6
  173. package/src/skf-audit-skill/references/structural-diff.md +75 -90
  174. package/src/skf-audit-skill/references/upstream-checkout.md +77 -0
  175. package/src/skf-audit-skill/scripts/render-drift-tables.py +685 -0
  176. package/src/skf-brief-skill/SKILL.md +36 -25
  177. package/src/skf-brief-skill/assets/description-voice-examples.md +1 -1
  178. package/src/skf-brief-skill/assets/scope-templates.md +17 -24
  179. package/src/skf-brief-skill/assets/skill-brief-schema.md +29 -15
  180. package/src/skf-brief-skill/customize.toml +48 -20
  181. package/src/skf-brief-skill/references/analyze-target.md +142 -125
  182. package/src/skf-brief-skill/references/confirm-brief.md +10 -8
  183. package/src/skf-brief-skill/references/draft-checkpoint.md +36 -17
  184. package/src/skf-brief-skill/references/gather-intent-ratify.md +68 -0
  185. package/src/skf-brief-skill/references/gather-intent.md +87 -182
  186. package/src/skf-brief-skill/references/headless-args.md +84 -16
  187. package/src/skf-brief-skill/references/health-check.md +1 -1
  188. package/src/skf-brief-skill/references/invocation-contract.md +18 -13
  189. package/src/skf-brief-skill/references/portfolio-similarity-check.md +1 -1
  190. package/src/skf-brief-skill/references/qmd-collection-registration.md +7 -10
  191. package/src/skf-brief-skill/references/scope-definition.md +123 -47
  192. package/src/skf-brief-skill/references/step-auto-brief.md +24 -80
  193. package/src/skf-brief-skill/references/step-auto-validate.md +20 -121
  194. package/src/skf-brief-skill/references/version-resolution.md +3 -2
  195. package/src/skf-brief-skill/references/write-brief.md +66 -57
  196. package/src/skf-campaign/SKILL.md +31 -34
  197. package/src/skf-campaign/assets/campaign-state-schema.json +25 -4
  198. package/src/skf-campaign/customize.toml +47 -16
  199. package/src/skf-campaign/references/campaign-contracts.md +39 -29
  200. package/src/skf-campaign/references/campaign-directive-spec.md +4 -2
  201. package/src/skf-campaign/references/health-check.md +7 -16
  202. package/src/skf-campaign/references/step-01-setup.md +46 -84
  203. package/src/skf-campaign/references/step-02-strategy.md +31 -30
  204. package/src/skf-campaign/references/step-03-pins.md +26 -18
  205. package/src/skf-campaign/references/step-04-provenance.md +24 -23
  206. package/src/skf-campaign/references/step-05-skill-loop.md +67 -48
  207. package/src/skf-campaign/references/step-06-batch.md +41 -34
  208. package/src/skf-campaign/references/step-07-capstone.md +40 -25
  209. package/src/skf-campaign/references/step-08-verify.md +28 -29
  210. package/src/skf-campaign/references/step-09-refine.md +29 -30
  211. package/src/skf-campaign/references/step-10-export.md +48 -36
  212. package/src/skf-campaign/references/step-11-maintenance.md +31 -34
  213. package/src/skf-campaign/references/step-resume.md +37 -68
  214. package/src/skf-campaign/scripts/campaign-deps.py +57 -18
  215. package/src/skf-campaign/scripts/campaign-parse-manifest.py +361 -54
  216. package/src/skf-campaign/scripts/campaign-provenance.py +96 -53
  217. package/src/skf-campaign/scripts/campaign-quality-gate.py +592 -0
  218. package/src/skf-campaign/scripts/campaign-render-batch.py +257 -49
  219. package/src/skf-campaign/scripts/campaign-render-kickoff.py +343 -50
  220. package/src/skf-campaign/scripts/campaign-report.py +204 -37
  221. package/src/skf-campaign/scripts/campaign-state.py +1128 -0
  222. package/src/skf-campaign/scripts/campaign-status.py +10 -74
  223. package/src/skf-campaign/scripts/campaign-validate-pins.py +30 -3
  224. package/src/skf-campaign/scripts/campaign-validate-state.py +34 -4
  225. package/src/skf-campaign/templates/campaign-brief-template.yaml +4 -5
  226. package/src/skf-campaign/templates/campaign-report-template.md +5 -1
  227. package/src/skf-campaign/templates/kickoff-template.md +0 -14
  228. package/src/skf-create-skill/SKILL.md +27 -20
  229. package/src/skf-create-skill/assets/compile-assembly-rules.md +6 -5
  230. package/src/skf-create-skill/assets/skill-sections.md +15 -5
  231. package/src/skf-create-skill/customize.toml +20 -8
  232. package/src/skf-create-skill/references/authoritative-files-protocol.md +41 -42
  233. package/src/skf-create-skill/references/batch-mode.md +81 -0
  234. package/src/skf-create-skill/references/ccc-index-check.md +27 -0
  235. package/src/skf-create-skill/references/compile.md +107 -29
  236. package/src/skf-create-skill/references/component-extraction.md +113 -91
  237. package/src/skf-create-skill/references/enrich.md +41 -23
  238. package/src/skf-create-skill/references/entry-points-by-hand.md +20 -0
  239. package/src/skf-create-skill/references/extract.md +200 -161
  240. package/src/skf-create-skill/references/extraction-patterns-by-hand.md +141 -0
  241. package/src/skf-create-skill/references/extraction-patterns-tracing.md +2 -2
  242. package/src/skf-create-skill/references/extraction-patterns.md +584 -196
  243. package/src/skf-create-skill/references/generate-artifacts.md +145 -93
  244. package/src/skf-create-skill/references/health-check.md +1 -1
  245. package/src/skf-create-skill/references/load-brief.md +53 -36
  246. package/src/skf-create-skill/references/relabel-rule.md +12 -0
  247. package/src/skf-create-skill/references/report.md +28 -59
  248. package/src/skf-create-skill/references/source-resolution-protocols.md +49 -155
  249. package/src/skf-create-skill/references/step-auto-shard.md +20 -93
  250. package/src/skf-create-skill/references/step-doc-rot.md +44 -60
  251. package/src/skf-create-skill/references/step-doc-sources.md +28 -19
  252. package/src/skf-create-skill/references/sub/ccc-discover.md +18 -7
  253. package/src/skf-create-skill/references/sub/fetch-docs.md +106 -59
  254. package/src/skf-create-skill/references/sub/fetch-temporal.md +53 -166
  255. package/src/skf-create-skill/references/tier-degradation-rules.md +18 -9
  256. package/src/skf-create-skill/references/validate.md +173 -99
  257. package/src/skf-create-skill/scripts/scan-doc-rot.py +232 -99
  258. package/src/skf-create-stack-skill/SKILL.md +29 -56
  259. package/src/skf-create-stack-skill/assets/metadata-contract.md +51 -0
  260. package/src/skf-create-stack-skill/assets/provenance-map-schema.md +15 -15
  261. package/src/skf-create-stack-skill/assets/stack-skill-template.md +23 -57
  262. package/src/skf-create-stack-skill/customize.toml +23 -12
  263. package/src/skf-create-stack-skill/references/compile-stack.md +57 -66
  264. package/src/skf-create-stack-skill/references/compose-mode-rules.md +24 -44
  265. package/src/skf-create-stack-skill/references/detect-integrations.md +92 -95
  266. package/src/skf-create-stack-skill/references/detect-manifests.md +46 -71
  267. package/src/skf-create-stack-skill/references/generate-output.md +138 -128
  268. package/src/skf-create-stack-skill/references/init.md +90 -32
  269. package/src/skf-create-stack-skill/references/integration-patterns.md +1 -17
  270. package/src/skf-create-stack-skill/references/invocation-contract.md +34 -0
  271. package/src/skf-create-stack-skill/references/manifest-patterns.md +32 -20
  272. package/src/skf-create-stack-skill/references/parallel-extract.md +112 -38
  273. package/src/skf-create-stack-skill/references/rank-and-confirm.md +57 -75
  274. package/src/skf-create-stack-skill/references/report.md +39 -33
  275. package/src/skf-create-stack-skill/references/validate.md +20 -39
  276. package/src/skf-drop-skill/SKILL.md +28 -51
  277. package/src/skf-drop-skill/customize.toml +44 -30
  278. package/src/skf-drop-skill/references/execute.md +79 -146
  279. package/src/skf-drop-skill/references/invocation-contract.md +64 -0
  280. package/src/skf-drop-skill/references/report.md +46 -30
  281. package/src/skf-drop-skill/references/select.md +118 -124
  282. package/src/skf-drop-skill/scripts/dir-sizes.py +54 -26
  283. package/src/skf-drop-skill/scripts/drop-roster.py +322 -0
  284. package/src/skf-export-skill/SKILL.md +40 -62
  285. package/src/skf-export-skill/assets/managed-section-format.md +14 -50
  286. package/src/skf-export-skill/assets/snippet-format.md +3 -1
  287. package/src/skf-export-skill/customize.toml +42 -13
  288. package/src/skf-export-skill/references/generate-snippet.md +54 -46
  289. package/src/skf-export-skill/references/health-check.md +2 -0
  290. package/src/skf-export-skill/references/invocation-contract.md +15 -0
  291. package/src/skf-export-skill/references/load-skill.md +88 -62
  292. package/src/skf-export-skill/references/manifest-rebuild.md +9 -10
  293. package/src/skf-export-skill/references/multi-skill-mode.md +4 -4
  294. package/src/skf-export-skill/references/orphan-context-detection.md +13 -5
  295. package/src/skf-export-skill/references/orphan-row-detection.md +31 -22
  296. package/src/skf-export-skill/references/package.md +15 -73
  297. package/src/skf-export-skill/references/preflight-snippet-root-probe.md +51 -70
  298. package/src/skf-export-skill/references/result-envelope.md +31 -16
  299. package/src/skf-export-skill/references/summary.md +41 -17
  300. package/src/skf-export-skill/references/token-report.md +4 -12
  301. package/src/skf-export-skill/references/update-context.md +118 -237
  302. package/src/skf-forger/SKILL.md +61 -37
  303. package/src/skf-forger/bmad-skill-manifest.yaml +4 -1
  304. package/src/skf-forger/customize.toml +43 -0
  305. package/src/skf-forger/references/pipeline-mode.md +73 -18
  306. package/src/skf-forger/scripts/forge-status.py +299 -0
  307. package/src/skf-forger/scripts/parse-pipeline.py +322 -54
  308. package/src/skf-forger/scripts/pipeline-gate.py +428 -0
  309. package/src/skf-forger/scripts/pipeline-journal.py +1052 -0
  310. package/src/skf-quick-skill/SKILL.md +54 -53
  311. package/src/skf-quick-skill/assets/skill-template.md +4 -2
  312. package/src/skf-quick-skill/customize.toml +51 -20
  313. package/src/skf-quick-skill/references/batch-mode.md +74 -21
  314. package/src/skf-quick-skill/references/compile.md +30 -31
  315. package/src/skf-quick-skill/references/ecosystem-check.md +7 -57
  316. package/src/skf-quick-skill/references/finalize.md +69 -18
  317. package/src/skf-quick-skill/references/halt-contract.md +41 -29
  318. package/src/skf-quick-skill/references/health-check.md +5 -3
  319. package/src/skf-quick-skill/references/quick-extract.md +114 -45
  320. package/src/skf-quick-skill/references/registry-resolution.md +9 -64
  321. package/src/skf-quick-skill/references/resolve-target.md +116 -66
  322. package/src/skf-quick-skill/references/write-and-validate.md +79 -40
  323. package/src/skf-refine-architecture/SKILL.md +43 -53
  324. package/src/skf-refine-architecture/customize.toml +49 -20
  325. package/src/skf-refine-architecture/references/compile.md +97 -54
  326. package/src/skf-refine-architecture/references/exit-codes.md +30 -0
  327. package/src/skf-refine-architecture/references/finding-storage.md +21 -0
  328. package/src/skf-refine-architecture/references/gap-analysis.md +101 -38
  329. package/src/skf-refine-architecture/references/improvements.md +14 -20
  330. package/src/skf-refine-architecture/references/init.md +98 -57
  331. package/src/skf-refine-architecture/references/issue-detection.md +32 -39
  332. package/src/skf-refine-architecture/references/refinement-rules.md +37 -48
  333. package/src/skf-refine-architecture/references/report.md +65 -54
  334. package/src/skf-refine-architecture/scripts/skf-check-preservation.py +1619 -0
  335. package/src/skf-rename-skill/SKILL.md +19 -54
  336. package/src/skf-rename-skill/customize.toml +32 -26
  337. package/src/skf-rename-skill/references/execute.md +230 -184
  338. package/src/skf-rename-skill/references/exit-codes.md +7 -7
  339. package/src/skf-rename-skill/references/health-check.md +1 -7
  340. package/src/skf-rename-skill/references/invocation-contract.md +25 -0
  341. package/src/skf-rename-skill/references/report.md +63 -52
  342. package/src/skf-rename-skill/references/select.md +197 -115
  343. package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -21
  344. package/src/skf-setup/SKILL.md +26 -26
  345. package/src/skf-setup/customize.toml +14 -10
  346. package/src/skf-setup/references/auto-index.md +50 -63
  347. package/src/skf-setup/references/ccc-index.md +29 -90
  348. package/src/skf-setup/references/detect-and-tier.md +58 -67
  349. package/src/skf-setup/references/health-check.md +2 -0
  350. package/src/skf-setup/references/invocation-contract.md +14 -0
  351. package/src/skf-setup/references/report.md +43 -166
  352. package/src/skf-setup/references/tier-rules.md +5 -5
  353. package/src/skf-setup/references/write-config.md +34 -57
  354. package/src/skf-test-skill/SKILL.md +17 -57
  355. package/src/skf-test-skill/assets/output-section-formats.md +33 -34
  356. package/src/skf-test-skill/customize.toml +27 -13
  357. package/src/skf-test-skill/references/coherence-check.md +134 -62
  358. package/src/skf-test-skill/references/coverage-check-tiers.md +35 -0
  359. package/src/skf-test-skill/references/coverage-check.md +217 -254
  360. package/src/skf-test-skill/references/detect-mode.md +5 -5
  361. package/src/skf-test-skill/references/external-validators.md +94 -82
  362. package/src/skf-test-skill/references/health-check.md +6 -7
  363. package/src/skf-test-skill/references/init.md +163 -88
  364. package/src/skf-test-skill/references/invocation-contract.md +66 -0
  365. package/src/skf-test-skill/references/migration-section-rules.md +5 -2
  366. package/src/skf-test-skill/references/report.md +162 -142
  367. package/src/skf-test-skill/references/score.md +80 -107
  368. package/src/skf-test-skill/references/scoring-rules.md +41 -155
  369. package/src/skf-test-skill/references/source-access-protocol.md +19 -92
  370. package/src/skf-test-skill/references/step-hard-gate.md +51 -33
  371. package/src/skf-test-skill/scripts/aggregate-coherence.py +218 -30
  372. package/src/skf-test-skill/scripts/build-result-context.py +287 -0
  373. package/src/skf-test-skill/scripts/check-metadata-coherence.py +37 -4
  374. package/src/skf-test-skill/scripts/combine-external-scores.py +23 -6
  375. package/src/skf-test-skill/scripts/compute-score.py +223 -45
  376. package/src/skf-test-skill/scripts/gap-ledger.py +1381 -0
  377. package/src/skf-test-skill/scripts/hard-gate.py +203 -0
  378. package/src/skf-test-skill/scripts/load-coverage-inputs.py +760 -0
  379. package/src/skf-test-skill/scripts/locate-export-segments.py +835 -0
  380. package/src/skf-test-skill/scripts/reconcile-coverage.py +273 -79
  381. package/src/skf-test-skill/scripts/score-signatures.py +459 -0
  382. package/src/skf-test-skill/scripts/stage-helper-payload.py +148 -0
  383. package/src/skf-test-skill/scripts/validate-inventory.py +311 -16
  384. package/src/skf-test-skill/scripts/verify-declared-numerator.py +127 -52
  385. package/src/skf-test-skill/templates/test-report-template.md +21 -15
  386. package/src/skf-update-skill/SKILL.md +38 -25
  387. package/src/skf-update-skill/customize.toml +27 -12
  388. package/src/skf-update-skill/references/detect-changes.md +233 -222
  389. package/src/skf-update-skill/references/gap-driven.md +336 -0
  390. package/src/skf-update-skill/references/health-check.md +31 -5
  391. package/src/skf-update-skill/references/init.md +250 -84
  392. package/src/skf-update-skill/references/invocation-contract.md +12 -0
  393. package/src/skf-update-skill/references/manual-section-rules.md +6 -23
  394. package/src/skf-update-skill/references/merge.md +180 -47
  395. package/src/skf-update-skill/references/re-extract.md +53 -262
  396. package/src/skf-update-skill/references/report.md +100 -35
  397. package/src/skf-update-skill/references/write.md +331 -149
  398. package/src/skf-update-skill/scripts/skf-new-file-diff.py +29 -7
  399. package/src/skf-verify-stack/SKILL.md +23 -42
  400. package/src/skf-verify-stack/assets/feasibility-report-template.md +11 -5
  401. package/src/skf-verify-stack/customize.toml +55 -22
  402. package/src/skf-verify-stack/references/coverage-patterns.md +17 -8
  403. package/src/skf-verify-stack/references/coverage.md +67 -64
  404. package/src/skf-verify-stack/references/exit-codes.md +21 -8
  405. package/src/skf-verify-stack/references/init.md +107 -62
  406. package/src/skf-verify-stack/references/integration-verification-rules.md +12 -13
  407. package/src/skf-verify-stack/references/integrations.md +106 -65
  408. package/src/skf-verify-stack/references/report.md +59 -42
  409. package/src/skf-verify-stack/references/requirements.md +32 -9
  410. package/src/skf-verify-stack/references/synthesize.md +53 -39
  411. package/src/skf-verify-stack/scripts/skf-coverage-tally.py +398 -52
  412. package/src/skf-verify-stack/scripts/skf-previous-report.py +330 -0
  413. package/src/skf-verify-stack/scripts/skf-report-delta.py +453 -49
  414. package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +240 -27
  415. package/tools/cli/commands/install.js +11 -4
  416. package/tools/cli/commands/status.js +15 -3
  417. package/tools/cli/commands/update.js +11 -4
  418. package/tools/cli/lib/tool-check.js +320 -0
  419. package/tools/cli/lib/version-check.js +22 -9
  420. package/tools/cli/skf-cli.js +4 -1
  421. package/.gitattributes +0 -16
  422. package/.nvmrc +0 -1
  423. package/docs/_internal/RELEASING.md +0 -679
  424. package/release-audits/v1.0.0-launch-audit.md +0 -728
  425. package/src/skf-audit-skill/references/step-doc-drift.md +0 -177
  426. package/src/skf-brief-skill/references/headless-source-authority-detection.md +0 -26
  427. package/src/skf-campaign/manifest.yaml +0 -15
  428. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +0 -106
  429. package/src/skf-create-skill/references/ecosystem-check.md +0 -74
  430. package/src/skf-drop-skill/references/headless-contract.md +0 -18
  431. package/src/skf-update-skill/references/merge-conflict-rules.md +0 -15
  432. package/src/skf-update-skill/references/remote-source-resolution.md +0 -98
  433. package/src/skf-update-skill/references/validate.md +0 -124
  434. package/tools/validate-docs-drift.js +0 -233
  435. package/tools/validate-docs-links.js +0 -665
@@ -1,16 +1,16 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Update Active Symlink — atomic + idempotent + verified flip of
6
6
  {skill_group}/active to point at a target version directory.
7
7
 
8
- `skf-update-skill/references/write.md §5b` previously asked the LLM to
8
+ update-skill's `write.md` (its active-link step) previously asked the LLM to
9
9
  "create or update the active symlink at {skill_group}/active pointing
10
10
  to the new {version}; if the symlink already exists, remove it first
11
11
  and recreate". That two-step (`rm` then `ln`) leaves a brief window
12
12
  where the symlink doesn't exist — readers that resolve `active`
13
- mid-update see a `FileNotFoundError`. §6 then re-reads the symlink
13
+ mid-update see a `FileNotFoundError`. A second check then re-reads the symlink
14
14
  to verify it points where expected and halts on divergence.
15
15
 
16
16
  This helper consolidates the two steps into one workflow-specific
@@ -142,7 +142,7 @@ def _envelope_missing_target(skill_group: Path, version: str) -> dict:
142
142
  "halt_message": (
143
143
  f"Cannot point {skill_group / ACTIVE_NAME} at `{version}` — "
144
144
  f"target directory `{skill_group / version}` does not exist on "
145
- "disk. Verify the version directory was written before §5b runs."
145
+ "disk. Verify the version directory was written before the flip runs."
146
146
  ),
147
147
  }
148
148
 
@@ -190,7 +190,7 @@ def _envelope_mismatch(skill_group: Path, version: str, current: str | None) ->
190
190
  "halt_message": (
191
191
  f"Active symlink divergence. `{skill_group / ACTIVE_NAME}` "
192
192
  f"resolves to `{cur}` but metadata.json reports `version: "
193
- f"{version}`. §5b did not apply. Re-point the symlink manually "
193
+ f"{version}`. The flip did not apply. Re-point the symlink manually "
194
194
  f"(`ln -sfn {version} {skill_group / ACTIVE_NAME}`) or re-run "
195
195
  "update-skill, then re-verify."
196
196
  ),
@@ -0,0 +1,624 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Update Run State: what an update-skill run wrote, so a halt can undo it.
6
+
7
+ An update writes the skill in one of two ways. A gap-driven repair edits the
8
+ current version in place (SKILL.md, references/, metadata.json and the
9
+ version's provenance map and evidence report, and for a rescope the skill
10
+ brief). Every other mode creates a new version folder and its forge folder
11
+ beside the unchanged previous version. Before the first write, merge.md
12
+ runs `begin`, which records in the run folder what the run is about to
13
+ change: for an in-place repair a snapshot of every file it may write, for a
14
+ new version the folders it creates. A HALT after that runs `rollback`,
15
+ which puts the skill back as it was: it restores the snapshot, or removes
16
+ the folders this run created. write.md runs `finish` once the run's writes
17
+ stand (after its last check), and from then on `rollback` changes
18
+ nothing. A run that died (its run lock went stale) never rolled
19
+ back: the next update's init.md hands the stale lock's owner to
20
+ `rollback --run-root --owner --remove-run-dir`, which finds that run's
21
+ folder by the run id in the owner. Every HALT of an update runs `halt`,
22
+ which does a halt's cleanup in one call.
23
+
24
+ CLI:
25
+
26
+ uv run skf-update-run-state.py begin --run-dir <dir> --mode in-place \\
27
+ --package <skill package> --forge-version <forge version folder> \\
28
+ [--brief <skill-brief.yaml>]
29
+ uv run skf-update-run-state.py begin --run-dir <dir> --mode new-version \\
30
+ --created <folder> [--created <folder> ...] [--staging <folder>] \\
31
+ [--skill-group <skill group>]
32
+ uv run skf-update-run-state.py rollback --run-dir <dir> [--remove-run-dir]
33
+ uv run skf-update-run-state.py rollback --run-root <.skf-run folder> \\
34
+ --owner <the stale lock's owner> --skill <skill> --remove-run-dir
35
+ uv run skf-update-run-state.py finish --run-dir <dir>
36
+ uv run skf-update-run-state.py halt --run-dir <dir> [--tree <tree>] \\
37
+ [--lock <lock file> --owner <owner>] [--emit] < <halt payload>
38
+
39
+ begin (in-place) copies the package folder to <run-dir>/snapshot/package
40
+ (each link copied as a link, never followed), the forge version folder's
41
+ provenance-map.json and evidence-report.md to <run-dir>/snapshot/forge/,
42
+ and the brief to <run-dir>/snapshot/skill-brief.yaml, noting which of the
43
+ files existed. begin (new-version) records each --created folder, which
44
+ must not exist yet (exit 3 names the first that does: a version is never
45
+ overwritten), and the --staging folder, which may (a staging folder an
46
+ interrupted run left is the stage step's to clear). Either way it writes
47
+ <run-dir>/run-state.json and refuses a run folder that already holds one.
48
+
49
+ rollback reads run-state.json. Nothing to do when there is none, when the
50
+ run finished, or when it already rolled back. In place, it rebuilds the
51
+ package from the snapshot beside it and swaps it in, restores each forge
52
+ file and the brief, and removes one the run created. For a new version it
53
+ removes each recorded folder, never a link; once the skill group's
54
+ `active` link names the run's version folder, that version is live and
55
+ every recorded folder is kept (each named, with the reason). With
56
+ --remove-run-dir it then deletes the run folder (an interrupted run's),
57
+ which must be `.skf-run/skf-update-skill-<run id>` with a run id that holds
58
+ no path separator and no `..` (exit 1 otherwise, before anything changes).
59
+ With --run-root and --owner in place of --run-dir, the run folder is
60
+ <run-root>/skf-update-skill-<run id>, the run id taken from an owner
61
+ `update-skill:<skill>:<run id>`; an owner of another workflow or another
62
+ skill, or one with no run id (an older SKF's lock), names no run folder
63
+ and changes nothing (status `no-run-folder`).
64
+
65
+ finish marks the run finished, then deletes the snapshot.
66
+
67
+ halt is a HALT's cleanup, in this order, never stopping on a result: the
68
+ rollback above when the run folder holds a run state; with --tree, the
69
+ private source tree removed (skf-source-tree.py close); with --lock and
70
+ --owner, the run lock released (skf-run-lock.py release, which never
71
+ removes a lock another run holds); with --emit, the halt payload read on
72
+ stdin printed through skf-emit-result-envelope.py emit-halt --workflow
73
+ skf-update-skill (with --run-dir when the folder still exists, so the
74
+ line carries the decisions and warnings the run recorded). Each step it
75
+ could not finish adds a warning to the payload's warnings[] (and to its
76
+ own output): `rollback-incomplete: <path>: <error>`,
77
+ `source-tree-not-removed: <tree> (<close status>)` and
78
+ `run-lock-not-released: <lock>`. The three helpers are read from beside
79
+ this script. Without --emit, stdin is not read.
80
+
81
+ Output: one JSON line,
82
+ {"status": "began" | "exists" | "rolled-back" | "nothing-to-roll-back"
83
+ | "already-rolled-back" | "finished" | "no-run-folder",
84
+ "restored": [paths], "removed": [paths], "kept": [{"path", "why"}],
85
+ "failed": [{"path", "error"}], "run_dir_removed": bool, "path": <exists>}
86
+ and for halt
87
+ {"status": "halted", "rollback": <rollback's status, or "error">,
88
+ "restored", "removed", "kept", "failed": as above,
89
+ "tree": <close's status, or null>, "lock": "released" | "absent" |
90
+ "not-owner" | "failed" | null, "warnings": [...], "emitted": bool}
91
+ followed, with --emit, by the emitter's SKF_UPDATE_RESULT_JSON line.
92
+
93
+ Exit codes:
94
+ 0 done (see status); halt: every step ran, its failures in warnings[]
95
+ 1 bad arguments, or a run-state.json that cannot be read or written;
96
+ halt: a payload that is not a JSON object, or one the emitter refused
97
+ (its message on stderr): fix the payload and run halt again, which
98
+ redoes nothing already done
99
+ 2 a copy, restore or removal failed (each named in failed[]; rollback
100
+ goes on with the rest)
101
+ 3 begin: a --created folder already exists
102
+ """
103
+
104
+ from __future__ import annotations
105
+
106
+ import argparse
107
+ import json
108
+ import os
109
+ import shutil
110
+ import stat
111
+ import subprocess
112
+ import sys
113
+ from pathlib import Path
114
+
115
+ STATE = "run-state.json"
116
+ SNAPSHOT = "snapshot"
117
+ FORGE_FILES = ("provenance-map.json", "evidence-report.md")
118
+ TOOL = "skf-update-run-state"
119
+ EXIT_FAILED = 2
120
+ EXIT_EXISTS = 3
121
+ HERE = Path(__file__).resolve().parent
122
+ SOURCE_TREE_HELPER = HERE / "skf-source-tree.py"
123
+ RUN_LOCK_HELPER = HERE / "skf-run-lock.py"
124
+ EMITTER = HERE / "skf-emit-result-envelope.py"
125
+ WORKFLOW = "skf-update-skill"
126
+ OWNER_PREFIX = "update-skill"
127
+ HELPER_TIMEOUT_SEC = 120
128
+
129
+
130
+ def _out(status: str, **fields) -> dict:
131
+ result = {"status": status, "restored": [], "removed": [], "kept": [], "failed": [],
132
+ "run_dir_removed": False}
133
+ result.update(fields)
134
+ return result
135
+
136
+
137
+ def _make_writable(func, path, _exc) -> None:
138
+ """rmtree's error handler: clear a read-only bit (Windows) and retry once."""
139
+ try:
140
+ os.chmod(path, stat.S_IWRITE)
141
+ func(path)
142
+ except OSError:
143
+ raise
144
+
145
+
146
+ def _rmtree(path: Path) -> None:
147
+ if sys.version_info >= (3, 12):
148
+ shutil.rmtree(path, onexc=_make_writable)
149
+ else: # pragma: no cover - Python 3.11
150
+ shutil.rmtree(path, onerror=_make_writable)
151
+
152
+
153
+ def _remove(path: Path) -> None:
154
+ """Remove a file, a link or a folder (the folder's content, never a link's target)."""
155
+ if path.is_symlink() or path.is_file():
156
+ path.unlink()
157
+ elif path.is_dir():
158
+ _rmtree(path)
159
+
160
+
161
+ def _copy_file(src: Path, dst: Path) -> None:
162
+ """Copy src over dst through a temporary file and one rename."""
163
+ tmp = dst.with_name(f".{dst.name}.skf-restore-tmp")
164
+ shutil.copy2(src, tmp)
165
+ os.replace(tmp, dst)
166
+
167
+
168
+ def _read_state(run_dir: Path) -> dict | None:
169
+ path = run_dir / STATE
170
+ if not path.exists():
171
+ return None
172
+ try:
173
+ state = json.loads(path.read_text(encoding="utf-8"))
174
+ except (OSError, json.JSONDecodeError) as exc:
175
+ raise ValueError(f"cannot read {path.as_posix()}: {exc}") from exc
176
+ if not isinstance(state, dict) or state.get("tool") != TOOL:
177
+ raise ValueError(f"{path.as_posix()} is not a run state this helper wrote")
178
+ return state
179
+
180
+
181
+ def _write_state(run_dir: Path, state: dict) -> None:
182
+ path = run_dir / STATE
183
+ tmp = run_dir / f".{STATE}.skf-tmp"
184
+ try:
185
+ tmp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
186
+ os.replace(tmp, path)
187
+ except OSError as exc:
188
+ raise ValueError(f"cannot write {path.as_posix()}: {exc}") from exc
189
+
190
+
191
+ def _abs(path: str) -> Path:
192
+ return Path(os.path.abspath(os.path.expanduser(path)))
193
+
194
+
195
+ # --------------------------------------------------------------------------
196
+ # begin
197
+ # --------------------------------------------------------------------------
198
+
199
+
200
+ def begin_in_place(run_dir: Path, package: Path, forge_version: Path, brief: Path | None) -> dict:
201
+ if _read_state(run_dir) is not None:
202
+ raise ValueError(f"{(run_dir / STATE).as_posix()} exists: this run already began writing")
203
+ if not package.is_dir():
204
+ raise ValueError(f"--package is not a folder: {package.as_posix()}")
205
+ snap = run_dir / SNAPSHOT
206
+ failed = []
207
+ try:
208
+ if snap.exists():
209
+ _remove(snap)
210
+ (snap / "forge").mkdir(parents=True)
211
+ shutil.copytree(package, snap / "package", symlinks=True)
212
+ except OSError as exc:
213
+ return _out("began", failed=[{"path": package.as_posix(), "error": str(exc)}])
214
+ forge_files = {}
215
+ for name in FORGE_FILES:
216
+ source = forge_version / name
217
+ forge_files[name] = source.is_file()
218
+ if source.is_file():
219
+ try:
220
+ shutil.copy2(source, snap / "forge" / name)
221
+ except OSError as exc:
222
+ failed.append({"path": source.as_posix(), "error": str(exc)})
223
+ brief_existed = bool(brief and brief.is_file())
224
+ if brief_existed:
225
+ try:
226
+ shutil.copy2(brief, snap / "skill-brief.yaml")
227
+ except OSError as exc:
228
+ failed.append({"path": brief.as_posix(), "error": str(exc)})
229
+ if failed:
230
+ return _out("began", failed=failed)
231
+ _write_state(run_dir, {
232
+ "tool": TOOL, "mode": "in-place", "finished": False, "rolled_back": False,
233
+ "snapshot": {"package": package.as_posix(), "forge_version": forge_version.as_posix(),
234
+ "forge_files": forge_files, "brief": brief.as_posix() if brief else None,
235
+ "brief_existed": brief_existed},
236
+ "created": [], "staging": None, "skill_group": None,
237
+ })
238
+ return _out("began")
239
+
240
+
241
+ def begin_new_version(run_dir: Path, created: list[Path], staging: Path | None, skill_group: Path | None) -> dict:
242
+ if _read_state(run_dir) is not None:
243
+ raise ValueError(f"{(run_dir / STATE).as_posix()} exists: this run already began writing")
244
+ for path in created:
245
+ if os.path.lexists(path):
246
+ return _out("exists", path=path.as_posix())
247
+ _write_state(run_dir, {
248
+ "tool": TOOL, "mode": "new-version", "finished": False, "rolled_back": False, "snapshot": None,
249
+ "created": [p.as_posix() for p in created], "staging": staging.as_posix() if staging else None,
250
+ "skill_group": skill_group.as_posix() if skill_group else None,
251
+ })
252
+ return _out("began")
253
+
254
+
255
+ # --------------------------------------------------------------------------
256
+ # rollback and finish
257
+ # --------------------------------------------------------------------------
258
+
259
+
260
+ def _active_target(skill_group: str | None) -> Path | None:
261
+ """The folder the group's `active` link resolves to, or None."""
262
+ if not skill_group:
263
+ return None
264
+ link = Path(skill_group) / "active"
265
+ try:
266
+ return link.resolve(strict=True) if os.path.lexists(link) else None
267
+ except OSError:
268
+ return None
269
+
270
+
271
+ def _restore_in_place(run_dir: Path, snap_state: dict, result: dict) -> None:
272
+ snap = run_dir / SNAPSHOT
273
+ package = Path(snap_state["package"])
274
+ copy = snap / "package"
275
+ if copy.is_dir():
276
+ fresh = package.with_name(f"{package.name}.skf-restore")
277
+ aside = package.with_name(f"{package.name}.skf-rollback")
278
+ try:
279
+ for leftover in (fresh, aside):
280
+ if os.path.lexists(leftover):
281
+ _remove(leftover)
282
+ shutil.copytree(copy, fresh, symlinks=True)
283
+ if os.path.lexists(package):
284
+ os.replace(package, aside)
285
+ os.replace(fresh, package)
286
+ if os.path.lexists(aside):
287
+ _remove(aside)
288
+ result["restored"].append(package.as_posix())
289
+ except OSError as exc:
290
+ result["failed"].append({"path": package.as_posix(), "error": str(exc)})
291
+ forge = Path(snap_state["forge_version"])
292
+ for name, existed in (snap_state.get("forge_files") or {}).items():
293
+ target = forge / name
294
+ try:
295
+ if existed:
296
+ _copy_file(snap / "forge" / name, target)
297
+ result["restored"].append(target.as_posix())
298
+ elif os.path.lexists(target):
299
+ _remove(target)
300
+ result["removed"].append(target.as_posix())
301
+ except OSError as exc:
302
+ result["failed"].append({"path": target.as_posix(), "error": str(exc)})
303
+ brief = snap_state.get("brief")
304
+ if brief:
305
+ target = Path(brief)
306
+ try:
307
+ if snap_state.get("brief_existed"):
308
+ _copy_file(snap / "skill-brief.yaml", target)
309
+ result["restored"].append(target.as_posix())
310
+ elif os.path.lexists(target):
311
+ _remove(target)
312
+ result["removed"].append(target.as_posix())
313
+ except OSError as exc:
314
+ result["failed"].append({"path": target.as_posix(), "error": str(exc)})
315
+
316
+
317
+ def _remove_created(state: dict, result: dict) -> None:
318
+ active = _active_target(state.get("skill_group"))
319
+ paths = list(state.get("created") or [])
320
+ if state.get("staging"):
321
+ paths.append(state["staging"])
322
+ live = active is not None and any(
323
+ os.path.lexists(raw) and not Path(raw).is_symlink() and Path(raw).resolve() == active for raw in paths)
324
+ for raw in paths:
325
+ path = Path(raw)
326
+ if not os.path.lexists(path):
327
+ continue
328
+ if path.is_symlink():
329
+ result["kept"].append({"path": path.as_posix(), "why": "a link: this run never creates one"})
330
+ continue
331
+ if live:
332
+ # The active link already names this run's version: it is live,
333
+ # and so is its forge folder.
334
+ why = "the active link names it" if path.resolve() == active else "the active link names its version"
335
+ result["kept"].append({"path": path.as_posix(), "why": why})
336
+ continue
337
+ try:
338
+ _remove(path)
339
+ result["removed"].append(path.as_posix())
340
+ except OSError as exc:
341
+ result["failed"].append({"path": path.as_posix(), "error": str(exc)})
342
+
343
+
344
+ RUN_DIR_PREFIX = "skf-update-skill-"
345
+ RUN_ROOT_NAME = ".skf-run"
346
+
347
+
348
+ def _is_update_run_dir(run_dir: Path) -> bool:
349
+ """True for `<...>/.skf-run/skf-update-skill-<run id>`: the only folder --remove-run-dir deletes.
350
+
351
+ init.md builds the path from a run id it reads in a stale lock's owner;
352
+ a run id holding a path separator or `..` must never reach a folder
353
+ outside the run root.
354
+ """
355
+ name = run_dir.name
356
+ run_id = name[len(RUN_DIR_PREFIX):]
357
+ return (name.startswith(RUN_DIR_PREFIX) and bool(run_id) and run_dir.parent.name == RUN_ROOT_NAME
358
+ and not any(ch in run_id for ch in "/\\") and ".." not in run_id)
359
+
360
+
361
+ def rollback(run_dir: Path, remove_run_dir: bool) -> dict:
362
+ if remove_run_dir and not _is_update_run_dir(run_dir):
363
+ raise ValueError(f"--remove-run-dir deletes only an update run folder (.skf-run/{RUN_DIR_PREFIX}<run id>), "
364
+ f"not {run_dir.as_posix()}")
365
+ state = _read_state(run_dir)
366
+ if state is None:
367
+ result = _out("nothing-to-roll-back")
368
+ elif state.get("finished"):
369
+ result = _out("finished")
370
+ elif state.get("rolled_back"):
371
+ result = _out("already-rolled-back")
372
+ else:
373
+ result = _out("rolled-back")
374
+ if state.get("mode") == "in-place" and isinstance(state.get("snapshot"), dict):
375
+ _restore_in_place(run_dir, state["snapshot"], result)
376
+ else:
377
+ _remove_created(state, result)
378
+ if not result["failed"]:
379
+ _write_state(run_dir, {**state, "rolled_back": True})
380
+ if remove_run_dir and not result["failed"] and run_dir.is_dir():
381
+ try:
382
+ _rmtree(run_dir)
383
+ result["run_dir_removed"] = True
384
+ except OSError as exc:
385
+ result["failed"].append({"path": run_dir.as_posix(), "error": str(exc)})
386
+ return result
387
+
388
+
389
+ def finish(run_dir: Path) -> dict:
390
+ state = _read_state(run_dir)
391
+ if state is None:
392
+ raise ValueError(f"no {STATE} in {run_dir.as_posix()}: begin never ran")
393
+ # Finished first: a crash before the snapshot goes leaves a finished run
394
+ # whose writes stand, never an unfinished one with no snapshot to restore.
395
+ _write_state(run_dir, {**state, "finished": True})
396
+ snap = run_dir / SNAPSHOT
397
+ failed = []
398
+ if snap.exists():
399
+ try:
400
+ _remove(snap)
401
+ except OSError as exc:
402
+ failed.append({"path": snap.as_posix(), "error": str(exc)})
403
+ return _out("finished", failed=failed)
404
+
405
+
406
+ def run_dir_of_owner(run_root: Path, owner: str, skill: str | None) -> Path | None:
407
+ """The run folder an update's lock owner names, or None when it names none.
408
+
409
+ An owner is `update-skill:<skill>:<run id>`; one of another workflow or
410
+ skill, or with no run id (an older SKF's lock), has no run folder here.
411
+ """
412
+ parts = owner.split(":", 2) if isinstance(owner, str) else []
413
+ if len(parts) != 3 or parts[0] != OWNER_PREFIX or not parts[2]:
414
+ return None
415
+ if skill is not None and parts[1] != skill:
416
+ return None
417
+ return run_root / f"{RUN_DIR_PREFIX}{parts[2]}"
418
+
419
+
420
+ # --------------------------------------------------------------------------
421
+ # halt
422
+ # --------------------------------------------------------------------------
423
+
424
+
425
+ def _call(helper: Path, args: list[str], stdin: str | None = None) -> tuple[int | None, str, str]:
426
+ """Run a helper beside this script: (exit code, stdout, stderr); None when it could not run."""
427
+ if not helper.is_file():
428
+ return None, "", f"{helper.name} is missing beside {Path(__file__).name}; re-install SKF"
429
+ try:
430
+ proc = subprocess.run([sys.executable, str(helper), *args], input=stdin, capture_output=True,
431
+ encoding="utf-8", errors="replace", timeout=HELPER_TIMEOUT_SEC)
432
+ except (OSError, subprocess.SubprocessError) as exc:
433
+ return None, "", f"{helper.name}: {exc}"
434
+ return proc.returncode, proc.stdout, proc.stderr
435
+
436
+
437
+ def _json_line(text: str) -> dict | None:
438
+ for line in reversed(text.strip().splitlines()):
439
+ try:
440
+ value = json.loads(line)
441
+ except json.JSONDecodeError:
442
+ continue
443
+ return value if isinstance(value, dict) else None
444
+ return None
445
+
446
+
447
+ def _first_line(text: str) -> str:
448
+ return next((line.strip() for line in text.splitlines() if line.strip()), "no output")
449
+
450
+
451
+ def halt(run_dir: Path, tree: str | None, lock: str | None, owner: str | None) -> dict:
452
+ """A HALT's cleanup: rollback, source tree, run lock. Never stops on a result."""
453
+ result = {"status": "halted", "rollback": None, "restored": [], "removed": [], "kept": [], "failed": [],
454
+ "tree": None, "lock": None, "warnings": [], "emitted": False}
455
+ try:
456
+ undone = rollback(run_dir, remove_run_dir=False)
457
+ except ValueError as exc:
458
+ result["rollback"] = "error"
459
+ result["warnings"].append(f"rollback-incomplete: {exc}")
460
+ else:
461
+ result["rollback"] = undone["status"]
462
+ for key in ("restored", "removed", "kept", "failed"):
463
+ result[key] = undone[key]
464
+ for item in undone["failed"]:
465
+ result["warnings"].append(f"rollback-incomplete: {item['path']}: {item['error']}")
466
+ if tree:
467
+ code, out, err = _call(SOURCE_TREE_HELPER, ["close", "--tree", tree])
468
+ closed = _json_line(out) if code == 0 else None
469
+ result["tree"] = closed.get("status") if closed else "failed"
470
+ if result["tree"] not in ("removed", "missing"):
471
+ result["warnings"].append(f"source-tree-not-removed: {tree} "
472
+ f"({result['tree'] if closed else _first_line(err)})")
473
+ if lock and owner:
474
+ code, out, err = _call(RUN_LOCK_HELPER, ["release", "--lock", lock, "--owner", owner])
475
+ released = _json_line(out) if code == 0 else None
476
+ if released is None:
477
+ result["lock"] = "failed"
478
+ result["warnings"].append(f"run-lock-not-released: {lock}")
479
+ else:
480
+ result["lock"] = "released" if released.get("released") else released.get("reason") or "absent"
481
+ return result
482
+
483
+
484
+ def emit_halt(run_dir: Path, payload: dict, warnings: list[str]) -> tuple[int, str, str]:
485
+ """Print the halt's line through the shared emitter: (exit code, its stdout, its stderr)."""
486
+ listed = payload.get("warnings")
487
+ payload = {**payload, "warnings": [*(listed if isinstance(listed, list) else []), *warnings]}
488
+ args = ["emit-halt", "--workflow", WORKFLOW]
489
+ if run_dir.is_dir():
490
+ args += ["--run-dir", str(run_dir)]
491
+ code, out, err = _call(EMITTER, args, stdin=json.dumps(payload))
492
+ return (1 if code is None else code), out, err
493
+
494
+
495
+ # --------------------------------------------------------------------------
496
+ # CLI
497
+ # --------------------------------------------------------------------------
498
+
499
+
500
+ def _build_parser() -> argparse.ArgumentParser:
501
+ parser = argparse.ArgumentParser(
502
+ prog="skf-update-run-state",
503
+ description="Record what an update-skill run writes, undo it on a halt, and close the window "
504
+ "once the writes stand.",
505
+ )
506
+ sub = parser.add_subparsers(dest="cmd", required=True)
507
+
508
+ p_begin = sub.add_parser("begin", help="record what the run is about to write (before the first write)")
509
+ p_begin.add_argument("--run-dir", required=True, help="the run folder")
510
+ p_begin.add_argument("--mode", required=True, choices=("in-place", "new-version"),
511
+ help="in-place (a gap-driven repair) or new-version (every other mode)")
512
+ p_begin.add_argument("--package", help="in-place: the skill package folder to snapshot")
513
+ p_begin.add_argument("--forge-version", help="in-place: the version's forge folder")
514
+ p_begin.add_argument("--brief", help="in-place: the skill brief a rescope amends")
515
+ p_begin.add_argument("--created", action="append", default=[],
516
+ help="new-version: a folder the run creates (repeat); it must not exist yet")
517
+ p_begin.add_argument("--staging", help="new-version: the staging folder, removed with the rest")
518
+ p_begin.add_argument("--skill-group", help="new-version: the skill group, whose active link is never removed")
519
+
520
+ p_rollback = sub.add_parser("rollback", help="undo what the run wrote, unless it finished")
521
+ p_rollback.add_argument("--run-dir", help="the run folder")
522
+ p_rollback.add_argument("--run-root", help="in place of --run-dir: the .skf-run folder, with --owner")
523
+ p_rollback.add_argument("--owner", help="with --run-root: the stale lock's owner, update-skill:<skill>:<run id>")
524
+ p_rollback.add_argument("--skill", help="with --run-root: the skill the owner must name")
525
+ p_rollback.add_argument("--remove-run-dir", action="store_true",
526
+ help="then delete the run folder (an interrupted run's)")
527
+
528
+ p_finish = sub.add_parser("finish", help="the writes stand: close the rollback window, drop the snapshot")
529
+ p_finish.add_argument("--run-dir", required=True, help="the run folder")
530
+
531
+ p_halt = sub.add_parser("halt", help="a HALT's cleanup: rollback, source tree, run lock, then the halt's line")
532
+ p_halt.add_argument("--run-dir", required=True, help="the run folder")
533
+ p_halt.add_argument("--tree", help="the private source tree to remove (skf-source-tree.py open's tree)")
534
+ p_halt.add_argument("--lock", help="the run lock file to release, with --owner")
535
+ p_halt.add_argument("--owner", help="the run lock's owner, as acquire printed it")
536
+ p_halt.add_argument("--emit", action="store_true",
537
+ help="read the halt payload on stdin and print its line through the shared emitter")
538
+ return parser
539
+
540
+
541
+ def _cmd_halt(args: argparse.Namespace, run_dir: Path) -> int:
542
+ if bool(args.lock) != bool(args.owner):
543
+ raise ValueError("--lock and --owner go together")
544
+ payload = None
545
+ if args.emit:
546
+ try:
547
+ payload = json.loads(sys.stdin.read())
548
+ except json.JSONDecodeError as exc:
549
+ raise ValueError(f"the halt payload on stdin is not JSON: {exc}") from exc
550
+ if not isinstance(payload, dict):
551
+ raise ValueError("the halt payload on stdin must be a JSON object")
552
+ result = halt(run_dir, args.tree, args.lock, args.owner)
553
+ if payload is None:
554
+ print(json.dumps(result))
555
+ return 0
556
+ code, out, err = emit_halt(run_dir, payload, result["warnings"])
557
+ result["emitted"] = code == 0
558
+ print(json.dumps(result))
559
+ sys.stdout.write(out)
560
+ sys.stdout.flush()
561
+ if err:
562
+ sys.stderr.write(err)
563
+ return 0 if code == 0 else 1
564
+
565
+
566
+ def main(argv: list[str] | None = None) -> int:
567
+ parser = _build_parser()
568
+ args = parser.parse_args(argv)
569
+ try:
570
+ if args.cmd == "rollback" and args.run_root:
571
+ if args.run_dir or not args.owner:
572
+ raise ValueError("--run-root takes --owner (and --skill), in place of --run-dir")
573
+ found = run_dir_of_owner(_abs(args.run_root), args.owner, args.skill)
574
+ if found is None:
575
+ print(json.dumps(_out("no-run-folder")))
576
+ return 0
577
+ run_dir = found
578
+ elif args.cmd == "rollback" and (not args.run_dir or args.owner or args.skill):
579
+ raise ValueError("rollback takes --run-dir, or --run-root with --owner")
580
+ else:
581
+ run_dir = _abs(args.run_dir)
582
+ if args.cmd == "halt":
583
+ return _cmd_halt(args, run_dir)
584
+ if args.cmd == "begin":
585
+ if not run_dir.is_dir():
586
+ raise ValueError(f"--run-dir is not a folder: {run_dir.as_posix()}")
587
+ if args.mode == "in-place":
588
+ if not args.package or not args.forge_version or args.created or args.staging:
589
+ raise ValueError("--mode in-place takes --package and --forge-version (and --brief)")
590
+ result = begin_in_place(run_dir, _abs(args.package), _abs(args.forge_version),
591
+ _abs(args.brief) if args.brief else None)
592
+ else:
593
+ if not args.created or args.package or args.forge_version or args.brief:
594
+ raise ValueError("--mode new-version takes --created (and --staging, --skill-group)")
595
+ result = begin_new_version(run_dir, [_abs(p) for p in args.created],
596
+ _abs(args.staging) if args.staging else None,
597
+ _abs(args.skill_group) if args.skill_group else None)
598
+ elif args.cmd == "rollback":
599
+ result = rollback(run_dir, args.remove_run_dir)
600
+ else:
601
+ result = finish(run_dir)
602
+ except ValueError as exc:
603
+ print(json.dumps({"status": "error", "message": str(exc)}), file=sys.stderr)
604
+ return 1
605
+ print(json.dumps(result))
606
+ if result["status"] == "exists":
607
+ return EXIT_EXISTS
608
+ return EXIT_FAILED if result["failed"] else 0
609
+
610
+
611
+ def _force_utf8(*streams) -> None:
612
+ """Reconfigure the JSON streams to UTF-8 (a Windows console uses cp1252)."""
613
+ for stream in streams:
614
+ if hasattr(stream, "reconfigure"):
615
+ errors = getattr(stream, "errors", None)
616
+ if errors is None:
617
+ stream.reconfigure(encoding="utf-8")
618
+ else:
619
+ stream.reconfigure(encoding="utf-8", errors=errors)
620
+
621
+
622
+ if __name__ == "__main__":
623
+ _force_utf8(sys.stdin, sys.stdout, sys.stderr)
624
+ sys.exit(main())