bmad-module-skill-forge 0.10.0 → 1.0.0-rc.3

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 (269) hide show
  1. package/.claude-plugin/marketplace.json +56 -0
  2. package/.gitattributes +16 -0
  3. package/README.md +105 -38
  4. package/docs/404.md +13 -10
  5. package/docs/RELEASING.md +616 -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 +30 -9
  22. package/release-audits/v1.0.0-launch-audit.md +207 -0
  23. package/src/README.md +23 -25
  24. package/src/forger/forge-tier.yaml +4 -2
  25. package/src/forger/preferences.yaml +7 -1
  26. package/src/knowledge/agentskills-spec.md +1 -1
  27. package/src/knowledge/architecture-verification.md +1 -1
  28. package/src/knowledge/ccc-bridge.md +16 -15
  29. package/src/knowledge/overview.md +11 -11
  30. package/src/knowledge/progressive-capability.md +3 -3
  31. package/src/knowledge/provenance-tracking.md +9 -3
  32. package/src/knowledge/qmd-registry.md +8 -8
  33. package/src/knowledge/skf-knowledge-index.csv +2 -2
  34. package/src/knowledge/skill-lifecycle.md +16 -1
  35. package/src/knowledge/split-body-strategy.md +1 -1
  36. package/src/knowledge/version-paths.md +17 -12
  37. package/src/module-help.csv +18 -17
  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 -71
  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 -64
  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 -70
  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 +16 -68
  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 +11 -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 -69
  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 +4 -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 +6 -4
  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 -179
  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 -150
  229. package/src/workflows/create-stack-skill/workflow.md +0 -59
  230. package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -136
  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 -178
  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 -133
  239. package/src/workflows/rename-skill/workflow.md +0 -64
  240. package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -159
  241. package/src/workflows/setup-forge/workflow.md +0 -51
  242. package/src/workflows/shared/health-check.md +0 -231
  243. package/src/workflows/test-skill/data/scoring-rules.md +0 -123
  244. package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
  245. package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
  246. package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
  247. package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
  248. package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
  249. package/src/workflows/test-skill/steps-c/step-06-report.md +0 -208
  250. package/src/workflows/test-skill/templates/test-report-template.md +0 -28
  251. package/src/workflows/test-skill/workflow.md +0 -57
  252. package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
  253. package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
  254. package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
  255. package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
  256. package/src/workflows/update-skill/workflow.md +0 -56
  257. package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
  258. package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
  259. package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
  260. package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
  261. package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
  262. package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -200
  263. package/src/workflows/verify-stack/workflow.md +0 -61
  264. package/tools/cli/lib/compiler.js +0 -306
  265. package/tools/cli/lib/ide-commands.js +0 -244
  266. /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
  267. /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
  268. /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
  269. /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
@@ -0,0 +1,86 @@
1
+ # Feasibility Report Schema
2
+
3
+ **Purpose:** Canonical contract shared between `skf-create-stack-skill` (consumer) and `skf-verify-stack` (producer). Any change here must be applied to both skills in lockstep.
4
+
5
+ ## Filename
6
+
7
+ ```
8
+ {forge_data_folder}/feasibility-report-{project_slug}-{YYYYMMDD-HHmmss}.md
9
+ ```
10
+
11
+ `{project_slug}` is the slugified `project_name` (lowercase, hyphens only, no unicode). A stable `feasibility-report-{project_slug}-latest.md` copy (not symlink) is written next to the timestamped file for pipeline consumers.
12
+
13
+ ## Frontmatter (required)
14
+
15
+ ```yaml
16
+ ---
17
+ schemaVersion: "1.0"
18
+ reportType: feasibility
19
+ projectName: "{project_name}"
20
+ projectSlug: "{project_slug}"
21
+ generatedAt: "{ISO-8601 UTC}"
22
+ generatedBy: skf-verify-stack
23
+ overallVerdict: "FEASIBLE|CONDITIONALLY_FEASIBLE|NOT_FEASIBLE"
24
+ coveragePercentage: <0..100 integer>
25
+ pairsVerified: <non-negative integer>
26
+ pairsPlausible: <non-negative integer>
27
+ pairsRisky: <non-negative integer>
28
+ pairsBlocked: <non-negative integer>
29
+ recommendationCount: <non-negative integer>
30
+ prdAvailable: <true|false>
31
+ ---
32
+ ```
33
+
34
+ **Unknown `schemaVersion` MUST fail loudly in consumers — never silently proceed.** Consumers check `schemaVersion == "1.0"` and emit an explicit error if mismatched.
35
+
36
+ ## Per-pair verdict tokens (case-sensitive)
37
+
38
+ Exactly one of:
39
+
40
+ | Token | Meaning | Required evidence |
41
+ |---|---|---|
42
+ | `Verified` | All compatibility checks pass with declared evidence in both skills | Documentation cross-reference (Check 4) MUST pass with literal substring/name citation; language + protocol + type checks all pass |
43
+ | `Plausible` | Checks pass but at least one relies on inferred rather than declared evidence | Language + protocol + type checks pass; Check 4 weak or missing |
44
+ | `Risky` | At least one check produced incompatibility that a workaround may resolve | Any single check fails; workaround cited in recommendation |
45
+ | `Blocked` | Fundamental incompatibility that cannot be worked around | Language mismatch, protocol mismatch with no bridge, or type mismatch with no adapter |
46
+
47
+ ## Overall verdict tokens (case-sensitive)
48
+
49
+ Exactly one of `FEASIBLE`, `CONDITIONALLY_FEASIBLE`, `NOT_FEASIBLE`.
50
+
51
+ - `FEASIBLE` — 100% coverage AND zero Blocked pairs AND zero pairs with Check 4 missing.
52
+ - `NOT_FEASIBLE` — Any Blocked pair OR zero coverage.
53
+ - `CONDITIONALLY_FEASIBLE` — Everything else.
54
+
55
+ ## Body section headings (required, in order)
56
+
57
+ ```markdown
58
+ ## Executive Summary
59
+ ## Coverage Analysis
60
+ ## Integration Verdicts
61
+ ## Recommendations
62
+ ## Evidence Sources
63
+ ```
64
+
65
+ Consumers grep for `## Integration Verdicts` to locate the pair table. The table header is fixed:
66
+
67
+ ```markdown
68
+ | lib_a | lib_b | verdict | rationale |
69
+ ```
70
+
71
+ ## Producer obligations (skf-verify-stack)
72
+
73
+ - Set `schemaVersion: "1.0"` in frontmatter.
74
+ - Never emit a verdict token outside the defined set.
75
+ - When Check 4 (documentation cross-reference) produces weak/missing evidence, cap the per-pair verdict at `Plausible`.
76
+ - When `coveragePercentage == 0`, force `overallVerdict: NOT_FEASIBLE` regardless of pair results.
77
+
78
+ ## Consumer obligations (skf-create-stack-skill)
79
+
80
+ - Verify `schemaVersion == "1.0"`; halt with explicit error on mismatch.
81
+ - Treat any unknown verdict token as a hard error (do not silently drop or map).
82
+ - Use filename pattern above when referencing prior reports.
83
+
84
+ ## Versioning policy
85
+
86
+ Any change to the verdict token set, frontmatter keys, or section headers is a schema-breaking change and MUST bump `schemaVersion`. Additive changes (new optional frontmatter keys) bump the minor version; breaking changes bump the major version.
@@ -0,0 +1,62 @@
1
+ # Headless Gate Convention
2
+
3
+ ## Overview
4
+
5
+ Gates are user interaction points where a workflow pauses for confirmation or input. In headless mode, gates auto-resolve with their default action. This convention ensures one code path with two behaviors — interactive and headless use the same gates, the same output, and the same progression logic.
6
+
7
+ ## How It Works
8
+
9
+ Every gate in a step file follows this pattern:
10
+
11
+ ```
12
+ **GATE: [default action]** — Present [options] to user.
13
+ If `{headless_mode}`: auto-proceed with [default action], log: "headless: auto-[action]".
14
+ ```
15
+
16
+ The gate always:
17
+ 1. Prepares the same output (summary, preview, menu) regardless of mode
18
+ 2. In interactive mode: displays the output and waits for user input
19
+ 3. In headless mode: displays the output, logs the auto-action, and proceeds with the default
20
+
21
+ ## Resolving `{headless_mode}`
22
+
23
+ `{headless_mode}` is resolved during activation from:
24
+ 1. **Args:** `--headless` or `-H` passed to the skill invocation
25
+ 2. **Preferences:** `headless_mode: true` in `{sidecar_path}/preferences.yaml`
26
+ 3. **Default:** `false`
27
+
28
+ Each workflow's On Activation section resolves this variable alongside other config. The forger passes it through when dispatching to workflows.
29
+
30
+ ## Gate Types
31
+
32
+ ### Confirm Gate (default: Continue)
33
+ The most common gate. Presents a summary and asks to continue.
34
+ - Default action: `[C] Continue`
35
+ - Headless behavior: auto-continue after displaying summary
36
+
37
+ ### Review Gate (default: Approve)
38
+ Presents compiled output for review before writing.
39
+ - Default action: `[C] Continue` (approve)
40
+ - Headless behavior: auto-approve after displaying preview
41
+
42
+ ### Input Gate (default: use provided args)
43
+ Requires user-supplied data (skill name, path, etc.).
44
+ - Default action: use `{headless_args}` if provided
45
+ - Headless behavior: consume pre-supplied arguments; halt if missing required input
46
+
47
+ ### Choice Gate (default: first safe option)
48
+ Presents a menu with multiple options (P/I/A, etc.).
49
+ - Default action: varies per gate (documented in step file)
50
+ - Headless behavior: auto-select the default, log the choice
51
+
52
+ ## Headless Args
53
+
54
+ For skills that require user input (skill name, target path, etc.), headless mode accepts arguments via the invocation. Each skill's Invocation Contract documents its required headless args.
55
+
56
+ Example: `@Ferris QS cocoindex --headless` passes `cocoindex` as the target and skips all gates.
57
+
58
+ ## What Headless Does NOT Skip
59
+
60
+ - Error halts (hard halts on missing files, invalid state)
61
+ - Progress output (summaries, status updates still display)
62
+ - Quality thresholds (if a step produces output below spec, it still reports the issue)
@@ -0,0 +1,35 @@
1
+ # Output Contract Schema
2
+
3
+ Every pipeline-capable skill writes a result JSON file at its final step. This enables reliable CI integration and pipeline chaining.
4
+
5
+ ## Schema
6
+
7
+ ```json
8
+ {
9
+ "skill": "skf-skill-name",
10
+ "status": "success" | "failed" | "partial",
11
+ "timestamp": "ISO-8601",
12
+ "outputs": [
13
+ {"type": "report|skill|manifest|config", "path": "relative/path/to/file"}
14
+ ],
15
+ "summary": {
16
+ // skill-specific summary fields
17
+ }
18
+ }
19
+ ```
20
+
21
+ ## Filenames
22
+
23
+ Each run writes **two files** to `{output_dir}`:
24
+
25
+ 1. **Per-run record** (audit trail): `{skill-name}-result-{YYYYMMDD-HHmmss}.json`
26
+ - Timestamp is UTC, resolution to seconds — e.g., `update-skill-result-20260413-145230.json`
27
+ - Never overwritten by subsequent runs — preserves a durable audit trail across retries, aborts, and re-runs
28
+ 2. **Stable latest pointer** (pipeline consumption): `{skill-name}-result-latest.json`
29
+ - A **copy** (not a symlink) of the per-run record just written
30
+ - Always present at a deterministic path so CI / pipelines / the forger can read `summary.*` without enumerating timestamps
31
+ - Overwritten on every successful write
32
+
33
+ Write the per-run record first, then copy it to the `-latest.json` path. If the copy fails, the per-run record still exists — the run is not lost.
34
+
35
+ **Consumers (forger, CI, chained workflows):** read from `{skill-name}-result-latest.json`. Do not enumerate timestamped files unless inspecting prior-run history.
@@ -0,0 +1,102 @@
1
+ # Pipeline Contracts
2
+
3
+ ## Overview
4
+
5
+ Pipelines chain multiple SKF workflows in sequence. The forger orchestrates the chain, passing data between workflows via filesystem artifacts and validating output contracts at each transition.
6
+
7
+ ## Syntax
8
+
9
+ The forger recognizes pipeline invocations when the user provides multiple workflow codes:
10
+
11
+ ```
12
+ AN CS TS EX — space-separated codes
13
+ AN -> CS -> TS -> EX — arrow-separated (equivalent)
14
+ BS CS[cocoindex] TS EX — with target argument in brackets
15
+ CS TS[min:80] EX — with circuit breaker threshold
16
+ ```
17
+
18
+ The forger also accepts common pipeline aliases:
19
+
20
+ | Alias | Expands To | Description |
21
+ |-------|-----------|-------------|
22
+ | `forge` | `BS CS TS EX` | Full skill creation pipeline (brief through export) |
23
+ | `forge-quick` | `QS TS EX` | Quick skill pipeline |
24
+ | `onboard` | `AN CS TS EX` | Full brownfield onboarding (AN generates briefs, CS consumes them directly) |
25
+ | `maintain` | `AS US TS EX` | Maintenance cycle (audit → update → test → export) |
26
+
27
+ ## Pipeline Rules
28
+
29
+ 1. **Left to right execution** — each workflow completes before the next begins
30
+ 2. **Headless implied** — pipelines activate `{headless_mode}` automatically for all workflows in the chain (the user already committed to the sequence)
31
+ 3. **Data forwarding** — the forger resolves output-to-input mapping between adjacent workflows (see Data Flow table)
32
+ 4. **Circuit breakers** — if a workflow's output fails its quality check, the pipeline halts with a summary of what completed and what remains
33
+ 5. **Error halts propagate** — if any workflow hard-halts, the pipeline stops immediately
34
+ 6. **Progress reporting** — the forger reports completion of each workflow before starting the next
35
+
36
+ ## Data Flow
37
+
38
+ How outputs from one workflow become inputs to the next:
39
+
40
+ | From | To | Data Passed | How |
41
+ |------|-----|------------|-----|
42
+ | AN | CS | `skill-brief.yaml` paths from generated briefs | Forger passes each `brief_path` written by AN to CS; in batch mode, CS processes all sequentially |
43
+ | BS | CS | `skill-brief.yaml` path | Forger passes the brief path written by BS as `brief_path` to CS |
44
+ | CS | TS | skill name (derived from brief) | Forger passes the `skill_name` from the completed CS to TS |
45
+ | CS | EX | skill name | Same — forger resolves the created skill's name |
46
+ | TS | EX | skill name + test result | Forger checks `result` field in test report; if FAIL and circuit breaker active, halts |
47
+ | QS | TS | skill name (from `repo_name`) | Forger passes the quick-skill's output name to TS |
48
+ | QS | EX | skill name | Same |
49
+ | AS | US | skill name + drift severity | Forger checks `summary.severity` in `audit-skill-result-latest.json`; if CLEAN, skips US |
50
+ | VS | RA | architecture doc path | Already known from VS invocation |
51
+
52
+ ## Circuit Breakers
53
+
54
+ Circuit breakers halt the pipeline when a workflow's output doesn't meet a quality threshold:
55
+
56
+ | Workflow | Check | Default Threshold | Halt Condition |
57
+ |----------|-------|-------------------|----------------|
58
+ | AN | recommended units count | min: 1 | Zero skillable units found |
59
+ | CS | compilation success | must complete | Hard error during compilation |
60
+ | TS | completeness score | min: 60 | Score below threshold |
61
+ | AS | drift score | not CRITICAL | Critical drift found |
62
+ | VS | feasibility verdict | not BLOCKED | All integrations blocked |
63
+
64
+ Override syntax: `TS[min:80]` sets the test-skill threshold to 80 for this pipeline run.
65
+
66
+ ### Bracket Syntax
67
+
68
+ Brackets after a workflow code (`CODE[value]`) are parsed as follows:
69
+
70
+ - **Circuit breaker override**: `min:N` where N is a number — e.g., `TS[min:80]` sets the threshold for that workflow
71
+ - **Target argument**: any other value — e.g., `CS[cocoindex]` passes "cocoindex" as the target to CS
72
+
73
+ Only workflows with a circuit breaker entry (AN, CS, TS, AS, VS) accept `min:N` overrides. All other workflows ignore `min:N` brackets. Target arguments are valid for any workflow that accepts a named input (CS, QS, BS, US, etc.).
74
+
75
+ ## Pipeline State
76
+
77
+ The forger tracks pipeline state in memory during execution:
78
+
79
+ ```yaml
80
+ pipeline:
81
+ workflows: [AN, CS, TS, EX]
82
+ current_index: 1
83
+ completed:
84
+ - {code: AN, status: ok, output: {units: 3, briefs: [...]}}
85
+ pending: [CS, TS, EX]
86
+ data:
87
+ skill_name: "cocoindex"
88
+ brief_path: "/path/to/skill-brief.yaml"
89
+ target: "cocoindex"
90
+ ```
91
+
92
+ ## Anti-Patterns
93
+
94
+ The forger validates the pipeline sequence and warns about:
95
+
96
+ | Pattern | Issue | Suggestion |
97
+ |---------|-------|------------|
98
+ | EX before TS | Exporting untested skill | Add TS before EX |
99
+ | US without AS | Updating without audit | Run AS first to detect what changed |
100
+ | CS without BS or AN | Compiling without brief | Need a brief — use QS for quick path, or AN for brownfield |
101
+ | TS after EX | Testing after export | Move TS before EX |
102
+ | Duplicate codes | Same workflow twice | Remove duplicate |
@@ -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()