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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +86 -25
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/docs/assurance-controls.md +12 -2
  31. package/docs/atelier-runtime.md +13 -2
  32. package/docs/atelier-sync.md +172 -0
  33. package/docs/blocks/claims.md +28 -18
  34. package/docs/blocks/will-not-do.md +12 -3
  35. package/docs/coauthor-session.md +57 -0
  36. package/docs/connected-composition.md +75 -0
  37. package/docs/decisions.md +167 -0
  38. package/docs/exact-upgrades.md +159 -0
  39. package/docs/extension-pack-lifecycle.md +33 -0
  40. package/docs/guided-upgrades.md +99 -0
  41. package/docs/install.md +65 -9
  42. package/docs/intake-and-guides.md +65 -0
  43. package/docs/integration-and-review-plan.md +409 -0
  44. package/docs/integration-contract-decisions.md +22 -0
  45. package/docs/local-review.md +84 -0
  46. package/docs/local-services.md +227 -0
  47. package/docs/local-state.md +107 -0
  48. package/docs/obsidian-contract.md +687 -0
  49. package/docs/obsidian.md +387 -0
  50. package/docs/portable-presentation.md +328 -0
  51. package/docs/presentation-consumer-boundaries.md +75 -0
  52. package/docs/presentation-review-disposition.md +47 -0
  53. package/docs/project-command-inventory.md +38 -0
  54. package/docs/project-options.md +53 -0
  55. package/docs/release-engineering.md +99 -2
  56. package/docs/review-portability.md +55 -0
  57. package/docs/skill-steward.md +156 -0
  58. package/docs/upgrade.md +70 -2
  59. package/docs/vault-service.md +292 -0
  60. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  61. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  62. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  64. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  65. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  66. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  67. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  69. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  70. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  71. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  72. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  73. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  74. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  75. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  76. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  77. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  78. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  79. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  80. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  81. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  82. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  83. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  84. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  85. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  86. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  87. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  88. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  89. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  90. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  91. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  92. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  93. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  94. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  95. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  96. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  97. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  98. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  99. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  100. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  101. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  102. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  103. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  104. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  105. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  106. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  107. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  108. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  109. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  113. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  114. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  115. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  116. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  121. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  122. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  123. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  125. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  131. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  132. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  133. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  136. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  137. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  138. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  139. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  142. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  143. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  144. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  145. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  146. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  147. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  161. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  162. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  163. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  164. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  169. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  170. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  171. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  172. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  177. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  178. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  179. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  181. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  182. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  183. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  187. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  188. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  189. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  190. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  193. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  194. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  195. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  196. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  197. package/fixtures/obsidian/edits/workspace.json +65 -0
  198. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  199. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  200. package/fixtures/obsidian/materialization/workspace.json +54 -0
  201. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  202. package/fixtures/ui/presentation/pane.v1.json +7 -0
  203. package/fixtures/ui/presentation/reference.v1.json +293 -0
  204. package/package.json +61 -6
  205. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  206. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  207. package/skills/claude/atelier-open/SKILL.md +5 -0
  208. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  209. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  210. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  211. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  212. package/skills/codex/atelier-open/SKILL.md +5 -0
  213. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  214. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  215. package/src/access/preview-enforcer.mjs +84 -0
  216. package/src/boundary/content-rules.mjs +9 -4
  217. package/src/boundary/policy.mjs +74 -42
  218. package/src/cli/project-options.mjs +58 -0
  219. package/src/cli/run.mjs +61 -3
  220. package/src/coauthor/session.mjs +161 -0
  221. package/src/coauthor/store.mjs +173 -0
  222. package/src/collaboration/event-ledger.mjs +16 -33
  223. package/src/collaboration/inspection-bundle.mjs +223 -0
  224. package/src/collaboration/review-contracts.mjs +25 -0
  225. package/src/collaboration/review-store.mjs +305 -0
  226. package/src/commands/coauthor.mjs +26 -0
  227. package/src/commands/config.mjs +6 -1
  228. package/src/commands/extension-pack.mjs +15 -8
  229. package/src/commands/init.mjs +44 -5
  230. package/src/commands/obsidian.mjs +368 -0
  231. package/src/commands/review.mjs +147 -0
  232. package/src/commands/setup.mjs +51 -14
  233. package/src/commands/skills.mjs +199 -0
  234. package/src/commands/sync.mjs +107 -0
  235. package/src/composition/registry.mjs +27 -0
  236. package/src/composition/wire.mjs +122 -0
  237. package/src/contracts/corpus.mjs +35 -0
  238. package/src/decisions/contracts.d.mts +96 -0
  239. package/src/decisions/contracts.mjs +321 -0
  240. package/src/disclosure/content-scan.mjs +13 -0
  241. package/src/export/atelier-export-contract.mjs +16 -1
  242. package/src/extension-packs/lifecycle.mjs +154 -0
  243. package/src/graph/graph.mjs +64 -2
  244. package/src/graph/knowledge-graph.mjs +548 -24
  245. package/src/guides/contracts.mjs +54 -0
  246. package/src/index.mjs +29 -0
  247. package/src/intake/store.mjs +119 -0
  248. package/src/preview/channel.mjs +89 -0
  249. package/src/project/config.mjs +50 -49
  250. package/src/project/durable-state.mjs +161 -0
  251. package/src/project/file-class.mjs +166 -0
  252. package/src/project/private-state.mjs +11 -1
  253. package/src/projection/obsidian/contracts.mjs +571 -0
  254. package/src/projection/obsidian/edits/align.mjs +169 -0
  255. package/src/projection/obsidian/edits/apply.mjs +889 -0
  256. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  257. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  258. package/src/projection/obsidian/edits/index.mjs +19 -0
  259. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  260. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  261. package/src/projection/obsidian/edits/observe.mjs +123 -0
  262. package/src/projection/obsidian/edits/policy.mjs +123 -0
  263. package/src/projection/obsidian/edits/regions.mjs +648 -0
  264. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  265. package/src/projection/obsidian/materialize/index.mjs +7 -0
  266. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  267. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  268. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  269. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  270. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  271. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  272. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  273. package/src/projection/obsidian/proposals/index.mjs +23 -0
  274. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  275. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  276. package/src/projection/obsidian/proposals/router.mjs +168 -0
  277. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  278. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  279. package/src/projection/obsidian/publication/index.mjs +5 -0
  280. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  281. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  282. package/src/projection/obsidian/publication/transport.mjs +139 -0
  283. package/src/projection/obsidian/recovery/index.mjs +6 -0
  284. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  285. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  286. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  287. package/src/projection/obsidian/recovery/store.mjs +234 -0
  288. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  289. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  290. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  291. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  292. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  293. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  294. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  295. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  296. package/src/projection/project.mjs +28 -19
  297. package/src/readiness-protocols/evidence.mjs +282 -0
  298. package/src/readiness-protocols/runtime.mjs +4 -3
  299. package/src/readiness-protocols/source-read.mjs +39 -0
  300. package/src/runtime/git-adapter.mjs +189 -0
  301. package/src/runtime/local-state.mjs +439 -0
  302. package/src/runtime/obsidian/app-capability.mjs +110 -0
  303. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  304. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  305. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  306. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  307. package/src/runtime/obsidian/contributions.mjs +27 -0
  308. package/src/runtime/obsidian/documents.mjs +37 -0
  309. package/src/runtime/obsidian/enablement.mjs +53 -0
  310. package/src/runtime/obsidian/engine.mjs +532 -0
  311. package/src/runtime/obsidian/errors.mjs +14 -0
  312. package/src/runtime/obsidian/extension-points.mjs +103 -0
  313. package/src/runtime/obsidian/index.mjs +34 -0
  314. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  315. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  316. package/src/runtime/obsidian/observation.mjs +158 -0
  317. package/src/runtime/obsidian/opening.mjs +243 -0
  318. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  319. package/src/runtime/obsidian/pipeline.mjs +122 -0
  320. package/src/runtime/obsidian/private-lock.mjs +156 -0
  321. package/src/runtime/obsidian/process-identity.mjs +58 -0
  322. package/src/runtime/obsidian/service-client.mjs +66 -0
  323. package/src/runtime/obsidian/service-main.mjs +89 -0
  324. package/src/runtime/obsidian/service-record.mjs +169 -0
  325. package/src/runtime/obsidian/service-server.mjs +121 -0
  326. package/src/runtime/obsidian/service.mjs +216 -0
  327. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  328. package/src/runtime/obsidian/state-store.mjs +180 -0
  329. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  330. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  331. package/src/runtime/obsidian/watchers.mjs +36 -0
  332. package/src/runtime/repository-observation.mjs +491 -0
  333. package/src/runtime/supervisor.mjs +803 -0
  334. package/src/server/local-sidecar.mjs +44 -0
  335. package/src/server/server.mjs +1 -0
  336. package/src/skills/steward.mjs +780 -0
  337. package/src/ui/presentation/browser.mjs +278 -0
  338. package/src/ui/presentation/contract.mjs +116 -0
  339. package/src/ui/presentation/index.mjs +7 -0
  340. package/src/ui/presentation/native.mjs +153 -0
  341. package/src/ui/presentation/proof.mjs +57 -0
  342. package/src/ui/presentation/schema-check.mjs +30 -0
  343. package/src/ui/presentation/schema.generated.mjs +1371 -0
  344. package/src/ui/presentation/state.mjs +43 -0
  345. package/src/ui/presentation/styles.mjs +67 -0
  346. package/src/ui/presentation/tokens.mjs +74 -0
  347. package/src/ui/presentation/web.mjs +85 -0
  348. package/src/ui/review-page.mjs +32 -0
  349. package/src/upgrade/explanation.mjs +33 -0
  350. package/src/upgrade/provenance.mjs +338 -0
  351. package/src/upgrade/transaction-files.mjs +103 -0
  352. package/src/upgrade/transaction.mjs +475 -0
  353. package/src/upgrade/upgrade.mjs +63 -28
  354. package/src/vault/hosts.mjs +21 -0
  355. package/src/vault/identity.mjs +19 -0
  356. package/src/vault/index.mjs +7 -0
  357. package/src/vault/interface.mjs +11 -0
  358. package/src/vault/metadata.mjs +31 -0
  359. package/src/vault/privacy.mjs +148 -0
  360. package/src/vault/probe.mjs +89 -0
  361. package/src/vault/service.mjs +131 -0
  362. package/src/vault/source.mjs +45 -0
  363. package/src/vault/storage.mjs +23 -0
  364. package/templates/distribution-workspace/.claude/launch.json +6 -2
  365. package/templates/distribution-workspace/gitignore +1 -0
  366. package/templates/distribution-workspace/project/README.md +2 -2
  367. package/templates/external-project-workspace/README.md +45 -0
  368. package/templates/external-project-workspace/answers.example.json +4 -0
  369. package/templates/external-project-workspace/atelier.project.json +40 -0
  370. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  371. package/templates/external-project-workspace/gitignore +8 -0
  372. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  373. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  374. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  375. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  376. package/templates/external-project-workspace/source/README.md +11 -0
  377. package/templates/launch.json +6 -2
  378. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  379. package/templates/private-domain-workspace/domain/README.md +2 -2
  380. package/templates/private-domain-workspace/gitignore +1 -0
  381. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  382. package/templates/shared-project-workspace/gitignore +1 -0
  383. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,58 @@
1
+ // Shared project-location syntax. Command-specific arguments remain owned by
2
+ // their command; this parser does not make unknown options globally valid.
3
+ export const PROJECT_OPTION_SPEC = Object.freeze({
4
+ project: 'value',
5
+ 'project-config': 'value',
6
+ 'repo-path': 'value',
7
+ })
8
+
9
+ function invalid(message) {
10
+ const error = new Error(message)
11
+ error.code = 'project-option-invalid'
12
+ error.exitCode = 2
13
+ throw error
14
+ }
15
+
16
+ export function projectOptionAt(argv, index, configArgPrefix = '--project-config=') {
17
+ const arg = argv[index]
18
+ if (typeof arg !== 'string' || !arg.startsWith('--')) return null
19
+ const name = arg.slice(2).split('=')[0]
20
+ const custom = configArgPrefix !== '--project-config=' && arg.startsWith(configArgPrefix)
21
+ if (!custom && !Object.hasOwn(PROJECT_OPTION_SPEC, name)) return null
22
+ const inline = custom || arg.includes('=')
23
+ const value = custom ? arg.slice(configArgPrefix.length) : inline ? arg.slice(arg.indexOf('=') + 1) : argv[index + 1]
24
+ if (typeof value !== 'string' || !value.trim() || value.startsWith('--')) {
25
+ invalid(`--${custom ? 'project-config' : name} requires a non-empty value`)
26
+ }
27
+ const option = custom ? 'project-config' : name
28
+ if (option === 'repo-path') {
29
+ const separator = value.indexOf('=')
30
+ if (separator < 1 || !value.slice(0, separator).trim() || !value.slice(separator + 1).trim()) {
31
+ invalid('--repo-path requires NAME=PATH')
32
+ }
33
+ }
34
+ return { name: option, value, nextIndex: index + (inline ? 1 : 2) }
35
+ }
36
+
37
+ export function parseProjectOptions(argv = [], configArgPrefix = '--project-config=') {
38
+ let project = null
39
+ const repoPaths = new Map()
40
+ const remaining = []
41
+ for (let index = 0; index < argv.length;) {
42
+ const option = projectOptionAt(argv, index, configArgPrefix)
43
+ if (!option) {
44
+ remaining.push(argv[index++])
45
+ continue
46
+ }
47
+ if (option.name === 'repo-path') {
48
+ const separator = option.value.indexOf('=')
49
+ repoPaths.set(option.value.slice(0, separator).trim(), option.value.slice(separator + 1))
50
+ } else if (project === null) {
51
+ // Preserve the resolver's first project selection and last named repo
52
+ // override behavior, including aliases in the same invocation.
53
+ project = option.value.trim()
54
+ }
55
+ index = option.nextIndex
56
+ }
57
+ return { project, repoPaths, remaining }
58
+ }
package/src/cli/run.mjs CHANGED
@@ -58,6 +58,11 @@ export const commandMap = new Map([
58
58
  ['feedback:check', ['src/commands/feedback.mjs', 'check']],
59
59
  ['announcements', ['src/commands/announcements.mjs']],
60
60
  ['announcements:list', ['src/commands/announcements.mjs', 'list']],
61
+ ['sync', ['src/commands/sync.mjs']],
62
+ ['review', ['src/commands/review.mjs']],
63
+ ['coauthor', ['src/commands/coauthor.mjs']],
64
+ ['skills', ['src/commands/skills.mjs']],
65
+ ['obsidian', ['src/commands/obsidian.mjs']],
61
66
  ])
62
67
 
63
68
  function isDefaultBrand(brand) {
@@ -115,14 +120,30 @@ Core commands:
115
120
  support bundle --dry-run Preview a no-send support bundle.
116
121
  feedback --message TEXT Write a local, never-sent feedback report.
117
122
  announcements list List and verify MNSTRY announcements.
123
+ sync enroll Explicitly enroll one repository for supervision.
124
+ sync status Fully observe the enrolled repository.
125
+ sync reconcile Fetch and perform only proven fast-forwards.
126
+ sync plan Prepare one bounded, reviewable commit plan.
127
+ sync commit Execute an exactly confirmed commit plan.
128
+ sync run --once Run one full-state supervisor cycle.
129
+ obsidian status|open|apply Maintain and open Obsidian views of the corpus; apply edits made in them.
118
130
  egress check Check extracted Atelier paths for forbidden egress.
119
131
  boundary check Enforce private/shared repo placement rules.
120
132
  boundary audit Report content-rule matches tree-wide without blocking.
121
133
  boundary install-hooks Install staged/private-domain Git guards.
122
134
  promote Record a git.promote disclosure event.
123
- upgrade --dry-run Plan a safe package/template upgrade.
135
+ upgrade plan --save Save an exact local candidate plan.
136
+ upgrade explain --plan Explain saved changes and consent without writes.
137
+ upgrade apply --plan --confirm Apply the selected plan through commit hooks.
138
+ upgrade status|recover Inspect receipts or preview recovery.
139
+ upgrade --dry-run Plan a legacy package/template upgrade.
124
140
  upgrade --apply Apply a branch-based reviewable upgrade.
125
141
  lock check|write Verify or create atelier.lock.json.
142
+ lock provenance Inspect actual installed package identity.
143
+ review run|history|handoff Evidence-bound local human review.
144
+ review export|inspect|packs Preview inspection bundles and pack lifecycle.
145
+ coauthor start|read|event|recover Save and resume private authoring drafts.
146
+ skills audit|observe|candidates|sync Audit and manage local skill projections.
126
147
  config check Validate project config.
127
148
  extension-pack validate Validate declared extension packs.
128
149
  extension-pack list List declared extension packs.
@@ -138,6 +159,9 @@ Attestation commands:
138
159
  Commands whose usage names --project accept --project=PATH or --project PATH.
139
160
  The project resolver also accepts --project-config=PATH and
140
161
  MNSTRY_ATELIER_PROJECT_CONFIG=PATH; each command's own help is authoritative.
162
+ Project-aware commands accept repeated --repo-path NAME=PATH or
163
+ --repo-path=NAME=PATH. Overrides change location, never declared read authority.
164
+ Run config check --explain for path-free resolution sources.
141
165
  Machine-local repo paths belong in
142
166
  .atelier-local/, atelier.local.json, or atelier.workspace.local.json.`
143
167
  }
@@ -145,7 +169,14 @@ Machine-local repo paths belong in
145
169
  export function buildCommandHelpText(command, brand = DEFAULT_BRAND) {
146
170
  const c = brand.command
147
171
  const help = {
148
- init: `Usage: ${c} init [--template private-domain|shared-project|sample-workspace|distribution] [--target DIR] [--actor ID]
172
+ skills: `Usage: ${c} skills audit|observe|candidates|sync
173
+ Audit bundled skills or --root DIR [--peer DIR].
174
+ Sync previews by default; applying requires --apply --confirm PLAN_DIGEST.
175
+ Observations are local metadata, never automatic telemetry.`,
176
+ coauthor: `Usage: ${c} coauthor start|read|event|recover
177
+
178
+ Read one JSON request from stdin (maximum 1 MiB). Start takes {"config":{"id":"SESSION","fields":[{"id":"FIELD","source":{"ref":"packet.md","digest":"SHA256"}}]}}. Read/recover take {"sessionId":"SESSION"}. Event takes {"sessionId":"SESSION","event":{"id":"UNIQUE","expectedRevision":0,"type":"answer","text":"ANSWER"}}. Run from the intended Git workspace with ignored .atelier-local/. Saves are private drafts, never canonical source edits.`,
179
+ init: `Usage: ${c} init [--template private-domain|shared-project|sample-workspace|distribution|external-project] [--target DIR] [--actor ID]
149
180
 
150
181
  Creates tracked starter files and an Atelier lockfile. It does not install hooks unless asked separately. An unrecognized --template exits 1 and writes nothing; omit --template for the blank scaffold.`,
151
182
  adopt: `Usage: ${c} adopt [--profile single-repo|private-domain|shared-project|multi-repo|monorepo|control-workspace] [--target DIR] [--yes]
@@ -176,7 +207,7 @@ Runs the bundled MNSTRY readiness pack claim-first. Protocol state and packet dr
176
207
  ${c} extension-pack validate [--json] [--project ./atelier.project.json]
177
208
  ${c} extension-pack list [--json] [--project ./atelier.project.json]
178
209
 
179
- Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks in the tracked project config and reports one line per pack. Packs load additively alongside the bundled MNSTRY protocols and can never replace them. list is the default subcommand.`,
210
+ Loads every extension pack declared under ext["mnstry.atelier"].extensionPacks in the tracked project config and reports one line per pack. Packs load additively alongside the bundled MNSTRY protocols and can never replace them. list is the default subcommand. Shared --project-config and repeated --repo-path NAME=PATH options are supported in both space and equals forms.`,
180
211
  distribution: `Usage: ${c} distribution check [--target DIR] [--pack DIR]
181
212
 
182
213
  Checks a distribution package for the required MNSTRY attribution markers. Blocking: the distribution README.md byte check, and a CLI probe that EXECUTES the target's declared bin with --version (spawned with the current Node, cwd set to the target — only run this against distributions you trust) and requires the attribution in its output; a target that looks like a distribution but declares no probe-able bin, or ships a malformed package.json, is also blocking. The extension-pack manifest attribution key is advisory and reported only. The normative wording lives in TRADEMARKS.md under "Required attribution"; see also docs/attestation.md and docs/distributions.md.`,
@@ -198,6 +229,33 @@ Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0
198
229
  announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
199
230
 
200
231
  MNSTRY announcements are a pull-only channel: signed JSON documents under announcements/ in the repository. The trust anchor is always the committed MNSTRY key, or one you pass explicitly with --public-key; --dir changes only where documents are read from and never which key verifies them. Every run names the key and keyId it used. The kit never fetches anything — receiving announcements is the git pull you chose to run, and show refuses to print a body whose signature does not verify.`,
232
+ obsidian: `Usage: ${c} obsidian status|scope|audience|mode|policy|service|open|apply|conflicts|apply-policy|selection|proposals [--project atelier.project.json] [--data-root DIR] [--json]
233
+
234
+ ${c} obsidian status
235
+ ${c} obsidian scope list | scope show ID
236
+ ${c} obsidian audience show | set A,B | clear
237
+ ${c} obsidian mode show | set manual|automatic
238
+ ${c} obsidian policy show | install FILE | revoke
239
+ ${c} obsidian service start [--consent-actor ID] --adapter=obsidian-cli | status | stop | unit --print --adapter=obsidian-cli
240
+ ${c} obsidian open [--scope ID] [--consent-actor ID] [--allow-stale] --adapter=obsidian-cli
241
+ ${c} obsidian apply list | show EDIT | run EDIT [--actor ID] | recover
242
+ ${c} obsidian conflicts [ID]
243
+ ${c} obsidian apply-policy create FILE | show | revoke
244
+ ${c} obsidian selection resolve ID | persist ID [allow-empty] | show ID | list
245
+ ${c} obsidian proposals list | show OPERATION
246
+
247
+ Noninteractive. With --json exactly one JSON document is printed, for a refusal too. status, scope, audience show, mode show, policy show and service status are read-only. open reports current only when the owned maintenance service ticked after the request, the view's trusted generation is the prepared one and reads back byte for byte, and the installed Obsidian meets the minimum version and answers for that vault; every other outcome is typed and exits 3. Reaching the installed app is never a default: open, service start and service unit need --adapter=obsidian-cli. Audiences, mode and the apply policy are private machine settings, never project configuration and never a note. apply, conflicts, apply-policy, selection and proposals are contributed operations registered from modules shipped under src/runtime/obsidian/contributions/; the built-in names cannot be replaced. While no apply operation is registered every apply reports apply-unavailable. Exit codes: 0 done, 1 internal error, 2 refusal or usage, 3 ran and the answer is not success.`,
248
+ sync: `Usage:
249
+ ${c} sync enroll --repo DIR [--project atelier.project.json] [--git ABSOLUTE_PATH]
250
+ ${c} sync status --repo DIR
251
+ ${c} sync reconcile --repo DIR [--retries 3]
252
+ ${c} sync run --repo DIR [--once] [--interval 30]
253
+ ${c} sync plan --repo DIR --path FILE [--path FILE] --message TEXT [--publish]
254
+ ${c} sync commit --repo DIR --operation ID --confirm ID
255
+ ${c} sync pause|freeze|resume --repo DIR
256
+ ${c} sync trace --repo DIR
257
+
258
+ Enrolls exactly one repository and keeps Git plus readable files authoritative. Reconciliation observes the complete repository every cycle and performs only fast-forward updates. Commit creation is a two-phase user-confirmed operation: plan shows one bounded change set, and commit refuses unless the repository is unchanged and --confirm exactly repeats the operation id. No semantic conflict resolution, force operation, browser apply endpoint, telemetry, or desktop shell is present.`,
201
259
  }
202
260
  return help[command] || `Usage: ${c} ${command} [args]\n\nRun ${c} --help for the command list.`
203
261
  }
@@ -0,0 +1,161 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalize } from '../attestation/jcs.mjs';
3
+
4
+ // Experimental adapter seam. This reducer performs no storage or authorization.
5
+ export const COAUTHOR_SESSION_VERSION = 'atelier-coauthor-session/experimental-v1';
6
+ export function contentDigest(text) {
7
+ if (typeof text !== 'string') throw new TypeError('text required');
8
+ if (Buffer.from(text, 'utf8').toString('utf8') !== text) throw new TypeError('text must be lossless UTF-8');
9
+ return createHash('sha256').update(text, 'utf8').digest('hex');
10
+ }
11
+ const digestPattern = /^[a-f0-9]{64}$/;
12
+ function requireText(value, label) {
13
+ if (typeof value !== 'string' || !value.trim() || value.length > 100000) {
14
+ throw new TypeError(`${label} required and bounded`);
15
+ }
16
+ }
17
+ function sourceCheck(source) {
18
+ if (!source || typeof source !== 'object') throw new TypeError('source required');
19
+ requireText(source.ref, 'source ref');
20
+ if (!digestPattern.test(source.digest)) throw new TypeError('source digest required');
21
+ }
22
+ export function createSession({ id, fields }) {
23
+ requireText(id, 'session id');
24
+ if (!Array.isArray(fields) || fields.length === 0 || fields.length > 1000) throw new TypeError('fields required');
25
+ const ids = new Set();
26
+ const targets = fields.map(({ id: fieldId, source }) => {
27
+ requireText(fieldId, 'field id');
28
+ if (ids.has(fieldId)) throw new Error('duplicate field');
29
+ ids.add(fieldId);
30
+ sourceCheck(source);
31
+ return { id: fieldId, source: { ref: source.ref, digest: source.digest } };
32
+ });
33
+ return { version: COAUTHOR_SESSION_VERSION, id, revision: 0, phase: 'input', index: 0,
34
+ fields: targets, answers: [], proposal: null, saved: [], pending: null, events: [] };
35
+ }
36
+ function checkEvent(event) {
37
+ if (!event || typeof event !== 'object' || Array.isArray(event)) throw new TypeError('event required');
38
+ requireText(event.id, 'event id');
39
+ if (!Number.isSafeInteger(event.expectedRevision) || event.expectedRevision < 0) throw new TypeError('revision required');
40
+ const keys = {
41
+ answer: ['text', 'continueAfterSave'], propose: ['text'], confirm: [], reject: [],
42
+ save: [], receipt: ['receipt'], failed: [], retry: [], advance: [], pause: [], resume: [],
43
+ }[event.type];
44
+ if (!keys || Object.keys(event).some(key => !['id', 'type', 'expectedRevision', ...keys].includes(key))) {
45
+ throw new TypeError('unknown event or field');
46
+ }
47
+ if (['answer', 'propose'].includes(event.type)) requireText(event.text, 'answer text');
48
+ if (event.continueAfterSave !== undefined && typeof event.continueAfterSave !== 'boolean') throw new TypeError('continue must be boolean');
49
+ if (event.type === 'receipt') {
50
+ const r = event.receipt;
51
+ if (!r || Object.keys(r).sort().join(',') !== 'fieldId,requestId,sessionId,sourceDigest,sourceRef,valueDigest') throw new TypeError('invalid receipt shape');
52
+ for (const key of ['requestId', 'sessionId', 'fieldId', 'sourceRef']) requireText(r[key], key);
53
+ for (const key of ['sourceDigest', 'valueDigest']) if (!digestPattern.test(r[key])) throw new TypeError('invalid receipt digest');
54
+ }
55
+ }
56
+ function need(state, phases) {
57
+ if (!phases.includes(state.phase)) throw new Error(`event refused in ${state.phase}`);
58
+ }
59
+ function writeEffect(state) {
60
+ const field = state.fields[state.index];
61
+ return { type: 'write-field', requestId: state.pending.requestId, sessionId: state.id,
62
+ fieldId: field.id, sourceRef: field.source.ref, sourceDigest: field.source.digest,
63
+ text: state.proposal.text, valueDigest: contentDigest(state.proposal.text) };
64
+ }
65
+ function advance(state) {
66
+ state.index += 1;
67
+ state.phase = state.index === state.fields.length ? 'complete' : 'input';
68
+ state.proposal = null;
69
+ state.pending = null;
70
+ }
71
+ export function transition(session, event) {
72
+ // State is trusted adapter-owned memory. Rehydrate by replaying validated events,
73
+ // not by accepting an arbitrary caller-supplied session snapshot.
74
+ if (session?.version !== COAUTHOR_SESSION_VERSION) throw new Error('unsupported session');
75
+ checkEvent(event);
76
+ const fingerprint = contentDigest(canonicalize(event));
77
+ const prior = session.events.find(item => item.id === event.id);
78
+ if (prior) {
79
+ if (prior.fingerprint !== fingerprint) throw new Error('event id conflict');
80
+ return { session: structuredClone(session), effects: [], duplicate: true };
81
+ }
82
+ if (event.expectedRevision !== session.revision) throw new Error('stale revision');
83
+ const state = structuredClone(session);
84
+ const effects = [];
85
+ switch (event.type) {
86
+ case 'answer':
87
+ need(state, ['input', 'draft']);
88
+ state.answers.push({ eventId: event.id, fieldId: state.fields[state.index].id, text: event.text });
89
+ state.proposal = { text: event.text, originalEventId: event.id, confirmed: true,
90
+ continueAfterSave: event.continueAfterSave ?? false };
91
+ state.phase = 'draft';
92
+ break;
93
+ case 'propose':
94
+ need(state, ['draft']);
95
+ // Every machine revision requires confirmation; no heuristic decides meaning.
96
+ state.proposal.text = event.text;
97
+ state.proposal.confirmed = false;
98
+ state.phase = 'confirmation';
99
+ break;
100
+ case 'confirm':
101
+ need(state, ['confirmation']);
102
+ state.proposal.confirmed = true;
103
+ state.phase = 'draft';
104
+ break;
105
+ case 'reject':
106
+ need(state, ['confirmation']);
107
+ state.proposal.text = state.answers.find(answer => answer.eventId === state.proposal.originalEventId).text;
108
+ state.proposal.confirmed = true;
109
+ state.phase = 'draft';
110
+ break;
111
+ case 'save':
112
+ need(state, ['draft']);
113
+ state.pending = { requestId: event.id, retries: 0 };
114
+ state.phase = 'saving';
115
+ effects.push(writeEffect(state));
116
+ break;
117
+ case 'failed':
118
+ need(state, ['saving']);
119
+ state.phase = 'recovery';
120
+ break;
121
+ case 'retry':
122
+ need(state, ['recovery']);
123
+ if (state.pending.retries >= 1) throw new Error('retry budget exhausted; adapter reconciliation required');
124
+ state.pending.retries += 1;
125
+ state.phase = 'saving';
126
+ effects.push(writeEffect(state));
127
+ break;
128
+ case 'receipt': {
129
+ need(state, ['saving', 'recovery']);
130
+ const expected = writeEffect(state);
131
+ for (const key of ['requestId', 'sessionId', 'fieldId', 'sourceRef', 'sourceDigest', 'valueDigest']) {
132
+ if (event.receipt[key] !== expected[key]) throw new Error(`receipt mismatch: ${key}`);
133
+ }
134
+ state.saved.push({ fieldId: expected.fieldId, text: state.proposal.text, receipt: structuredClone(event.receipt) });
135
+ state.phase = 'saved';
136
+ state.pending = null;
137
+ if (state.proposal.continueAfterSave) advance(state);
138
+ break;
139
+ }
140
+ case 'advance':
141
+ need(state, ['saved']);
142
+ advance(state);
143
+ break;
144
+ case 'pause':
145
+ need(state, ['input', 'draft', 'confirmation', 'saved', 'recovery']);
146
+ state.resumePhase = state.phase;
147
+ state.phase = 'paused';
148
+ break;
149
+ case 'resume':
150
+ need(state, ['paused']);
151
+ state.phase = state.resumePhase;
152
+ delete state.resumePhase;
153
+ break;
154
+ }
155
+ state.revision += 1;
156
+ state.events.push({ id: event.id, fingerprint, event: structuredClone(event) });
157
+ return { session: state, effects, duplicate: false };
158
+ }
159
+ export function replaySession(config, events) {
160
+ return events.reduce((session, event) => transition(session, event).session, createSession(config));
161
+ }
@@ -0,0 +1,173 @@
1
+ import { withPrivateLock, publishPrivateFile } from '../project/durable-state.mjs';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { canonicalize } from '../attestation/jcs.mjs';
6
+ import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs';
7
+ import { ensureContainedPrivateDirectory, openRegularFileNoFollow } from '../project/private-state.mjs';
8
+ import { createSession, transition, contentDigest } from './session.mjs';
9
+ import { validateJsonSchema } from '../export/atelier-export-contract.mjs';
10
+
11
+ const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-coauthor.v1.schema.json', import.meta.url), 'utf8'));
12
+ function validate(value, kind) {
13
+ const errors = validateJsonSchema({ $schema: schema.$schema, $defs: schema.$defs, $ref: `#/$defs/${kind}` }, value);
14
+ if (errors.length) throw new Error(`invalid coauthor ${kind}`);
15
+ }
16
+
17
+ const MAX_SOURCE_BYTES = 1024 * 1024;
18
+ function readText(file) {
19
+ const fd = openRegularFileNoFollow(file);
20
+ try {
21
+ if (fs.fstatSync(fd).size > MAX_SOURCE_BYTES) throw new Error('coauthor file exceeds byte limit');
22
+ return new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(fd));
23
+ } finally { fs.closeSync(fd); }
24
+ }
25
+ function sourcePath(root, ref) {
26
+ if (typeof ref !== 'string' || !ref || ref.includes('\\') || path.isAbsolute(ref) || ref.split('/').some(s => !s || s === '.' || s === '..' || s.startsWith('.'))) {
27
+ throw new Error('source must be a visible workspace-relative file');
28
+ }
29
+ let current = root;
30
+ for (const part of ref.split('/')) {
31
+ current = path.join(current, part);
32
+ if (fs.lstatSync(current).isSymbolicLink()) throw new Error('redirected source refused');
33
+ }
34
+ return current;
35
+ }
36
+
37
+ // Local CLI boundary, not an authenticated multi-user service. Responses remain
38
+ // private drafts; this store never modifies a canonical source file.
39
+ export function createCoauthorStore({ workspaceRoot = process.cwd() } = {}) {
40
+ const root = fs.realpathSync(workspaceRoot);
41
+ function privatePlacement() {
42
+ const gitEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
43
+ try {
44
+ const tracked = execFileSync('git', ['-C', root, 'ls-files', '-z', '--', '.atelier-local'], { env: gitEnv, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
45
+ if (tracked) throw new Error('tracked private state');
46
+ execFileSync('git', ['-C', root, 'check-ignore', '--quiet', '.atelier-local/'], { env: gitEnv, stdio: 'ignore' });
47
+ } catch { throw new Error('coauthor requires a Git workspace with untracked, ignored .atelier-local/ state'); }
48
+ }
49
+ privatePlacement();
50
+ const directory = () => ensureContainedPrivateDirectory({ workspaceRoot: root,
51
+ directory: path.join(root, '.atelier-local', 'coauthor'), label: 'coauthor state' });
52
+ const ledger = () => createCollaborationEventLedger({ workspaceRoot: root,
53
+ ledgerPath: path.join(directory(), 'events.ndjson') });
54
+ const aggregate = id => `coauthor-${contentDigest(id)}`;
55
+ function locked(operation) {
56
+ privatePlacement();
57
+ const lockPath = path.join(directory(), 'operation.lock');
58
+ return withPrivateLock(lockPath, operation);
59
+ }
60
+ function load(id) {
61
+ const result = ledger().eventsFor(aggregate(id));
62
+ if (!result.ok) throw new Error(result.error);
63
+ if (!result.events.length) throw new Error('coauthor session not found');
64
+ let state;
65
+ let previous = null;
66
+ let version = 0;
67
+ let config;
68
+ for (const record of result.events) {
69
+ if (record.version !== ++version || record.payload.previousEventId !== previous) throw new Error('coauthor event chain is incomplete');
70
+ if (version === 1) {
71
+ if (record.type !== 'coauthor.started') throw new Error('coauthor start missing');
72
+ config = record.payload.config;
73
+ validate(config, 'config');
74
+ if (config.id !== id) throw new Error('coauthor identity mismatch');
75
+ state = createSession(config);
76
+ } else {
77
+ if (record.type !== 'coauthor.transition') throw new Error('unknown coauthor record');
78
+ const next = transition(state, record.payload.event);
79
+ if (next.duplicate) throw new Error('duplicate persisted transition');
80
+ state = next.session;
81
+ }
82
+ previous = record.id;
83
+ }
84
+ return { state, config, version, previous };
85
+ }
86
+ function verifySources(config) {
87
+ for (const field of config.fields) {
88
+ if (contentDigest(readText(sourcePath(root, field.source.ref))) !== field.source.digest) throw new Error('source changed; preserve history and start a newly bound session');
89
+ }
90
+ }
91
+ function append(id, current, event) {
92
+ const next = transition(current.state, event);
93
+ if (next.duplicate) return current;
94
+ const result = ledger().append({ aggregateId: aggregate(id), expectedVersion: current.version,
95
+ type: 'coauthor.transition', actor: 'local-harness', payload: { previousEventId: current.previous, event } });
96
+ if (!result.ok) throw new Error(result.error);
97
+ return { ...current, state: next.session, version: result.event.version, previous: result.event.id };
98
+ }
99
+ function reconcile(id, current) {
100
+ if (!['saving', 'recovery'].includes(current.state.phase)) return current;
101
+ verifySources(current.config);
102
+ const state = current.state;
103
+ const field = state.fields[state.index];
104
+ const requestId = state.pending.requestId;
105
+ const values = ensureContainedPrivateDirectory({ workspaceRoot: root,
106
+ directory: path.join(directory(), 'values', contentDigest(id)), label: 'coauthor values' });
107
+ const target = path.join(values, `${contentDigest(requestId)}.json`);
108
+ const receipt = { sessionId: id, requestId, fieldId: field.id, sourceRef: field.source.ref,
109
+ sourceDigest: field.source.digest, valueDigest: contentDigest(state.proposal.text) };
110
+ const saved = { schema: 'atelier-coauthor-draft@v1', status: 'draft', text: state.proposal.text, receipt };
111
+ validate(saved, 'draft');
112
+ const bytes = canonicalize(saved);
113
+ try {
114
+ // Existing values are immutable. A retry only verifies identical bytes.
115
+ try {
116
+ fs.lstatSync(target);
117
+ if (readText(target) !== bytes) throw new Error('saved draft differs from pending value');
118
+ } catch (error) {
119
+ if (error.code !== 'ENOENT') throw error;
120
+ publishPrivateFile(target, bytes);
121
+ }
122
+ if (readText(target) !== bytes) throw new Error('saved draft readback mismatch');
123
+ verifySources(current.config);
124
+ } catch (error) {
125
+ if (state.phase === 'saving') append(id, current, {
126
+ id: `failure-${contentDigest(requestId)}-${state.revision}`, expectedRevision: state.revision, type: 'failed',
127
+ });
128
+ throw error;
129
+ }
130
+ return append(id, current, { id: `receipt-${contentDigest(requestId)}`,
131
+ expectedRevision: state.revision, type: 'receipt', receipt });
132
+ }
133
+ return {
134
+ start(config) {
135
+ return locked(() => {
136
+ validate(config, 'config');
137
+ const clean = createSession(config);
138
+ const canonicalConfig = { id: clean.id, fields: clean.fields };
139
+ verifySources(canonicalConfig);
140
+ const current = ledger().eventsFor(aggregate(clean.id));
141
+ if (!current.ok) throw new Error(current.error);
142
+ if (current.events.length) {
143
+ const existing = load(clean.id);
144
+ if (canonicalize(existing.config) !== canonicalize(canonicalConfig)) throw new Error('session id already bound to another configuration');
145
+ return existing.state;
146
+ }
147
+ const result = ledger().append({ aggregateId: aggregate(clean.id), expectedVersion: 0,
148
+ type: 'coauthor.started', actor: 'local-harness', payload: { previousEventId: null, config: canonicalConfig } });
149
+ if (!result.ok) throw new Error(result.error);
150
+ return load(clean.id).state;
151
+ });
152
+ },
153
+ read(id) { return load(id).state; },
154
+ dispatch(id, event) {
155
+ return locked(() => {
156
+ if (['receipt', 'failed'].includes(event?.type)) throw new Error('receipt and failure events are store-owned');
157
+ const current = load(id);
158
+ verifySources(current.config);
159
+ const next = append(id, current, event);
160
+ // Duplicate historical events never cause repeated effects.
161
+ if (next === current) return next.state;
162
+ return ['save', 'retry'].includes(event.type) ? reconcile(id, next).state : next.state;
163
+ });
164
+ },
165
+ recover(id) { return locked(() => {
166
+ const current = load(id);
167
+ // Paused and exhausted sessions require an explicit resume or operator
168
+ // reconciliation. Never turn a read into another automatic write attempt.
169
+ if (current.state.phase !== 'saving') return current.state;
170
+ return reconcile(id, current).state;
171
+ }); },
172
+ };
173
+ }
@@ -1,6 +1,7 @@
1
1
  import crypto from 'node:crypto'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
+ import { withPrivateLock } from '../project/durable-state.mjs'
4
5
  import { canonicalize } from '../attestation/jcs.mjs'
5
6
  import {
6
7
  atomicReplacePrivateText,
@@ -26,18 +27,13 @@ function validTimestamp(value) {
26
27
  }
27
28
 
28
29
  function secureAppendLine(file, line) {
29
- const descriptor = openRegularFileNoFollow(
30
- file,
31
- fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT,
32
- 0o600,
33
- )
30
+ let previous = '';
34
31
  try {
35
- fs.writeFileSync(descriptor, line)
36
- fs.fsyncSync(descriptor)
37
- fs.fchmodSync(descriptor, 0o600)
38
- } finally {
39
- fs.closeSync(descriptor)
40
- }
32
+ const fd = openRegularFileNoFollow(file);
33
+ try { previous = fs.readFileSync(fd, 'utf8'); } finally { fs.closeSync(fd); }
34
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
35
+ // The ledger is bounded. Replace its complete validated prefix atomically.
36
+ atomicReplacePrivateText(file, previous + line);
41
37
  }
42
38
 
43
39
  function eventId(event) {
@@ -215,30 +211,12 @@ export function createCollaborationEventLedger({
215
211
  }
216
212
 
217
213
  function withWriteLock(operation) {
218
- let descriptor
219
- try {
220
- descriptor = fs.openSync(lockPath, 'wx', 0o600)
221
- fs.writeFileSync(descriptor, `${process.pid}\n`)
222
- fs.fsyncSync(descriptor)
223
- } catch (error) {
224
- if (descriptor != null) fs.closeSync(descriptor)
225
- if (error?.code === 'EEXIST') return { ok: false, status: 423, error: 'collaboration ledger is locked; retry after the active writer finishes' }
226
- return { ok: false, status: 500, error: `collaboration ledger lock failed: ${error.message}` }
227
- }
228
- try {
229
- return operation()
230
- } finally {
231
- fs.closeSync(descriptor)
232
- try {
233
- fs.unlinkSync(lockPath)
234
- } catch {
235
- // The completed write remains authoritative; cleanup can be diagnosed
236
- // by the next writer's explicit locked result.
237
- }
238
- }
214
+ try { return withPrivateLock(lockPath, operation); }
215
+ catch (error) { return { ok: false, status: error.code === 'EEXIST' ? 423 : 500,
216
+ error: `collaboration ledger lock/write failed: ${error.message}` }; }
239
217
  }
240
218
 
241
- function append({ aggregateId, expectedVersion = 0, type, actor, at = clock(), payload = {} }) {
219
+ function append({ aggregateId, expectedVersion = 0, type, actor, at = clock(), payload = {}, precondition = null }) {
242
220
  return withWriteLock(() => {
243
221
  const current = eventsFor(aggregateId)
244
222
  if (!current.ok) return current
@@ -249,6 +227,11 @@ export function createCollaborationEventLedger({
249
227
  error: `stale collaboration event refused: expected ${expectedVersion}, current ${current.currentVersion}`,
250
228
  }
251
229
  }
230
+ if (precondition !== null) {
231
+ if (typeof precondition !== 'function') return { ok: false, status: 400, error: 'append precondition must be a function' }
232
+ const permitted = precondition()
233
+ if (!permitted?.ok) return { ok: false, status: permitted?.status ?? 409, error: permitted?.error ?? 'append precondition refused' }
234
+ }
252
235
  if (current.stats.eventCount >= limits.maxEvents) {
253
236
  return { ok: false, status: 413, error: `collaboration ledger reached ${limits.maxEvents} event hard ceiling; compact it explicitly before appending` }
254
237
  }