@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,21 @@
1
+ import { createVaultService } from './service.mjs'
2
+ import { vaultIdentity } from './identity.mjs'
3
+ import { r2PrivateStorage, vercelPrivateStorage } from './storage.mjs'
4
+ import { d1VaultMetadata, postgresVaultMetadata } from './metadata.mjs'
5
+
6
+ /** Vercel host passes its established session verifier, credential store and SDKs. */
7
+ export function createVercelVault({ blob, postgres, verifySession, lookupCredential, privacy, deployment }) {
8
+ return createVaultService({ privacy, deployment, storage: vercelPrivateStorage(blob), metadata: postgresVaultMetadata(postgres), identity: vaultIdentity({ verifySession, lookupCredential, privacy, deployment }) })
9
+ }
10
+ /** Create a handler per request so D1 sessions cannot carry stale bookmarks. */
11
+ export function createCloudflareVault({ verifySession, lookupCredential, privacy, deployment }) {
12
+ vaultIdentity({ verifySession, lookupCredential })
13
+ async function handle(request, env) {
14
+ try {
15
+ const db = env.VAULT_DB.withSession('first-primary')
16
+ const identity = vaultIdentity({ verifySession: req => verifySession(req, env), lookupCredential: hash => lookupCredential(hash, env, db) })
17
+ return await createVaultService({ privacy, deployment, storage: r2PrivateStorage(env.VAULT_OBJECTS), metadata: d1VaultMetadata(db), identity })(request)
18
+ } catch { return new Response(null, { status: 503, headers: { "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" } }) }
19
+ }
20
+ return { fetch: handle }
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The host verifies browser sessions using its established identity provider.
3
+ * Machine credentials are high-entropy secrets whose SHA-256 hashes are stored
4
+ * by the host. Lookup must read authoritative, current expiry/revocation state.
5
+ */
6
+ import { digest } from './service.mjs'
7
+ export function vaultIdentity({ verifySession, lookupCredential, now = () => Date.now() }) {
8
+ if (typeof verifySession !== 'function' || typeof lookupCredential !== 'function') throw new TypeError('Identity bindings required')
9
+ return {
10
+ read: request => verifySession(request),
11
+ async publish(request, vault) {
12
+ const value = request.headers.get('authorization')
13
+ if (!value || !/^Bearer [A-Za-z0-9_-]{43,128}$/.test(value)) return null
14
+ const record = await lookupCredential(await digest(new TextEncoder().encode(value.slice(7))))
15
+ if (!record || record.vault !== vault || record.revoked !== false || !Number.isFinite(record.expiresAt) || record.expiresAt <= now()) return null
16
+ return record.owner
17
+ },
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ export { createVaultService, preparePublication } from './service.mjs'
2
+ export { vaultIdentity } from './identity.mjs'
3
+ export { r2PrivateStorage, vercelPrivateStorage } from './storage.mjs'
4
+ export { VAULT_TABLE_SQL, d1VaultMetadata, postgresVaultMetadata } from './metadata.mjs'
5
+ export { createVercelVault, createCloudflareVault } from './hosts.mjs'
6
+ export { assessVaultPrivacy, createVaultPrivacyState, createVaultContext, VaultAlarmPersistenceError } from './privacy.mjs'
7
+ export { createVaultPrivacyProbe } from './probe.mjs'
@@ -0,0 +1,11 @@
1
+ const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
2
+ export function renderVaultHome({ vault, revision, manifest, privacy, query = '' }) {
3
+ const files = manifest.filter(file => /\.(html|pdf|txt)$/.test(file.path))
4
+ const matches = files.filter(file => file.path.toLowerCase().includes(query.toLowerCase()))
5
+ const verified = privacy.status === 'verified'
6
+ const label = verified ? 'Unauthorized-access checks passed' : privacy.status === 'exposed' ? 'Privacy failure — external routes may remain exposed' : 'Privacy not verified — access blocked'
7
+ const list = verified ? matches.map(file => `<li><a href="/${escape(vault)}/${file.path.split('/').map(encodeURIComponent).join('/')}">${escape(file.path)}</a><span>${escape(file.type.split(';')[0])}</span></li>`).join('') : ''
8
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(vault)} · Atelier</title><style>
9
+ *{box-sizing:border-box}body{margin:0;background:#f6f5ef;color:#20382d;font:17px/1.6 system-ui,sans-serif}main{max-width:960px;margin:auto;padding:clamp(20px,5vw,64px)}header{border-bottom:1px solid #cad3c7;padding-bottom:24px}h1{font-size:clamp(30px,6vw,52px);line-height:1.15;overflow-wrap:anywhere}a{color:inherit;text-underline-offset:4px}a:focus-visible,input:focus-visible,button:focus-visible{outline:3px solid #28734e;outline-offset:4px}.status{padding:16px;border:1px solid #738975;border-radius:8px;margin:24px 0}.blocked{background:#fff0dc;border-color:#936125}label{display:block;font-weight:600}form{display:flex;gap:12px;flex-wrap:wrap}input{flex:1;min-width:0;font:inherit;padding:12px;border:1px solid #738975;border-radius:6px}button{font:inherit;padding:12px 20px;background:#20382d;color:white;border:0;border-radius:6px}ul{list-style:none;padding:0}li{padding:20px 0;border-bottom:1px solid #cad3c7}li a{display:block;overflow-wrap:anywhere}li span,footer{font-size:14px;color:#506354}footer{margin-top:40px}.skip{position:absolute;left:-10000px}.skip:focus{left:16px;top:8px}</style></head><body><a class="skip" href="#artifacts">Skip to artifacts</a><main><header><p>ATELIER · PRIVATE VAULT</p><h1>${escape(vault)}</h1><p>Published revision ${escape(revision)} · ${files.length} artifact${files.length === 1 ? '' : 's'}</p></header><section class="status ${verified ? '' : 'blocked'}" aria-label="Privacy status"><strong>${label}</strong><p>${escape(privacy.reason)}</p>${verified ? `<small>Checked ${escape(new Date(privacy.checkedAt).toISOString())}. Authorization and evidence freshness are checked when you open an artifact.</small>` : '<p>Ask the workspace operator to verify destination protection, then reload. Artifacts are unavailable until the checks pass.</p>'}</section><label for="search">Find an artifact</label><form role="search" method="get" action="/${escape(vault)}/"><input id="search" name="q" type="search" value="${escape(query)}" maxlength="200"><button type="submit">Search</button></form>${verified ? `<ul id="artifacts" tabindex="-1" aria-label="Artifacts">${list}</ul>${matches.length ? '' : `<p>${files.length ? 'No artifacts match your search.' : 'No artifacts have been published yet.'}</p>`}` : ''}<footer>This website displays published artifacts. Saving or reviewing source material does not publish it. Generated artifacts run without script privileges.</footer></main></body></html>`
10
+ }
11
+ export const vaultHomePolicy = "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'"
@@ -0,0 +1,31 @@
1
+ /** SQL adapters require pre-provisioned vaults; neither endpoint creates owners. */
2
+ export const VAULT_TABLE_SQL = `CREATE TABLE atelier_vaults (
3
+ id TEXT PRIMARY KEY,
4
+ owner_issuer TEXT NOT NULL,
5
+ owner_subject TEXT NOT NULL,
6
+ revision INTEGER NOT NULL DEFAULT 0,
7
+ manifest TEXT NOT NULL DEFAULT '[]',
8
+ publication TEXT
9
+ )`
10
+ function record(row) {
11
+ return row ? { owner: { issuer: row.owner_issuer, subject: row.owner_subject }, revision: Number(row.revision), manifest: JSON.parse(row.manifest), publication: row.publication } : null
12
+ }
13
+ export function d1VaultMetadata(db) {
14
+ return {
15
+ async get(vault) { return record(await db.prepare('SELECT * FROM atelier_vaults WHERE id = ?').bind(vault).first()) },
16
+ async commit(vault, update) {
17
+ const result = await db.prepare('UPDATE atelier_vaults SET revision = revision + 1, manifest = ?, publication = ? WHERE id = ? AND revision = ? AND owner_issuer = ? AND owner_subject = ?')
18
+ .bind(JSON.stringify(update.manifest), update.publication, vault, update.expectedRevision, update.owner.issuer, update.owner.subject).run()
19
+ return result.success === true && result.meta.changes === 1
20
+ },
21
+ }
22
+ }
23
+ export function postgresVaultMetadata(db) {
24
+ return {
25
+ async get(vault) { return record((await db.query('SELECT * FROM atelier_vaults WHERE id = $1', [vault])).rows[0]) },
26
+ async commit(vault, update) {
27
+ const result = await db.query('UPDATE atelier_vaults SET revision = revision + 1, manifest = $1, publication = $2 WHERE id = $3 AND revision = $4 AND owner_issuer = $5 AND owner_subject = $6', [JSON.stringify(update.manifest), update.publication, vault, update.expectedRevision, update.owner.issuer, update.owner.subject])
28
+ return result.rowCount === 1
29
+ },
30
+ }
31
+ }
@@ -0,0 +1,148 @@
1
+ /** Trusted host evidence only: never accept evidence from a publisher. */
2
+ const unknown = reason => ({ status: 'unknown', reason })
3
+ export function vaultObjects(vault, manifest) {
4
+ return [...new Map(manifest.map(file => [`${vault}/${file.sha256}`, { key: `${vault}/${file.sha256}`, sha256: file.sha256, size: file.size }])).values()]
5
+ }
6
+ export function validDeployment(deployment) {
7
+ if (!deployment || typeof deployment.id !== 'string' || !deployment.id || !Array.isArray(deployment.origins) || !deployment.origins.length || deployment.origins.length > 16) return false
8
+ return new Set(deployment.origins).size === deployment.origins.length && deployment.origins.every(origin => {
9
+ try { const u = new URL(origin); return u.protocol === 'https:' && u.origin === origin && !u.username && !u.password } catch { return false }
10
+ })
11
+ }
12
+ export function assessVaultPrivacy(evidence, context) {
13
+ const { vault, publication, revision, phase, owner, deployment, manifest = [], objects = [], request, now = Date.now() } = context
14
+ // Exposure is a sticky alarm, not authorization. Even stale/mismatched trusted
15
+ // evidence must not turn a known exposure into an ordinary availability error.
16
+ if (evidence?.targets?.some?.(t => t?.anonymous === 'content' || t?.otherUser === 'content')) return { status: 'exposed', reason: 'Unauthorized content observed. This handler refuses delivery; external routes may remain exposed.' }
17
+ if (!evidence || evidence.vault !== vault || evidence.publication !== publication) return unknown('Protection evidence does not match this publication.')
18
+ if (!Number.isSafeInteger(revision) || revision < 0 || evidence.revision !== revision) return unknown('Protection evidence does not match the metadata revision.')
19
+ if (evidence.phase !== phase || !['before-upload', 'before-activation', 'read'].includes(phase) || !owner?.issuer || !owner?.subject || evidence.owner?.issuer !== owner.issuer || evidence.owner?.subject !== owner.subject) return unknown('Protection evidence does not match the owner and operation.')
20
+ if (!validDeployment(deployment) || evidence.deployment?.id !== deployment.id || JSON.stringify(evidence.deployment?.origins) !== JSON.stringify(deployment.origins)) return unknown('Protection evidence does not match this deployment.')
21
+ if (request && (!deployment.origins.includes(request.origin) || (phase === 'read' && request.path !== `/${vault}` && request.path !== `/${vault}/` && !manifest.some(f => request.path === `/${vault}/${f.path}`)))) return unknown('Request is outside the verified inventory.')
22
+ if (!Number.isFinite(evidence.checkedAt) || !Number.isFinite(evidence.validUntil) || evidence.checkedAt > now || evidence.validUntil <= now || evidence.validUntil <= evidence.checkedAt || evidence.validUntil - evidence.checkedAt > 300000) return unknown('Protection evidence is missing or expired.')
23
+ if (typeof evidence.policyRevision !== 'string' || !evidence.policyRevision) return unknown('Provider policy identity is missing.')
24
+ const config = evidence.configuration
25
+ if (config?.ownerOnly !== true || config?.privateStorage !== true || config?.completeInventory !== true) return unknown('Provider protection or route inventory is unverified.')
26
+ if (!Array.isArray(evidence.targets) || !evidence.targets.length || evidence.targets.length > 2000) return unknown('Access checks are missing or oversized.')
27
+ const urls = new Set(), routes = new Set(), keys = new Set()
28
+ for (const target of evidence.targets) {
29
+ let url
30
+ try { url = new URL(target.url) } catch { return unknown('Access check URL is invalid.') }
31
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || urls.has(url.href)) return unknown('Access check inventory is invalid.')
32
+ urls.add(url.href)
33
+ if (target.anonymous !== 'denied' || target.otherUser !== 'denied') return unknown('Required access checks did not complete successfully.')
34
+ if (target.kind === 'storage') {
35
+ if (!objects.some(o => o.key === target.key && o.sha256 === target.sha256 && o.size === target.size)) return unknown('Storage check does not match a staged object.')
36
+ keys.add(target.key)
37
+ } else if (target.kind === 'route') {
38
+ if (!deployment.origins.includes(url.origin)) return unknown('Access check uses an unconfigured origin.')
39
+ const file = manifest.find(f => url.pathname === `/${vault}/${f.path}`)
40
+ const home = url.pathname === `/${vault}` || url.pathname === `/${vault}/`
41
+ if (!home && (!file || file.sha256 !== target.sha256)) return unknown('Access check does not match a manifest path and digest.')
42
+ routes.add(url.href)
43
+ } else return unknown('Access check target is invalid.')
44
+ }
45
+ for (const origin of deployment.origins) {
46
+ for (const path of [`/${vault}`, `/${vault}/`, ...manifest.map(f => `/${vault}/${f.path}`)]) if (!routes.has(origin + path)) return unknown('A configured route is missing from access checks.')
47
+ }
48
+ for (const object of objects) if (!keys.has(object.key)) return unknown('A staged storage object is missing from access checks.')
49
+ return { status: 'verified', reason: 'Configured routes refused unauthorized access at the recorded check time. Owner rendering is qualified separately.', checkedAt: evidence.checkedAt, validUntil: evidence.validUntil, policyRevision: evidence.policyRevision }
50
+ }
51
+ export async function verifyVaultPrivacy(privacy, context) {
52
+ const method = context.phase === 'read' ? 'current' : 'verify'
53
+ if (typeof privacy?.[method] !== 'function') return unknown('No provider privacy evidence is configured.')
54
+ try { return assessVaultPrivacy(await privacy[method](context), context) }
55
+ catch (error) {
56
+ if (error instanceof VaultAlarmPersistenceError) return { ...assessVaultPrivacy(error.evidence, context), persistence: 'unconfirmed' }
57
+ return unknown('Provider privacy verification is unavailable.')
58
+ }
59
+ }
60
+
61
+ /** The host must alert/retry this failure; the local read latch remains set. */
62
+ export class VaultAlarmPersistenceError extends Error {
63
+ constructor(evidence, cause) {
64
+ super('Vault exposure is latched locally but durable persistence is unconfirmed.', { cause })
65
+ this.name = 'VaultAlarmPersistenceError'
66
+ this.code = 'VAULT_ALARM_NOT_PERSISTED'
67
+ this.evidence = evidence
68
+ }
69
+ }
70
+
71
+ /** Public builder keeps refresh contexts aligned with service storage keys. */
72
+ export function createVaultContext({ vault, record, deployment, request, phase = 'read' }) {
73
+ if (!record || !Number.isSafeInteger(record.revision) || record.revision < 0 || !Array.isArray(record.manifest)) throw new TypeError('Authoritative revision and manifest required')
74
+ return { vault, owner: record.owner, publication: record.publication ?? null, revision: record.revision, manifest: record.manifest, objects: vaultObjects(vault, record.manifest), deployment, ...(request ? { request } : {}), phase }
75
+ }
76
+
77
+ /** Host-owned versioned evidence store. Refresh is explicit; reads never probe. */
78
+ export function createVaultPrivacyState({ probe, load, compareAndSet }) {
79
+ if (typeof probe?.verify !== 'function' || typeof load !== 'function' || typeof compareAndSet !== 'function') throw new TypeError('Probe and versioned evidence store required')
80
+ const alarms = new Map()
81
+ const key = context => {
82
+ if (!Number.isSafeInteger(context?.revision) || context.revision < 0) throw new TypeError('Authoritative nonnegative metadata revision required')
83
+ return JSON.stringify([context.deployment?.id, context.vault])
84
+ }
85
+ const exposed = (evidence, context) => assessVaultPrivacy(evidence, context).status === 'exposed'
86
+ async function snapshot(id, context) {
87
+ const record = await load(id)
88
+ if (!record || !Number.isSafeInteger(record.version) || record.version < 0 || !Object.hasOwn(record, 'evidence')) throw new TypeError('Invalid evidence store snapshot')
89
+ if (exposed(record.evidence, context)) alarms.set(id, { evidence: record.evidence, persisted: true })
90
+ return record
91
+ }
92
+ async function persistAlarm(id, context) {
93
+ // Alarm writes may replace newer green evidence regardless of revision.
94
+ // Reload on conflict, never reuse a stale version or retry green evidence.
95
+ for (let attempt = 0; attempt < 3; attempt++) {
96
+ if (alarms.get(id).persisted) return alarms.get(id).evidence
97
+ try {
98
+ const record = await snapshot(id, context)
99
+ if (alarms.get(id).persisted) return alarms.get(id).evidence
100
+ const evidence = alarms.get(id).evidence
101
+ if (await compareAndSet(id, evidence, { expectedVersion: record.version }) === true) {
102
+ alarms.set(id, { evidence, persisted: true })
103
+ return evidence
104
+ }
105
+ } catch (cause) { throw new VaultAlarmPersistenceError(alarms.get(id).evidence, cause) }
106
+ }
107
+ throw new VaultAlarmPersistenceError(alarms.get(id).evidence, new Error('Alarm write conflict limit reached'))
108
+ }
109
+ async function collect(context) {
110
+ const id = key(context)
111
+ // Pending latches retry persistence without re-probing a possibly vanished
112
+ // or staged-only exposure. Confirmed durable alarms need no rewrite.
113
+ if (alarms.has(id)) return persistAlarm(id, context)
114
+ const previous = await snapshot(id, context)
115
+ if (alarms.has(id)) return persistAlarm(id, context)
116
+ if (previous.evidence?.revision > context.revision) return null
117
+ const evidence = await probe.verify(context)
118
+ if (exposed(evidence, context)) {
119
+ if (!alarms.has(id)) alarms.set(id, { evidence, persisted: false })
120
+ return persistAlarm(id, context)
121
+ }
122
+ if (alarms.has(id)) return persistAlarm(id, context)
123
+ if (context.phase !== 'read') {
124
+ // Another instance may have latched an incident during the slow probe.
125
+ // This is still a check, not a transaction with metadata activation.
126
+ const latest = await snapshot(id, context)
127
+ if (alarms.has(id)) return persistAlarm(id, context)
128
+ return latest.evidence?.revision > context.revision ? null : evidence
129
+ }
130
+ // Green writes retain their original version. Conflicts and store failures
131
+ // must never be interpreted as a successful refresh.
132
+ if (await compareAndSet(id, evidence, { expectedVersion: previous.version }) !== true) {
133
+ await snapshot(id, context)
134
+ return alarms.has(id) ? persistAlarm(id, context) : null
135
+ }
136
+ return alarms.has(id) ? persistAlarm(id, context) : evidence
137
+ }
138
+ return {
139
+ verify: collect,
140
+ async current(context) {
141
+ const id = key(context)
142
+ if (alarms.has(id)) return alarms.get(id).evidence
143
+ const record = await snapshot(id, context)
144
+ return alarms.get(id)?.evidence ?? record.evidence
145
+ },
146
+ async refresh(context) { return collect({ ...context, phase: 'read' }) },
147
+ }
148
+ }
@@ -0,0 +1,89 @@
1
+ import { digest } from './service.mjs'
2
+ const canonical = value => JSON.stringify(value, (_, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(k => [k, item[k]])) : item)
3
+ const MAX_BYTES = 4 * 1024 * 1024
4
+ const cleanUrl = value => {
5
+ const url = new URL(value)
6
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw new Error('Unsafe probe URL')
7
+ return url.href
8
+ }
9
+ async function classify(response, target, loginUrls) {
10
+ if (!(response instanceof Response)) return 'unknown'
11
+ const reader = response.body?.getReader()
12
+ const chunks = []; let length = 0
13
+ try {
14
+ for (; reader;) {
15
+ const { value, done } = await reader.read()
16
+ if (done) break
17
+ length += value.byteLength
18
+ if (length > MAX_BYTES) { await reader.cancel(); return 'unknown' }
19
+ chunks.push(value)
20
+ }
21
+ } finally { reader?.releaseLock() }
22
+ const bytes = new Uint8Array(length); let offset = 0
23
+ for (const value of chunks) { bytes.set(value, offset); offset += value.length }
24
+ // Content exposure wins over misleading error or redirect status codes.
25
+ if (length > 0 && (await digest(bytes) === target.sha256 || (typeof target.canary === 'string' && target.canary.length >= 32 && new TextDecoder().decode(bytes).includes(target.canary)))) return 'content'
26
+ if (response.redirected) return 'unknown'
27
+ if ([401, 403, 404].includes(response.status)) return length === 0 ? 'denied' : 'unknown'
28
+ if ([302, 303, 307, 308].includes(response.status)) {
29
+ try {
30
+ const location = new URL(response.headers.get('location'), target.url)
31
+ if (location.protocol !== 'https:' || location.username || location.password || location.hash) return 'unknown'
32
+ const endpoint = loginUrls.find(item => item.url === location.origin + location.pathname)
33
+ return endpoint && [...location.searchParams.keys()].every(key => endpoint.queryKeys.includes(key)) ? 'denied' : 'unknown'
34
+ } catch { return 'unknown' }
35
+ }
36
+ return 'unknown'
37
+ }
38
+ /**
39
+ * inspect(context) is a trusted provider configuration/inventory collector.
40
+ * request({url,identity,redirect,signal}) is host transport; credentials never
41
+ * cross identities or redirects. No global fetch or ambient credentials here.
42
+ * Each collection re-inspects policy after probing to refuse changed settings.
43
+ */
44
+ export function createVaultPrivacyProbe({ inspect, request, now = () => Date.now(), timeoutMs = 60000, onExposure }) {
45
+ if (typeof inspect !== 'function' || typeof request !== 'function' || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60000) throw new TypeError('Explicit probe bindings required')
46
+ return {
47
+ async verify(context) {
48
+ const controller = new AbortController()
49
+ let timer
50
+ const checkedAt = now()
51
+ const deadline = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('Probe deadline exceeded')) }, timeoutMs) })
52
+ const collect = async () => {
53
+ const inventory = structuredClone(await inspect(context, { signal: controller.signal }))
54
+ if (!inventory || !Array.isArray(inventory.targets) || !inventory.targets.length || inventory.targets.length > 2000) throw new Error('Invalid probe inventory')
55
+ const targets = inventory.targets.map(target => {
56
+ if (!target || !/^[a-f0-9]{64}$/.test(target.sha256)) throw new Error('Expected content digest required')
57
+ return { ...target, url: cleanUrl(target.url) }
58
+ })
59
+ const loginUrls = (inventory.loginEndpoints ?? []).map(item => {
60
+ if (!Array.isArray(item.queryKeys) || item.queryKeys.some(key => !['state', 'redirect_uri', 'redirect_url', 'returnTo', 'next', 'client_id', 'response_type', 'scope', 'nonce', 'code_challenge', 'code_challenge_method'].includes(key))) throw new Error('Unsafe login parameters')
61
+ return { url: cleanUrl(item.url), queryKeys: item.queryKeys }
62
+ })
63
+ const evidence = results => ({ vault: context.vault, publication: context.publication, revision: context.revision, phase: context.phase, owner: context.owner, deployment: context.deployment, checkedAt, validUntil: checkedAt + 60000, policyRevision: inventory.policyRevision, configuration: inventory.configuration, targets: results })
64
+ const results = []
65
+ for (const target of targets) {
66
+ const result = { url: target.url, kind: target.kind, sha256: target.sha256, key: target.key, size: target.size }
67
+ for (const identity of ['anonymous', 'otherUser']) {
68
+ if (controller.signal.aborted) throw new Error('Probe aborted')
69
+ try { result[identity] = await classify(await request({ url: target.url, identity, redirect: 'manual', signal: controller.signal }), target, loginUrls) }
70
+ catch { result[identity] = 'unknown' }
71
+ if (result[identity] === 'content') {
72
+ const alarm = evidence([...results, result])
73
+ // Notification cannot delay or downgrade the exposure verdict. The
74
+ // host owns durable incident persistence and provider containment.
75
+ if (typeof onExposure === 'function') Promise.resolve().then(() => onExposure(alarm)).catch(() => {})
76
+ return alarm
77
+ }
78
+ }
79
+ results.push(result)
80
+ }
81
+ const after = await inspect(context, { signal: controller.signal })
82
+ if (canonical(after) !== canonical(inventory)) throw new Error('Provider protection changed during verification')
83
+ return evidence(results)
84
+ }
85
+ try { return await Promise.race([collect(), deadline]) }
86
+ finally { clearTimeout(timer); controller.abort() }
87
+ },
88
+ }
89
+ }
@@ -0,0 +1,131 @@
1
+ import { verifyVaultPrivacy, createVaultContext } from './privacy.mjs'
2
+ import { renderVaultHome, vaultHomePolicy } from './interface.mjs'
3
+ /** Optional hosted artifact service. No default network, storage, or identity provider. */
4
+ const TYPES = Object.freeze({ html: 'text/html; charset=utf-8', css: 'text/css; charset=utf-8', txt: 'text/plain; charset=utf-8', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', pdf: 'application/pdf' })
5
+ const LIMIT = 4 * 1024 * 1024
6
+ const encoder = new TextEncoder()
7
+ const validId = value => typeof value === 'string' && /^[a-z][a-z0-9-]{0,62}$/.test(value)
8
+ const validPath = value => typeof value === 'string' && value.length <= 512 && value.split('/').every(p => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(p) && p !== '.' && p !== '..')
9
+ const validExt = value => value === undefined || (value !== null && typeof value === 'object' && !Array.isArray(value))
10
+ const sameOwner = (a, b) => a && b && typeof a.issuer === 'string' && a.issuer.length > 0 && typeof a.subject === 'string' && a.subject.length > 0 && a.issuer === b.issuer && a.subject === b.subject
11
+ const headers = {
12
+ 'Cache-Control': 'private, no-store',
13
+ 'Content-Security-Policy': "sandbox; default-src 'none'; img-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
14
+ 'X-Content-Type-Options': 'nosniff',
15
+ 'Referrer-Policy': 'no-referrer',
16
+ 'X-Robots-Tag': 'noindex, nofollow',
17
+ }
18
+ export async function digest(bytes) {
19
+ return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), b => b.toString(16).padStart(2, '0')).join('')
20
+ }
21
+ function reply(status, text = '', extra = {}) { return new Response(text, { status, headers: { ...headers, 'Content-Type': 'text/plain; charset=utf-8', ...extra } }) }
22
+ async function boundedBody(request) {
23
+ if (!request.body) throw new Error('invalid')
24
+ const reader = request.body.getReader()
25
+ let size = 0
26
+ const chunks = []
27
+ try {
28
+ while (true) {
29
+ const { done, value } = await reader.read()
30
+ if (done) break
31
+ size += value.byteLength
32
+ if (size > LIMIT) { await reader.cancel(); throw new Error('too-large') }
33
+ chunks.push(value)
34
+ }
35
+ } finally { reader.releaseLock() }
36
+ const bytes = new Uint8Array(size)
37
+ let offset = 0
38
+ for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length }
39
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
40
+ }
41
+ export async function preparePublication(input) {
42
+ if (!input || input.schema !== 'atelier-vault-publication/v1' || (input.contractVersion !== undefined && (typeof input.contractVersion !== 'string' || !/^1\.[0-9]+\.[0-9]+$/.test(input.contractVersion))) || !validExt(input.ext) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || input.expectedRevision >= Number.MAX_SAFE_INTEGER || Object.keys(input).some(key => !['schema', 'contractVersion', 'expectedRevision', 'files', 'ext'].includes(key)) || !Array.isArray(input.files) || !input.files.length || input.files.length > 100) throw new Error('invalid')
43
+ if (encoder.encode(JSON.stringify(input)).length > LIMIT) throw new Error('too-large')
44
+ const seen = new Set()
45
+ const files = []
46
+ let total = 0
47
+ for (const file of input.files) {
48
+ if (!file || !validExt(file.ext) || Object.keys(file).some(key => !['path', 'base64', 'ext'].includes(key)) || !validPath(file.path) || seen.has(file.path) || typeof file.base64 !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(file.base64)) throw new Error('invalid')
49
+ const type = TYPES[file.path.split('.').at(-1)]
50
+ if (typeof type !== 'string') throw new Error('invalid')
51
+ seen.add(file.path)
52
+ const bytes = Uint8Array.from(atob(file.base64), c => c.charCodeAt(0))
53
+ total += bytes.length
54
+ if (total > LIMIT) throw new Error('too-large')
55
+ files.push({ path: file.path, type, size: bytes.length, sha256: await digest(bytes), bytes })
56
+ }
57
+ files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
58
+ const manifest = files.map(({ bytes, ...record }) => record)
59
+ return { files, manifest, publication: await digest(encoder.encode(JSON.stringify(manifest))) }
60
+ }
61
+ /**
62
+ * identity.read(request) -> verified {issuer,subject}, or null (revocation checked).
63
+ * identity.publish(request,vault) -> verified owner identity, or null; MUST use
64
+ * a revocable vault-scoped machine credential, never browser cookies.
65
+ * metadata.get(vault) -> {owner, revision, manifest}, or null.
66
+ * metadata.commit(vault,{owner,expectedRevision,manifest,publication}) -> boolean;
67
+ * MUST atomically compare both owner and revision before replacing manifest.
68
+ * storage.get/put accepts service-generated keys only and uses private storage.
69
+ */
70
+ export function createVaultService({ identity, metadata, storage, privacy, deployment }) {
71
+ for (const [object, methods] of [[identity, ['read', 'publish']], [metadata, ['get', 'commit']], [storage, ['get', 'put']]]) {
72
+ if (!object || methods.some(method => typeof object[method] !== 'function')) throw new TypeError('Incomplete vault adapter')
73
+ }
74
+ return async function handle(request) {
75
+ try {
76
+ const url = new URL(request.url)
77
+ if (/%|\\/.test(url.pathname)) return reply(400)
78
+ const parts = url.pathname.slice(1).split('/')
79
+ const publishing = parts[0] === '_publish'
80
+ const vault = publishing ? parts[1] : parts[0]
81
+ if (!validId(vault)) return reply(404)
82
+ const home = !publishing && (parts.length === 1 || (parts.length === 2 && parts[1] === ''))
83
+ if (url.search && (publishing || (home && ( [...url.searchParams.keys()].some(key => key !== 'q') || url.searchParams.getAll('q').length > 1 || (url.searchParams.get('q') || '').length > 200)))) return reply(400)
84
+ if (publishing) {
85
+ if (parts.length !== 2 || !['GET', 'POST'].includes(request.method)) return reply(405, '', { Allow: 'GET, POST' })
86
+ // Browser-originated writes are not a machine publication channel.
87
+ if (request.headers.has('origin') || request.headers.has('cookie')) return reply(403)
88
+ const principal = await identity.publish(request, vault)
89
+ if (!principal) return reply(401, '', { 'WWW-Authenticate': 'Bearer realm="vault-publication"' })
90
+ const record = await metadata.get(vault)
91
+ if (!sameOwner(principal, record?.owner)) return reply(404)
92
+ if (request.method === 'GET') {
93
+ return new Response(JSON.stringify({ schema: 'atelier-vault-status/v1', vault, revision: record.revision, publication: record.publication ?? null }), { headers: { ...headers, 'Content-Type': 'application/json' } })
94
+ }
95
+ if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') return reply(415)
96
+ let input, prepared
97
+ try { input = await boundedBody(request); prepared = await preparePublication(input) }
98
+ catch (error) { return reply(error.message === 'too-large' ? 413 : 400) }
99
+ if (input.expectedRevision !== record.revision) return reply(409)
100
+ const context = createVaultContext({ vault, record: { owner: record.owner, publication: prepared.publication, revision: input.expectedRevision + 1, manifest: prepared.manifest }, deployment, request: { origin: url.origin, path: url.pathname } })
101
+ const before = await verifyVaultPrivacy(privacy, { ...context, phase: 'before-upload' })
102
+ if (before.status !== 'verified') return reply(503, before.reason)
103
+ for (const file of prepared.files) await storage.put(`${vault}/${file.sha256}`, file.bytes)
104
+ const after = await verifyVaultPrivacy(privacy, { ...context, phase: 'before-activation' })
105
+ if (after.status !== 'verified' || after.policyRevision !== before.policyRevision) return reply(503, 'Protection changed or could not be verified. Publication was not activated.')
106
+ // Revalidate the credential after uploads and activation checks, immediately before commit.
107
+ const current = await identity.publish(request, vault)
108
+ if (!sameOwner(principal, current)) return reply(401, '', { 'WWW-Authenticate': 'Bearer realm="vault-publication"' })
109
+ const committed = await metadata.commit(vault, { owner: principal, expectedRevision: input.expectedRevision, manifest: prepared.manifest, publication: prepared.publication })
110
+ if (!committed) return reply(409)
111
+ return new Response(JSON.stringify({ schema: 'atelier-vault-receipt/v1', vault, revision: input.expectedRevision + 1, publication: prepared.publication }), { status: 201, headers: { ...headers, 'Content-Type': 'application/json' } })
112
+ }
113
+ if (!['GET', 'HEAD'].includes(request.method)) return reply(405, '', { Allow: 'GET, HEAD' })
114
+ const principal = await identity.read(request)
115
+ if (!principal) return reply(401)
116
+ const record = await metadata.get(vault)
117
+ if (!sameOwner(principal, record?.owner)) return reply(404)
118
+ if (!home && !record.manifest.some(file => file.path === parts.slice(1).join('/'))) return reply(404)
119
+ const protection = await verifyVaultPrivacy(privacy, createVaultContext({ vault, record, deployment, request: { origin: url.origin, path: url.pathname } }))
120
+ if (home) return new Response(request.method === 'HEAD' ? null : renderVaultHome({ vault, revision: record.revision, manifest: record.manifest, privacy: protection, query: url.searchParams.get('q') || '' }), { headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8', 'Content-Security-Policy': vaultHomePolicy } })
121
+ if (protection.status !== 'verified') return reply(503, protection.reason)
122
+ const path = parts.slice(1).join('/')
123
+ if (!validPath(path)) return reply(404)
124
+ const file = record.manifest.find(entry => entry.path === path)
125
+ if (!file || !/^[a-f0-9]{64}$/.test(file.sha256) || !Object.values(TYPES).includes(file.type)) return reply(404)
126
+ const bytes = await storage.get(`${vault}/${file.sha256}`)
127
+ if (!bytes || bytes.length !== file.size || await digest(bytes) !== file.sha256) return reply(503)
128
+ return new Response(request.method === 'HEAD' ? null : bytes, { headers: { ...headers, 'Content-Type': file.type, 'Content-Length': String(bytes.length), ...(file.type === 'application/pdf' ? { 'Content-Disposition': 'attachment' } : {}) } })
129
+ } catch { return reply(503) }
130
+ }
131
+ }
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { preparePublication } from './service.mjs'
4
+ const LIMIT = 4 * 1024 * 1024
5
+ /** Node-only bundle preparation. Select paths explicitly; never scan a repo. */
6
+ export async function prepareVaultSource({ root, paths, expectedRevision }) {
7
+ if (typeof root !== 'string' || !path.isAbsolute(root) || !Array.isArray(paths) || !paths.length || paths.length > 100) throw new TypeError('Absolute artifact root and explicit paths required')
8
+ if (fs.lstatSync(root).isSymbolicLink()) throw new Error('Artifact root must not be a symlink')
9
+ const realRoot = fs.realpathSync(root)
10
+ if (!fs.statSync(realRoot).isDirectory()) throw new Error('Artifact root must be a directory')
11
+ let total = 0
12
+ const files = []
13
+ for (const name of paths) {
14
+ if (typeof name !== 'string' || name.length > 512 || !name.split('/').every(part => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(part))) throw new Error('Invalid selected artifact path')
15
+ let file = realRoot
16
+ for (const part of name.split('/')) {
17
+ file = path.join(file, part)
18
+ if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Selected artifacts must not traverse symlinks')
19
+ }
20
+ const before = fs.lstatSync(file)
21
+ if (!before.isFile() || before.nlink !== 1 || before.size > LIMIT) throw new Error('Selected artifact must be a bounded, unlinked regular file')
22
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0))
23
+ try {
24
+ const opened = fs.fstatSync(fd)
25
+ const resolved = fs.realpathSync(file)
26
+ if ((path.relative(realRoot, resolved).startsWith('..' + path.sep) || path.isAbsolute(path.relative(realRoot, resolved))) || opened.ino !== before.ino || opened.dev !== before.dev || !opened.isFile() || opened.nlink !== 1 || opened.size > LIMIT) throw new Error('Artifact changed during selection')
27
+ const data = Buffer.alloc(opened.size)
28
+ let offset = 0
29
+ while (offset < data.length) {
30
+ const count = fs.readSync(fd, data, offset, data.length - offset, offset)
31
+ if (!count) throw new Error('Artifact changed during read')
32
+ offset += count
33
+ }
34
+ const after = fs.fstatSync(fd)
35
+ const current = fs.lstatSync(file)
36
+ if (after.nlink !== 1 || current.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || current.ino !== opened.ino || current.dev !== opened.dev || fs.realpathSync(file) !== resolved) throw new Error('Artifact changed during read')
37
+ total += data.length
38
+ if (total > LIMIT) throw new Error('Artifact bundle too large')
39
+ files.push({ path: name, base64: data.toString('base64') })
40
+ } finally { fs.closeSync(fd) }
41
+ }
42
+ const request = { schema: 'atelier-vault-publication/v1', contractVersion: '1.0.0', expectedRevision, files }
43
+ const prepared = await preparePublication(request)
44
+ return { request, publication: prepared.publication, manifest: prepared.manifest }
45
+ }
@@ -0,0 +1,23 @@
1
+ /** Provider SDKs/bindings are explicitly supplied by the host, never auto-loaded. */
2
+ export function r2PrivateStorage(bucket) {
3
+ return {
4
+ async put(key, bytes) { await bucket.put(key, bytes) },
5
+ async get(key) {
6
+ const object = await bucket.get(key)
7
+ return object ? new Uint8Array(await object.arrayBuffer()) : null
8
+ },
9
+ }
10
+ }
11
+ export function vercelPrivateStorage(blob) {
12
+ return {
13
+ async put(key, bytes) {
14
+ await blob.put(key, bytes, { access: 'private', addRandomSuffix: false, allowOverwrite: true, contentType: 'application/octet-stream' })
15
+ },
16
+ async get(key) {
17
+ const object = await blob.get(key, { access: 'private', useCache: false })
18
+ if (!object) return null
19
+ if (object.statusCode !== 200 || !object.stream) throw new Error('Private storage unavailable')
20
+ return new Uint8Array(await new Response(object.stream).arrayBuffer())
21
+ },
22
+ }
23
+ }
@@ -3,8 +3,12 @@
3
3
  "configurations": [
4
4
  {
5
5
  "name": "atelier",
6
- "runtimeExecutable": "npx",
7
- "runtimeArgs": ["atelier", "server"],
6
+ "runtimeExecutable": "node",
7
+ "runtimeArgs": [
8
+ "--input-type=module",
9
+ "--eval",
10
+ "import { runCli } from '@mnstry/atelier/cli'; process.exit(await runCli({ argv: ['server', ...process.argv.slice(1)] }));"
11
+ ],
8
12
  "port": 8137,
9
13
  "autoPort": true
10
14
  }
@@ -1,4 +1,5 @@
1
1
  .atelier-local/
2
+ .atelier-local/obsidian.json
2
3
  atelier.local.json
3
4
  atelier.workspace.local.json
4
5
  atelier-attestation-key.local.json
@@ -2,10 +2,10 @@
2
2
  title: "Shared Project Seed"
3
3
  kg:
4
4
  id: "project-alpha:seed"
5
- type: "project-note"
5
+ type: "document"
6
6
  domain: "project"
7
7
  lifecycle: "draft"
8
- status: "seed"
8
+ status: "draft"
9
9
  audience: "team"
10
10
  relations: []
11
11
  ---
@@ -0,0 +1,45 @@
1
+ # External-project adapter starter
2
+
3
+ This invented example uses MNSTRY Atelier. The adapter owns configuration and
4
+ a sample extension pack; the separate source repository owns canonical text.
5
+ Runs, answers and review history remain ignored local state. Public audience
6
+ labels do not grant publication, consent or runtime access.
7
+
8
+ Create this scaffold with `atelier init --template external-project --target DIR`.
9
+ The target must not exist. No Git repository, hook or remote is created.
10
+ Initialize the adapter and its source directory as separate repositories when
11
+ ready; the adapter ignores `source/`. To connect an existing repository instead,
12
+ pass `--repo-path source=PATH` consistently, or use an ignored local overlay.
13
+ Keep domain vocabulary and methods in your own adapter and source repositories.
14
+ Replace the synthetic actor in the boundary policy with your actual policy.
15
+
16
+ Using the installed package's CLI, from the adapter directory:
17
+
18
+ ```sh
19
+ atelier lock provenance
20
+ atelier config check --explain
21
+ atelier extension-pack validate
22
+ atelier graph
23
+ atelier project
24
+ atelier readiness
25
+ atelier review packs
26
+ atelier review run sample.readiness:contract-gate --answers answers.example.json
27
+ atelier dev --review
28
+ ```
29
+
30
+ Open `/review` on the displayed local address. Use repository `source` and
31
+ `README.md` to leave a question. Use the returned run ID to review its claims.
32
+ The evaluator measures answer completeness only. Read and judge the evidence;
33
+ acceptance creates a handoff, never a canonical edit. Use `atelier review history`
34
+ and `atelier review handoff REQUEST_ID` to inspect records locally.
35
+
36
+ Real answers and responses belong in ignored local state, not the tracked
37
+ example answer file. Reopening recovers saved contributions; missing or failed
38
+ saves remain visible. Stop/start the foreground sidecar with the same project
39
+ and reopen the same document. This does not establish cross-computer access.
40
+
41
+ For CI, compose the root's existing consumer, portability, boundary and
42
+ private-disclosure checks. No new CI run or provider is triggered by this template.
43
+ Before changing the pack, run `atelier upgrade --dry-run`, retain historical
44
+ pack content, update its lifecycle declaration and lock through your reviewed
45
+ migration process, and run the checks again. Never reinterpret old run evidence.