@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8

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 (383) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +86 -25
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/docs/assurance-controls.md +12 -2
  31. package/docs/atelier-runtime.md +13 -2
  32. package/docs/atelier-sync.md +172 -0
  33. package/docs/blocks/claims.md +28 -18
  34. package/docs/blocks/will-not-do.md +12 -3
  35. package/docs/coauthor-session.md +57 -0
  36. package/docs/connected-composition.md +75 -0
  37. package/docs/decisions.md +167 -0
  38. package/docs/exact-upgrades.md +159 -0
  39. package/docs/extension-pack-lifecycle.md +33 -0
  40. package/docs/guided-upgrades.md +99 -0
  41. package/docs/install.md +65 -9
  42. package/docs/intake-and-guides.md +65 -0
  43. package/docs/integration-and-review-plan.md +409 -0
  44. package/docs/integration-contract-decisions.md +22 -0
  45. package/docs/local-review.md +84 -0
  46. package/docs/local-services.md +227 -0
  47. package/docs/local-state.md +107 -0
  48. package/docs/obsidian-contract.md +687 -0
  49. package/docs/obsidian.md +387 -0
  50. package/docs/portable-presentation.md +328 -0
  51. package/docs/presentation-consumer-boundaries.md +75 -0
  52. package/docs/presentation-review-disposition.md +47 -0
  53. package/docs/project-command-inventory.md +38 -0
  54. package/docs/project-options.md +53 -0
  55. package/docs/release-engineering.md +99 -2
  56. package/docs/review-portability.md +55 -0
  57. package/docs/skill-steward.md +156 -0
  58. package/docs/upgrade.md +70 -2
  59. package/docs/vault-service.md +292 -0
  60. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  61. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  62. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  64. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  65. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  66. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  67. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  69. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  70. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  71. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  72. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  73. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  74. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  75. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  76. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  77. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  78. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  79. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  80. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  81. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  82. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  83. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  84. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  85. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  86. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  87. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  88. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  89. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  90. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  91. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  92. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  93. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  94. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  95. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  96. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  97. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  98. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  99. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  100. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  101. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  102. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  103. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  104. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  105. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  106. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  107. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  108. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  109. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  113. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  114. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  115. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  116. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  121. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  122. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  123. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  125. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  131. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  132. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  133. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  136. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  137. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  138. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  139. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  142. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  143. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  144. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  145. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  146. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  147. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  161. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  162. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  163. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  164. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  169. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  170. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  171. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  172. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  177. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  178. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  179. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  181. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  182. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  183. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  187. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  188. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  189. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  190. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  193. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  194. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  195. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  196. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  197. package/fixtures/obsidian/edits/workspace.json +65 -0
  198. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  199. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  200. package/fixtures/obsidian/materialization/workspace.json +54 -0
  201. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  202. package/fixtures/ui/presentation/pane.v1.json +7 -0
  203. package/fixtures/ui/presentation/reference.v1.json +293 -0
  204. package/package.json +61 -6
  205. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  206. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  207. package/skills/claude/atelier-open/SKILL.md +5 -0
  208. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  209. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  210. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  211. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  212. package/skills/codex/atelier-open/SKILL.md +5 -0
  213. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  214. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  215. package/src/access/preview-enforcer.mjs +84 -0
  216. package/src/boundary/content-rules.mjs +9 -4
  217. package/src/boundary/policy.mjs +74 -42
  218. package/src/cli/project-options.mjs +58 -0
  219. package/src/cli/run.mjs +61 -3
  220. package/src/coauthor/session.mjs +161 -0
  221. package/src/coauthor/store.mjs +173 -0
  222. package/src/collaboration/event-ledger.mjs +16 -33
  223. package/src/collaboration/inspection-bundle.mjs +223 -0
  224. package/src/collaboration/review-contracts.mjs +25 -0
  225. package/src/collaboration/review-store.mjs +305 -0
  226. package/src/commands/coauthor.mjs +26 -0
  227. package/src/commands/config.mjs +6 -1
  228. package/src/commands/extension-pack.mjs +15 -8
  229. package/src/commands/init.mjs +44 -5
  230. package/src/commands/obsidian.mjs +368 -0
  231. package/src/commands/review.mjs +147 -0
  232. package/src/commands/setup.mjs +51 -14
  233. package/src/commands/skills.mjs +199 -0
  234. package/src/commands/sync.mjs +107 -0
  235. package/src/composition/registry.mjs +27 -0
  236. package/src/composition/wire.mjs +122 -0
  237. package/src/contracts/corpus.mjs +35 -0
  238. package/src/decisions/contracts.d.mts +96 -0
  239. package/src/decisions/contracts.mjs +321 -0
  240. package/src/disclosure/content-scan.mjs +13 -0
  241. package/src/export/atelier-export-contract.mjs +16 -1
  242. package/src/extension-packs/lifecycle.mjs +154 -0
  243. package/src/graph/graph.mjs +64 -2
  244. package/src/graph/knowledge-graph.mjs +548 -24
  245. package/src/guides/contracts.mjs +54 -0
  246. package/src/index.mjs +29 -0
  247. package/src/intake/store.mjs +119 -0
  248. package/src/preview/channel.mjs +89 -0
  249. package/src/project/config.mjs +50 -49
  250. package/src/project/durable-state.mjs +161 -0
  251. package/src/project/file-class.mjs +166 -0
  252. package/src/project/private-state.mjs +11 -1
  253. package/src/projection/obsidian/contracts.mjs +571 -0
  254. package/src/projection/obsidian/edits/align.mjs +169 -0
  255. package/src/projection/obsidian/edits/apply.mjs +889 -0
  256. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  257. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  258. package/src/projection/obsidian/edits/index.mjs +19 -0
  259. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  260. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  261. package/src/projection/obsidian/edits/observe.mjs +123 -0
  262. package/src/projection/obsidian/edits/policy.mjs +123 -0
  263. package/src/projection/obsidian/edits/regions.mjs +648 -0
  264. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  265. package/src/projection/obsidian/materialize/index.mjs +7 -0
  266. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  267. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  268. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  269. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  270. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  271. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  272. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  273. package/src/projection/obsidian/proposals/index.mjs +23 -0
  274. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  275. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  276. package/src/projection/obsidian/proposals/router.mjs +168 -0
  277. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  278. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  279. package/src/projection/obsidian/publication/index.mjs +5 -0
  280. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  281. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  282. package/src/projection/obsidian/publication/transport.mjs +139 -0
  283. package/src/projection/obsidian/recovery/index.mjs +6 -0
  284. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  285. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  286. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  287. package/src/projection/obsidian/recovery/store.mjs +234 -0
  288. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  289. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  290. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  291. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  292. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  293. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  294. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  295. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  296. package/src/projection/project.mjs +28 -19
  297. package/src/readiness-protocols/evidence.mjs +282 -0
  298. package/src/readiness-protocols/runtime.mjs +4 -3
  299. package/src/readiness-protocols/source-read.mjs +39 -0
  300. package/src/runtime/git-adapter.mjs +189 -0
  301. package/src/runtime/local-state.mjs +439 -0
  302. package/src/runtime/obsidian/app-capability.mjs +110 -0
  303. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  304. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  305. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  306. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  307. package/src/runtime/obsidian/contributions.mjs +27 -0
  308. package/src/runtime/obsidian/documents.mjs +37 -0
  309. package/src/runtime/obsidian/enablement.mjs +53 -0
  310. package/src/runtime/obsidian/engine.mjs +532 -0
  311. package/src/runtime/obsidian/errors.mjs +14 -0
  312. package/src/runtime/obsidian/extension-points.mjs +103 -0
  313. package/src/runtime/obsidian/index.mjs +34 -0
  314. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  315. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  316. package/src/runtime/obsidian/observation.mjs +158 -0
  317. package/src/runtime/obsidian/opening.mjs +243 -0
  318. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  319. package/src/runtime/obsidian/pipeline.mjs +122 -0
  320. package/src/runtime/obsidian/private-lock.mjs +156 -0
  321. package/src/runtime/obsidian/process-identity.mjs +58 -0
  322. package/src/runtime/obsidian/service-client.mjs +66 -0
  323. package/src/runtime/obsidian/service-main.mjs +89 -0
  324. package/src/runtime/obsidian/service-record.mjs +169 -0
  325. package/src/runtime/obsidian/service-server.mjs +121 -0
  326. package/src/runtime/obsidian/service.mjs +216 -0
  327. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  328. package/src/runtime/obsidian/state-store.mjs +180 -0
  329. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  330. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  331. package/src/runtime/obsidian/watchers.mjs +36 -0
  332. package/src/runtime/repository-observation.mjs +491 -0
  333. package/src/runtime/supervisor.mjs +803 -0
  334. package/src/server/local-sidecar.mjs +44 -0
  335. package/src/server/server.mjs +1 -0
  336. package/src/skills/steward.mjs +780 -0
  337. package/src/ui/presentation/browser.mjs +278 -0
  338. package/src/ui/presentation/contract.mjs +116 -0
  339. package/src/ui/presentation/index.mjs +7 -0
  340. package/src/ui/presentation/native.mjs +153 -0
  341. package/src/ui/presentation/proof.mjs +57 -0
  342. package/src/ui/presentation/schema-check.mjs +30 -0
  343. package/src/ui/presentation/schema.generated.mjs +1371 -0
  344. package/src/ui/presentation/state.mjs +43 -0
  345. package/src/ui/presentation/styles.mjs +67 -0
  346. package/src/ui/presentation/tokens.mjs +74 -0
  347. package/src/ui/presentation/web.mjs +85 -0
  348. package/src/ui/review-page.mjs +32 -0
  349. package/src/upgrade/explanation.mjs +33 -0
  350. package/src/upgrade/provenance.mjs +338 -0
  351. package/src/upgrade/transaction-files.mjs +103 -0
  352. package/src/upgrade/transaction.mjs +475 -0
  353. package/src/upgrade/upgrade.mjs +63 -28
  354. package/src/vault/hosts.mjs +21 -0
  355. package/src/vault/identity.mjs +19 -0
  356. package/src/vault/index.mjs +7 -0
  357. package/src/vault/interface.mjs +11 -0
  358. package/src/vault/metadata.mjs +31 -0
  359. package/src/vault/privacy.mjs +148 -0
  360. package/src/vault/probe.mjs +89 -0
  361. package/src/vault/service.mjs +131 -0
  362. package/src/vault/source.mjs +45 -0
  363. package/src/vault/storage.mjs +23 -0
  364. package/templates/distribution-workspace/.claude/launch.json +6 -2
  365. package/templates/distribution-workspace/gitignore +1 -0
  366. package/templates/distribution-workspace/project/README.md +2 -2
  367. package/templates/external-project-workspace/README.md +45 -0
  368. package/templates/external-project-workspace/answers.example.json +4 -0
  369. package/templates/external-project-workspace/atelier.project.json +40 -0
  370. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  371. package/templates/external-project-workspace/gitignore +8 -0
  372. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  373. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  374. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  375. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  376. package/templates/external-project-workspace/source/README.md +11 -0
  377. package/templates/launch.json +6 -2
  378. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  379. package/templates/private-domain-workspace/domain/README.md +2 -2
  380. package/templates/private-domain-workspace/gitignore +1 -0
  381. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  382. package/templates/shared-project-workspace/gitignore +1 -0
  383. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,780 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { execFileSync } from 'node:child_process'
5
+
6
+ import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs'
7
+ import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
8
+ import { atomicReplacePrivateText, ensureContainedPrivateDirectory, openRegularFileNoFollow } from '../project/private-state.mjs'
9
+ import { packageRootFrom } from '../project/package-root.mjs'
10
+
11
+ export const ATELIER_SKILL_AUDIT_SCHEMA = 'mnstry.atelier-skill-audit@v1'
12
+ export const ATELIER_SKILL_CANDIDATES_SCHEMA = 'mnstry.atelier-skill-candidates@v1'
13
+ export const ATELIER_SKILL_SYNC_PLAN_SCHEMA = 'mnstry.atelier-skill-sync-plan@v1'
14
+ export const ATELIER_SKILL_SYNC_LOCK_SCHEMA = 'mnstry.atelier-skill-sync-lock@v1'
15
+ export const SKILL_SYNC_LOCK_FILE = '.atelier-skill-lock.json'
16
+
17
+ export const SKILL_OBSERVATION_SIGNALS = Object.freeze([
18
+ 'repeated-task',
19
+ 'missing-workflow',
20
+ 'user-correction',
21
+ 'trigger-miss',
22
+ 'trigger-collision',
23
+ 'tool-failure',
24
+ 'stale-guidance',
25
+ 'successful-run',
26
+ 'unused-skill',
27
+ 'superseded-skill',
28
+ ])
29
+
30
+ export const SKILL_OBSERVATION_OUTCOMES = Object.freeze([
31
+ 'success',
32
+ 'failure',
33
+ 'corrected',
34
+ 'missing',
35
+ 'unknown',
36
+ ])
37
+
38
+ const packageRoot = packageRootFrom(import.meta.url)
39
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
40
+ const stewardSchema = JSON.parse(fs.readFileSync(path.join(packageRoot, 'contracts', 'atelier-skill-steward.v1.schema.json'), 'utf8'))
41
+ const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
42
+ const WORKFLOW_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/
43
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/
44
+ const MAX_BUNDLE_FILES = 256
45
+ const MAX_BUNDLE_BYTES = 2 * 1024 * 1024
46
+ const MAX_SKILL_DESCRIPTION = 600
47
+ const OBSERVATION_LEDGER = path.join('.atelier-local', 'skill-steward', 'observations.ndjson')
48
+
49
+ const stableCompare = (left, right) => String(left).localeCompare(String(right), 'en')
50
+ const portablePath = (value) => String(value).split(path.sep).join('/')
51
+ const isObject = (value) => value && typeof value === 'object' && !Array.isArray(value)
52
+
53
+ function sha256(value) {
54
+ return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
55
+ }
56
+
57
+ function canonicalDigest(value) {
58
+ return sha256(JSON.stringify(value))
59
+ }
60
+
61
+ function lstatIfPresent(file) {
62
+ try {
63
+ return fs.lstatSync(file)
64
+ } catch (error) {
65
+ if (error?.code === 'ENOENT') return null
66
+ throw error
67
+ }
68
+ }
69
+
70
+ function escaped(root, candidate) {
71
+ const relative = path.relative(root, candidate)
72
+ return relative.startsWith('..') || path.isAbsolute(relative)
73
+ }
74
+
75
+ function containedPath(root, candidate, label) {
76
+ const resolvedRoot = path.resolve(root)
77
+ const resolved = path.resolve(candidate)
78
+ if (resolved === resolvedRoot || escaped(resolvedRoot, resolved)) throw new Error(`${label} escapes workspace`)
79
+ return resolved
80
+ }
81
+
82
+ function relativeTo(root, file) {
83
+ const relative = path.relative(root, file)
84
+ if (relative === '' || escaped(root, file)) return null
85
+ return portablePath(relative)
86
+ }
87
+
88
+ function strictUtf8(bytes, label) {
89
+ try {
90
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
91
+ } catch {
92
+ throw new Error(`${label} is not valid UTF-8 text`)
93
+ }
94
+ }
95
+
96
+ function readBounded(file) {
97
+ const fd = openRegularFileNoFollow(file)
98
+ try {
99
+ if (fs.fstatSync(fd).size > MAX_BUNDLE_BYTES) throw new Error('skill file exceeds byte ceiling')
100
+ const bytes = fs.readFileSync(fd)
101
+ if (bytes.length > MAX_BUNDLE_BYTES) throw new Error('skill file exceeds byte ceiling')
102
+ return bytes
103
+ } finally { fs.closeSync(fd) }
104
+ }
105
+
106
+ function requirePrivatePlacement(workspaceRoot) {
107
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')))
108
+ try {
109
+ const tracked = execFileSync('git', ['-C', workspaceRoot, 'ls-files', '-z', '--', '.atelier-local'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
110
+ if (tracked) throw new Error('tracked private state')
111
+ execFileSync('git', ['-C', workspaceRoot, 'check-ignore', '--quiet', '.atelier-local/'], { env, stdio: 'ignore' })
112
+ } catch { throw new Error('skill stewardship requires untracked, ignored .atelier-local/ state in a Git workspace') }
113
+ }
114
+
115
+ function scalarValue(raw) {
116
+ const value = raw.trim()
117
+ if (value.startsWith('"') && value.endsWith('"')) {
118
+ try {
119
+ return JSON.parse(value)
120
+ } catch {
121
+ return value.slice(1, -1)
122
+ }
123
+ }
124
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replaceAll("''", "'")
125
+ return value
126
+ }
127
+
128
+ export function parseSkillFrontmatter(text) {
129
+ const normalized = String(text).replaceAll('\r\n', '\n')
130
+ if (!normalized.startsWith('---\n')) return { ok: false, values: {}, body: normalized, error: 'SKILL.md must begin with YAML frontmatter' }
131
+ const closing = normalized.indexOf('\n---\n', 4)
132
+ if (closing < 0) return { ok: false, values: {}, body: normalized, error: 'SKILL.md frontmatter is not closed' }
133
+ const header = normalized.slice(4, closing)
134
+ const values = {}
135
+ for (const line of header.split('\n')) {
136
+ if (!line || /^\s/.test(line) || line.trimStart().startsWith('#')) continue
137
+ const match = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line)
138
+ if (!match) continue
139
+ values[match[1]] = scalarValue(match[2])
140
+ }
141
+ return { ok: true, values, body: normalized.slice(closing + 5), error: null }
142
+ }
143
+
144
+ function bundleFiles(root, relative = '', state = { files: [], bytes: 0, directories: 0 }) {
145
+ if (++state.directories > 256 || relative.split(path.sep).length > 16) throw new Error('skill bundle directory ceiling exceeded')
146
+ const directory = relative ? path.join(root, relative) : root
147
+ const stat = fs.lstatSync(directory)
148
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('skill bundle contains a redirected or non-directory component')
149
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => stableCompare(left.name, right.name))) {
150
+ const childRelative = relative ? path.join(relative, entry.name) : entry.name
151
+ const child = path.join(root, childRelative)
152
+ const childStat = fs.lstatSync(child)
153
+ if (childStat.isSymbolicLink()) throw new Error(`skill bundle contains symbolic link ${portablePath(childRelative)}`)
154
+ if (childStat.isDirectory()) {
155
+ bundleFiles(root, childRelative, state)
156
+ continue
157
+ }
158
+ if (!childStat.isFile()) throw new Error(`skill bundle contains non-regular file ${portablePath(childRelative)}`)
159
+ if (state.files.length >= MAX_BUNDLE_FILES) throw new Error(`skill bundle exceeds ${MAX_BUNDLE_FILES} file ceiling`)
160
+ if (state.bytes + childStat.size > MAX_BUNDLE_BYTES) throw new Error(`skill bundle exceeds ${MAX_BUNDLE_BYTES} byte ceiling`)
161
+ const bytes = readBounded(child)
162
+ if (state.bytes + bytes.length > MAX_BUNDLE_BYTES) throw new Error('skill bundle exceeds byte ceiling')
163
+ state.files.push({ path: portablePath(childRelative), bytes })
164
+ state.bytes += bytes.length
165
+ }
166
+ return state
167
+ }
168
+
169
+ function bundleDigest(files) {
170
+ const hash = crypto.createHash('sha256')
171
+ for (const file of files) {
172
+ const name = Buffer.from(file.path, 'utf8')
173
+ const size = Buffer.alloc(8)
174
+ size.writeBigUInt64BE(BigInt(file.bytes.length))
175
+ hash.update(Buffer.from(`${name.length}:`, 'utf8'))
176
+ hash.update(name)
177
+ hash.update(size)
178
+ hash.update(file.bytes)
179
+ }
180
+ return `sha256:${hash.digest('hex')}`
181
+ }
182
+
183
+ function readSkillBundle(root, directoryName) {
184
+ const skillRoot = path.join(root, directoryName)
185
+ const state = bundleFiles(skillRoot)
186
+ const skillFile = state.files.find((file) => file.path === 'SKILL.md')
187
+ if (!skillFile) throw new Error('skill bundle is missing SKILL.md')
188
+ const skillText = strictUtf8(skillFile.bytes, 'SKILL.md')
189
+ return {
190
+ directoryName,
191
+ root: skillRoot,
192
+ files: state.files,
193
+ bytes: state.bytes,
194
+ digest: bundleDigest(state.files),
195
+ skillText,
196
+ frontmatter: parseSkillFrontmatter(skillText),
197
+ }
198
+ }
199
+
200
+ function markdownResourceLinks(text) {
201
+ const links = []
202
+ const pattern = /\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
203
+ for (const match of text.matchAll(pattern)) {
204
+ const target = match[1].replace(/^<|>$/g, '')
205
+ if (!target || target.startsWith('#') || /^[a-z][a-z0-9+.-]*:/i.test(target)) continue
206
+ links.push(target.split('#')[0])
207
+ }
208
+ return [...new Set(links)].sort(stableCompare)
209
+ }
210
+
211
+ function auditSurface({ name, root, reportRoot }) {
212
+ const entries = []
213
+ const findings = []
214
+ const surfacePath = relativeTo(reportRoot, root) ?? portablePath(path.basename(root))
215
+ const rootStat = lstatIfPresent(root)
216
+ if (!rootStat) {
217
+ findings.push({ severity: 'error', code: 'surface-missing', surface: name, skill: null, path: surfacePath, message: 'skill surface does not exist' })
218
+ return { entries, findings }
219
+ }
220
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
221
+ findings.push({ severity: 'error', code: 'surface-redirected', surface: name, skill: null, path: surfacePath, message: 'skill surface must be a real directory' })
222
+ return { entries, findings }
223
+ }
224
+
225
+ for (const directory of fs.readdirSync(root, { withFileTypes: true }).sort((left, right) => stableCompare(left.name, right.name))) {
226
+ if (directory.name.startsWith('.')) continue
227
+ const entryPath = path.join(root, directory.name)
228
+ const entryDisplay = portablePath(path.join(surfacePath, directory.name))
229
+ if (directory.isSymbolicLink()) {
230
+ findings.push({ severity: 'error', code: 'skill-redirected', surface: name, skill: directory.name, path: entryDisplay, message: 'skill entry must not be a symbolic link' })
231
+ continue
232
+ }
233
+ if (!directory.isDirectory()) {
234
+ findings.push({ severity: 'warning', code: 'unexpected-surface-file', surface: name, skill: null, path: entryDisplay, message: 'non-directory entry is ignored' })
235
+ continue
236
+ }
237
+ let bundle
238
+ try {
239
+ bundle = readSkillBundle(root, directory.name)
240
+ } catch (error) {
241
+ findings.push({ severity: 'error', code: 'skill-bundle-invalid', surface: name, skill: directory.name, path: entryDisplay, message: error.message })
242
+ continue
243
+ }
244
+ const { frontmatter } = bundle
245
+ if (!frontmatter.ok) {
246
+ findings.push({ severity: 'error', code: 'frontmatter-invalid', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: frontmatter.error })
247
+ }
248
+ const declaredName = typeof frontmatter.values.name === 'string' ? frontmatter.values.name.trim() : ''
249
+ const description = typeof frontmatter.values.description === 'string' ? frontmatter.values.description.trim() : ''
250
+ if (!declaredName) findings.push({ severity: 'error', code: 'name-missing', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter name is required' })
251
+ else {
252
+ if (!SKILL_NAME_PATTERN.test(declaredName) || declaredName.length > 64) {
253
+ findings.push({ severity: 'error', code: 'name-invalid', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter name must be lowercase kebab-case and at most 64 characters' })
254
+ }
255
+ if (declaredName !== directory.name) {
256
+ findings.push({ severity: 'error', code: 'name-directory-mismatch', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter name must match its directory' })
257
+ }
258
+ }
259
+ if (!description) findings.push({ severity: 'error', code: 'description-missing', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'frontmatter description is required' })
260
+ else if (description.length > MAX_SKILL_DESCRIPTION) {
261
+ findings.push({ severity: 'warning', code: 'description-long', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: `frontmatter description exceeds ${MAX_SKILL_DESCRIPTION} characters` })
262
+ }
263
+ const fileNames = new Set(bundle.files.map((file) => file.path))
264
+ for (const link of markdownResourceLinks(bundle.skillText)) {
265
+ const normalized = portablePath(path.posix.normalize(link.replaceAll('\\', '/')))
266
+ if (normalized === '..' || normalized.startsWith('../')) {
267
+ findings.push({ severity: 'error', code: 'reference-escapes-bundle', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'relative resource link escapes the skill bundle' })
268
+ } else if (!fileNames.has(normalized)) {
269
+ findings.push({ severity: 'error', code: 'reference-missing', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: `relative resource link is missing: ${normalized}` })
270
+ }
271
+ }
272
+ if (/\b(?:TODO|TBD|REPLACE_ME)\b/.test(bundle.skillText)) {
273
+ findings.push({ severity: 'warning', code: 'unfinished-placeholder', surface: name, skill: directory.name, path: `${entryDisplay}/SKILL.md`, message: 'skill contains an unfinished scaffold placeholder' })
274
+ }
275
+ const entryName = SKILL_NAME_PATTERN.test(declaredName) && declaredName.length <= 64
276
+ ? declaredName
277
+ : SKILL_NAME_PATTERN.test(directory.name) && directory.name.length <= 64
278
+ ? directory.name
279
+ : null
280
+ if (entryName) {
281
+ entries.push({
282
+ surface: name,
283
+ name: entryName,
284
+ description,
285
+ lifecycle: 'active',
286
+ risk: bundle.files.length === 1 ? 'instruction-only' : 'resource-backed',
287
+ digest: bundle.digest,
288
+ fileCount: bundle.files.length,
289
+ bytes: bundle.bytes,
290
+ })
291
+ }
292
+ }
293
+ return { entries, findings }
294
+ }
295
+
296
+ function parityFindings(entries, surfaces) {
297
+ if (surfaces.length < 2) return []
298
+ const findings = []
299
+ const canonical = surfaces[0].name
300
+ const bySurface = new Map(surfaces.map((surface) => [surface.name, new Map()]))
301
+ for (const entry of entries) bySurface.get(entry.surface)?.set(entry.name, entry)
302
+ const names = [...new Set(entries.map((entry) => entry.name))].sort(stableCompare)
303
+ for (const name of names) {
304
+ const base = bySurface.get(canonical)?.get(name) ?? null
305
+ for (const surface of surfaces.slice(1)) {
306
+ const peer = bySurface.get(surface.name)?.get(name) ?? null
307
+ if (!base) {
308
+ findings.push({ severity: 'error', code: 'peer-only-skill', surface: surface.name, skill: name, path: name, message: `skill is absent from canonical surface ${canonical}` })
309
+ } else if (!peer) {
310
+ findings.push({ severity: 'error', code: 'peer-missing-skill', surface: surface.name, skill: name, path: name, message: `skill is missing from peer surface ${surface.name}` })
311
+ } else if (base.digest !== peer.digest) {
312
+ findings.push({ severity: 'error', code: 'peer-bundle-mismatch', surface: surface.name, skill: name, path: name, message: `skill bundle differs from canonical surface ${canonical}` })
313
+ }
314
+ }
315
+ }
316
+ return findings
317
+ }
318
+
319
+ function reportAuthority() {
320
+ return {
321
+ telemetry: false,
322
+ egress: false,
323
+ promptCapture: false,
324
+ sourceMutation: false,
325
+ runtimeMutation: false,
326
+ applyEndpoint: null,
327
+ }
328
+ }
329
+
330
+ export function auditSkillCatalog({ surfaces, reportRoot = process.cwd(), clock = () => new Date().toISOString() } = {}) {
331
+ const selected = Array.isArray(surfaces) && surfaces.length
332
+ ? surfaces.map((surface, index) => ({ name: String(surface.name || `surface-${index + 1}`), root: path.resolve(surface.root) }))
333
+ : [
334
+ { name: 'codex', root: path.join(packageRoot, 'skills', 'codex') },
335
+ { name: 'claude', root: path.join(packageRoot, 'skills', 'claude') },
336
+ ]
337
+ const entries = []
338
+ const findings = []
339
+ for (const surface of selected) {
340
+ const audited = auditSurface({ ...surface, reportRoot: path.resolve(reportRoot) })
341
+ entries.push(...audited.entries)
342
+ findings.push(...audited.findings)
343
+ }
344
+ findings.push(...parityFindings(entries, selected))
345
+ entries.sort((left, right) => stableCompare(`${left.surface}:${left.name}`, `${right.surface}:${right.name}`))
346
+ findings.sort((left, right) => stableCompare(`${left.severity}:${left.surface}:${left.skill}:${left.code}`, `${right.severity}:${right.surface}:${right.skill}:${right.code}`))
347
+ const errors = findings.filter((finding) => finding.severity === 'error').length
348
+ const warnings = findings.filter((finding) => finding.severity === 'warning').length
349
+ const report = {
350
+ schema: ATELIER_SKILL_AUDIT_SCHEMA,
351
+ generatedAt: clock(),
352
+ ok: errors === 0,
353
+ surfaces: selected.map((surface) => ({ name: surface.name, path: relativeTo(path.resolve(reportRoot), surface.root) ?? portablePath(path.basename(surface.root)) })),
354
+ summary: { skills: entries.length, errors, warnings },
355
+ entries,
356
+ findings,
357
+ authority: reportAuthority(),
358
+ }
359
+ const validation = validateSkillStewardDocument(report, '#/$defs/auditReport')
360
+ if (validation.length) throw new Error(`generated skill audit violates its contract: ${validation.join('; ')}`)
361
+ return report
362
+ }
363
+
364
+ function skillObservationLedger(workspaceRoot) {
365
+ requirePrivatePlacement(workspaceRoot)
366
+ return createCollaborationEventLedger({
367
+ workspaceRoot,
368
+ ledgerPath: path.join(workspaceRoot, OBSERVATION_LEDGER),
369
+ })
370
+ }
371
+
372
+ function validateObservation({ workflowKey, signal, skill, outcome }) {
373
+ const errors = []
374
+ if (typeof workflowKey !== 'string' || !WORKFLOW_KEY_PATTERN.test(workflowKey)) errors.push('workflow key is invalid')
375
+ if (!SKILL_OBSERVATION_SIGNALS.includes(signal)) errors.push(`signal must be one of ${SKILL_OBSERVATION_SIGNALS.join(', ')}`)
376
+ if (skill != null && (typeof skill !== 'string' || skill.length > 64 || !SKILL_NAME_PATTERN.test(skill))) errors.push('skill name is invalid')
377
+ if (!SKILL_OBSERVATION_OUTCOMES.includes(outcome)) errors.push(`outcome must be one of ${SKILL_OBSERVATION_OUTCOMES.join(', ')}`)
378
+ return errors
379
+ }
380
+
381
+ export function recordSkillObservation({
382
+ workspaceRoot = process.cwd(),
383
+ workflowKey,
384
+ signal,
385
+ skill = null,
386
+ outcome = 'unknown',
387
+ at = new Date().toISOString(),
388
+ ...unknown
389
+ } = {}) {
390
+ if (Object.keys(unknown).length) throw new Error('unknown skill observation fields refused')
391
+ const errors = validateObservation({ workflowKey, signal, skill, outcome })
392
+ if (errors.length) throw new Error(errors.join('; '))
393
+ const ledger = skillObservationLedger(workspaceRoot)
394
+ const current = ledger.eventsFor(workflowKey)
395
+ if (!current.ok) throw new Error(current.error)
396
+ const result = ledger.append({
397
+ aggregateId: workflowKey,
398
+ expectedVersion: current.currentVersion,
399
+ type: `skill-observation.${signal}`,
400
+ actor: 'atelier-skill-steward',
401
+ at,
402
+ payload: { signal, skill, outcome },
403
+ })
404
+ if (!result.ok) throw new Error(result.error)
405
+ return {
406
+ ok: true,
407
+ schema: 'mnstry.atelier-skill-observation-receipt@v1',
408
+ observation: {
409
+ id: result.event.id,
410
+ workflowKey,
411
+ version: result.event.version,
412
+ signal,
413
+ skill,
414
+ outcome,
415
+ observedAt: result.event.at,
416
+ },
417
+ storage: portablePath(OBSERVATION_LEDGER),
418
+ authority: { telemetry: false, egress: false, promptCapture: false },
419
+ }
420
+ }
421
+
422
+ function candidateKind(events) {
423
+ const lastSkill = [...events].reverse().find((event) => typeof event.payload?.skill === 'string')?.payload.skill ?? null
424
+ const counts = Object.fromEntries(SKILL_OBSERVATION_SIGNALS.map((signal) => [signal, 0]))
425
+ for (const event of events) {
426
+ if (Object.hasOwn(counts, event.payload?.signal)) counts[event.payload.signal] += 1
427
+ }
428
+ const creationEvidence = counts['repeated-task'] + counts['missing-workflow']
429
+ const improvementEvidence = counts['user-correction'] + counts['trigger-miss'] + counts['tool-failure'] + counts['stale-guidance']
430
+ const collisionEvidence = counts['trigger-collision']
431
+ let kind = null
432
+ let eligible = false
433
+ if (collisionEvidence >= 2) {
434
+ kind = 'reconcile'
435
+ eligible = true
436
+ } else if (lastSkill && (counts['superseded-skill'] >= 2 || counts['unused-skill'] >= 3)) {
437
+ kind = 'retire'
438
+ eligible = true
439
+ } else if (lastSkill && (counts['user-correction'] >= 2 || improvementEvidence >= 3)) {
440
+ kind = 'improve'
441
+ eligible = true
442
+ } else if (!lastSkill && creationEvidence >= 3) {
443
+ kind = 'create'
444
+ eligible = true
445
+ }
446
+ return { kind, eligible, skill: lastSkill, counts }
447
+ }
448
+
449
+ export function buildSkillCandidates({ workspaceRoot = process.cwd(), includeInsufficient = false, clock = () => new Date().toISOString() } = {}) {
450
+ const ledger = skillObservationLedger(workspaceRoot)
451
+ const result = ledger.readAll()
452
+ if (!result.ok) throw new Error(result.error)
453
+ const groups = new Map()
454
+ for (const event of result.events) {
455
+ if (!event.type.startsWith('skill-observation.')) continue
456
+ if (validateObservation({ workflowKey: event.aggregateId, ...event.payload }).length || event.type !== `skill-observation.${event.payload.signal}`) throw new Error('invalid stored skill observation')
457
+ const key = JSON.stringify([event.aggregateId, event.payload.skill ?? null])
458
+ const group = groups.get(key) ?? []
459
+ group.push(event)
460
+ groups.set(key, group)
461
+ }
462
+ const candidates = []
463
+ for (const [key, events] of [...groups.entries()].sort(([left], [right]) => stableCompare(left, right))) {
464
+ const [workflowKey] = JSON.parse(key)
465
+ const classified = candidateKind(events)
466
+ if (!classified.eligible && !includeInsufficient) continue
467
+ candidates.push({
468
+ workflowKey,
469
+ kind: classified.kind ?? 'observe',
470
+ status: classified.eligible ? 'eligible' : 'insufficient-evidence',
471
+ skill: classified.skill,
472
+ evidenceCount: events.length,
473
+ signals: classified.counts,
474
+ lastObservedAt: events.at(-1)?.at ?? null,
475
+ })
476
+ }
477
+ const report = {
478
+ schema: ATELIER_SKILL_CANDIDATES_SCHEMA,
479
+ generatedAt: clock(),
480
+ ok: true,
481
+ candidates,
482
+ observationCount: result.events.length,
483
+ diagnostics: [],
484
+ authority: reportAuthority(),
485
+ }
486
+ const validation = validateSkillStewardDocument(report, '#/$defs/candidateReport')
487
+ if (validation.length) throw new Error(`generated skill candidates violate their contract: ${validation.join('; ')}`)
488
+ return report
489
+ }
490
+
491
+ function catalogEntries(sourceRoot) {
492
+ const audit = auditSkillCatalog({ surfaces: [{ name: 'source', root: sourceRoot }], reportRoot: path.dirname(sourceRoot) })
493
+ return { audit, entries: audit.entries.map((entry) => ({ name: entry.name, digest: entry.digest })).sort((left, right) => stableCompare(left.name, right.name)) }
494
+ }
495
+
496
+ function syncAuthority() {
497
+ return {
498
+ telemetry: false,
499
+ egress: false,
500
+ sourceMutation: false,
501
+ targetMutation: 'content-bound-confirmed',
502
+ destructiveDelete: false,
503
+ }
504
+ }
505
+
506
+ function readSyncLock(lockPath) {
507
+ const stat = lstatIfPresent(lockPath)
508
+ if (!stat) return { lock: null, errors: [] }
509
+ if (stat.isSymbolicLink() || !stat.isFile()) return { lock: null, errors: ['skill sync lock must be a regular file'] }
510
+ let lock
511
+ try {
512
+ lock = JSON.parse(strictUtf8(readBounded(lockPath), 'skill sync lock'))
513
+ } catch (error) {
514
+ return { lock: null, errors: [`skill sync lock cannot be read: ${error.message}`] }
515
+ }
516
+ const errors = validateSkillStewardDocument(lock, '#/$defs/syncLock').map((error) => `skill sync lock ${error}`)
517
+ if (errors.length) return { lock: null, errors }
518
+ const names = (lock?.skills ?? []).map((entry) => entry.name)
519
+ if (new Set(names).size !== names.length) errors.push('skill sync lock contains duplicate skill names')
520
+ return { lock, errors }
521
+ }
522
+
523
+ function currentSkillDigest(targetRoot, name) {
524
+ const stat = lstatIfPresent(path.join(targetRoot, name))
525
+ if (!stat) return null
526
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`target skill ${name} must be a real directory`)
527
+ return readSkillBundle(targetRoot, name).digest
528
+ }
529
+
530
+ function verifyDirectoryPath(workspace, directory) {
531
+ const relative = path.relative(workspace, directory)
532
+ let current = workspace
533
+ for (const segment of relative.split(path.sep)) {
534
+ current = path.join(current, segment)
535
+ const stat = lstatIfPresent(current)
536
+ if (!stat) return
537
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
538
+ throw new Error('skill sync target contains a redirected or non-directory component')
539
+ }
540
+ }
541
+ }
542
+
543
+ export function planSkillSync({
544
+ workspaceRoot = process.cwd(),
545
+ sourceRoot = path.join(packageRoot, 'skills', 'codex'),
546
+ target = path.join('.agents', 'skills'),
547
+ clock = () => new Date().toISOString(),
548
+ } = {}) {
549
+ const workspace = fs.realpathSync(path.resolve(workspaceRoot))
550
+ const targetRoot = containedPath(workspace, path.resolve(workspace, target), 'skill sync target')
551
+ const sourcePath = fs.realpathSync(path.resolve(sourceRoot))
552
+ if (sourcePath === targetRoot || !escaped(sourcePath, targetRoot) || !escaped(targetRoot, sourcePath)) throw new Error('skill source and target must not overlap')
553
+ const privatePath = path.join(workspace, '.atelier-local')
554
+ if (targetRoot === privatePath || !escaped(privatePath, targetRoot)) throw new Error('skill target must not overlap private state')
555
+ verifyDirectoryPath(workspace, targetRoot)
556
+ const targetRelative = portablePath(path.relative(workspace, targetRoot))
557
+ if (targetRelative.split('/').includes('.git')) throw new Error('skill target must not overlap Git metadata')
558
+ const lockPath = path.join(targetRoot, SKILL_SYNC_LOCK_FILE)
559
+ const lockRelative = portablePath(path.relative(workspace, lockPath))
560
+ const blockers = []
561
+ let source
562
+ try {
563
+ source = catalogEntries(path.resolve(sourceRoot))
564
+ } catch (error) {
565
+ source = { audit: null, entries: [] }
566
+ blockers.push(`source catalog cannot be audited: ${error.message}`)
567
+ }
568
+ if (source.audit && !source.audit.ok) blockers.push(...source.audit.findings.filter((finding) => finding.severity === 'error').map((finding) => `${finding.skill ?? finding.surface}: ${finding.message}`))
569
+ const lockRead = readSyncLock(lockPath)
570
+ blockers.push(...lockRead.errors)
571
+ const lock = lockRead.lock
572
+ const locked = new Map((lock?.skills ?? []).map((entry) => [entry.name, entry.digest]))
573
+ const next = new Map(source.entries.map((entry) => [entry.name, entry.digest]))
574
+ const names = [...new Set([...next.keys(), ...locked.keys()])].sort(stableCompare)
575
+ const actions = []
576
+ const planToken = canonicalDigest({ source: source.entries, target: targetRelative }).slice('sha256:'.length, 'sha256:'.length + 16)
577
+ for (const name of names) {
578
+ const expectedDigest = locked.get(name) ?? null
579
+ const nextDigest = next.get(name) ?? null
580
+ let currentDigest = null
581
+ try {
582
+ currentDigest = currentSkillDigest(targetRoot, name)
583
+ } catch (error) {
584
+ blockers.push(error.message)
585
+ continue
586
+ }
587
+ if (expectedDigest && expectedDigest !== currentDigest) {
588
+ blockers.push(`managed target skill ${name} has local drift; preserve or review it before sync`)
589
+ continue
590
+ }
591
+ if (!nextDigest && expectedDigest) {
592
+ if (!currentDigest) continue
593
+ actions.push({ type: 'quarantine', name, expectedDigest, currentDigest, nextDigest: null, quarantinePath: portablePath(path.join('.atelier-local', 'skill-steward', 'quarantine', planToken, name)) })
594
+ } else if (nextDigest && !currentDigest) {
595
+ actions.push({ type: 'add', name, expectedDigest: null, currentDigest: null, nextDigest, quarantinePath: null })
596
+ } else if (nextDigest && currentDigest && !expectedDigest) {
597
+ blockers.push(`target skill ${name} already exists and is not managed by Atelier`)
598
+ } else if (nextDigest && currentDigest && nextDigest === currentDigest) {
599
+ actions.push({ type: 'unchanged', name, expectedDigest, currentDigest, nextDigest, quarantinePath: null })
600
+ } else if (nextDigest && currentDigest) {
601
+ actions.push({ type: 'update', name, expectedDigest, currentDigest, nextDigest, quarantinePath: portablePath(path.join('.atelier-local', 'skill-steward', 'quarantine', planToken, name)) })
602
+ }
603
+ }
604
+ actions.sort((left, right) => stableCompare(left.name, right.name))
605
+ blockers.sort(stableCompare)
606
+ const catalogDigest = canonicalDigest(source.entries)
607
+ const authoritative = {
608
+ source: { packageName: packageJson.name, packageVersion: packageJson.version, catalogDigest,
609
+ ext: { catalogOrigin: sourcePath === path.join(packageRoot, 'skills', 'codex') ? 'bundled-codex' : 'caller-selected' } },
610
+ target: { path: targetRelative, lockPath: lockRelative },
611
+ actions,
612
+ blockers,
613
+ authority: syncAuthority(),
614
+ ext: { workspaceBinding: sha256(workspace), sourceBinding: sha256(sourcePath) },
615
+ }
616
+ const plan = {
617
+ schema: ATELIER_SKILL_SYNC_PLAN_SCHEMA,
618
+ generatedAt: clock(),
619
+ planDigest: canonicalDigest(authoritative),
620
+ applyAllowed: blockers.length === 0,
621
+ ...authoritative,
622
+ }
623
+ const validation = validateSkillStewardDocument(plan, '#/$defs/syncPlan')
624
+ if (validation.length) throw new Error(`generated skill sync plan violates its contract: ${validation.join('; ')}`)
625
+ return plan
626
+ }
627
+
628
+ function copyBundle(sourceRoot, targetRoot, name, expectedDigest) {
629
+ const source = path.join(sourceRoot, name)
630
+ const target = path.join(targetRoot, name)
631
+ fs.mkdirSync(target, { recursive: false, mode: 0o700 })
632
+ const bundle = readSkillBundle(sourceRoot, name)
633
+ for (const file of bundle.files) {
634
+ const output = path.join(target, ...file.path.split('/'))
635
+ fs.mkdirSync(path.dirname(output), { recursive: true, mode: 0o700 })
636
+ fs.writeFileSync(output, file.bytes, { mode: 0o600, flag: 'wx' })
637
+ }
638
+ const copied = readSkillBundle(targetRoot, name)
639
+ if (copied.digest !== bundle.digest) throw new Error(`staged skill ${name} digest mismatch`)
640
+ if (copied.digest !== expectedDigest) throw new Error(`source skill ${name} changed after the confirmed plan was prepared`)
641
+ }
642
+
643
+ function ensureTargetDirectory(workspace, targetRoot) {
644
+ const relative = path.relative(workspace, targetRoot)
645
+ let current = workspace
646
+ for (const segment of relative.split(path.sep)) {
647
+ current = path.join(current, segment)
648
+ let stat = lstatIfPresent(current)
649
+ if (!stat) {
650
+ fs.mkdirSync(current, { mode: 0o700 })
651
+ stat = fs.lstatSync(current)
652
+ }
653
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('skill sync target contains a redirected or non-directory component')
654
+ }
655
+ }
656
+
657
+ function applySkillSyncLocked({
658
+ workspaceRoot = process.cwd(),
659
+ sourceRoot = path.join(packageRoot, 'skills', 'codex'),
660
+ target = path.join('.agents', 'skills'),
661
+ confirm,
662
+ clock = () => new Date().toISOString(),
663
+ } = {}) {
664
+ const workspace = fs.realpathSync(path.resolve(workspaceRoot))
665
+ const targetRoot = containedPath(workspace, path.resolve(workspace, target), 'skill sync target')
666
+ const plan = planSkillSync({ workspaceRoot: workspace, sourceRoot, target, clock })
667
+ if (!DIGEST_PATTERN.test(String(confirm ?? '')) || confirm !== plan.planDigest) throw new Error('skill sync confirmation must exactly match the current plan digest')
668
+ if (!plan.applyAllowed) throw new Error(plan.blockers.join('\n'))
669
+ ensureTargetDirectory(workspace, targetRoot)
670
+
671
+ const planKey = plan.actions.find((action) => action.quarantinePath)?.quarantinePath.split('/').at(-2) ?? plan.planDigest.slice('sha256:'.length, 'sha256:'.length + 16)
672
+ const privateRoot = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(workspace, '.atelier-local', 'skill-steward'), label: 'skill steward private state' })
673
+ const stagingRoot = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(privateRoot, 'staging', planKey), label: 'skill sync staging directory' })
674
+ const quarantineRoot = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(privateRoot, 'quarantine', planKey), label: 'skill sync quarantine directory' })
675
+ if (fs.readdirSync(stagingRoot).length || fs.readdirSync(quarantineRoot).length) throw new Error('skill sync plan state already exists; inspect or clear the preserved state before retrying')
676
+
677
+ const mutating = plan.actions.filter((action) => ['add', 'update'].includes(action.type))
678
+ try {
679
+ for (const action of mutating) copyBundle(path.resolve(sourceRoot), stagingRoot, action.name, action.nextDigest)
680
+ } catch (error) {
681
+ throw new Error(`skill sync staging failed: ${error.message}`)
682
+ }
683
+
684
+ const applied = []
685
+ try {
686
+ verifyDirectoryPath(workspace, targetRoot)
687
+ if (planSkillSync({ workspaceRoot: workspace, sourceRoot, target, clock }).planDigest !== plan.planDigest) throw new Error('skill sync plan changed during staging')
688
+ for (const action of plan.actions) {
689
+ const currentDigest = currentSkillDigest(targetRoot, action.name)
690
+ if (currentDigest !== action.currentDigest) throw new Error(`target skill ${action.name} changed after the confirmed plan was prepared`)
691
+ }
692
+ for (const action of plan.actions) {
693
+ const targetSkill = path.join(targetRoot, action.name)
694
+ const stagedSkill = path.join(stagingRoot, action.name)
695
+ const quarantinedSkill = path.join(quarantineRoot, action.name)
696
+ if (action.type === 'add') {
697
+ fs.renameSync(stagedSkill, targetSkill)
698
+ applied.push({ action, targetSkill, stagedSkill, quarantinedSkill })
699
+ } else if (action.type === 'update') {
700
+ fs.renameSync(targetSkill, quarantinedSkill)
701
+ try {
702
+ fs.renameSync(stagedSkill, targetSkill)
703
+ } catch (error) {
704
+ fs.renameSync(quarantinedSkill, targetSkill)
705
+ throw error
706
+ }
707
+ applied.push({ action, targetSkill, stagedSkill, quarantinedSkill })
708
+ } else if (action.type === 'quarantine') {
709
+ fs.renameSync(targetSkill, quarantinedSkill)
710
+ applied.push({ action, targetSkill, stagedSkill, quarantinedSkill })
711
+ }
712
+ }
713
+
714
+ const lock = {
715
+ schema: ATELIER_SKILL_SYNC_LOCK_SCHEMA,
716
+ generatedAt: clock(),
717
+ source: plan.source,
718
+ target: plan.target.path,
719
+ skills: plan.actions
720
+ .filter((action) => action.nextDigest)
721
+ .map((action) => ({ name: action.name, digest: action.nextDigest }))
722
+ .sort((left, right) => stableCompare(left.name, right.name)),
723
+ authority: syncAuthority(),
724
+ }
725
+ const validation = validateSkillStewardDocument(lock, '#/$defs/syncLock')
726
+ if (validation.length) throw new Error(`generated skill sync lock violates its contract: ${validation.join('; ')}`)
727
+ atomicReplacePrivateText(path.join(targetRoot, SKILL_SYNC_LOCK_FILE), `${JSON.stringify(lock, null, 2)}\n`, 0o600)
728
+ try {
729
+ fs.rmSync(stagingRoot, { recursive: true })
730
+ } catch {
731
+ // The applied projection is authoritative; leftover empty private state is diagnosable and ignored.
732
+ }
733
+ return {
734
+ ok: true,
735
+ schema: 'mnstry.atelier-skill-sync-result@v1',
736
+ plan,
737
+ lock,
738
+ quarantined: applied.filter((item) => ['update', 'quarantine'].includes(item.action.type)).map((item) => portablePath(path.relative(workspace, item.quarantinedSkill))),
739
+ }
740
+ } catch (error) {
741
+ for (const item of [...applied].reverse()) {
742
+ try {
743
+ if (item.action.type === 'add' && lstatIfPresent(item.targetSkill)) fs.renameSync(item.targetSkill, item.stagedSkill)
744
+ if (item.action.type === 'update') {
745
+ if (lstatIfPresent(item.targetSkill)) fs.renameSync(item.targetSkill, item.stagedSkill)
746
+ if (lstatIfPresent(item.quarantinedSkill)) fs.renameSync(item.quarantinedSkill, item.targetSkill)
747
+ }
748
+ if (item.action.type === 'quarantine' && lstatIfPresent(item.quarantinedSkill)) fs.renameSync(item.quarantinedSkill, item.targetSkill)
749
+ } catch {
750
+ // Preserve every directory for manual recovery; never delete ambiguous state.
751
+ }
752
+ }
753
+ throw new Error(`skill sync apply failed and preserved recovery state: ${error.message}`)
754
+ }
755
+ }
756
+
757
+ export function applySkillSync(options = {}) {
758
+ const workspace = fs.realpathSync(path.resolve(options.workspaceRoot ?? process.cwd()))
759
+ // Confirmation is checked before creating private operation state.
760
+ const plan = planSkillSync(options)
761
+ if (options.confirm !== plan.planDigest) throw new Error('skill sync confirmation must exactly match the current plan digest')
762
+ if (!plan.applyAllowed) throw new Error(plan.blockers.join('\n'))
763
+ requirePrivatePlacement(workspace)
764
+ const directory = ensureContainedPrivateDirectory({ workspaceRoot: workspace, directory: path.join(workspace, '.atelier-local', 'skill-steward'), label: 'skill steward private state' })
765
+ const lockPath = path.join(directory, '.operation.lock')
766
+ const descriptor = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW, 0o600)
767
+ try {
768
+ return applySkillSyncLocked(options)
769
+ } finally {
770
+ fs.closeSync(descriptor)
771
+ fs.unlinkSync(lockPath)
772
+ }
773
+ }
774
+
775
+ export function validateSkillStewardDocument(document, pointer = null) {
776
+ const schema = pointer
777
+ ? { $schema: stewardSchema.$schema, $defs: stewardSchema.$defs, $ref: pointer }
778
+ : stewardSchema
779
+ return validateJsonSchema(schema, document)
780
+ }