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
@@ -0,0 +1,577 @@
1
+ # /// script
2
+ # requires-python = ">=3.11"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Run Lock: one lock file per run, held across tool calls.
6
+
7
+ Every Bash tool call runs in a fresh shell, and an SKF workflow spans many
8
+ calls and turns. A lock that lives in a process (a held `flock`, the `$$`
9
+ PID that a later `kill -0` tests) is gone by the next call, so it can never
10
+ serialize two runs. This helper keeps the lock in a file instead: acquire
11
+ writes the owner and the time into the lock file, release deletes it only
12
+ for the run that owns it, and a lock older than the stale timeout counts as
13
+ left by a run that ended without releasing it.
14
+
15
+ CLI:
16
+
17
+ uv run skf-run-lock.py acquire --lock <path> \\
18
+ --owner <workflow>:<skill>:<run_id> [--stale-after <minutes>]
19
+ uv run skf-run-lock.py release --lock <path> \\
20
+ --owner <workflow>:<skill>:<run_id>
21
+ uv run skf-run-lock.py run-id
22
+
23
+ --lock is the full path of the lock file. Name it by its full path in every
24
+ call, never through a shell variable an earlier call set.
25
+
26
+ --owner names the run: the workflow, the skill and a run id, joined by
27
+ colons (`update-skill:cocoindex:20260930T101500Z-3f9a1c2e`). When the run id
28
+ is missing (`update-skill:cocoindex`), acquire adds a new one and prints the
29
+ whole owner; pass that owner to release.
30
+
31
+ acquire
32
+
33
+ Takes the lock when no lock file exists, when the lock is this owner's
34
+ own (its time is renewed: a run that may outlast the timeout re-runs
35
+ acquire to keep its lock), or when the lock is stale: older than
36
+ --stale-after minutes (60 by default, at most 525600: one year). A stale
37
+ lock is replaced, and stale_replaced says whose it was. Any other lock is
38
+ held by another run: nothing changes, and the output names that run.
39
+ Missing parent folders of --lock are created.
40
+
41
+ A lock file that is not this helper's record (the PID and time lines that
42
+ SKF versions before 3.0.0 wrote, the empty file an `flock` left) names no
43
+ owner: held_by is null, and its time is the ISO time on its second line
44
+ when there is one, else the file's modification time. It goes stale the
45
+ same way.
46
+
47
+ Output (one ASCII line; every key is always present):
48
+ acquired true | false
49
+ refreshed true when the lock was already this owner's
50
+ lock absolute path of the lock file
51
+ owner the owner this call used, with its run id
52
+ run_id the run id in owner
53
+ held_by the owner holding the lock after this call (this
54
+ owner when acquired), or null for a lock that names
55
+ no owner
56
+ held_since ISO UTC time the holder took or renewed the lock
57
+ stale_at ISO UTC time the holder's lock goes stale
58
+ stale_after_minutes the timeout this call applied
59
+ stale_replaced null | {"held_by": ..., "held_since": ...}: the
60
+ stale lock this call replaced
61
+ message one line for the user; when acquired is false it
62
+ names the lock file to delete when no run is active
63
+
64
+ release
65
+
66
+ Deletes the lock only when --owner holds it. A lock another run holds
67
+ (one that replaced this run's stale lock, or one taken while this run
68
+ never acquired) is left in place, so a halt path may always release.
69
+ --owner is the whole owner acquire printed: acquire always writes a run
70
+ id, so an owner without one could never match and is a bad argument.
71
+
72
+ Output (one ASCII line; every key is always present):
73
+ released true | false
74
+ reason null | "absent" (no lock file) | "not-owner"
75
+ lock absolute path of the lock file
76
+ owner the owner given
77
+ held_by the owner still holding the lock (not-owner), else null
78
+ held_since its time (not-owner), else null
79
+ message one line
80
+
81
+ run-id
82
+
83
+ Prints {"run_id": "<UTC stamp>-<8 hex>"}, for example
84
+ `20260930T101500Z-3f9a1c2e`: the time of the call and a random suffix,
85
+ safe in file names.
86
+
87
+ Lock file: one JSON line, {"owner": ..., "acquired_at": ..., "tool":
88
+ "skf-run-lock"}, written to `<lock>.skf-<8 hex>-tmp` and renamed over the
89
+ lock, so it is never seen half written. Each acquire and release reads and
90
+ changes the lock while it holds `<lock>.skf-guard`, an OS lock
91
+ (`fcntl.flock` on POSIX, `msvcrt.locking` on Windows) that lives only for
92
+ that one call and is released if the call dies, so two runs that start
93
+ together never both take the lock and a release never deletes a lock
94
+ another run just took. The guard file is removed when the call ends. Every
95
+ name this helper creates beside the lock carries `.skf-`, which
96
+ skf-skill-inventory.py counts as SKF's own.
97
+
98
+ Library use: skf-forge-tier-rw.py loads this file by its path and calls
99
+ acquire_within() and release() around one read-modify-write. It passes
100
+ empty_is_stale=True: an empty lock file is what an `flock` on the same
101
+ path leaves (this helper never writes one), so it is replaced at once
102
+ instead of when it goes stale.
103
+
104
+ Exit codes (1 and 2 print one JSON line on stderr, {"status": "error",
105
+ "message": ...}, and nothing on stdout):
106
+ 0 acquire took or renewed the lock; release and run-id answered
107
+ 1 bad arguments: a missing or unknown argument, an owner that is not
108
+ <workflow>:<skill>:<run_id> (acquire adds a missing run id, release
109
+ needs it), a --stale-after that is not a number of minutes above 0
110
+ and at most 525600, a --lock that is a folder
111
+ 2 the lock could not be read or written
112
+ 3 acquire found the lock held by another run; the output says which
113
+ """
114
+
115
+ from __future__ import annotations
116
+
117
+ import argparse
118
+ import errno
119
+ import json
120
+ import math
121
+ import os
122
+ import re
123
+ import secrets
124
+ import sys
125
+ import time
126
+ from datetime import datetime, timedelta, timezone
127
+ from pathlib import Path
128
+
129
+ if os.name == "nt":
130
+ import msvcrt
131
+ else:
132
+ import fcntl
133
+
134
+
135
+ DEFAULT_STALE_AFTER_MIN = 60
136
+ MAX_STALE_AFTER_MIN = 525600 # one year
137
+ TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
138
+ RUN_ID_STAMP_FORMAT = "%Y%m%dT%H%M%SZ"
139
+ RUN_ID_RE = re.compile(r"^\d{8}T\d{6}Z-[0-9a-f]{8}$")
140
+ OWNER_MAX_LEN = 200
141
+ GUARD_SUFFIX = ".skf-guard"
142
+ GUARD_WAIT_SEC = 10.0
143
+ EXIT_HELD = 3
144
+ TOOL = "skf-run-lock"
145
+
146
+
147
+ class LockBusy(Exception):
148
+ """acquire_within() ran out of time; .result is the last acquire output."""
149
+
150
+ def __init__(self, result: dict):
151
+ super().__init__(result["message"])
152
+ self.result = result
153
+
154
+
155
+ # ─── Owner, time and run id ──────────────────────────────────────────────────
156
+
157
+
158
+ def new_run_id(now: datetime | None = None) -> str:
159
+ """A run id: the UTC time and 8 random hex characters."""
160
+ now = now or datetime.now(timezone.utc)
161
+ return f"{now.strftime(RUN_ID_STAMP_FORMAT)}-{secrets.token_hex(4)}"
162
+
163
+
164
+ def _stamp(t: datetime) -> str:
165
+ return t.astimezone(timezone.utc).strftime(TIME_FORMAT)
166
+
167
+
168
+ def _stale_at(since: datetime, window: timedelta) -> str:
169
+ """The time a lock taken at `since` goes stale, or the last time a datetime can hold."""
170
+ try:
171
+ return _stamp(since + window)
172
+ except OverflowError:
173
+ # A lock file dated near year 9999: it only goes stale when deleted.
174
+ return _stamp(datetime.max.replace(tzinfo=timezone.utc))
175
+
176
+
177
+ def _parse_time(text) -> datetime | None:
178
+ if not isinstance(text, str):
179
+ return None
180
+ try:
181
+ return datetime.strptime(text.strip(), TIME_FORMAT).replace(tzinfo=timezone.utc)
182
+ except ValueError:
183
+ return None
184
+
185
+
186
+ def _split_owner(owner: str) -> tuple[str, str, str]:
187
+ """(workflow, skill, run_id) of an owner; run_id is "" when missing.
188
+
189
+ Raises ValueError for an owner that is not `<workflow>:<skill>[:<run_id>]`.
190
+ """
191
+ if not isinstance(owner, str) or not owner or len(owner) > OWNER_MAX_LEN:
192
+ raise ValueError(f"--owner must be <workflow>:<skill>:<run_id>, at most "
193
+ f"{OWNER_MAX_LEN} characters with its run id")
194
+ if any(ch.isspace() or not ch.isprintable() for ch in owner):
195
+ raise ValueError(f"--owner must not hold spaces or control characters: {owner!r}")
196
+ parts = owner.split(":", 2)
197
+ if len(parts) < 2 or not parts[0] or not parts[1]:
198
+ raise ValueError(f"--owner must be <workflow>:<skill>:<run_id>, got {owner!r}")
199
+ return parts[0], parts[1], parts[2] if len(parts) == 3 else ""
200
+
201
+
202
+ def _lock_path(lock) -> Path:
203
+ path = Path(os.path.abspath(os.path.expanduser(str(lock))))
204
+ if path.is_dir():
205
+ raise ValueError(f"--lock names a folder, not a lock file: {path}")
206
+ return path
207
+
208
+
209
+ def _minutes(seconds: float):
210
+ minutes = seconds / 60
211
+ return int(minutes) if minutes == int(minutes) else round(minutes, 4)
212
+
213
+
214
+ # ─── The guard: an OS lock for one call ──────────────────────────────────────
215
+
216
+
217
+ def _try_lock(fd: int) -> bool:
218
+ try:
219
+ if os.name == "nt":
220
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
221
+ else:
222
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
223
+ except OSError as e:
224
+ if e.errno in (errno.EAGAIN, errno.EACCES, errno.EDEADLK, errno.EWOULDBLOCK):
225
+ return False
226
+ raise
227
+ return True
228
+
229
+
230
+ def _unlock(fd: int) -> None:
231
+ try:
232
+ if os.name == "nt":
233
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
234
+ else:
235
+ fcntl.flock(fd, fcntl.LOCK_UN)
236
+ except OSError:
237
+ pass
238
+
239
+
240
+ def _still_named(fd: int, guard: Path) -> bool:
241
+ """True when `guard` still names the file fd holds.
242
+
243
+ On POSIX the holder removes the guard while it still holds it, so a call
244
+ that was waiting on the removed file must open the path again. Windows
245
+ refuses to remove a file another call has open, so there the path always
246
+ names the file.
247
+ """
248
+ if os.name == "nt":
249
+ return True
250
+ try:
251
+ named = os.stat(guard)
252
+ except FileNotFoundError:
253
+ return False
254
+ held = os.fstat(fd)
255
+ return (held.st_dev, held.st_ino) == (named.st_dev, named.st_ino)
256
+
257
+
258
+ class _Guard:
259
+ """Hold `<lock>.skf-guard` while one call reads and changes the lock."""
260
+
261
+ def __init__(self, lock: Path):
262
+ self.path = lock.with_name(lock.name + GUARD_SUFFIX)
263
+ self.fd: int | None = None
264
+
265
+ def __enter__(self) -> "_Guard":
266
+ deadline = time.monotonic() + GUARD_WAIT_SEC
267
+ while True:
268
+ try:
269
+ fd = os.open(self.path, os.O_RDWR | os.O_CREAT | getattr(os, "O_BINARY", 0), 0o644)
270
+ except PermissionError:
271
+ # Windows: the file is being removed by the call that held it.
272
+ if os.name != "nt" or time.monotonic() >= deadline:
273
+ raise
274
+ time.sleep(0.01)
275
+ continue
276
+ try:
277
+ locked = _try_lock(fd)
278
+ if locked and _still_named(fd, self.path):
279
+ self.fd = fd
280
+ return self
281
+ except BaseException:
282
+ os.close(fd)
283
+ raise
284
+ if locked:
285
+ _unlock(fd)
286
+ os.close(fd)
287
+ if time.monotonic() >= deadline:
288
+ raise OSError(errno.EAGAIN, f"{self.path} stayed locked for "
289
+ f"{GUARD_WAIT_SEC:g} seconds")
290
+ time.sleep(0.01)
291
+
292
+ def __exit__(self, *exc) -> None:
293
+ fd, self.fd = self.fd, None
294
+ if os.name != "nt":
295
+ # Remove it while still holding it (see _still_named).
296
+ _unlink_quietly(self.path)
297
+ _unlock(fd)
298
+ os.close(fd)
299
+ else:
300
+ _unlock(fd)
301
+ os.close(fd)
302
+ # Refused while another call has it open; that call removes it.
303
+ _unlink_quietly(self.path)
304
+
305
+
306
+ def _unlink_quietly(path: Path) -> None:
307
+ try:
308
+ os.unlink(path)
309
+ except OSError:
310
+ pass
311
+
312
+
313
+ def _windows_retry(action) -> None:
314
+ """Run action(); on Windows retry briefly while another process has the file open."""
315
+ for attempt in range(5):
316
+ try:
317
+ action()
318
+ return
319
+ except PermissionError:
320
+ if os.name != "nt" or attempt == 4:
321
+ raise
322
+ time.sleep(0.02 * (attempt + 1))
323
+
324
+
325
+ # ─── The lock record ─────────────────────────────────────────────────────────
326
+
327
+
328
+ class _Held:
329
+ """What a lock file says: its owner (or None), since when, and whether it is empty."""
330
+
331
+ def __init__(self, owner: str | None, since: datetime, empty: bool = False):
332
+ self.owner = owner
333
+ self.since = since
334
+ self.empty = empty
335
+
336
+
337
+ def _read(lock: Path) -> _Held | None:
338
+ """The lock's holder, or None when there is no lock file."""
339
+ try:
340
+ raw = lock.read_bytes()
341
+ mtime = lock.stat().st_mtime
342
+ except FileNotFoundError:
343
+ return None
344
+ text = raw.decode("utf-8", "replace")
345
+ try:
346
+ record = json.loads(text)
347
+ except ValueError:
348
+ record = None
349
+ if isinstance(record, dict):
350
+ owner = record.get("owner")
351
+ owner = owner if isinstance(owner, str) and owner else None
352
+ since = _parse_time(record.get("acquired_at"))
353
+ else:
354
+ # Not this helper's record: the PID and time lines older SKF versions
355
+ # wrote, an empty file an `flock` left, or anything else.
356
+ owner = None
357
+ lines = text.splitlines()
358
+ since = _parse_time(lines[1]) if len(lines) > 1 else None
359
+ if since is None:
360
+ since = datetime.fromtimestamp(mtime, timezone.utc)
361
+ return _Held(owner, since, empty=not raw)
362
+
363
+
364
+ def _write(lock: Path, owner: str, now: datetime) -> None:
365
+ """Write the record to a temp file beside the lock, then rename it over the lock."""
366
+ record = {"owner": owner, "acquired_at": _stamp(now), "tool": TOOL}
367
+ content = (json.dumps(record) + "\n").encode("utf-8")
368
+ tmp = lock.with_name(f"{lock.name}.skf-{secrets.token_hex(4)}-tmp")
369
+ # O_BINARY (Windows only; 0 elsewhere) keeps the bytes verbatim.
370
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
371
+ try:
372
+ fd = os.open(tmp, flags, 0o644)
373
+ try:
374
+ os.write(fd, content)
375
+ os.fsync(fd)
376
+ finally:
377
+ os.close(fd)
378
+ _windows_retry(lambda: os.replace(tmp, lock))
379
+ except OSError:
380
+ _unlink_quietly(tmp)
381
+ raise
382
+
383
+
384
+ # ─── acquire / release ───────────────────────────────────────────────────────
385
+
386
+
387
+ def acquire(lock, owner: str, stale_after: float, now: datetime | None = None,
388
+ *, empty_is_stale: bool = False) -> dict:
389
+ """Take the lock for owner, or report who holds it.
390
+
391
+ stale_after is in seconds, at most one year. `now` is for tests. With
392
+ empty_is_stale, an empty lock file counts as stale at once. Raises
393
+ ValueError for a bad owner, lock or timeout, OSError when the lock cannot
394
+ be read or written.
395
+ """
396
+ if (not isinstance(stale_after, (int, float)) or not math.isfinite(stale_after)
397
+ or not 0 < stale_after <= MAX_STALE_AFTER_MIN * 60):
398
+ raise ValueError(f"stale_after must be a number of seconds above 0 and at most "
399
+ f"{MAX_STALE_AFTER_MIN * 60} (one year), got {stale_after!r}")
400
+ workflow, skill, run_id = _split_owner(owner)
401
+ if not run_id:
402
+ run_id = new_run_id(now)
403
+ owner = f"{workflow}:{skill}:{run_id}"
404
+ # Checked again with its run id, so release accepts every owner acquire prints.
405
+ _split_owner(owner)
406
+ path = _lock_path(lock)
407
+ path.parent.mkdir(parents=True, exist_ok=True)
408
+ window = timedelta(seconds=stale_after)
409
+
410
+ with _Guard(path):
411
+ held = _read(path)
412
+ t = now or datetime.now(timezone.utc)
413
+ refreshed = held is not None and held.owner == owner
414
+ replaced = None
415
+ if held is not None and not refreshed:
416
+ if t - held.since < window and not (empty_is_stale and held.empty):
417
+ who = held.owner or "an owner the lock file does not name"
418
+ stale_at = _stale_at(held.since, window)
419
+ return {
420
+ "acquired": False,
421
+ "refreshed": False,
422
+ "lock": str(path),
423
+ "owner": owner,
424
+ "run_id": run_id,
425
+ "held_by": held.owner,
426
+ "held_since": _stamp(held.since),
427
+ "stale_at": stale_at,
428
+ "stale_after_minutes": _minutes(stale_after),
429
+ "stale_replaced": None,
430
+ "message": (f"another run holds {path}: {who} since {_stamp(held.since)}. "
431
+ f"Wait for it to finish; if no run is active, delete {path} "
432
+ f"or wait until {stale_at}, when the lock goes stale."),
433
+ }
434
+ replaced = {"held_by": held.owner, "held_since": _stamp(held.since)}
435
+ # Computed before the write, so no error can follow a lock taken.
436
+ held_since, stale_at = _stamp(t), _stale_at(t, window)
437
+ _write(path, owner, t)
438
+
439
+ if refreshed:
440
+ message = f"renewed the run lock {path} for {owner}"
441
+ elif replaced:
442
+ message = (f"took the run lock {path} for {owner}, replacing the stale lock of "
443
+ f"{replaced['held_by'] or 'an unnamed owner'} (since {replaced['held_since']})")
444
+ else:
445
+ message = f"took the run lock {path} for {owner}"
446
+ return {
447
+ "acquired": True,
448
+ "refreshed": refreshed,
449
+ "lock": str(path),
450
+ "owner": owner,
451
+ "run_id": run_id,
452
+ "held_by": owner,
453
+ "held_since": held_since,
454
+ "stale_at": stale_at,
455
+ "stale_after_minutes": _minutes(stale_after),
456
+ "stale_replaced": replaced,
457
+ "message": message,
458
+ }
459
+
460
+
461
+ def acquire_within(lock, owner: str, stale_after: float, wait: float,
462
+ *, empty_is_stale: bool = False) -> dict:
463
+ """acquire(), retried until it takes the lock or `wait` seconds pass.
464
+
465
+ For a lock held inside one call. Raises LockBusy with the last output
466
+ when the time runs out.
467
+ """
468
+ deadline = time.monotonic() + max(wait, 0)
469
+ delay = 0.05
470
+ while True:
471
+ result = acquire(lock, owner, stale_after, empty_is_stale=empty_is_stale)
472
+ if result["acquired"]:
473
+ return result
474
+ left = deadline - time.monotonic()
475
+ if left <= 0:
476
+ raise LockBusy(result)
477
+ time.sleep(min(delay, left))
478
+ delay = min(delay * 2, 0.5)
479
+
480
+
481
+ def release(lock, owner: str) -> dict:
482
+ """Delete the lock when owner holds it; leave any other lock in place.
483
+
484
+ Raises ValueError for an owner that is not <workflow>:<skill>:<run_id>.
485
+ """
486
+ if not _split_owner(owner)[2]:
487
+ raise ValueError(f"release needs the owner acquire printed, with its run id "
488
+ f"(<workflow>:<skill>:<run_id>), got {owner!r}")
489
+ path = _lock_path(lock)
490
+ out = {"released": False, "reason": None, "lock": str(path), "owner": owner,
491
+ "held_by": None, "held_since": None, "message": ""}
492
+ if not path.parent.is_dir() or not os.path.lexists(path):
493
+ out.update(reason="absent", message=f"no run lock at {path}; nothing to release")
494
+ return out
495
+ with _Guard(path):
496
+ held = _read(path)
497
+ if held is None:
498
+ out.update(reason="absent", message=f"no run lock at {path}; nothing to release")
499
+ return out
500
+ if held.owner != owner:
501
+ out.update(reason="not-owner", held_by=held.owner, held_since=_stamp(held.since),
502
+ message=(f"{path} belongs to {held.owner or 'an unnamed owner'}, "
503
+ f"not {owner}; left in place"))
504
+ return out
505
+ _windows_retry(lambda: os.unlink(path))
506
+ out.update(released=True, message=f"released the run lock {path}")
507
+ return out
508
+
509
+
510
+ # ─── CLI ─────────────────────────────────────────────────────────────────────
511
+
512
+
513
+ def _error(message: str) -> None:
514
+ print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
515
+
516
+
517
+ class _JsonErrorParser(argparse.ArgumentParser):
518
+ """Report a usage error as a JSON error with exit 1, like every other bad argument.
519
+
520
+ The subcommand parsers are made with this class too, so a missing --lock
521
+ or a --stale-after that is not a number never reaches argparse's own
522
+ plain-text usage and exit 2.
523
+ """
524
+
525
+ def error(self, message: str) -> None:
526
+ _error(f"usage error: {self.prog}: {message}")
527
+ sys.exit(1)
528
+
529
+
530
+ def _build_parser() -> argparse.ArgumentParser:
531
+ parser = _JsonErrorParser(
532
+ description="Take and release an SKF run lock that holds across tool calls.",
533
+ formatter_class=argparse.RawDescriptionHelpFormatter,
534
+ )
535
+ sub = parser.add_subparsers(dest="cmd", required=True, metavar="{acquire,release,run-id}")
536
+
537
+ p_acquire = sub.add_parser("acquire", help="take the lock, or report the run that holds it")
538
+ p_acquire.add_argument("--lock", required=True, help="full path of the lock file")
539
+ p_acquire.add_argument("--owner", required=True,
540
+ help="<workflow>:<skill>:<run_id>; a new run id is added when missing")
541
+ p_acquire.add_argument("--stale-after", type=float, default=DEFAULT_STALE_AFTER_MIN,
542
+ help="minutes after which another run's lock is stale "
543
+ "(default 60, at most 525600)")
544
+
545
+ p_release = sub.add_parser("release", help="delete the lock when this owner holds it")
546
+ p_release.add_argument("--lock", required=True, help="full path of the lock file")
547
+ p_release.add_argument("--owner", required=True, help="the owner acquire printed")
548
+
549
+ sub.add_parser("run-id", help="print a new run id")
550
+ return parser
551
+
552
+
553
+ def main(argv: list[str] | None = None) -> int:
554
+ args = _build_parser().parse_args(argv)
555
+ try:
556
+ if args.cmd == "acquire":
557
+ if not math.isfinite(args.stale_after) or not 0 < args.stale_after <= MAX_STALE_AFTER_MIN:
558
+ raise ValueError(f"--stale-after must be a number of minutes above 0 and at "
559
+ f"most {MAX_STALE_AFTER_MIN} (one year), got {args.stale_after:g}")
560
+ out = acquire(args.lock, args.owner, args.stale_after * 60)
561
+ code = 0 if out["acquired"] else EXIT_HELD
562
+ elif args.cmd == "release":
563
+ out, code = release(args.lock, args.owner), 0
564
+ else:
565
+ out, code = {"run_id": new_run_id()}, 0
566
+ except ValueError as e:
567
+ _error(str(e))
568
+ return 1
569
+ except OSError as e:
570
+ _error(f"{type(e).__name__}: {e}")
571
+ return 2
572
+ print(json.dumps(out))
573
+ return code
574
+
575
+
576
+ if __name__ == "__main__":
577
+ raise SystemExit(main())