@mnstry/atelier 0.2.0-alpha.6 → 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 (374) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +59 -15
  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-review.v1.schema.json +1601 -0
  25. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  26. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  27. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  28. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  29. package/docs/assurance-controls.md +9 -1
  30. package/docs/atelier-sync.md +5 -4
  31. package/docs/blocks/claims.md +15 -10
  32. package/docs/blocks/will-not-do.md +4 -1
  33. package/docs/coauthor-session.md +57 -0
  34. package/docs/connected-composition.md +75 -0
  35. package/docs/decisions.md +167 -0
  36. package/docs/exact-upgrades.md +159 -0
  37. package/docs/extension-pack-lifecycle.md +33 -0
  38. package/docs/guided-upgrades.md +99 -0
  39. package/docs/install.md +65 -9
  40. package/docs/intake-and-guides.md +65 -0
  41. package/docs/integration-and-review-plan.md +409 -0
  42. package/docs/integration-contract-decisions.md +22 -0
  43. package/docs/local-review.md +84 -0
  44. package/docs/local-services.md +227 -0
  45. package/docs/local-state.md +107 -0
  46. package/docs/obsidian-contract.md +687 -0
  47. package/docs/obsidian.md +387 -0
  48. package/docs/portable-presentation.md +328 -0
  49. package/docs/presentation-consumer-boundaries.md +75 -0
  50. package/docs/presentation-review-disposition.md +47 -0
  51. package/docs/project-command-inventory.md +38 -0
  52. package/docs/project-options.md +53 -0
  53. package/docs/release-engineering.md +88 -0
  54. package/docs/review-portability.md +55 -0
  55. package/docs/skill-steward.md +156 -0
  56. package/docs/upgrade.md +55 -2
  57. package/docs/vault-service.md +292 -0
  58. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  59. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  60. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  61. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  62. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  64. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  65. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  66. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  67. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  69. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  70. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  71. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  72. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  73. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  74. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  75. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  76. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  77. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  78. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  79. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  80. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  81. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  82. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  83. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  84. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  85. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  86. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  87. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  88. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  89. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  90. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  91. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  92. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  93. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  94. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  95. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  96. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  97. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  98. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  99. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  100. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  101. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  102. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  103. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  104. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  105. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  106. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  107. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  108. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  109. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  113. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  114. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  115. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  116. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  121. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  122. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  123. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  124. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  126. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  131. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  132. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  133. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  136. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  137. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  138. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  142. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  143. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  144. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  145. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  146. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  147. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  155. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  156. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  157. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  158. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  161. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  162. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  163. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  164. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  169. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  170. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  171. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  172. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  177. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  178. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  179. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  180. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  181. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  182. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  183. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  184. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  187. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  188. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  189. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  190. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  193. package/fixtures/obsidian/edits/workspace.json +65 -0
  194. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  195. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  196. package/fixtures/obsidian/materialization/workspace.json +54 -0
  197. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  198. package/fixtures/ui/presentation/pane.v1.json +7 -0
  199. package/fixtures/ui/presentation/reference.v1.json +293 -0
  200. package/package.json +57 -6
  201. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  202. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  203. package/skills/claude/atelier-open/SKILL.md +5 -0
  204. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  205. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  206. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  207. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  208. package/skills/codex/atelier-open/SKILL.md +5 -0
  209. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  210. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  211. package/src/access/preview-enforcer.mjs +84 -0
  212. package/src/boundary/policy.mjs +57 -25
  213. package/src/cli/project-options.mjs +58 -0
  214. package/src/cli/run.mjs +43 -3
  215. package/src/coauthor/session.mjs +161 -0
  216. package/src/coauthor/store.mjs +173 -0
  217. package/src/collaboration/event-ledger.mjs +16 -33
  218. package/src/collaboration/inspection-bundle.mjs +223 -0
  219. package/src/collaboration/review-contracts.mjs +25 -0
  220. package/src/collaboration/review-store.mjs +305 -0
  221. package/src/commands/coauthor.mjs +26 -0
  222. package/src/commands/config.mjs +6 -1
  223. package/src/commands/extension-pack.mjs +15 -8
  224. package/src/commands/init.mjs +44 -5
  225. package/src/commands/obsidian.mjs +368 -0
  226. package/src/commands/review.mjs +147 -0
  227. package/src/commands/setup.mjs +51 -14
  228. package/src/commands/skills.mjs +199 -0
  229. package/src/commands/sync.mjs +8 -1
  230. package/src/composition/registry.mjs +27 -0
  231. package/src/composition/wire.mjs +122 -0
  232. package/src/contracts/corpus.mjs +29 -0
  233. package/src/decisions/contracts.d.mts +96 -0
  234. package/src/decisions/contracts.mjs +321 -0
  235. package/src/disclosure/content-scan.mjs +13 -0
  236. package/src/export/atelier-export-contract.mjs +16 -1
  237. package/src/extension-packs/lifecycle.mjs +154 -0
  238. package/src/graph/graph.mjs +64 -2
  239. package/src/graph/knowledge-graph.mjs +548 -24
  240. package/src/guides/contracts.mjs +54 -0
  241. package/src/intake/store.mjs +119 -0
  242. package/src/preview/channel.mjs +89 -0
  243. package/src/project/config.mjs +26 -27
  244. package/src/project/durable-state.mjs +161 -0
  245. package/src/project/file-class.mjs +166 -0
  246. package/src/project/private-state.mjs +11 -1
  247. package/src/projection/obsidian/contracts.mjs +571 -0
  248. package/src/projection/obsidian/edits/align.mjs +169 -0
  249. package/src/projection/obsidian/edits/apply.mjs +889 -0
  250. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  251. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  252. package/src/projection/obsidian/edits/index.mjs +19 -0
  253. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  254. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  255. package/src/projection/obsidian/edits/observe.mjs +123 -0
  256. package/src/projection/obsidian/edits/policy.mjs +123 -0
  257. package/src/projection/obsidian/edits/regions.mjs +648 -0
  258. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  259. package/src/projection/obsidian/materialize/index.mjs +7 -0
  260. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  261. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  262. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  263. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  264. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  265. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  266. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  267. package/src/projection/obsidian/proposals/index.mjs +23 -0
  268. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  269. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  270. package/src/projection/obsidian/proposals/router.mjs +168 -0
  271. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  272. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  273. package/src/projection/obsidian/publication/index.mjs +5 -0
  274. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  275. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  276. package/src/projection/obsidian/publication/transport.mjs +139 -0
  277. package/src/projection/obsidian/recovery/index.mjs +6 -0
  278. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  279. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  280. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  281. package/src/projection/obsidian/recovery/store.mjs +234 -0
  282. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  283. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  284. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  285. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  286. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  287. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  288. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  289. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  290. package/src/projection/project.mjs +28 -19
  291. package/src/readiness-protocols/evidence.mjs +282 -0
  292. package/src/readiness-protocols/runtime.mjs +4 -3
  293. package/src/readiness-protocols/source-read.mjs +39 -0
  294. package/src/runtime/obsidian/app-capability.mjs +110 -0
  295. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  296. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  297. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  298. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  299. package/src/runtime/obsidian/contributions.mjs +27 -0
  300. package/src/runtime/obsidian/documents.mjs +37 -0
  301. package/src/runtime/obsidian/enablement.mjs +53 -0
  302. package/src/runtime/obsidian/engine.mjs +532 -0
  303. package/src/runtime/obsidian/errors.mjs +14 -0
  304. package/src/runtime/obsidian/extension-points.mjs +103 -0
  305. package/src/runtime/obsidian/index.mjs +34 -0
  306. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  307. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  308. package/src/runtime/obsidian/observation.mjs +158 -0
  309. package/src/runtime/obsidian/opening.mjs +243 -0
  310. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  311. package/src/runtime/obsidian/pipeline.mjs +122 -0
  312. package/src/runtime/obsidian/private-lock.mjs +156 -0
  313. package/src/runtime/obsidian/process-identity.mjs +58 -0
  314. package/src/runtime/obsidian/service-client.mjs +66 -0
  315. package/src/runtime/obsidian/service-main.mjs +89 -0
  316. package/src/runtime/obsidian/service-record.mjs +169 -0
  317. package/src/runtime/obsidian/service-server.mjs +121 -0
  318. package/src/runtime/obsidian/service.mjs +216 -0
  319. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  320. package/src/runtime/obsidian/state-store.mjs +180 -0
  321. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  322. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  323. package/src/runtime/obsidian/watchers.mjs +36 -0
  324. package/src/runtime/supervisor.mjs +16 -1
  325. package/src/server/local-sidecar.mjs +44 -0
  326. package/src/server/server.mjs +1 -0
  327. package/src/skills/steward.mjs +780 -0
  328. package/src/ui/presentation/browser.mjs +278 -0
  329. package/src/ui/presentation/contract.mjs +116 -0
  330. package/src/ui/presentation/index.mjs +7 -0
  331. package/src/ui/presentation/native.mjs +153 -0
  332. package/src/ui/presentation/proof.mjs +57 -0
  333. package/src/ui/presentation/schema-check.mjs +30 -0
  334. package/src/ui/presentation/schema.generated.mjs +1371 -0
  335. package/src/ui/presentation/state.mjs +43 -0
  336. package/src/ui/presentation/styles.mjs +67 -0
  337. package/src/ui/presentation/tokens.mjs +74 -0
  338. package/src/ui/presentation/web.mjs +85 -0
  339. package/src/ui/review-page.mjs +32 -0
  340. package/src/upgrade/explanation.mjs +33 -0
  341. package/src/upgrade/provenance.mjs +338 -0
  342. package/src/upgrade/transaction-files.mjs +103 -0
  343. package/src/upgrade/transaction.mjs +475 -0
  344. package/src/upgrade/upgrade.mjs +63 -28
  345. package/src/vault/hosts.mjs +21 -0
  346. package/src/vault/identity.mjs +19 -0
  347. package/src/vault/index.mjs +7 -0
  348. package/src/vault/interface.mjs +11 -0
  349. package/src/vault/metadata.mjs +31 -0
  350. package/src/vault/privacy.mjs +148 -0
  351. package/src/vault/probe.mjs +89 -0
  352. package/src/vault/service.mjs +131 -0
  353. package/src/vault/source.mjs +45 -0
  354. package/src/vault/storage.mjs +23 -0
  355. package/templates/distribution-workspace/.claude/launch.json +6 -2
  356. package/templates/distribution-workspace/gitignore +1 -0
  357. package/templates/distribution-workspace/project/README.md +2 -2
  358. package/templates/external-project-workspace/README.md +45 -0
  359. package/templates/external-project-workspace/answers.example.json +4 -0
  360. package/templates/external-project-workspace/atelier.project.json +40 -0
  361. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  362. package/templates/external-project-workspace/gitignore +8 -0
  363. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  364. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  365. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  366. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  367. package/templates/external-project-workspace/source/README.md +11 -0
  368. package/templates/launch.json +6 -2
  369. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  370. package/templates/private-domain-workspace/domain/README.md +2 -2
  371. package/templates/private-domain-workspace/gitignore +1 -0
  372. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  373. package/templates/shared-project-workspace/gitignore +1 -0
  374. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { AtelierDiagnosticError, resolveProjectConfig } from '../project/config.mjs'
6
+ import { ObsidianContractRefusal } from '../projection/obsidian/contracts.mjs'
7
+ import { applyPolicyDigest } from '../projection/obsidian/edits/policy.mjs'
8
+ import { MINIMUM_APP_VERSION } from '../runtime/obsidian/app-capability.mjs'
9
+ import { loadContributions } from '../runtime/obsidian/contributions.mjs'
10
+ import { isoTime } from '../runtime/obsidian/documents.mjs'
11
+ import { readObsidianEnablement } from '../runtime/obsidian/enablement.mjs'
12
+ import { ObsidianMaintenanceRefusal, refuse } from '../runtime/obsidian/errors.mjs'
13
+ import { BUILT_IN_OPERATIONS, UNAVAILABLE_APPLY_OPERATION, createObsidianRegistry } from '../runtime/obsidian/extension-points.mjs'
14
+ import { LIFECYCLE_PRIMITIVES, readServiceStatusDocument, serviceStatus, startService, stopService } from '../runtime/obsidian/lifecycle.mjs'
15
+ import {
16
+ authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy, protectedRoots, readInstalledApplyPolicy, readMachineSettings,
17
+ revokeApplyPolicy, writeMachineSettings,
18
+ } from '../runtime/obsidian/machine-settings.mjs'
19
+ import { APPLY_UNAVAILABLE, OPENING_OUTCOMES, OPENING_PRIMITIVES, openScopeForOracleTests, resolveScope, scopeReport } from '../runtime/obsidian/opening.mjs'
20
+ import { serviceNameFor, servicePaths } from '../runtime/obsidian/service-record.mjs'
21
+ import { resolveServiceWorkspace } from '../runtime/obsidian/service.mjs'
22
+ import { buildStartupAdapter } from '../runtime/obsidian/startup-adapters.mjs'
23
+
24
+ // `atelier obsidian <operation>`: status, views, audiences, apply policy, the
25
+ // owned maintenance service, and opening a view.
26
+ //
27
+ // Noninteractive: every input is an argument, nothing is ever asked. With
28
+ // `--json` exactly one JSON document is printed on stdout, for a refusal too.
29
+ //
30
+ // Exit codes: 0 done (for `open`: the view is current and open); 1 an error
31
+ // nobody typed; 2 a typed refusal or a usage error; 3 the operation ran and
32
+ // its answer is not success (any opening outcome but `current`, a service
33
+ // that did not start, a stop that was refused).
34
+ //
35
+ // Reaching the installed app or the operating system goes through seams
36
+ // (`appProbe`, `launcher`, `service`). A caller passes them. The production
37
+ // seams are constructed only when this module runs as the real command-line
38
+ // entry (`production: true`) AND `--adapter=obsidian-cli` was given, the same
39
+ // explicit rule the service entry has; there is no default.
40
+
41
+ export const COMMAND_SCHEMA = 'atelier-obsidian-command/v1'
42
+ export const EXIT = Object.freeze({ ok: 0, error: 1, refused: 2, notSuccess: 3 })
43
+ export { BUILT_IN_OPERATIONS }
44
+ const PRODUCTION_ADAPTER = 'obsidian-cli'
45
+ const MAX_POLICY_BYTES = 64 * 1024
46
+ const AUDIENCE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
47
+
48
+ export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.project.json] [--data-root DIR] [--json]
49
+
50
+ status Enablement, machine settings, service and per-view freshness. Read-only.
51
+ scope list | scope show ID The views this project declares. Read-only.
52
+ audience show | set A,B | clear The audiences this machine lets into a view (private; none by default).
53
+ mode show | set manual|automatic Whether queued edits wait for a person or are applied under the policy.
54
+ policy show | install FILE | revoke The private apply policy. Automatic mode needs an installed, active one.
55
+ policy digest FILE The digest FILE has to carry to be installed. Reads FILE; writes nothing.
56
+ service start [--consent-actor ID] --adapter=${PRODUCTION_ADAPTER}
57
+ service status | stop The owned maintenance service of this workspace.
58
+ service unit --print --adapter=${PRODUCTION_ADAPTER}
59
+ Print an operating-system startup unit. Writes and installs nothing.
60
+ open [--scope ID] [--consent-actor ID] [--allow-stale] --adapter=${PRODUCTION_ADAPTER}
61
+ Start or reconnect maintenance, verify the view, open it in Obsidian.
62
+
63
+ Contributed operations, registered by the modules shipped under src/runtime/obsidian/contributions/:
64
+ apply list | show EDIT | run EDIT [--actor ID] | recover
65
+ Pending edits, and the explicit apply of one of them to its source file.
66
+ conflicts [ID] The shared conflict state of edited objects. Read-only.
67
+ apply-policy create FILE | show | revoke
68
+ Build, digest and install an apply policy from a JSON request; show it; revoke it.
69
+ selection resolve ID | persist ID [allow-empty] | show ID | list
70
+ The exact selected set of a declared view; persisted in Atelier state.
71
+ proposals list | show OPERATION Structural edits routed as copy-only proposals. Read-only.
72
+ \`status\` lists the operations this command registered under "operations".
73
+
74
+ Opening outcomes: ${Object.keys(OPENING_OUTCOMES).join(', ')}.
75
+ Pending edits additionally report ${APPLY_UNAVAILABLE} while no apply operation is registered.
76
+ Minimum Obsidian version: ${MINIMUM_APP_VERSION}.
77
+ Exit codes: 0 done; 1 internal error; 2 refusal or usage; 3 ran, and the answer is not success.`
78
+
79
+ const FLAGS = Object.freeze({ json: 'flag', 'allow-stale': 'flag', print: 'flag', help: 'flag', project: 'value', 'project-config': 'value', 'data-root': 'value', scope: 'value', 'consent-actor': 'value', actor: 'value', adapter: 'value', 'wait-ms': 'value' })
80
+
81
+ function parse(argv) {
82
+ const positionals = []
83
+ const flags = {}
84
+ for (let index = 0; index < argv.length; index += 1) {
85
+ const arg = argv[index]
86
+ if (arg === '-h') { flags.help = true; continue }
87
+ if (!arg.startsWith('--')) { positionals.push(arg); continue }
88
+ const [name, inline] = arg.slice(2).split(/=(.*)/s, 2)
89
+ if (!Object.hasOwn(FLAGS, name)) refuse('usage', `unknown option --${name}`)
90
+ if (FLAGS[name] === 'flag') { if (inline !== undefined) refuse('usage', `--${name} takes no value`); flags[name] = true; continue }
91
+ const value = inline ?? argv[index += 1]
92
+ if (value === undefined || value === '' || (inline === undefined && value.startsWith('--'))) refuse('usage', `--${name} needs a value`)
93
+ flags[name] = value
94
+ }
95
+ return { positionals, flags }
96
+ }
97
+
98
+ const isTyped = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal
99
+ const plainMessage = (error) => String(error.message ?? '').replace(new RegExp(`^${error.code}: `), '')
100
+ const NEXT = Object.freeze({
101
+ usage: 'run `atelier obsidian --help`',
102
+ 'seams-required': 'pass the seams, or run the real command-line entry',
103
+ 'app-adapter-not-selected': `pass --adapter=${PRODUCTION_ADAPTER} to let this command reach the installed app`,
104
+ 'startup-consent-required': 'pass --consent-actor ID to record who allows the maintenance service to run',
105
+ 'automatic-mode-refused': 'install an active automatic policy with `obsidian policy install FILE`',
106
+ [APPLY_UNAVAILABLE]: 'no apply operation is registered on this command; edits stay preserved and queued',
107
+ 'policy-digest-mismatch': 'set the "digest" member of the file to the expected digest (`obsidian policy digest FILE` prints it), then install again',
108
+ disabled: 'declare the Obsidian settings in the project configuration',
109
+ })
110
+
111
+ // The decisions the command's oracles are sensitive to are those of opening and of the lifecycle; tests substitute
112
+ // broken ones here to prove the oracles can fail. `runObsidianCommand` always uses the production ones.
113
+ export async function runObsidianCommandForOracleTests(options = {}, rules = {}) {
114
+ const openingRules = rules.opening ?? OPENING_PRIMITIVES
115
+ const lifecycleRules = rules.lifecycle ?? LIFECYCLE_PRIMITIVES
116
+ const {
117
+ argv = [], seams = null, production = false, env = process.env, cwd = process.cwd(), platform = process.platform, clock = () => new Date(),
118
+ stdout = (text) => process.stdout.write(`${text}\n`), stderr = (text) => process.stderr.write(`${text}\n`), contributions = null, contributionsDirectory,
119
+ } = options
120
+ let json = argv.includes('--json')
121
+ let operationName = null
122
+ const emit = ({ exit, document, human }) => {
123
+ const complete = { schema: COMMAND_SCHEMA, ok: exit === EXIT.ok, operation: operationName, ...document }
124
+ if (json) stdout(JSON.stringify(complete, null, 2))
125
+ else for (const line of human ?? [JSON.stringify(complete, null, 2)]) (exit === EXIT.ok ? stdout : stderr)(line)
126
+ return exit
127
+ }
128
+ try {
129
+ const { positionals, flags } = parse(argv)
130
+ json = flags.json === true
131
+ operationName = positionals[0] ?? null
132
+ if (flags.help === true || operationName === null || operationName === 'help') { operationName = 'help'; return emit({ exit: EXIT.ok, document: { usage: USAGE }, human: [USAGE] }) }
133
+ // Nothing runs without a decision about the seams: a caller that forgot them never falls through to a real app.
134
+ if (seams === null && production !== true) refuse('seams-required', 'no seams were passed and this is not the real command-line entry')
135
+
136
+ const dataRoot = flags['data-root'] === undefined ? options.dataRoot : path.resolve(cwd, flags['data-root'])
137
+ const configFlag = flags.project ?? flags['project-config']
138
+ const loadProject = options.loadProject ?? (() => resolveProjectConfig({ argv: configFlag === undefined ? [] : [`--project=${path.resolve(cwd, configFlag)}`], cwd, env, writeLocalState: false }))
139
+ const registry = createObsidianRegistry({ reservedOperations: BUILT_IN_OPERATIONS, contributions: contributions ?? await loadContributions(contributionsDirectory === undefined ? {} : { directory: contributionsDirectory }) })
140
+ const applyAvailable = registry.extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
141
+ const lifecycle = { loadProject, dataRoot, env, platform, ...(options.probeTimeoutMs === undefined ? {} : { probeTimeoutMs: options.probeTimeoutMs }) }
142
+
143
+ const serviceSeam = async () => {
144
+ if (seams !== null) { if (typeof seams.service?.entryPath !== 'string') refuse('seams-required', 'the service seam names the entry this command may start'); return seams.service }
145
+ if (flags.adapter !== PRODUCTION_ADAPTER) refuse('app-adapter-not-selected', 'no editor adapter was selected; a service that reaches the installed app is never a default')
146
+ const { SERVICE_ENTRY_PATH } = await import('../runtime/obsidian/service-main.mjs')
147
+ return { entryPath: SERVICE_ENTRY_PATH, entryArgs: [`--adapter=${PRODUCTION_ADAPTER}`] }
148
+ }
149
+ const appSeams = async () => {
150
+ if (seams !== null) return { appProbe: seams.appProbe, launcher: seams.launcher }
151
+ if (flags.adapter !== PRODUCTION_ADAPTER) refuse('app-adapter-not-selected', 'no editor adapter was selected; reaching the installed app is never a default')
152
+ const { createProductionAppSeams } = await import('../runtime/obsidian/app-production-seams.mjs')
153
+ return createProductionAppSeams({ env, platform })
154
+ }
155
+
156
+ const configured = () => {
157
+ const project = loadProject()
158
+ const enablement = readObsidianEnablement(project)
159
+ if (enablement.reason === 'not-configured') refuse('disabled', 'this project declares no Obsidian settings')
160
+ return { project, enablement }
161
+ }
162
+ // Private state of this workspace, created on demand for an operation that writes it.
163
+ const writable = () => {
164
+ const { project, enablement } = configured()
165
+ ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
166
+ const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
167
+ return { project, enablement, workspace, repositoryRoots: protectedRoots(project), now: isoTime(clock) }
168
+ }
169
+ const readable = () => {
170
+ const project = loadProject()
171
+ const enablement = readObsidianEnablement(project)
172
+ const workspace = enablement.reason === 'not-configured' ? null : resolveServiceWorkspace({ project, dataRoot, env, platform })
173
+ return { project, enablement, workspace: workspace?.workspaceRoot ? workspace : null, workspaceId: workspace?.workspaceId ?? null }
174
+ }
175
+ const machineOf = (workspace) => (workspace === null ? null : readMachineSettings(workspace))
176
+ const consent = flags['consent-actor'] === undefined ? undefined : { actor: flags['consent-actor'], coverage: 'service' }
177
+ const shownMachine = (machine, workspace) => {
178
+ const settings = machine ?? { maintenanceMode: 'manual', audienceAllow: [], applyPolicy: null }
179
+ const automatic = workspace === null ? { authorized: false, reason: 'machine-settings-absent' } : authorizeAutomaticApply(workspace)
180
+ return { maintenanceMode: settings.maintenanceMode, audienceAllow: settings.audienceAllow, applyPolicy: settings.applyPolicy, automaticApply: { authorized: automatic.authorized, reason: automatic.reason } }
181
+ }
182
+ const applyShown = { available: applyAvailable, state: applyAvailable ? 'available' : APPLY_UNAVAILABLE, operationId: registry.extensions.applyOperation().id }
183
+ const [, sub, value] = positionals
184
+
185
+ const operations = {
186
+ async status() {
187
+ const { project, enablement, workspace, workspaceId } = readable()
188
+ const service = enablement.reason === 'not-configured' ? { state: 'stopped', reason: 'not-configured' } : await serviceStatus(lifecycle, lifecycleRules)
189
+ const running = service.state === 'healthy' ? (await readServiceStatusDocument(lifecycle, lifecycleRules)).document : null
190
+ const scopes = workspace === null
191
+ ? enablement.scopes.map(({ scopeId }) => ({ scopeId, outcome: enablement.state === 'disabled' ? 'disabled' : 'not-prepared', reason: enablement.state === 'disabled' ? enablement.reason : 'workspace-not-prepared' }))
192
+ : enablement.scopes.map(({ scopeId }) => {
193
+ const { vaultRoot: _vault, summary: _summary, ...report } = scopeReport({ workspace, scopeId, repositoryRoots: protectedRoots(project), serviceState: service.state, applyAvailable }, openingRules)
194
+ return enablement.state === 'disabled' ? { ...report, outcome: 'disabled', reason: enablement.reason, next: OPENING_OUTCOMES.disabled.next } : report
195
+ })
196
+ const document = {
197
+ enablement: { state: enablement.state, reason: enablement.reason, defaultScopeId: enablement.defaultScopeId }, workspace: { workspaceId, prepared: workspace !== null },
198
+ machine: shownMachine(machineOf(workspace), workspace), apply: applyShown,
199
+ service: { state: service.state, reason: service.reason ?? null, address: service.address ?? null, runtimeId: service.record?.runtimeId ?? null, pid: service.record?.pid ?? null, lastTick: running?.lastTick ?? null, lastError: running?.lastError ?? null, app: running?.app ?? null },
200
+ app: { probed: false, minimumVersion: MINIMUM_APP_VERSION }, scopes, extensions: registry.extensions.describe(), operations: registry.operations.describe(),
201
+ }
202
+ return {
203
+ exit: EXIT.ok, document,
204
+ human: [
205
+ `obsidian: ${enablement.state} (${enablement.reason}); mode ${document.machine.maintenanceMode}; audiences ${document.machine.audienceAllow.join(', ') || 'none'}`,
206
+ `service: ${service.state} (${service.reason ?? 'no reason'})${running?.app ? `; app ${running.app.outcome}` : ''}`,
207
+ `apply: ${applyShown.state}`,
208
+ ...scopes.map((scope) => `view ${scope.scopeId}: ${scope.outcome} (${scope.reason})${scope.pendingEdits?.open ? `; ${scope.pendingEdits.open} pending edit(s), apply ${scope.pendingEdits.apply}` : ''}`),
209
+ ],
210
+ }
211
+ },
212
+
213
+ async scope() {
214
+ const { enablement } = readable()
215
+ if (sub === 'list' || sub === undefined) return { exit: EXIT.ok, document: { defaultScopeId: enablement.defaultScopeId, scopes: enablement.scopes }, human: enablement.scopes.map((scope) => `${scope.scopeId}\t${scope.mode}${scope.scopeId === enablement.defaultScopeId ? '\tdefault' : ''}`) }
216
+ if (sub !== 'show' || value === undefined) refuse('usage', 'scope list | scope show ID')
217
+ const scope = enablement.scopes.find((item) => item.scopeId === resolveScope(enablement, value))
218
+ return { exit: EXIT.ok, document: { scope }, human: [JSON.stringify(scope, null, 2)] }
219
+ },
220
+
221
+ async audience() {
222
+ if (sub === 'show' || sub === undefined) {
223
+ const { workspace } = readable()
224
+ const { audienceAllow } = shownMachine(machineOf(workspace), workspace)
225
+ return { exit: EXIT.ok, document: { audienceAllow }, human: [audienceAllow.join(', ') || 'no audience is allowed: every view is empty'] }
226
+ }
227
+ if (sub !== 'set' && sub !== 'clear') refuse('usage', 'audience show | audience set A,B | audience clear')
228
+ const audienceAllow = sub === 'clear' ? [] : String(value ?? '').split(',').map((item) => item.trim()).filter((item) => item !== '')
229
+ if (sub === 'set' && audienceAllow.length === 0) refuse('usage', 'audience set needs at least one audience; `audience clear` allows none')
230
+ if (audienceAllow.some((item) => !AUDIENCE.test(item)) || new Set(audienceAllow).size !== audienceAllow.length || audienceAllow.length > 64) refuse('invalid-audience', 'audiences are distinct identifiers, at most 64')
231
+ const { workspace, repositoryRoots, now } = writable()
232
+ const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
233
+ const changed = JSON.stringify(current.audienceAllow) !== JSON.stringify(audienceAllow)
234
+ writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...current, audienceAllow, updatedAt: now } })
235
+ // The engine compares a digest of these on every tick: a change invalidates every view at the next one.
236
+ return { exit: EXIT.ok, document: { audienceAllow, changed, takesEffect: 'next-tick' }, human: [`audiences: ${audienceAllow.join(', ') || 'none'}; views are rebuilt at the next tick`] }
237
+ },
238
+
239
+ async mode() {
240
+ if (sub === 'show' || sub === undefined) {
241
+ const { workspace } = readable()
242
+ const { maintenanceMode, automaticApply } = shownMachine(machineOf(workspace), workspace)
243
+ return { exit: EXIT.ok, document: { maintenanceMode, automaticApply, apply: applyShown }, human: [`${maintenanceMode}; automatic apply ${automaticApply.authorized ? 'authorized' : `not authorized (${automaticApply.reason})`}; apply ${applyShown.state}`] }
244
+ }
245
+ if (sub !== 'set' || !['manual', 'automatic'].includes(value)) refuse('usage', 'mode show | mode set manual|automatic')
246
+ const { workspace, repositoryRoots, now } = writable()
247
+ const current = machineOf(workspace) ?? writeMachineSettings({ ...workspace, repositoryRoots, settings: defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now }) })
248
+ if (value === 'automatic') {
249
+ const would = authorizeAutomaticApply({ ...workspace, assumeAutomatic: true })
250
+ if (!would.authorized) refuse('automatic-mode-refused', 'automatic mode needs an installed, matching, active automatic policy; nothing was changed', { reason: would.reason })
251
+ }
252
+ writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...current, maintenanceMode: value, updatedAt: now } })
253
+ return { exit: EXIT.ok, document: { maintenanceMode: value, apply: applyShown }, human: [`mode: ${value}${value === 'automatic' && !applyAvailable ? `; every apply reports ${APPLY_UNAVAILABLE} while no apply operation is registered` : ''}`] }
254
+ },
255
+
256
+ async policy() {
257
+ if (sub === 'show' || sub === undefined) {
258
+ const { workspace } = readable()
259
+ const policy = workspace === null ? null : readInstalledApplyPolicy(workspace)
260
+ const { applyPolicy, automaticApply } = shownMachine(machineOf(workspace), workspace)
261
+ return { exit: EXIT.ok, document: { installed: policy !== null, reference: applyPolicy, policy, automaticApply }, human: [policy === null ? 'no apply policy is installed' : `${policy.policyId} v${policy.version} ${policy.mode} ${policy.status}; automatic apply ${automaticApply.authorized ? 'authorized' : `not authorized (${automaticApply.reason})`}`] }
262
+ }
263
+ const readPolicyFile = () => {
264
+ if (value === undefined) refuse('usage', `policy ${sub} FILE`)
265
+ const file = path.resolve(cwd, value)
266
+ try {
267
+ if (fs.statSync(file).size > MAX_POLICY_BYTES) refuse('invalid-apply-policy', 'the policy file is larger than a policy can be')
268
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
269
+ } catch (error) {
270
+ if (error instanceof ObsidianMaintenanceRefusal) throw error
271
+ return refuse('invalid-apply-policy', 'the policy file cannot be read as JSON', { cause: error.code ?? 'not-json' })
272
+ }
273
+ }
274
+ if (sub === 'digest') {
275
+ // Read-only: the digest of the canonical form of FILE, for a person or an agent to fill in. Nothing is installed.
276
+ const policy = readPolicyFile()
277
+ if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) refuse('invalid-apply-policy', 'a policy is a JSON object')
278
+ const digest = applyPolicyDigest(policy)
279
+ const carried = typeof policy.digest === 'string' ? policy.digest : null
280
+ return { exit: EXIT.ok, document: { digest, carried, matches: carried === digest }, human: [digest, carried === digest ? 'the file carries this digest' : 'set the "digest" member of the file to this value, then run `policy install FILE`'] }
281
+ }
282
+ if (sub === 'install') {
283
+ const policy = readPolicyFile()
284
+ const { workspace, repositoryRoots, now } = writable()
285
+ // Validated against the frozen contract, its digest verified, and stored owner-only beside the machine settings, outside every repository and vault.
286
+ installApplyPolicy({ ...workspace, policy, repositoryRoots, updatedAt: now, digestOf: applyPolicyDigest })
287
+ return { exit: EXIT.ok, document: { installed: true, reference: { policyId: policy.policyId, version: policy.version, digest: policy.digest }, status: policy.status, mode: policy.mode }, human: [`installed ${policy.policyId} v${policy.version} (${policy.mode}, ${policy.status}); the maintenance mode is unchanged`] }
288
+ }
289
+ if (sub !== 'revoke') refuse('usage', 'policy show | policy install FILE | policy digest FILE | policy revoke')
290
+ const { workspace, repositoryRoots, now } = writable()
291
+ const result = revokeApplyPolicy({ ...workspace, repositoryRoots, updatedAt: now })
292
+ return { exit: EXIT.ok, document: { revoked: result.revoked, reason: result.reason, maintenanceMode: 'manual' }, human: [result.revoked ? 'revoked; no queued edit is applied from now on, and the mode is manual' : 'no apply policy was installed'] }
293
+ },
294
+
295
+ async service() {
296
+ const shown = ({ child: _child, ...result }) => result
297
+ if (sub === 'status' || sub === undefined) {
298
+ const status = await serviceStatus(lifecycle, lifecycleRules)
299
+ return { exit: EXIT.ok, document: { service: status }, human: [`${status.state} (${status.reason})`] }
300
+ }
301
+ if (sub === 'start') {
302
+ configured()
303
+ const seam = await serviceSeam()
304
+ const result = shown(await startService({ ...lifecycle, detached: true, ...seam, ...(consent === undefined ? {} : { consent }) }, lifecycleRules))
305
+ const running = result.state === 'healthy' || result.state === 'busy'
306
+ return { exit: running ? EXIT.ok : EXIT.notSuccess, document: { service: result }, human: [result.state === 'busy' ? 'running, and busy in a long tick; nothing was started beside it' : `${result.state}${result.started ? ' (started)' : result.alreadyRunning ? ' (already running)' : ` (${result.reason})`}`] }
307
+ }
308
+ if (sub === 'stop') {
309
+ const result = await stopService(lifecycle, lifecycleRules)
310
+ return { exit: result.refused ? EXIT.notSuccess : EXIT.ok, document: { service: result }, human: [result.refused ? `not stopped: ${result.state} (${result.reason})${result.retry ? '; ask again in a moment' : ''}` : result.stopped ? 'stopped' : `nothing to stop (${result.reason})`] }
311
+ }
312
+ if (sub !== 'unit' || flags.print !== true) refuse('usage', 'service start | status | stop | unit --print (installing a startup unit is not offered here)')
313
+ const { project, workspace } = readable()
314
+ if (workspace === null) refuse('service-workspace-not-prepared', 'this workspace has no private state yet; `service start` prepares it')
315
+ const seam = await serviceSeam()
316
+ // Pure text from the builder. Nothing is written, installed or handed to a service manager.
317
+ const unit = buildStartupAdapter({
318
+ platform, label: serviceNameFor(workspace.workspaceId), nodePath: process.execPath, entryPath: seam.entryPath, logPath: servicePaths(workspace.workspaceRoot).log,
319
+ args: [`--project=${project.configPath}`, ...(dataRoot === undefined ? [] : [`--data-root=${dataRoot}`]), ...(seam.entryArgs ?? [])],
320
+ })
321
+ return { exit: EXIT.ok, document: { unit, installed: false, note: 'running this unit needs a recorded consent that covers operating-system startup' }, human: [unit.text] }
322
+ },
323
+
324
+ async open() {
325
+ const seam = await serviceSeam()
326
+ const result = await openScopeForOracleTests({
327
+ ...lifecycle, ...(await appSeams()), service: seam, scopeId: flags.scope, consent, allowStale: flags['allow-stale'] === true, extensions: registry.extensions,
328
+ ...(flags['wait-ms'] === undefined ? {} : { tickTimeoutMs: Number(flags['wait-ms']) || undefined }), ...(options.open ?? {}),
329
+ }, openingRules, lifecycleRules)
330
+ const { ok: _ok, ...document } = result
331
+ return { exit: result.ok ? EXIT.ok : EXIT.notSuccess, document, human: [`${result.outcome}: ${result.summary}${result.reason ? ` (${result.reason})` : ''}`, `Next: ${result.next}`, ...(result.pendingEdits?.open ? [`${result.pendingEdits.open} pending edit(s); apply ${result.pendingEdits.apply}`] : [])] }
332
+ },
333
+
334
+ // The placeholder that answers when no contribution registered an apply operation; the shipped source-apply contribution replaces it.
335
+ async apply() {
336
+ return refuse(APPLY_UNAVAILABLE, 'no apply operation is registered; pending edits stay preserved and queued', { operationId: registry.extensions.applyOperation().id })
337
+ },
338
+ }
339
+
340
+ const contributed = registry.operations.get(operationName)
341
+ if (contributed === null && !Object.hasOwn(operations, operationName)) refuse('usage', `unknown operation: ${String(operationName).slice(0, 40)}`)
342
+ // One option table serves every operation. No built-in operation has a use for an actor, and a contributed one takes
343
+ // it only when it declares so (`options: ['actor']`); the apply operation then decides which of its verbs takes it.
344
+ if (flags.actor !== undefined && !(Array.isArray(contributed?.options) && contributed.options.includes('actor'))) refuse('usage', '--actor belongs to `apply run`; this operation does not take it')
345
+ const result = contributed !== null
346
+ ? await contributed.run({ args: positionals.slice(1), flags: { ...flags }, registry, loadProject, dataRoot, env, platform, clock, readable, writable })
347
+ : await operations[operationName]()
348
+ if (result === null || typeof result !== 'object' || !Number.isInteger(result.exit) || result.document === null || typeof result.document !== 'object') refuse('invalid-extension', 'an operation answers { exit, document, human }')
349
+ return emit(result)
350
+ } catch (error) {
351
+ if (!isTyped(error)) {
352
+ if (env.ATELIER_DEBUG === '1') stderr(error?.stack ?? String(error))
353
+ return emit({ exit: EXIT.error, document: { error: { code: 'internal-error', message: 'the command failed without a typed reason', next: 'rerun with ATELIER_DEBUG=1 to see the stack locally' } }, human: ['[internal-error] the command failed without a typed reason', 'Next: rerun with ATELIER_DEBUG=1 to see the stack locally'] })
354
+ }
355
+ const next = NEXT[error.code] ?? error.hint ?? 'see `atelier obsidian status`'
356
+ return emit({ exit: EXIT.refused, document: { error: { code: error.code, message: plainMessage(error), next, detail: error.detail ?? {} } }, human: [`[${error.code}] ${plainMessage(error)}`, `Next: ${next}`] })
357
+ }
358
+ }
359
+
360
+ // Production entry point: the production decisions, always.
361
+ export function runObsidianCommand(options = {}) {
362
+ return runObsidianCommandForOracleTests(options, {})
363
+ }
364
+
365
+ export default runObsidianCommand
366
+
367
+ const invokedDirectly = (() => { try { return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)) } catch { return false } })()
368
+ if (invokedDirectly) process.exitCode = await runObsidianCommand({ argv: process.argv.slice(2), production: true })
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path'
3
+ import { parseProjectOptions } from '../cli/project-options.mjs'
4
+ import { commandProject, parseArgs, readJson } from '../project/config.mjs'
5
+ import {
6
+ loadExtensionPacks,
7
+ createProtocolRegistry,
8
+ } from '../extension-packs/loader.mjs'
9
+ import { runProtocol } from '../readiness-protocols/runtime.mjs'
10
+ import { recordReviewEvidence } from '../readiness-protocols/evidence.mjs'
11
+ import { createReviewStore } from '../collaboration/review-store.mjs'
12
+ import {
13
+ inspectPackLifecycle,
14
+ planPackLifecycleMigration,
15
+ } from '../extension-packs/lifecycle.mjs'
16
+ import {
17
+ prepareInspectionBundle,
18
+ writeInspectionBundle,
19
+ readInspectionBundle,
20
+ } from '../collaboration/inspection-bundle.mjs'
21
+
22
+ const argv = process.argv.slice(2)
23
+ const remaining = parseProjectOptions(argv).remaining
24
+ const subcommand = remaining[0]
25
+ const specs = {
26
+ run: { answers: 'value' },
27
+ history: {},
28
+ handoff: {},
29
+ packs: { 'migration-plan': 'flag' },
30
+ export: {
31
+ runs: 'value',
32
+ responses: 'value',
33
+ denylist: 'value',
34
+ write: 'flag',
35
+ out: 'value',
36
+ },
37
+ inspect: {},
38
+ }
39
+ if (!Object.hasOwn(specs, subcommand ?? ''))
40
+ throw new Error(
41
+ 'Usage: atelier review run|history|handoff|packs|export|inspect',
42
+ )
43
+ const positional = []
44
+ for (let i = 1; i < remaining.length; i++) {
45
+ const arg = remaining[i]
46
+ if (!arg.startsWith('--')) {
47
+ positional.push(arg)
48
+ continue
49
+ }
50
+ const name = arg.slice(2).split('=')[0],
51
+ kind = specs[subcommand][name]
52
+ if (!Object.hasOwn(specs[subcommand], name))
53
+ throw new Error('unknown review option')
54
+ if (kind === 'flag') {
55
+ if (arg.includes('='))
56
+ throw new Error('review flag does not accept a value')
57
+ continue
58
+ }
59
+ const value = arg.includes('=')
60
+ ? arg.slice(arg.indexOf('=') + 1)
61
+ : remaining[++i]
62
+ if (!value || value.startsWith('--'))
63
+ throw new Error('review option requires a value')
64
+ }
65
+ if (
66
+ positional.length !==
67
+ (['run', 'handoff', 'inspect'].includes(subcommand) ? 1 : 0)
68
+ )
69
+ throw new Error('incorrect review arguments')
70
+ const args = parseArgs(argv)
71
+ const project = args._[0] === 'inspect' ? null : commandProject({ argv })
72
+ let result
73
+ if (args._[0] === 'run') {
74
+ const registry = createProtocolRegistry({
75
+ packs: loadExtensionPacks(project).packs,
76
+ })
77
+ const answers = args.answers ? readJson(path.resolve(args.answers)) : {}
78
+ const { run } = runProtocol(project, args._[1], {
79
+ answers,
80
+ write: false,
81
+ createProposal: false,
82
+ registry,
83
+ })
84
+ const bound = recordReviewEvidence(project, run)
85
+ result = {
86
+ ok: true,
87
+ runId: run.runId,
88
+ evidenceDigest: bound.snapshot.digest,
89
+ inputCompleteness: run.score,
90
+ blockers: run.blockers,
91
+ confidence: 'not-calibrated',
92
+ next: 'Run atelier dev --review with this project, then open /review.',
93
+ }
94
+ } else if (args._[0] === 'inspect')
95
+ result = readInspectionBundle(path.resolve(args._[1]))
96
+ else if (args._[0] === 'export') {
97
+ const denylistDocument = process.env.ATELIER_DENYLIST_JSON
98
+ ? JSON.parse(process.env.ATELIER_DENYLIST_JSON)
99
+ : args.denylist
100
+ ? readJson(path.resolve(args.denylist))
101
+ : null
102
+ const bundle = prepareInspectionBundle(
103
+ project,
104
+ String(args.runs ?? '')
105
+ .split(',')
106
+ .filter(Boolean),
107
+ {
108
+ denylistDocument,
109
+ responseIds: String(args.responses ?? '')
110
+ .split(',')
111
+ .filter(Boolean),
112
+ },
113
+ )
114
+ if (args.write) {
115
+ if (!args.out) throw new Error('--out is required for --write')
116
+ writeInspectionBundle(path.resolve(args.out), bundle)
117
+ }
118
+ result = {
119
+ ok: true,
120
+ purpose: bundle.purpose,
121
+ written: Boolean(args.write),
122
+ digest: bundle.digest,
123
+ members: bundle.members.map((member) => ({
124
+ id: member.id,
125
+ bytes: member.bytes,
126
+ digest: member.digest,
127
+ })),
128
+ responses: bundle.responses.map((record) => ({
129
+ id: record.input.requestId,
130
+ digest: record.inputDigest,
131
+ })),
132
+ notice:
133
+ 'Inspection only; no approval transfer. Export requires --write --out FILE.',
134
+ }
135
+ } else if (args._[0] === 'packs')
136
+ result = (
137
+ args['migration-plan'] ? planPackLifecycleMigration : inspectPackLifecycle
138
+ )(project, loadExtensionPacks(project).packs)
139
+ else if (args._[0] === 'handoff')
140
+ result = createReviewStore(project).handoff(args._[1])
141
+ else if (args._[0] === 'history') result = createReviewStore(project).records()
142
+ else
143
+ throw new Error(
144
+ 'Usage: atelier review run PROTOCOL --answers FILE | history | handoff REQUEST_ID | packs | export --runs ID [--responses ID] [--write --out FILE] | inspect FILE [--project PATH]',
145
+ )
146
+ console.log(JSON.stringify(result, null, 2))
147
+ process.exitCode = result.ok ? 0 : 1
@@ -9,8 +9,11 @@ import {
9
9
  firstString,
10
10
  parseArgs,
11
11
  resolveProjectConfig,
12
+ validateProjectConfigDoc,
12
13
  writeJson,
13
14
  } from '../project/config.mjs'
15
+ import { writeAtelierLock, loadAtelierLock, checkAtelierLock } from '../upgrade/upgrade.mjs'
16
+ import { loadBoundaryPolicy, validateBoundaryPolicy } from '../boundary/policy.mjs'
14
17
  import { auditRepoIdentities } from '../project/repo-identity.mjs'
15
18
 
16
19
  const PROFILE_SET = new Set(['single-repo', 'private-domain', 'shared-project', 'multi-repo', 'monorepo', 'control-workspace'])
@@ -121,8 +124,10 @@ function runSetup(argv) {
121
124
  }
122
125
 
123
126
  function baseBoundaryPolicy({ repoName, profile, actor }) {
124
- const privateDomain = profile === 'shared-project' ? `${actor}-private` : repoName
125
127
  const shared = profile === 'shared-project'
128
+ const privateDomain = shared
129
+ ? (repoName === `${actor}-private` ? `${actor}-private-domain` : `${actor}-private`)
130
+ : repoName
126
131
  return {
127
132
  schema: 'mnstry.atelier-boundary-policy@v1',
128
133
  mode: 'strict',
@@ -134,6 +139,11 @@ function baseBoundaryPolicy({ repoName, profile, actor }) {
134
139
  },
135
140
  },
136
141
  repos: {
142
+ ...(shared ? { [privateDomain]: {
143
+ kind: 'private_domain', ownerActor: actor, readBoundary: 'private',
144
+ allowedAudiences: ['private', 'sensitive', 'team', 'operator', 'staff', 'public'],
145
+ forbiddenAudiences: [], autoCommit: 'guarded',
146
+ } } : {}),
137
147
  [repoName]: {
138
148
  kind: shared ? 'shared' : 'private_domain',
139
149
  ownerActor: shared ? undefined : actor,
@@ -178,23 +188,42 @@ function runAdopt(argv) {
178
188
  if (profile === 'monorepo' && !firstString(args.include, args.includes)) {
179
189
  throw new Error('monorepo adopt requires --include so Atelier does not scan the whole repo by accident')
180
190
  }
181
- fs.mkdirSync(target, { recursive: true })
182
- appendIgnoreLines(target)
183
191
  const repoName = slug(firstString(args.name) || path.basename(target))
184
192
  const actor = slug(firstString(args.actor) || process.env.USER || 'owner')
185
193
  const projectPath = path.join(target, 'atelier.project.json')
186
- if (!fs.existsSync(projectPath)) {
187
- writeJson(projectPath, {
188
- schema: 'mnstry.atelier-project-config@v1',
189
- name: repoName,
190
- roots: { workspace: '.', repoOps: '.' },
191
- graph: { repoAccessPath: 'repo-access.v1.json', outputPath: 'atelier-output/knowledge.graph.json' },
192
- projection: { outputRoot: 'atelier-output', readinessPath: 'atelier-output/atelier-readiness.json' },
193
- boundaries: { policyPath: 'boundary-policy.v1.json', governanceLedgerPath: 'governance/repo-boundary-ledger.md', strictNewRepos: true },
194
- setup: { profile, include: firstString(args.include, args.includes) || null, exclude: firstString(args.exclude, args.excludes) || null },
195
- repos: [{ name: repoName, path: '.', readBoundary: profile === 'shared-project' ? 'team' : 'private', role: profile }],
196
- })
194
+ const existingProject = fs.existsSync(projectPath)
195
+ ? resolveProjectConfig({ cwd: target, argv: ['--project', projectPath], writeLocalState: false }) : null
196
+ const proposedConfig = existingProject?.config || {
197
+ schema: 'mnstry.atelier-project-config@v1',
198
+ name: repoName,
199
+ roots: { workspace: '.', repoOps: '.' },
200
+ graph: { repoAccessPath: 'repo-access.v1.json', outputPath: 'atelier-output/knowledge.graph.json' },
201
+ projection: { outputRoot: 'atelier-output', readinessPath: 'atelier-output/atelier-readiness.json' },
202
+ boundaries: { policyPath: 'boundary-policy.v1.json', governanceLedgerPath: 'governance/repo-boundary-ledger.md', strictNewRepos: true },
203
+ setup: { profile, include: firstString(args.include, args.includes) || null, exclude: firstString(args.exclude, args.excludes) || null },
204
+ repos: [{ name: repoName, path: '.', readBoundary: profile === 'shared-project' ? 'team' : 'private', role: profile }],
205
+ }
206
+ const configErrors = validateProjectConfigDoc(proposedConfig)
207
+ if (configErrors.length) throw new Error(`adopt requires a valid project config: ${configErrors.join('; ')}`)
208
+ const proposedProject = existingProject || { configDir: target, config: proposedConfig, repos: proposedConfig.repos }
209
+ // Parse and validate retained state before any scaffold or overlay writes.
210
+ const prior = loadAtelierLock(existingProject || { configDir: target })
211
+ if (fs.existsSync(prior.lockPath) && !prior.lock) throw new Error('existing lock must contain a valid lock object')
212
+ if (prior.lock && !existingProject) throw new Error('existing lock requires an existing project configuration')
213
+ const priorPolicy = loadBoundaryPolicy(proposedProject)
214
+ if (existingProject || fs.existsSync(priorPolicy.policyPath)) {
215
+ if (!priorPolicy.ok) throw new Error(`adopt requires a valid boundary policy: ${priorPolicy.errors.join('; ')}`)
197
216
  }
217
+ const proposedPolicy = priorPolicy.ok ? priorPolicy.policy : cleanUndefined(baseBoundaryPolicy({ repoName, profile, actor }))
218
+ const proposedPolicyErrors = validateBoundaryPolicy(proposedPolicy, proposedProject)
219
+ if (proposedPolicyErrors.length) throw new Error(`adopt requires a valid boundary policy: ${proposedPolicyErrors.join('; ')}`)
220
+ if (prior.lock) {
221
+ const report = checkAtelierLock(existingProject)
222
+ if (!report.ok) throw new Error(`adopt cannot accept existing lock drift: ${report.errors.join('; ')}`)
223
+ }
224
+ fs.mkdirSync(target, { recursive: true })
225
+ appendIgnoreLines(target)
226
+ if (!existingProject) writeJson(projectPath, proposedConfig)
198
227
  if (!fs.existsSync(path.join(target, 'repo-access.v1.json'))) {
199
228
  writeJson(path.join(target, 'repo-access.v1.json'), {
200
229
  schema: 'mnstry.atelier-repo-access@v1',
@@ -206,8 +235,16 @@ function runAdopt(argv) {
206
235
  writeJson(path.join(target, 'boundary-policy.v1.json'), cleanUndefined(baseBoundaryPolicy({ repoName, profile, actor })))
207
236
  }
208
237
  const project = resolveProjectConfig({ cwd: target, argv: ['--project', projectPath] })
238
+ const loadedPolicy = loadBoundaryPolicy(project)
239
+ const policyErrors = loadedPolicy.ok ? validateBoundaryPolicy(loadedPolicy.policy, project) : loadedPolicy.errors
240
+ if (policyErrors.length) throw new Error(`adopt requires a valid boundary policy: ${policyErrors.join('; ')}`)
209
241
  ensureLocalState(project, { write: true })
210
242
  writeLocalOverlay(project)
243
+ // Adoption creates the first lock, but never accepts drift in an existing one.
244
+ const { lockPath } = loadAtelierLock(project)
245
+ if (!fs.existsSync(lockPath)) writeAtelierLock({ project, templateId: `adopt:${profile}` })
246
+ const lockReport = checkAtelierLock(project)
247
+ if (!lockReport.ok) throw new Error(`adopt lock check failed: ${lockReport.errors.join('; ')}`)
211
248
  console.log(JSON.stringify({ ok: true, command: 'adopt', profile, target, projectPath }, null, 2))
212
249
  }
213
250