bmad-module-skill-forge 0.9.0 → 0.10.1-alpha.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 (268) hide show
  1. package/.claude-plugin/marketplace.json +39 -0
  2. package/.gitattributes +16 -0
  3. package/README.md +105 -38
  4. package/docs/404.md +13 -10
  5. package/docs/RELEASING.md +185 -0
  6. package/docs/STABILITY.md +123 -0
  7. package/docs/_data/pinned.yaml +98 -0
  8. package/docs/agents.md +16 -11
  9. package/docs/architecture.md +221 -0
  10. package/docs/bmad-synergy.md +11 -13
  11. package/docs/concepts.md +18 -108
  12. package/docs/examples.md +121 -80
  13. package/docs/getting-started.md +85 -147
  14. package/docs/how-it-works.md +25 -536
  15. package/docs/index.md +44 -27
  16. package/docs/skill-model.md +328 -0
  17. package/docs/troubleshooting.md +39 -0
  18. package/docs/verifying-a-skill.md +232 -0
  19. package/docs/why-skf.md +93 -0
  20. package/docs/workflows.md +119 -13
  21. package/package.json +25 -4
  22. package/src/README.md +23 -25
  23. package/src/forger/forge-tier.yaml +4 -2
  24. package/src/forger/preferences.yaml +7 -1
  25. package/src/knowledge/agentskills-spec.md +1 -1
  26. package/src/knowledge/architecture-verification.md +1 -1
  27. package/src/knowledge/ccc-bridge.md +16 -15
  28. package/src/knowledge/overview.md +11 -11
  29. package/src/knowledge/progressive-capability.md +3 -3
  30. package/src/knowledge/provenance-tracking.md +9 -3
  31. package/src/knowledge/qmd-registry.md +8 -8
  32. package/src/knowledge/skf-knowledge-index.csv +2 -2
  33. package/src/knowledge/skill-lifecycle.md +16 -1
  34. package/src/knowledge/split-body-strategy.md +1 -1
  35. package/src/knowledge/version-paths.md +17 -12
  36. package/src/module-help.csv +18 -17
  37. package/src/module.yaml +4 -0
  38. package/src/shared/health-check.md +372 -0
  39. package/src/shared/references/feasibility-report-schema.md +86 -0
  40. package/src/shared/references/headless-gate-convention.md +62 -0
  41. package/src/shared/references/output-contract-schema.md +35 -0
  42. package/src/shared/references/pipeline-contracts.md +102 -0
  43. package/src/shared/scripts/skf-atomic-write.py +370 -0
  44. package/src/shared/scripts/skf-manifest-ops.py +236 -0
  45. package/src/shared/scripts/skf-preflight.py +164 -0
  46. package/src/shared/scripts/skf-rebuild-managed-sections.py +201 -0
  47. package/src/shared/scripts/skf-severity-classify.py +163 -0
  48. package/src/shared/scripts/skf-skill-inventory.py +210 -0
  49. package/src/shared/scripts/skf-structural-diff.py +257 -0
  50. package/src/shared/scripts/skf-validate-frontmatter.py +318 -0
  51. package/src/shared/scripts/skf-validate-output.py +247 -0
  52. package/src/skf-analyze-source/SKILL.md +57 -0
  53. package/src/skf-analyze-source/assets/skill-brief-schema.md +125 -0
  54. package/src/skf-analyze-source/references/unit-detection-heuristics.md +124 -0
  55. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01-init.md +6 -66
  56. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01b-continue.md +4 -59
  57. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-02-scan-project.md +7 -68
  58. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-03-identify-units.md +6 -66
  59. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-04-map-and-detect.md +7 -68
  60. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-05-recommend.md +6 -66
  61. package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-06-generate-briefs.md +14 -66
  62. package/src/skf-analyze-source/steps-c/step-07-health-check.md +22 -0
  63. package/src/skf-audit-skill/SKILL.md +59 -0
  64. package/src/{workflows/audit-skill/data → skf-audit-skill/assets}/drift-report-template.md +7 -7
  65. package/src/{workflows/audit-skill/data → skf-audit-skill/references}/severity-rules.md +11 -11
  66. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-01-init.md +24 -68
  67. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-02-re-index.md +36 -71
  68. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-03-structural-diff.md +21 -63
  69. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-04-semantic-diff.md +6 -65
  70. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-05-severity-classify.md +5 -63
  71. package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-06-report.md +10 -59
  72. package/src/skf-audit-skill/steps-c/step-07-health-check.md +22 -0
  73. package/src/skf-brief-skill/SKILL.md +53 -0
  74. package/src/{workflows/brief-skill/data → skf-brief-skill/assets}/scope-templates.md +23 -0
  75. package/src/skf-brief-skill/assets/skill-brief-schema.md +206 -0
  76. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-01-gather-intent.md +6 -67
  77. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-02-analyze-target.md +6 -66
  78. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-03-scope-definition.md +12 -68
  79. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-04-confirm-brief.md +5 -63
  80. package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-05-write-brief.md +10 -69
  81. package/src/skf-brief-skill/steps-c/step-06-health-check.md +22 -0
  82. package/src/skf-create-skill/SKILL.md +63 -0
  83. package/src/{workflows/create-skill/data → skf-create-skill/assets}/compile-assembly-rules.md +60 -0
  84. package/src/{workflows/create-skill/data → skf-create-skill/assets}/skill-sections.md +92 -23
  85. package/src/skf-create-skill/assets/tessl-dismissal-rules.md +102 -0
  86. package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns-tracing.md +13 -13
  87. package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns.md +8 -3
  88. package/src/skf-create-skill/references/source-resolution-protocols.md +239 -0
  89. package/src/{workflows/create-skill/data → skf-create-skill/references}/tier-degradation-rules.md +8 -7
  90. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-01-load-brief.md +25 -63
  91. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-02-ecosystem-check.md +8 -64
  92. package/src/skf-create-skill/steps-c/step-03-extract.md +379 -0
  93. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-03d-component-extraction.md +18 -35
  94. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-04-enrich.md +12 -69
  95. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-05-compile.md +71 -75
  96. package/src/skf-create-skill/steps-c/step-06-validate.md +267 -0
  97. package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-07-generate-artifacts.md +30 -74
  98. package/src/skf-create-skill/steps-c/step-08-report.md +145 -0
  99. package/src/skf-create-skill/steps-c/step-09-health-check.md +23 -0
  100. package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-02b-ccc-discover.md +8 -66
  101. package/src/skf-create-skill/steps-c/sub/step-03b-fetch-temporal.md +229 -0
  102. package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-03c-fetch-docs.md +18 -42
  103. package/src/skf-create-stack-skill/SKILL.md +64 -0
  104. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/assets}/stack-skill-template.md +2 -1
  105. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/compose-mode-rules.md +19 -5
  106. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/integration-patterns.md +1 -1
  107. package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/manifest-patterns.md +10 -10
  108. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-01-init.md +14 -55
  109. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-02-detect-manifests.md +29 -63
  110. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-03-rank-and-confirm.md +9 -59
  111. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-04-parallel-extract.md +29 -64
  112. package/src/skf-create-stack-skill/steps-c/step-05-detect-integrations.md +164 -0
  113. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-06-compile-stack.md +7 -60
  114. package/src/skf-create-stack-skill/steps-c/step-07-generate-output.md +328 -0
  115. package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-08-validate.md +17 -63
  116. package/src/skf-create-stack-skill/steps-c/step-09-report.md +128 -0
  117. package/src/skf-create-stack-skill/steps-c/step-10-health-check.md +22 -0
  118. package/src/skf-drop-skill/SKILL.md +57 -0
  119. package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-01-select.md +9 -69
  120. package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-02-execute.md +30 -106
  121. package/src/skf-drop-skill/steps-c/step-03-report.md +83 -0
  122. package/src/skf-drop-skill/steps-c/step-04-health-check.md +22 -0
  123. package/src/skf-export-skill/SKILL.md +57 -0
  124. package/src/skf-export-skill/assets/managed-section-format.md +138 -0
  125. package/src/{workflows/export-skill/data → skf-export-skill/assets}/snippet-format.md +8 -12
  126. package/src/skf-export-skill/steps-c/step-01-load-skill.md +247 -0
  127. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-02-package.md +5 -58
  128. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-03-generate-snippet.md +15 -77
  129. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-04-update-context.md +59 -133
  130. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-05-token-report.md +4 -58
  131. package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-06-summary.md +20 -64
  132. package/src/skf-export-skill/steps-c/step-07-health-check.md +22 -0
  133. package/src/skf-forger/SKILL.md +110 -0
  134. package/src/skf-forger/bmad-skill-manifest.yaml +11 -0
  135. package/src/skf-quick-skill/SKILL.md +56 -0
  136. package/src/{workflows/quick-skill/data → skf-quick-skill/references}/registry-resolution.md +6 -6
  137. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-01-resolve-target.md +9 -62
  138. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-02-ecosystem-check.md +5 -59
  139. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-03-quick-extract.md +18 -61
  140. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-04-compile.md +6 -60
  141. package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-05-validate.md +58 -78
  142. package/src/skf-quick-skill/steps-c/step-06-write.md +73 -0
  143. package/src/skf-quick-skill/steps-c/step-07-health-check.md +22 -0
  144. package/src/skf-refine-architecture/SKILL.md +57 -0
  145. package/src/{workflows/refine-architecture/data → skf-refine-architecture/references}/refinement-rules.md +16 -16
  146. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-01-init.md +6 -63
  147. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-02-gap-analysis.md +29 -71
  148. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-03-issue-detection.md +5 -65
  149. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-04-improvements.md +5 -64
  150. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-05-compile.md +5 -56
  151. package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-06-report.md +14 -64
  152. package/src/skf-refine-architecture/steps-c/step-07-health-check.md +22 -0
  153. package/src/skf-rename-skill/SKILL.md +58 -0
  154. package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-01-select.md +12 -73
  155. package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-02-execute.md +32 -113
  156. package/src/skf-rename-skill/steps-c/step-03-report.md +83 -0
  157. package/src/skf-rename-skill/steps-c/step-04-health-check.md +22 -0
  158. package/src/skf-setup/SKILL.md +55 -0
  159. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01-detect-and-tier.md +6 -62
  160. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01b-ccc-index.md +9 -65
  161. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-02-write-config.md +15 -60
  162. package/src/{workflows/setup-forge → skf-setup}/steps-c/step-03-auto-index.md +7 -66
  163. package/src/skf-setup/steps-c/step-04-report.md +92 -0
  164. package/src/skf-setup/steps-c/step-05-health-check.md +22 -0
  165. package/src/skf-test-skill/SKILL.md +59 -0
  166. package/src/skf-test-skill/references/migration-section-rules.md +114 -0
  167. package/src/skf-test-skill/references/scoring-rules.md +207 -0
  168. package/src/skf-test-skill/references/source-access-protocol.md +109 -0
  169. package/src/skf-test-skill/scripts/compute-score.py +310 -0
  170. package/src/skf-test-skill/steps-c/step-01-init.md +238 -0
  171. package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-02-detect-mode.md +4 -62
  172. package/src/skf-test-skill/steps-c/step-03-coverage-check.md +347 -0
  173. package/src/skf-test-skill/steps-c/step-04-coherence-check.md +239 -0
  174. package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-04b-external-validators.md +42 -77
  175. package/src/skf-test-skill/steps-c/step-05-score.md +266 -0
  176. package/src/skf-test-skill/steps-c/step-06-report.md +290 -0
  177. package/src/skf-test-skill/steps-c/step-07-health-check.md +25 -0
  178. package/src/skf-test-skill/templates/test-report-template.md +58 -0
  179. package/src/skf-update-skill/SKILL.md +58 -0
  180. package/src/{workflows/update-skill/data → skf-update-skill/references}/manual-section-rules.md +6 -6
  181. package/src/{workflows/update-skill/data → skf-update-skill/references}/merge-conflict-rules.md +5 -5
  182. package/src/skf-update-skill/references/remote-source-resolution.md +94 -0
  183. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-01-init.md +22 -69
  184. package/src/skf-update-skill/steps-c/step-02-detect-changes.md +255 -0
  185. package/src/skf-update-skill/steps-c/step-03-re-extract.md +336 -0
  186. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-04-merge.md +60 -72
  187. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-05-validate.md +5 -62
  188. package/src/skf-update-skill/steps-c/step-06-write.md +278 -0
  189. package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-07-report.md +11 -66
  190. package/src/skf-update-skill/steps-c/step-08-health-check.md +22 -0
  191. package/src/skf-verify-stack/SKILL.md +60 -0
  192. package/src/skf-verify-stack/assets/feasibility-report-template.md +76 -0
  193. package/src/{workflows/verify-stack/data → skf-verify-stack/references}/coverage-patterns.md +12 -4
  194. package/src/skf-verify-stack/references/integration-verification-rules.md +78 -0
  195. package/src/skf-verify-stack/steps-c/step-01-init.md +178 -0
  196. package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-02-coverage.md +27 -72
  197. package/src/skf-verify-stack/steps-c/step-03-integrations.md +179 -0
  198. package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-04-requirements.md +14 -67
  199. package/src/skf-verify-stack/steps-c/step-05-synthesize.md +147 -0
  200. package/src/skf-verify-stack/steps-c/step-06-report.md +156 -0
  201. package/src/skf-verify-stack/steps-c/step-07-health-check.md +25 -0
  202. package/tools/cli/commands/install.js +1 -1
  203. package/tools/cli/commands/status.js +21 -23
  204. package/tools/cli/commands/uninstall.js +11 -19
  205. package/tools/cli/commands/update.js +2 -2
  206. package/tools/cli/lib/ide-skills.js +216 -0
  207. package/tools/cli/lib/installer.js +29 -52
  208. package/tools/cli/lib/manifest.js +11 -5
  209. package/tools/cli/lib/platform-codes.yaml +223 -0
  210. package/tools/cli/lib/ui.js +76 -47
  211. package/tools/validate-docs-drift.js +193 -0
  212. package/src/agents/forger.agent.yaml +0 -114
  213. package/src/workflows/README.md +0 -174
  214. package/src/workflows/analyze-source/data/skill-brief-schema.md +0 -125
  215. package/src/workflows/analyze-source/data/unit-detection-heuristics.md +0 -124
  216. package/src/workflows/analyze-source/workflow.md +0 -61
  217. package/src/workflows/audit-skill/workflow.md +0 -64
  218. package/src/workflows/brief-skill/data/skill-brief-schema.md +0 -159
  219. package/src/workflows/brief-skill/workflow.md +0 -58
  220. package/src/workflows/create-skill/data/source-resolution-protocols.md +0 -200
  221. package/src/workflows/create-skill/steps-c/step-03-extract.md +0 -301
  222. package/src/workflows/create-skill/steps-c/step-03b-fetch-temporal.md +0 -253
  223. package/src/workflows/create-skill/steps-c/step-06-validate.md +0 -254
  224. package/src/workflows/create-skill/steps-c/step-08-report.md +0 -176
  225. package/src/workflows/create-skill/workflow.md +0 -53
  226. package/src/workflows/create-stack-skill/steps-c/step-05-detect-integrations.md +0 -196
  227. package/src/workflows/create-stack-skill/steps-c/step-07-generate-output.md +0 -270
  228. package/src/workflows/create-stack-skill/steps-c/step-09-report.md +0 -145
  229. package/src/workflows/create-stack-skill/workflow.md +0 -59
  230. package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -135
  231. package/src/workflows/drop-skill/workflow.md +0 -63
  232. package/src/workflows/export-skill/data/managed-section-format.md +0 -103
  233. package/src/workflows/export-skill/steps-c/step-01-load-skill.md +0 -237
  234. package/src/workflows/export-skill/workflow.md +0 -58
  235. package/src/workflows/quick-skill/steps-c/step-06-write.md +0 -179
  236. package/src/workflows/quick-skill/workflow.md +0 -58
  237. package/src/workflows/refine-architecture/workflow.md +0 -61
  238. package/src/workflows/rename-skill/steps-c/step-03-report.md +0 -132
  239. package/src/workflows/rename-skill/workflow.md +0 -64
  240. package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -156
  241. package/src/workflows/setup-forge/workflow.md +0 -51
  242. package/src/workflows/test-skill/data/scoring-rules.md +0 -123
  243. package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
  244. package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
  245. package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
  246. package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
  247. package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
  248. package/src/workflows/test-skill/steps-c/step-06-report.md +0 -207
  249. package/src/workflows/test-skill/templates/test-report-template.md +0 -28
  250. package/src/workflows/test-skill/workflow.md +0 -57
  251. package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
  252. package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
  253. package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
  254. package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
  255. package/src/workflows/update-skill/workflow.md +0 -56
  256. package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
  257. package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
  258. package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
  259. package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
  260. package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
  261. package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -197
  262. package/src/workflows/verify-stack/workflow.md +0 -61
  263. package/tools/cli/lib/compiler.js +0 -306
  264. package/tools/cli/lib/ide-commands.js +0 -244
  265. /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
  266. /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
  267. /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
  268. /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
@@ -0,0 +1,370 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Atomic Write — Crash-safe artifact writing for skill workflows.
6
+
7
+ Provides three CLI subcommands skills can invoke via bash to avoid
8
+ partial-write corruption and active-symlink races.
9
+
10
+ Subcommands:
11
+ write Stage content into <target>.skf-tmp, fsync, then rename to <target>.
12
+ Content comes from stdin. Creates parent dirs as needed.
13
+
14
+ stage-dir Create <target>.skf-tmp/ as a staging directory (mkdir -p).
15
+ Caller writes files into it, then calls commit-dir to atomically
16
+ swap it into place as <target>/ (with prior target moved aside
17
+ to <target>.skf-rollback-<pid> and removed on success).
18
+
19
+ commit-dir Atomically swap <target>.skf-tmp/ into <target>/. If <target>/
20
+ exists, move it to <target>.skf-rollback-<pid> first; on failure,
21
+ restore. Supports rollback via --rollback to undo the most recent
22
+ commit by restoring the rollback dir if still present.
23
+
24
+ flip-link Atomically update symlink <link> to point at <target> using
25
+ the `ln -sfn tmp && mv -Tf tmp link` pattern (or equivalent via
26
+ os.replace on the link path). Holds an flock on <link>.lock.
27
+
28
+ Cross-platform: locking branches between fcntl (POSIX) and msvcrt
29
+ (Windows). Symlink semantics on Windows require dev mode or admin —
30
+ flip-link surfaces a clear error rather than silently falling back.
31
+ Native Windows is untested in CI; the supported path is WSL2.
32
+
33
+ Exit codes:
34
+ 0 on success
35
+ 1 on user error (bad args, missing input)
36
+ 2 on operation failure (disk full, permission, race-detected)
37
+
38
+ CLI examples:
39
+ cat metadata.json | python3 skf-atomic-write.py write --target /path/to/metadata.json
40
+ python3 skf-atomic-write.py stage-dir --target /path/to/1.0.0
41
+ python3 skf-atomic-write.py commit-dir --target /path/to/1.0.0
42
+ python3 skf-atomic-write.py flip-link --link /path/to/active --target 1.0.0
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import argparse
48
+ import errno
49
+ import json
50
+ import os
51
+ import shutil
52
+ import subprocess
53
+ import sys
54
+ from pathlib import Path
55
+
56
+ if os.name == "nt":
57
+ import msvcrt
58
+ else:
59
+ import fcntl
60
+
61
+
62
+ def _create_symlink_or_junction(target: str, link_path: Path) -> str:
63
+ """Create a directory link from link_path to target.
64
+
65
+ On POSIX: standard symlink. On Windows: try symlink first (works under
66
+ Developer Mode or admin), fall back to a directory junction via `mklink /J`
67
+ on PRIVILEGE_NOT_HELD / ACCESS_DENIED. Junctions don't need elevation and
68
+ behave like directory symlinks for the resolve() / is_dir() consumers in
69
+ skf-skill-inventory.py.
70
+
71
+ Returns "symlink" or "junction" so callers can report which kind was made.
72
+ """
73
+ try:
74
+ os.symlink(target, link_path)
75
+ return "symlink"
76
+ except OSError as e:
77
+ if os.name != "nt" or getattr(e, "winerror", None) not in (1314, 5):
78
+ raise
79
+ # Junctions are absolute-path-only and target must be a real directory
80
+ # at creation time. Resolve relative `target` against link_path's parent.
81
+ abs_target = (link_path.parent / target).resolve()
82
+ if not abs_target.is_dir():
83
+ raise OSError(
84
+ errno.ENOTDIR,
85
+ f"junction fallback requires existing directory target: {abs_target}",
86
+ ) from e
87
+ result = subprocess.run(
88
+ ["cmd", "/c", "mklink", "/J", str(link_path), str(abs_target)],
89
+ capture_output=True,
90
+ text=True,
91
+ timeout=30,
92
+ )
93
+ if result.returncode != 0:
94
+ raise OSError(
95
+ e.errno,
96
+ f"junction fallback failed: {result.stderr.strip() or result.stdout.strip()}",
97
+ ) from e
98
+ return "junction"
99
+
100
+
101
+ def _acquire_lock(fd: int) -> None:
102
+ """Acquire an exclusive non-blocking lock on fd (auto-released on close/exit)."""
103
+ if os.name == "nt":
104
+ try:
105
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
106
+ except OSError as e:
107
+ if e.errno in (errno.EAGAIN, errno.EACCES, errno.EDEADLK):
108
+ raise OSError(errno.EAGAIN, "lock held") from e
109
+ raise
110
+ else:
111
+ try:
112
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
113
+ except OSError as e:
114
+ if e.errno in (errno.EAGAIN, errno.EACCES):
115
+ raise OSError(errno.EAGAIN, "lock held") from e
116
+ raise
117
+
118
+
119
+ def _release_lock(fd: int) -> None:
120
+ if os.name == "nt":
121
+ try:
122
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
123
+ except OSError:
124
+ pass
125
+ else:
126
+ try:
127
+ fcntl.flock(fd, fcntl.LOCK_UN)
128
+ except OSError:
129
+ pass
130
+
131
+
132
+ def _die(code: int, message: str) -> None:
133
+ print(json.dumps({"status": "error", "message": message}), file=sys.stderr)
134
+ sys.exit(code)
135
+
136
+
137
+ def _ok(payload: dict) -> None:
138
+ payload.setdefault("status", "ok")
139
+ print(json.dumps(payload))
140
+
141
+
142
+ def cmd_write(target: Path) -> None:
143
+ """Write stdin to target atomically via temp + rename."""
144
+ data = sys.stdin.buffer.read()
145
+ target.parent.mkdir(parents=True, exist_ok=True)
146
+ tmp = target.with_name(target.name + ".skf-tmp")
147
+ try:
148
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
149
+ try:
150
+ os.write(fd, data)
151
+ os.fsync(fd)
152
+ finally:
153
+ os.close(fd)
154
+ os.replace(tmp, target)
155
+ except OSError as e:
156
+ if tmp.exists():
157
+ try:
158
+ tmp.unlink()
159
+ except OSError:
160
+ pass
161
+ _die(2, f"atomic write failed: {e}")
162
+ _ok({"wrote": str(target), "bytes": len(data)})
163
+
164
+
165
+ def cmd_stage_dir(target: Path) -> None:
166
+ """Create <target>.skf-tmp/ staging directory (clean if present)."""
167
+ staging = target.with_name(target.name + ".skf-tmp")
168
+ # A prior interrupted run can leave staging as a regular file or a dangling
169
+ # symlink; shutil.rmtree only handles real directories, so dispatch by type.
170
+ if staging.is_symlink() or (staging.exists() and not staging.is_dir()):
171
+ try:
172
+ staging.unlink()
173
+ except OSError as e:
174
+ _die(2, f"failed to clear stale staging entry {staging}: {e}")
175
+ elif staging.is_dir():
176
+ shutil.rmtree(staging)
177
+ staging.mkdir(parents=True)
178
+ _ok({"staging": str(staging)})
179
+
180
+
181
+ def cmd_commit_dir(target: Path, rollback: bool = False) -> None:
182
+ """Swap <target>.skf-tmp/ into <target>/.
183
+
184
+ The swap is two os.replace calls (move prior aside, then move staging
185
+ into place); a crash between them leaves the prior snapshot at
186
+ <target>.skf-rollback-<pid>/ recoverable via --rollback. Concurrent
187
+ commits against the same target are NOT supported — callers must
188
+ serialize via the per-target flock provided by flip-link or external
189
+ coordination.
190
+ """
191
+ staging = target.with_name(target.name + ".skf-tmp")
192
+ rollback_dir = target.with_name(target.name + f".skf-rollback-{os.getpid()}")
193
+
194
+ if rollback:
195
+ # Pick the newest rollback dir by mtime, not by lexical PID sort.
196
+ # Concurrent commits leave one rollback per PID; sorting by PID-as-string
197
+ # could restore an older snapshot (PID "9999" sorts after "10001"). mtime
198
+ # of the rollback dir = moment os.replace moved the prior target aside,
199
+ # which is the correct "most recent" anchor.
200
+ candidates = list(target.parent.glob(target.name + ".skf-rollback-*"))
201
+ if not candidates:
202
+ _die(1, f"no rollback dir for {target}")
203
+ chosen = max(candidates, key=lambda p: p.stat().st_mtime)
204
+ if target.exists():
205
+ shutil.rmtree(target)
206
+ os.replace(chosen, target)
207
+ _ok({"restored": str(target), "from": str(chosen)})
208
+ return
209
+
210
+ if not staging.is_dir():
211
+ _die(1, f"staging dir missing: {staging}")
212
+
213
+ # First-install case: target's parent may not exist yet (e.g. fresh
214
+ # {skill_group}/{version}/ install). os.replace requires the destination
215
+ # parent to exist, so create it before either replace fires.
216
+ target.parent.mkdir(parents=True, exist_ok=True)
217
+
218
+ prior_moved = False
219
+ if target.exists():
220
+ if target.is_symlink() or target.is_file():
221
+ _die(2, f"target is not a directory: {target}")
222
+ try:
223
+ os.replace(target, rollback_dir)
224
+ prior_moved = True
225
+ except OSError as e:
226
+ _die(2, f"failed to move prior target aside: {e}")
227
+
228
+ try:
229
+ os.replace(staging, target)
230
+ except OSError as e:
231
+ if prior_moved:
232
+ try:
233
+ os.replace(rollback_dir, target)
234
+ except OSError:
235
+ pass
236
+ _die(2, f"commit swap failed: {e}")
237
+
238
+ if prior_moved:
239
+ try:
240
+ shutil.rmtree(rollback_dir)
241
+ except OSError:
242
+ pass
243
+
244
+ _ok({"committed": str(target)})
245
+
246
+
247
+ def _is_link_or_junction(p: Path) -> bool:
248
+ """True for POSIX symlinks AND Windows junctions/symlinks.
249
+
250
+ `Path.is_symlink()` is False for Windows junctions; os.readlink succeeds
251
+ for both symlinks and junctions (since CPython 3.8 on Windows). A regular
252
+ directory raises OSError on readlink, which is the signal we want to
253
+ refuse replacement.
254
+ """
255
+ if p.is_symlink():
256
+ return True
257
+ if not p.exists() and not p.is_symlink():
258
+ return False
259
+ try:
260
+ os.readlink(p)
261
+ return True
262
+ except OSError:
263
+ return False
264
+
265
+
266
+ def cmd_flip_link(link: Path, target: str) -> None:
267
+ """Atomically point <link> at <target> using rename-over-symlink pattern.
268
+
269
+ target is the value of the symlink (may be relative, as is convention
270
+ for `active -> 1.0.0`). Held lock on <link>.lock prevents concurrent flips.
271
+
272
+ On Windows, os.symlink requires Developer Mode or admin. When that fails
273
+ with PRIVILEGE_NOT_HELD/ACCESS_DENIED the helper falls back to a directory
274
+ junction (no elevation needed); junctions resolve identically for
275
+ skf-skill-inventory's resolve_active_version().
276
+ """
277
+ lock_path = link.with_name(link.name + ".skf-lock")
278
+ link.parent.mkdir(parents=True, exist_ok=True)
279
+
280
+ # Refuse only if <link> is a real directory/file (not a symlink or junction).
281
+ # Replacing a real dir would lose user data; replacing a link is the point.
282
+ if link.exists() and not _is_link_or_junction(link):
283
+ _die(2, f"refusing to replace non-link: {link}")
284
+
285
+ lock_fd = os.open(lock_path, os.O_WRONLY | os.O_CREAT, 0o644)
286
+ lock_held = False
287
+ try:
288
+ try:
289
+ _acquire_lock(lock_fd)
290
+ lock_held = True
291
+ except OSError as e:
292
+ if e.errno == errno.EAGAIN:
293
+ _die(2, f"another process holds flip lock on {link}")
294
+ raise
295
+
296
+ tmp_link = link.with_name(link.name + ".skf-tmp-link")
297
+ if tmp_link.is_symlink() or tmp_link.exists():
298
+ # On Windows a junction returns is_dir()=True, exists()=True but
299
+ # is_symlink()=False — must rmdir/unlink based on type.
300
+ if tmp_link.is_dir() and not tmp_link.is_symlink():
301
+ tmp_link.rmdir()
302
+ else:
303
+ tmp_link.unlink()
304
+ try:
305
+ link_kind = _create_symlink_or_junction(target, tmp_link)
306
+ except OSError as e:
307
+ if os.name == "nt" and getattr(e, "winerror", None) in (1314, 5):
308
+ _die(
309
+ 2,
310
+ "symlink/junction creation failed on Windows. Junction fallback "
311
+ "requires the target directory to exist. Either enable Developer "
312
+ "Mode (Settings → Privacy & Security → For Developers) or use WSL2.",
313
+ )
314
+ raise
315
+ # os.replace can swap a symlink-over-symlink atomically, but Windows
316
+ # rejects renaming a directory (junction) over an existing directory
317
+ # (junction or real). Drop the existing link first when junction-based.
318
+ if link_kind == "junction" and (link.is_dir() or link.is_symlink() or link.exists()):
319
+ if link.is_symlink() or not link.is_dir():
320
+ link.unlink()
321
+ else:
322
+ link.rmdir()
323
+ os.replace(tmp_link, link)
324
+ finally:
325
+ if lock_held:
326
+ _release_lock(lock_fd)
327
+ os.close(lock_fd)
328
+ # Lock file is bookkeeping only — once the fd is closed the lock is
329
+ # gone, so leaving the file around just litters the skill_group dir.
330
+ # Best-effort unlink; a concurrent flipper may have already removed it.
331
+ try:
332
+ lock_path.unlink()
333
+ except OSError:
334
+ pass
335
+
336
+ _ok({"link": str(link), "points_to": target, "kind": link_kind})
337
+
338
+
339
+ def main() -> None:
340
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
341
+ sub = parser.add_subparsers(dest="cmd", required=True)
342
+
343
+ p_write = sub.add_parser("write", help="Atomic file write from stdin")
344
+ p_write.add_argument("--target", type=Path, required=True)
345
+
346
+ p_stage = sub.add_parser("stage-dir", help="Create staging directory")
347
+ p_stage.add_argument("--target", type=Path, required=True)
348
+
349
+ p_commit = sub.add_parser("commit-dir", help="Commit staging directory to target")
350
+ p_commit.add_argument("--target", type=Path, required=True)
351
+ p_commit.add_argument("--rollback", action="store_true", help="Restore from rollback dir instead of committing")
352
+
353
+ p_flip = sub.add_parser("flip-link", help="Atomic symlink flip")
354
+ p_flip.add_argument("--link", type=Path, required=True)
355
+ p_flip.add_argument("--target", type=str, required=True)
356
+
357
+ args = parser.parse_args()
358
+
359
+ if args.cmd == "write":
360
+ cmd_write(args.target)
361
+ elif args.cmd == "stage-dir":
362
+ cmd_stage_dir(args.target)
363
+ elif args.cmd == "commit-dir":
364
+ cmd_commit_dir(args.target, rollback=args.rollback)
365
+ elif args.cmd == "flip-link":
366
+ cmd_flip_link(args.link, args.target)
367
+
368
+
369
+ if __name__ == "__main__":
370
+ main()
@@ -0,0 +1,236 @@
1
+ # /// script
2
+ # requires-python = ">=3.10"
3
+ # dependencies = []
4
+ # ///
5
+ """SKF Manifest Ops — CRUD operations on the export manifest.
6
+
7
+ Reads, adds, updates, removes entries in .export-manifest.json.
8
+ Used by export-skill, drop-skill, and rename-skill.
9
+
10
+ CLI: python3 skf-manifest-ops.py <skills-folder> <command> [args]
11
+
12
+ Commands:
13
+ read — Read entire manifest
14
+ get <skill-name> — Get a single skill entry
15
+ set <skill-name> <version> — Add/update skill with active version
16
+ remove <skill-name> — Remove skill from manifest
17
+ deprecate <skill-name> [ver] — Mark skill or version as deprecated
18
+ rename <old-name> <new-name> — Rename a skill entry
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import sys
26
+ import tempfile
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+
30
+
31
+ def read_manifest(manifest_path):
32
+ """Read manifest file, returning (data, None) or (None, error).
33
+
34
+ Handles both v1 (flat list) and v2 (dict) manifest formats.
35
+ V1 manifests are migrated in-place to v2 on next write.
36
+ Legacy `platforms` field at the version level is renamed to `ides`.
37
+ """
38
+ try:
39
+ with open(manifest_path, encoding="utf-8") as f:
40
+ data = json.load(f)
41
+ if data.get("schema_version") != "2":
42
+ data = _migrate_v1_to_v2(data)
43
+ data = _normalize_platforms_to_ides(data)
44
+ return data, None
45
+ except FileNotFoundError:
46
+ return {"schema_version": "2", "exports": {}, "updated_at": None}, None
47
+ except json.JSONDecodeError as e:
48
+ return None, f"Manifest JSON parse error: {e}"
49
+
50
+
51
+ def _migrate_v1_to_v2(data):
52
+ """Migrate a v1 manifest to v2 format in memory."""
53
+ data["schema_version"] = "2"
54
+ for skill_name, entry in data.get("exports", {}).items():
55
+ versions = entry.get("versions", [])
56
+ if isinstance(versions, list):
57
+ active = entry.get("active_version", "")
58
+ deprecated = entry.get("deprecated", False)
59
+ new_versions = {}
60
+ for v in versions:
61
+ status = "active" if v == active and not deprecated else "archived"
62
+ new_versions[v] = {
63
+ "ides": [],
64
+ "last_exported": data.get("updated_at", ""),
65
+ "status": "deprecated" if deprecated and v == active else status,
66
+ }
67
+ entry["versions"] = new_versions
68
+ entry.pop("deprecated", None)
69
+ entry.pop("deprecated_versions", None)
70
+ return data
71
+
72
+
73
+ def _normalize_platforms_to_ides(data):
74
+ """Rename legacy `platforms` key → `ides` on each version entry.
75
+
76
+ Pre-rename v2 manifests used `platforms` for the list of IDE identifiers
77
+ the version was exported to. The field was ambiguous (IDE name vs context
78
+ file vs skill root), so it was renamed to `ides` to match
79
+ `config.yaml.ides`. Existing manifests are upgraded on next read.
80
+ """
81
+ for entry in data.get("exports", {}).values():
82
+ versions = entry.get("versions", {})
83
+ if not isinstance(versions, dict):
84
+ continue
85
+ for v_data in versions.values():
86
+ if not isinstance(v_data, dict):
87
+ continue
88
+ if "platforms" in v_data:
89
+ legacy = v_data.pop("platforms")
90
+ if "ides" not in v_data:
91
+ v_data["ides"] = legacy
92
+ return data
93
+
94
+
95
+ def write_manifest(manifest_path, data):
96
+ """Write manifest file atomically via write-to-temp-then-rename."""
97
+ data["updated_at"] = datetime.now(timezone.utc).isoformat()
98
+ manifest_path = Path(manifest_path)
99
+ tmp_fd, tmp_path = tempfile.mkstemp(dir=manifest_path.parent, suffix=".tmp")
100
+ try:
101
+ with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
102
+ json.dump(data, f, indent=2)
103
+ f.write("\n")
104
+ os.replace(tmp_path, manifest_path)
105
+ except Exception:
106
+ os.unlink(tmp_path)
107
+ raise
108
+
109
+
110
+ def cmd_read(manifest_path):
111
+ data, err = read_manifest(manifest_path)
112
+ if err:
113
+ return {"status": "error", "error": err}
114
+ return {"status": "ok", "manifest": data}
115
+
116
+
117
+ def cmd_get(manifest_path, skill_name):
118
+ data, err = read_manifest(manifest_path)
119
+ if err:
120
+ return {"status": "error", "error": err}
121
+ exports = data.get("exports", {})
122
+ if skill_name not in exports:
123
+ return {"status": "not_found", "skill": skill_name, "available": sorted(exports.keys())}
124
+ return {"status": "ok", "skill": skill_name, "entry": exports[skill_name]}
125
+
126
+
127
+ def cmd_set(manifest_path, skill_name, version):
128
+ data, err = read_manifest(manifest_path)
129
+ if err:
130
+ return {"status": "error", "error": err}
131
+ exports = data.setdefault("exports", {})
132
+ existing = exports.get(skill_name, {})
133
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
134
+
135
+ # Preserve existing versions dict, archive the previously-active version
136
+ versions = existing.get("versions", {})
137
+ if isinstance(versions, list):
138
+ versions = {} # Safety: handle any residual v1 data
139
+ old_active = existing.get("active_version")
140
+ if old_active and old_active in versions and old_active != version:
141
+ versions[old_active]["status"] = "archived"
142
+
143
+ # Add or update the new active version
144
+ existing_version = versions.get(version, {})
145
+ versions[version] = {
146
+ "ides": existing_version.get("ides", existing_version.get("platforms", [])),
147
+ "last_exported": today,
148
+ "status": "active",
149
+ }
150
+
151
+ exports[skill_name] = {
152
+ "active_version": version,
153
+ "versions": versions,
154
+ }
155
+ write_manifest(manifest_path, data)
156
+ return {"status": "ok", "action": "set", "skill": skill_name, "version": version}
157
+
158
+
159
+ def cmd_remove(manifest_path, skill_name):
160
+ data, err = read_manifest(manifest_path)
161
+ if err:
162
+ return {"status": "error", "error": err}
163
+ exports = data.get("exports", {})
164
+ if skill_name not in exports:
165
+ return {"status": "not_found", "skill": skill_name}
166
+ removed = exports.pop(skill_name)
167
+ write_manifest(manifest_path, data)
168
+ return {"status": "ok", "action": "removed", "skill": skill_name, "removed_entry": removed}
169
+
170
+
171
+ def cmd_deprecate(manifest_path, skill_name, version=None):
172
+ data, err = read_manifest(manifest_path)
173
+ if err:
174
+ return {"status": "error", "error": err}
175
+ exports = data.get("exports", {})
176
+ if skill_name not in exports:
177
+ return {"status": "not_found", "skill": skill_name}
178
+ versions = exports[skill_name].get("versions", {})
179
+ if version:
180
+ if version not in versions:
181
+ return {"status": "error", "error": f"Version '{version}' not found for '{skill_name}'"}
182
+ versions[version]["status"] = "deprecated"
183
+ else:
184
+ # Deprecate all versions
185
+ for v in versions.values():
186
+ v["status"] = "deprecated"
187
+ write_manifest(manifest_path, data)
188
+ return {"status": "ok", "action": "deprecated", "skill": skill_name, "version": version}
189
+
190
+
191
+ def cmd_rename(manifest_path, old_name, new_name):
192
+ data, err = read_manifest(manifest_path)
193
+ if err:
194
+ return {"status": "error", "error": err}
195
+ exports = data.get("exports", {})
196
+ if old_name not in exports:
197
+ return {"status": "not_found", "skill": old_name}
198
+ if new_name in exports:
199
+ return {"status": "error", "error": f"Target name '{new_name}' already exists in manifest"}
200
+ exports[new_name] = exports.pop(old_name)
201
+ write_manifest(manifest_path, data)
202
+ return {"status": "ok", "action": "renamed", "from": old_name, "to": new_name}
203
+
204
+
205
+ def main():
206
+ if len(sys.argv) < 3:
207
+ print("Usage: python3 skf-manifest-ops.py <skills-folder> <command> [args]", file=sys.stderr)
208
+ print("Commands: read, get <name>, set <name> <version>, remove <name>, deprecate <name> [version], rename <old> <new>", file=sys.stderr)
209
+ sys.exit(1)
210
+
211
+ skills_folder = Path(sys.argv[1])
212
+ command = sys.argv[2]
213
+ manifest_path = skills_folder / ".export-manifest.json"
214
+
215
+ if command == "read":
216
+ result = cmd_read(manifest_path)
217
+ elif command == "get" and len(sys.argv) >= 4:
218
+ result = cmd_get(manifest_path, sys.argv[3])
219
+ elif command == "set" and len(sys.argv) >= 5:
220
+ result = cmd_set(manifest_path, sys.argv[3], sys.argv[4])
221
+ elif command == "remove" and len(sys.argv) >= 4:
222
+ result = cmd_remove(manifest_path, sys.argv[3])
223
+ elif command == "deprecate" and len(sys.argv) >= 4:
224
+ ver = sys.argv[4] if len(sys.argv) >= 5 else None
225
+ result = cmd_deprecate(manifest_path, sys.argv[3], ver)
226
+ elif command == "rename" and len(sys.argv) >= 5:
227
+ result = cmd_rename(manifest_path, sys.argv[3], sys.argv[4])
228
+ else:
229
+ result = {"status": "error", "error": f"Unknown command or missing args: {command}"}
230
+
231
+ print(json.dumps(result, indent=2))
232
+ sys.exit(0 if result["status"] == "ok" else 1)
233
+
234
+
235
+ if __name__ == "__main__":
236
+ main()