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,10 +1,10 @@
1
1
  # /// script
2
- # requires-python = ">=3.10"
2
+ # requires-python = ">=3.11"
3
3
  # dependencies = []
4
4
  # ///
5
5
  """SKF Check Workspace Drift — pre-flight guard for gap-driven update-skill runs.
6
6
 
7
- `skf-update-skill/references/re-extract.md §0.a` defines a four-state guard:
7
+ `skf-update-skill/references/gap-driven.md §3` defines a four-state guard:
8
8
  the workspace at `source_root` must point at the commit the skill was pinned
9
9
  against (`metadata.source_commit`), otherwise gap-driven spot-checks read
10
10
  bytes that differ from the pinned tree and silently produce wrong results
@@ -14,13 +14,22 @@ code).
14
14
  The guard's logic is deterministic — three git commands and a comparison —
15
15
  but the prose form asked the LLM to chain them per run, with subtle short-
16
16
  SHA prefix matching and skip-paths for non-git workspaces. This script bakes
17
- the dispatch in.
17
+ the dispatch in. test-skill runs the same guard (`--workflow test-skill`,
18
+ one call per repository of a stack skill), and audit-skill asks the
19
+ `upstream` command below whether the remote moved past the skill's commit.
20
+
21
+ git runs with the git location variables (GIT_DIR, GIT_INDEX_FILE, ...)
22
+ removed from its environment. A git hook or `git rebase --exec` exports
23
+ them (GIT_DIR too when run in a linked worktree); inherited by
24
+ `git -C <source-root>`, they would make the check read another
25
+ repository's HEAD.
18
26
 
19
27
  CLI:
20
28
  uv run skf-check-workspace-drift.py <source-root> \\
21
29
  --pinned-commit <SHA or empty> \\
22
30
  [--source-ref <ref>] \\
23
- [--allow-drift]
31
+ [--allow-drift] \\
32
+ [--workflow update-skill|test-skill]
24
33
 
25
34
  Inputs:
26
35
  source_root Filesystem path to the workspace under test.
@@ -34,6 +43,10 @@ Inputs:
34
43
  --allow-drift Suppress the drift halt. Mismatches still return
35
44
  status="overridden" so the caller can surface a
36
45
  warning in the final report.
46
+ --workflow The workflow the halt message speaks for (WORKFLOWS):
47
+ update-skill (the default) or test-skill, which
48
+ checks each repository of a stack skill with its own
49
+ call. It changes halt_message only.
37
50
 
38
51
  Output (JSON on stdout):
39
52
  {
@@ -53,12 +66,91 @@ Exit codes:
53
66
  2 — drift detected and --allow-drift was NOT passed; caller MUST halt
54
67
  with status="halted-for-workspace-drift". The halt_message field
55
68
  is pre-formatted with all substitutions applied.
69
+
70
+ upstream: has the remote moved past the baseline?
71
+
72
+ uv run skf-check-workspace-drift.py upstream --source-root <path> \\
73
+ --baseline-commit <SHA> --baseline-ref <ref> [--timeout <seconds>]
74
+
75
+ A first argument `upstream` selects this command; a source root that is
76
+ a folder named `upstream` in the current folder is passed as
77
+ `./upstream` to the check above.
78
+
79
+ One `git ls-remote --symref origin` call reads the remote's default
80
+ branch, its tags (an annotated tag resolves to its commit) and the
81
+ baseline branch. Nothing is fetched and nothing in the clone changes, so
82
+ a `--depth 1 --branch <tag>` clone, which has no `origin/HEAD`, is read
83
+ like any other. The baseline ref decides what "moved" means:
84
+
85
+ - A tag whose name ends in a version (`v1.2.3`, `1.2.3-rc.1`,
86
+ `pkg@1.2.3`, `tokio/v1.0.0`): the newest tag of its family, a release
87
+ unless the baseline is a pre-release, sorted by version. A monorepo
88
+ prefix (`pkg@`, `tokio/v`) is a family of its own. The release
89
+ prefixes, none, `v` and `V` (RELEASE_PREFIXES), are one family, since
90
+ a project may switch between them (express went from `4.22.0` to
91
+ `v4.22.1`); a tag under another of them than the baseline's counts
92
+ only when its version has a major and a minor number, so a date or
93
+ build number (`20240101`, `1234`) does not. The remote moved when that
94
+ tag names another commit and is newer than the baseline, or when no
95
+ newer tag exists and the baseline tag itself now names another commit,
96
+ unless `git merge-base --is-ancestor` finds that commit in the
97
+ baseline's history (it cannot in a shallow clone that lacks the
98
+ commit, so version order decides there). Commits on the default branch
99
+ after the tag are not a move.
100
+ - Any other tag: the commit it names now.
101
+ - A branch: the commit the remote branch names now.
102
+ - `HEAD` or a commit: the remote's default branch.
103
+
104
+ Output (JSON on stdout; every key is always present):
105
+ status "unchanged" | "moved" | "skipped" | "fetch-failed"
106
+ skip_reason null | "no-baseline-ref" | "no-baseline-commit" |
107
+ "git-unavailable" | "not-a-git-tree" |
108
+ "invalid-baseline-ref" | "baseline-ref-missing"
109
+ (UPSTREAM_SKIP_REASONS)
110
+ fetch_error git's last error line (fetch-failed), else null
111
+ basis null | "same-commit" | "no-newer-tag" |
112
+ "contained" | "newer-tag" | "tag-moved" |
113
+ "ref-moved" (UPSTREAM_BASES): what decided it
114
+ baseline_ref as given
115
+ ref_kind "tag" | "branch" | "head" | "commit" | null
116
+ baseline_commit the full commit when the clone holds it, else as given
117
+ baseline_commit_short
118
+ remote_head the commit of the remote's default branch, else null
119
+ remote_head_short
120
+ remote_default_branch its name ("main"), else null
121
+ latest_tag the newest tag of the baseline's family (for a
122
+ branch, HEAD or commit: of a release prefix), else null
123
+ latest_tag_commit and its commit, else null
124
+ latest_tag_commit_short
125
+ upstream_ref moved: the ref to read instead (the newer tag, the
126
+ moved tag or branch, or "HEAD"), else null
127
+ upstream_commit moved: its commit, else null
128
+ upstream_commit_short
129
+ audit_ref the values an audit that stays on the baseline
130
+ audit_ref_source records: baseline_ref or "(unknown)", "baseline"
131
+ audit_commit ("unavailable" without ref and commit), and
132
+ baseline_commit or "(unknown)"
133
+ log_message "upstream_check: <status> (...)"
134
+ warnings list of strings
135
+
136
+ A baseline commit that is not 7 to 40 hex digits counts as missing.
137
+
138
+ Exit codes: 0 once the JSON is printed (it never gates; the caller
139
+ owns the choice), 1 unexpected error (one JSON line on stderr), 2 usage
140
+ error. git runs through skf-source-tree.py (the sibling in this folder,
141
+ which must sit beside this script) with LC_ALL=C, no hooks, no terminal
142
+ or credential-manager prompt and the git location variables removed. A
143
+ remote query still running after --timeout seconds (60 by default) is
144
+ stopped with everything git started, and reported as fetch-failed.
56
145
  """
57
146
 
58
147
  from __future__ import annotations
59
148
 
60
149
  import argparse
150
+ import importlib.util
61
151
  import json
152
+ import os
153
+ import re
62
154
  import shutil
63
155
  import subprocess
64
156
  import sys
@@ -67,6 +159,17 @@ from pathlib import Path
67
159
 
68
160
  SKIP_NO_PINNED = "no-pinned-commit"
69
161
  SKIP_NOT_GIT = "not-a-git-tree"
162
+ # Keep identical to GIT_LOCATION_VARS in skf-merge-ccc-exclusions.py.
163
+ GIT_LOCATION_VARS = (
164
+ "GIT_DIR",
165
+ "GIT_WORK_TREE",
166
+ "GIT_INDEX_FILE",
167
+ "GIT_PREFIX",
168
+ "GIT_COMMON_DIR",
169
+ "GIT_OBJECT_DIRECTORY",
170
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
171
+ "GIT_NAMESPACE",
172
+ )
70
173
 
71
174
 
72
175
  # --------------------------------------------------------------------------
@@ -75,12 +178,18 @@ SKIP_NOT_GIT = "not-a-git-tree"
75
178
 
76
179
 
77
180
  def _git(args: list[str], *, cwd: Path) -> tuple[int, str, str]:
78
- """Run a git command; return (rc, stdout, stderr). Stdout/stderr stripped."""
181
+ """Run a git command; return (rc, stdout, stderr). Stdout/stderr stripped.
182
+
183
+ The git location variables are dropped from the child's environment
184
+ (see the module docstring).
185
+ """
186
+ env = {k: v for k, v in os.environ.items() if k not in GIT_LOCATION_VARS}
79
187
  proc = subprocess.run(
80
188
  ["git", "-C", str(cwd), *args],
81
189
  capture_output=True,
82
190
  text=True,
83
191
  check=False,
192
+ env=env,
84
193
  )
85
194
  return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
86
195
 
@@ -123,8 +232,8 @@ def classify_match(pinned: str, head: str) -> str | None:
123
232
  return None
124
233
 
125
234
 
126
- def short(sha: str) -> str:
127
- return sha[:7]
235
+ def short(sha: str | None) -> str | None:
236
+ return sha[:7] if sha else None
128
237
 
129
238
 
130
239
  # --------------------------------------------------------------------------
@@ -149,12 +258,39 @@ that spot-checks will read bytes that differ from the pinned commit),
149
258
  re-run update-skill with `--allow-workspace-drift`."""
150
259
 
151
260
 
261
+ TEST_HALT_MESSAGE_TEMPLATE = """\
262
+ Workspace HEAD does not match the commit this skill was pinned against.
263
+
264
+ pinned (metadata.source_commit): {pinned_commit}
265
+ pinned ref (metadata.source_ref): {source_ref}
266
+ workspace HEAD ({source_root}): {head_sha}
267
+
268
+ Test-skill verifies against the source the skill was extracted from.
269
+ Testing against a drifted tree produces false gaps and mismatches. Re-sync:
270
+
271
+ git -C "{source_root}" checkout {checkout_target}
272
+
273
+ Or re-run test-skill with `--allow-workspace-drift` to test against the
274
+ current workspace (accepts that findings reflect HEAD, not the pin)."""
275
+
276
+ HALT_MESSAGES = {
277
+ "update-skill": HALT_MESSAGE_TEMPLATE,
278
+ "test-skill": TEST_HALT_MESSAGE_TEMPLATE,
279
+ }
280
+ WORKFLOWS = tuple(HALT_MESSAGES)
281
+
282
+
152
283
  def build_halt_message(
153
- *, source_root: Path, pinned_commit: str, head_sha_full: str, source_ref: str | None
284
+ *,
285
+ source_root: Path,
286
+ pinned_commit: str,
287
+ head_sha_full: str,
288
+ source_ref: str | None,
289
+ workflow: str = "update-skill",
154
290
  ) -> str:
155
291
  ref_display = source_ref if source_ref else "unset"
156
292
  checkout_target = source_ref if source_ref else pinned_commit
157
- return HALT_MESSAGE_TEMPLATE.format(
293
+ return HALT_MESSAGES[workflow].format(
158
294
  pinned_commit=pinned_commit,
159
295
  source_ref=ref_display,
160
296
  source_root=str(source_root),
@@ -174,6 +310,7 @@ def check(
174
310
  pinned_commit: str,
175
311
  source_ref: str | None,
176
312
  allow_drift: bool,
313
+ workflow: str = "update-skill",
177
314
  ) -> dict:
178
315
  """Run the four-state guard and return a result envelope."""
179
316
  if is_skippable_pinned(pinned_commit):
@@ -234,6 +371,7 @@ def check(
234
371
  pinned_commit=pinned_commit,
235
372
  head_sha_full=head,
236
373
  source_ref=source_ref,
374
+ workflow=workflow,
237
375
  )
238
376
  if allow_drift:
239
377
  return {
@@ -265,12 +403,396 @@ def check(
265
403
  }
266
404
 
267
405
 
406
+ # --------------------------------------------------------------------------
407
+ # upstream
408
+ # --------------------------------------------------------------------------
409
+
410
+
411
+ UPSTREAM_SKIP_REASONS = (
412
+ "no-baseline-ref",
413
+ "no-baseline-commit",
414
+ "git-unavailable",
415
+ "not-a-git-tree",
416
+ "invalid-baseline-ref",
417
+ "baseline-ref-missing",
418
+ )
419
+ UPSTREAM_BASES = (
420
+ "same-commit",
421
+ "no-newer-tag",
422
+ "contained",
423
+ "newer-tag",
424
+ "tag-moved",
425
+ "ref-moved",
426
+ )
427
+ DEFAULT_UPSTREAM_TIMEOUT_SEC = 60.0
428
+ LOCAL_TIMEOUT_SEC = 30.0
429
+ # The version prefixes of release tags (`4.22.0`, `v4.22.1`, `V2.0`): one
430
+ # family, since a project may switch from one to another.
431
+ RELEASE_PREFIXES = ("", "v", "V")
432
+ _MAJOR_MINOR_RE = re.compile(r"\d+\.\d+")
433
+ _UNSET = {"", "null", "none", "local"}
434
+ _HEAD_REFS = {"head"}
435
+ _SOURCE_TREE = None
436
+
437
+
438
+ def _sibling():
439
+ """skf-source-tree.py from this folder, loaded once: upstream runs git
440
+ through its runner (_run stops a call and all it started at the time
441
+ limit; _resolve_outside_cwd never takes a git planted in the current
442
+ folder), sorts tags with its split_version and shares its SHA40_RE,
443
+ REF_RE, _last_error and _stat_dir."""
444
+ global _SOURCE_TREE
445
+ if _SOURCE_TREE is None:
446
+ path = Path(__file__).resolve().parent / "skf-source-tree.py"
447
+ spec = importlib.util.spec_from_file_location("skf_source_tree", path)
448
+ if spec is None or spec.loader is None:
449
+ raise ImportError(f"cannot load {path}")
450
+ module = importlib.util.module_from_spec(spec)
451
+ spec.loader.exec_module(module)
452
+ _SOURCE_TREE = module
453
+ return _SOURCE_TREE
454
+
455
+
456
+ def _git_capped(cwd: Path, *args: str, timeout: float) -> tuple[int | None, str, str]:
457
+ """`git -C cwd <args>` with no hooks, no prompts and a time limit.
458
+
459
+ (returncode, stdout, stderr): returncode None when git cannot start or
460
+ the time limit stopped it (stderr then says which).
461
+ """
462
+ tree = _sibling()
463
+ exe = tree._resolve_outside_cwd("git")
464
+ if exe is None:
465
+ return None, "", "git is not installed"
466
+ argv = [exe, "-c", f"core.hooksPath={os.devnull}", "-C", str(cwd), *args]
467
+ try:
468
+ rc, out, err, _killed = tree._run(argv, max(0.1, timeout))
469
+ except (OSError, ValueError) as e:
470
+ return None, "", f"git could not run: {e}"
471
+ if rc is None:
472
+ return None, "", f"timed out after {timeout:g} seconds"
473
+ return (
474
+ rc,
475
+ out.decode("utf-8", errors="replace"),
476
+ err.decode("utf-8", errors="replace"),
477
+ )
478
+
479
+
480
+ def _parse_ls_remote(text: str) -> tuple[dict, dict, dict]:
481
+ """(refs, tags, symrefs) of `git ls-remote --symref` output.
482
+
483
+ tags maps a tag name to the commit it names: the peeled `^{}` line of
484
+ an annotated tag wins over the tag object.
485
+ """
486
+ sha40 = _sibling().SHA40_RE
487
+ refs, tags, symrefs = {}, {}, {}
488
+ for line in text.splitlines():
489
+ left, _, name = line.partition("\t")
490
+ left, name = left.strip(), name.strip()
491
+ if not name:
492
+ continue
493
+ if left.startswith("ref: "):
494
+ symrefs[name] = left[len("ref: ") :].strip()
495
+ elif sha40.match(left):
496
+ refs[name] = left
497
+ if name.startswith("refs/tags/"):
498
+ tag = name[len("refs/tags/") :]
499
+ if tag.endswith("^{}"):
500
+ tags[tag[:-3]] = left
501
+ else:
502
+ tags.setdefault(tag, left)
503
+ return refs, tags, symrefs
504
+
505
+
506
+ def _family(prefix: str):
507
+ """The test a tag passes, given its name and version prefix, to count
508
+ as a release of a baseline tag whose version prefix is `prefix`.
509
+
510
+ A monorepo prefix (`pkg@`, `tokio/v`) counts only itself. A release
511
+ prefix counts the other two as well, for a tag whose version has a
512
+ major and a minor number: a date or build number (`20240101`, `1234`)
513
+ under another release prefix never counts.
514
+ """
515
+ if prefix not in RELEASE_PREFIXES:
516
+ return lambda name, tag_prefix: tag_prefix == prefix
517
+ return lambda name, tag_prefix: tag_prefix == prefix or (
518
+ tag_prefix in RELEASE_PREFIXES
519
+ and _MAJOR_MINOR_RE.match(name, len(tag_prefix)) is not None
520
+ )
521
+
522
+
523
+ def _newest(tags: dict, counts, releases_only: bool) -> tuple[str | None, tuple | None]:
524
+ """(name, key) of the newest tag counts(name, prefix) takes, else (None, None)."""
525
+ best, best_key = None, None
526
+ for name in tags:
527
+ split = _sibling().split_version(name)
528
+ if split is None or not counts(name, split[0]):
529
+ continue
530
+ if releases_only and split[1][1] == 0:
531
+ continue
532
+ if best is None or (split[1], name) > (best_key, best):
533
+ best, best_key = name, split[1]
534
+ return best, best_key
535
+
536
+
537
+ def upstream(
538
+ source_root: Path,
539
+ *,
540
+ baseline_commit: str,
541
+ baseline_ref: str,
542
+ timeout: float = DEFAULT_UPSTREAM_TIMEOUT_SEC,
543
+ ) -> dict:
544
+ """Compare the baseline with what the remote names now (module docstring)."""
545
+ ref = (baseline_ref or "").strip()
546
+ commit = (baseline_commit or "").strip()
547
+ ref_unset = ref.lower() in _UNSET
548
+ # "", "local", "null" and "none" are not 7 to 40 hex digits either.
549
+ commit_unset = not re.fullmatch(r"[0-9A-Fa-f]{7,40}", commit)
550
+ out = {
551
+ "status": "skipped",
552
+ "skip_reason": None,
553
+ "fetch_error": None,
554
+ "basis": None,
555
+ "baseline_ref": baseline_ref,
556
+ "ref_kind": None,
557
+ "baseline_commit": baseline_commit,
558
+ "baseline_commit_short": None if commit_unset else short(commit),
559
+ "remote_head": None,
560
+ "remote_head_short": None,
561
+ "remote_default_branch": None,
562
+ "latest_tag": None,
563
+ "latest_tag_commit": None,
564
+ "latest_tag_commit_short": None,
565
+ "upstream_ref": None,
566
+ "upstream_commit": None,
567
+ "upstream_commit_short": None,
568
+ "audit_ref": "(unknown)" if ref_unset else ref,
569
+ "audit_ref_source": "unavailable" if ref_unset and commit_unset else "baseline",
570
+ "audit_commit": "(unknown)" if commit_unset else commit,
571
+ "log_message": "",
572
+ "warnings": [],
573
+ }
574
+
575
+ def done(status: str, reason: str | None = None) -> dict:
576
+ out["status"] = status
577
+ out["skip_reason"] = reason if status == "skipped" else None
578
+ if status == "skipped":
579
+ detail = reason
580
+ elif status == "fetch-failed":
581
+ detail = out["fetch_error"]
582
+ elif status == "moved":
583
+ detail = (
584
+ f"{ref} {out['baseline_commit_short']} -> "
585
+ f"{out['upstream_ref']} {out['upstream_commit_short']}"
586
+ )
587
+ else:
588
+ detail = f"{ref} at {out['baseline_commit_short']}"
589
+ out["log_message"] = f"upstream_check: {status} ({detail})"
590
+ return out
591
+
592
+ if ref_unset:
593
+ return done("skipped", "no-baseline-ref")
594
+ if commit_unset:
595
+ return done("skipped", "no-baseline-commit")
596
+ tree = _sibling()
597
+ if tree._resolve_outside_cwd("git") is None:
598
+ return done("skipped", "git-unavailable")
599
+ # os.stat, not pathlib: before Python 3.14, is_dir() raises inside a
600
+ # folder the user cannot search.
601
+ if tree._stat_dir(source_root) is not True:
602
+ return done("skipped", "not-a-git-tree")
603
+ rc, text, _err = _git_capped(
604
+ source_root, "rev-parse", "--is-inside-work-tree", timeout=LOCAL_TIMEOUT_SEC
605
+ )
606
+ if rc != 0 or text.strip() != "true":
607
+ return done("skipped", "not-a-git-tree")
608
+ head_ref = ref.lower() in _HEAD_REFS
609
+ if not head_ref and (
610
+ not tree.REF_RE.fullmatch(ref) or ".." in ref or ref.endswith((".", "/"))
611
+ ):
612
+ return done("skipped", "invalid-baseline-ref")
613
+
614
+ lowered = commit.lower()
615
+ rc, text, _err = _git_capped(
616
+ source_root,
617
+ "rev-parse",
618
+ "--verify",
619
+ "--quiet",
620
+ f"{lowered}^{{commit}}",
621
+ timeout=LOCAL_TIMEOUT_SEC,
622
+ )
623
+ full = text.strip() if rc == 0 and tree.SHA40_RE.match(text.strip()) else None
624
+ if full and classify_match(lowered, full):
625
+ out.update(
626
+ baseline_commit=full,
627
+ baseline_commit_short=short(full),
628
+ audit_commit=full,
629
+ )
630
+ base = full or lowered
631
+
632
+ patterns = ["HEAD", "refs/tags/*"]
633
+ if not head_ref:
634
+ patterns.append(f"refs/heads/{ref}")
635
+ rc, text, err = _git_capped(
636
+ source_root, "ls-remote", "--symref", "--", "origin", *patterns, timeout=timeout
637
+ )
638
+ if rc != 0:
639
+ out["fetch_error"] = tree._last_error(err) if rc is not None else err
640
+ return done("fetch-failed")
641
+ refs, tags, symrefs = _parse_ls_remote(text)
642
+ remote_head = refs.get("HEAD")
643
+ out.update(remote_head=remote_head, remote_head_short=short(remote_head))
644
+ head_target = symrefs.get("HEAD", "")
645
+ if head_target.startswith("refs/heads/"):
646
+ out["remote_default_branch"] = head_target[len("refs/heads/") :]
647
+
648
+ def newer(name: str | None, sha: str | None, basis: str) -> dict:
649
+ out.update(
650
+ upstream_ref=name,
651
+ upstream_commit=sha,
652
+ upstream_commit_short=short(sha),
653
+ basis=basis,
654
+ )
655
+ return done("moved")
656
+
657
+ def same(basis: str) -> dict:
658
+ out["basis"] = basis
659
+ return done("unchanged")
660
+
661
+ def show_latest(name: str) -> None:
662
+ out.update(
663
+ latest_tag=name,
664
+ latest_tag_commit=tags[name],
665
+ latest_tag_commit_short=short(tags[name]),
666
+ )
667
+
668
+ split = tree.split_version(ref)
669
+ if ref in tags:
670
+ kind = "tag"
671
+ elif f"refs/heads/{ref}" in refs:
672
+ kind = "branch"
673
+ elif head_ref:
674
+ kind = "head"
675
+ elif re.fullmatch(r"[0-9a-fA-F]{7,40}", ref):
676
+ kind = "commit"
677
+ elif split is not None:
678
+ kind = "tag" # deleted upstream: its newer siblings still count
679
+ out["warnings"].append(f"the baseline tag {ref} no longer exists upstream")
680
+ else:
681
+ return done("skipped", "baseline-ref-missing")
682
+ out["ref_kind"] = kind
683
+
684
+ if kind != "tag":
685
+ latest, _key = _newest(
686
+ tags, lambda _name, prefix: prefix in RELEASE_PREFIXES, releases_only=True
687
+ )
688
+ if latest:
689
+ show_latest(latest)
690
+ if kind == "branch":
691
+ name, tip = ref, refs[f"refs/heads/{ref}"]
692
+ else:
693
+ name, tip = "HEAD", remote_head
694
+ if tip is None:
695
+ return done("skipped", "baseline-ref-missing")
696
+ if classify_match(base, tip):
697
+ return same("same-commit")
698
+ return newer(name, tip, "ref-moved")
699
+
700
+ tip = tags.get(ref)
701
+ if split is None:
702
+ if classify_match(base, tip):
703
+ return same("same-commit")
704
+ return newer(ref, tip, "tag-moved")
705
+ prefix, key = split
706
+ latest, latest_key = _newest(tags, _family(prefix), releases_only=key[1] == 1)
707
+ if latest is None:
708
+ return done("skipped", "baseline-ref-missing")
709
+ latest_commit = tags[latest]
710
+ show_latest(latest)
711
+ if classify_match(base, latest_commit):
712
+ return same("same-commit")
713
+
714
+ def contained(sha: str) -> bool:
715
+ """True when the clone holds sha in the baseline commit's history."""
716
+ if not full:
717
+ return False
718
+ rc, _text, _err = _git_capped(
719
+ source_root,
720
+ "merge-base",
721
+ "--is-ancestor",
722
+ sha,
723
+ full,
724
+ timeout=LOCAL_TIMEOUT_SEC,
725
+ )
726
+ return rc == 0
727
+
728
+ if latest_key <= key:
729
+ if tip is None or classify_match(base, tip):
730
+ return same("no-newer-tag")
731
+ return same("contained") if contained(tip) else newer(ref, tip, "tag-moved")
732
+ if contained(latest_commit):
733
+ return same("contained")
734
+ return newer(latest, latest_commit, "newer-tag")
735
+
736
+
737
+ def _build_upstream_parser() -> argparse.ArgumentParser:
738
+ parser = argparse.ArgumentParser(
739
+ prog="skf-check-workspace-drift upstream",
740
+ description=(
741
+ "Tell whether the remote moved past the commit and ref a skill was "
742
+ "built from."
743
+ ),
744
+ )
745
+ parser.add_argument(
746
+ "--source-root",
747
+ required=True,
748
+ help="the skill's source clone",
749
+ )
750
+ parser.add_argument(
751
+ "--baseline-commit",
752
+ required=True,
753
+ help="the commit the skill was built from",
754
+ )
755
+ parser.add_argument(
756
+ "--baseline-ref",
757
+ required=True,
758
+ help="the tag, branch or HEAD it was built from",
759
+ )
760
+ parser.add_argument(
761
+ "--timeout",
762
+ type=float,
763
+ default=DEFAULT_UPSTREAM_TIMEOUT_SEC,
764
+ help="seconds the remote query may take (default 60)",
765
+ )
766
+ return parser
767
+
768
+
769
+ def upstream_main(argv: list[str]) -> int:
770
+ args = _build_upstream_parser().parse_args(argv)
771
+ try:
772
+ result = upstream(
773
+ Path(os.path.expanduser(args.source_root)),
774
+ baseline_commit=args.baseline_commit,
775
+ baseline_ref=args.baseline_ref,
776
+ timeout=args.timeout,
777
+ )
778
+ except Exception as e: # noqa: BLE001 - one JSON error line, never a traceback
779
+ error = {"status": "error", "message": f"{type(e).__name__}: {e}"}
780
+ print(json.dumps(error), file=sys.stderr)
781
+ return 1
782
+ json.dump(result, sys.stdout, indent=2)
783
+ sys.stdout.write("\n")
784
+ return 0
785
+
786
+
268
787
  # --------------------------------------------------------------------------
269
788
  # CLI
270
789
  # --------------------------------------------------------------------------
271
790
 
272
791
 
273
792
  def main(argv: list[str] | None = None) -> int:
793
+ argv = sys.argv[1:] if argv is None else list(argv)
794
+ if argv[:1] == ["upstream"]:
795
+ return upstream_main(argv[1:])
274
796
  parser = argparse.ArgumentParser(
275
797
  prog="skf-check-workspace-drift",
276
798
  description=(
@@ -294,6 +816,12 @@ def main(argv: list[str] | None = None) -> int:
294
816
  action="store_true",
295
817
  help="suppress the drift halt; mismatch becomes status=overridden",
296
818
  )
819
+ parser.add_argument(
820
+ "--workflow",
821
+ choices=WORKFLOWS,
822
+ default="update-skill",
823
+ help="the workflow the halt message speaks for (default update-skill)",
824
+ )
297
825
  args = parser.parse_args(argv)
298
826
 
299
827
  source_root = Path(args.source_root)
@@ -309,6 +837,7 @@ def main(argv: list[str] | None = None) -> int:
309
837
  pinned_commit=args.pinned_commit,
310
838
  source_ref=args.source_ref,
311
839
  allow_drift=args.allow_drift,
840
+ workflow=args.workflow,
312
841
  )
313
842
  json.dump(result, sys.stdout, indent=2)
314
843
  sys.stdout.write("\n")