@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.11

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 (414) hide show
  1. package/CHANGELOG.md +413 -0
  2. package/README.md +402 -205
  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/contracts/public-api-baseline.json +57 -0
  31. package/docs/assurance-controls.md +49 -0
  32. package/docs/atelier-runtime.md +28 -2
  33. package/docs/atelier-sync.md +172 -0
  34. package/docs/blocks/audience-visibility.md +9 -0
  35. package/docs/blocks/claims.md +61 -0
  36. package/docs/blocks/conformance-admission.md +15 -0
  37. package/docs/blocks/will-not-do.md +18 -0
  38. package/docs/coauthor-session.md +57 -0
  39. package/docs/connected-composition.md +75 -0
  40. package/docs/continuity.md +3 -4
  41. package/docs/decisions.md +167 -0
  42. package/docs/design.md +167 -0
  43. package/docs/exact-upgrades.md +159 -0
  44. package/docs/extension-pack-lifecycle.md +33 -0
  45. package/docs/guided-upgrades.md +99 -0
  46. package/docs/install.md +99 -16
  47. package/docs/intake-and-guides.md +65 -0
  48. package/docs/integration-and-review-plan.md +409 -0
  49. package/docs/integration-contract-decisions.md +22 -0
  50. package/docs/knowledge-graph.md +8 -4
  51. package/docs/local-review.md +84 -0
  52. package/docs/local-services.md +328 -0
  53. package/docs/local-state.md +107 -0
  54. package/docs/obsidian-contract.md +698 -0
  55. package/docs/obsidian.md +484 -0
  56. package/docs/portable-presentation.md +328 -0
  57. package/docs/presentation-consumer-boundaries.md +75 -0
  58. package/docs/presentation-review-disposition.md +47 -0
  59. package/docs/project-command-inventory.md +38 -0
  60. package/docs/project-options.md +53 -0
  61. package/docs/release-engineering.md +174 -12
  62. package/docs/repo-boundary-guard.md +12 -2
  63. package/docs/review-portability.md +55 -0
  64. package/docs/skill-steward.md +156 -0
  65. package/docs/upgrade.md +155 -5
  66. package/docs/vault-service.md +292 -0
  67. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  68. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  69. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  70. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  71. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  72. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  73. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  74. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  75. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  76. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  77. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  78. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  79. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  80. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  81. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  82. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  83. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  84. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  85. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  86. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  87. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  88. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  89. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  90. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  91. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  92. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  93. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  94. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  95. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  96. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  97. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  98. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  99. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  100. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  101. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  102. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  103. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  104. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  105. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  106. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  107. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  108. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  109. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  110. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  111. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  112. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  113. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  114. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  115. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  116. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  117. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  118. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  119. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  120. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  121. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  122. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  123. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  127. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  128. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  129. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  130. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  131. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  132. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  133. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  134. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  135. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  136. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  137. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  138. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  141. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  142. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  143. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  144. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  145. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  146. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  147. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  148. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  149. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  150. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  151. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  152. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  153. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  154. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  159. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  160. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  161. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  162. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  163. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  164. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  165. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  166. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  167. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  168. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  169. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  170. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  171. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  172. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  173. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  174. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  175. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  176. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  177. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  178. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  179. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  181. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  182. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  183. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  186. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  187. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  188. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  189. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  190. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  191. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  192. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  193. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  194. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  195. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  196. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  197. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  198. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  199. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  200. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  201. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  202. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  203. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  204. package/fixtures/obsidian/edits/workspace.json +65 -0
  205. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  206. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  207. package/fixtures/obsidian/materialization/workspace.json +54 -0
  208. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  209. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  210. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  211. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  212. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  213. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  214. package/fixtures/ui/presentation/pane.v1.json +7 -0
  215. package/fixtures/ui/presentation/reference.v1.json +293 -0
  216. package/package.json +86 -10
  217. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  218. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  219. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  220. package/skills/claude/atelier-open/SKILL.md +11 -5
  221. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  222. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  223. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  224. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  225. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  226. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  227. package/skills/codex/atelier-open/SKILL.md +11 -5
  228. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  229. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  230. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  231. package/src/access/preview-enforcer.mjs +84 -0
  232. package/src/boundary/content-rules.mjs +283 -20
  233. package/src/boundary/policy.mjs +213 -91
  234. package/src/cli/execute-command.mjs +36 -0
  235. package/src/cli/project-options.mjs +58 -0
  236. package/src/cli/run.mjs +78 -10
  237. package/src/coauthor/session.mjs +161 -0
  238. package/src/coauthor/store.mjs +173 -0
  239. package/src/collaboration/event-ledger.mjs +348 -0
  240. package/src/collaboration/index.mjs +17 -0
  241. package/src/collaboration/inspection-bundle.mjs +223 -0
  242. package/src/collaboration/proposals.mjs +265 -65
  243. package/src/collaboration/review-contracts.mjs +25 -0
  244. package/src/collaboration/review-store.mjs +305 -0
  245. package/src/commands/attestation.mjs +20 -6
  246. package/src/commands/coauthor.mjs +26 -0
  247. package/src/commands/config.mjs +6 -1
  248. package/src/commands/disclosure.mjs +133 -0
  249. package/src/commands/distribution.mjs +2 -1
  250. package/src/commands/extension-pack.mjs +17 -9
  251. package/src/commands/init.mjs +46 -6
  252. package/src/commands/obsidian.mjs +371 -0
  253. package/src/commands/review.mjs +147 -0
  254. package/src/commands/server.mjs +1 -4
  255. package/src/commands/setup.mjs +51 -14
  256. package/src/commands/skills.mjs +199 -0
  257. package/src/commands/sync.mjs +107 -0
  258. package/src/composition/registry.mjs +27 -0
  259. package/src/composition/wire.mjs +122 -0
  260. package/src/contracts/corpus.mjs +35 -0
  261. package/src/decisions/contracts.d.mts +96 -0
  262. package/src/decisions/contracts.mjs +321 -0
  263. package/src/disclosure/content-scan.mjs +206 -0
  264. package/src/egress/check.mjs +7 -38
  265. package/src/egress/forbidden-egress.mjs +32 -18
  266. package/src/export/atelier-export-contract.mjs +16 -1
  267. package/src/extension-packs/lifecycle.mjs +154 -0
  268. package/src/graph/graph.mjs +176 -316
  269. package/src/graph/knowledge-graph.mjs +643 -43
  270. package/src/guides/contracts.mjs +54 -0
  271. package/src/harness/context-client.mjs +9 -1
  272. package/src/index.mjs +41 -0
  273. package/src/intake/store.mjs +119 -0
  274. package/src/preview/channel.mjs +89 -0
  275. package/src/project/config.mjs +112 -52
  276. package/src/project/durable-state.mjs +161 -0
  277. package/src/project/file-class.mjs +180 -0
  278. package/src/project/package-root.mjs +10 -0
  279. package/src/project/path-match.mjs +38 -15
  280. package/src/project/private-state.mjs +120 -0
  281. package/src/projection/obsidian/contracts.mjs +571 -0
  282. package/src/projection/obsidian/edits/align.mjs +169 -0
  283. package/src/projection/obsidian/edits/apply.mjs +889 -0
  284. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  285. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  286. package/src/projection/obsidian/edits/index.mjs +19 -0
  287. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  288. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  289. package/src/projection/obsidian/edits/observe.mjs +123 -0
  290. package/src/projection/obsidian/edits/policy.mjs +123 -0
  291. package/src/projection/obsidian/edits/regions.mjs +648 -0
  292. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  293. package/src/projection/obsidian/materialize/index.mjs +7 -0
  294. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  295. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  296. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  297. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  298. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  299. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  300. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  301. package/src/projection/obsidian/proposals/index.mjs +23 -0
  302. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  303. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  304. package/src/projection/obsidian/proposals/router.mjs +168 -0
  305. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  306. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  307. package/src/projection/obsidian/publication/index.mjs +5 -0
  308. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  309. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  310. package/src/projection/obsidian/publication/transport.mjs +212 -0
  311. package/src/projection/obsidian/recovery/index.mjs +6 -0
  312. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  313. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  314. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  315. package/src/projection/obsidian/recovery/store.mjs +234 -0
  316. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  317. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  318. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  319. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  320. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  321. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  322. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  323. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  324. package/src/projection/project.mjs +28 -19
  325. package/src/readiness-protocols/evidence.mjs +282 -0
  326. package/src/readiness-protocols/runtime.mjs +4 -3
  327. package/src/readiness-protocols/source-read.mjs +39 -0
  328. package/src/runtime/git-adapter.mjs +189 -0
  329. package/src/runtime/local-state.mjs +439 -0
  330. package/src/runtime/obsidian/app-capability.mjs +138 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +107 -0
  332. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  333. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  334. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  335. package/src/runtime/obsidian/contributions.mjs +27 -0
  336. package/src/runtime/obsidian/documents.mjs +37 -0
  337. package/src/runtime/obsidian/enablement.mjs +53 -0
  338. package/src/runtime/obsidian/engine.mjs +532 -0
  339. package/src/runtime/obsidian/errors.mjs +14 -0
  340. package/src/runtime/obsidian/extension-points.mjs +103 -0
  341. package/src/runtime/obsidian/index.mjs +34 -0
  342. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  343. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  344. package/src/runtime/obsidian/observation.mjs +158 -0
  345. package/src/runtime/obsidian/opening.mjs +280 -0
  346. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  347. package/src/runtime/obsidian/pipeline.mjs +122 -0
  348. package/src/runtime/obsidian/private-lock.mjs +156 -0
  349. package/src/runtime/obsidian/process-identity.mjs +58 -0
  350. package/src/runtime/obsidian/service-client.mjs +66 -0
  351. package/src/runtime/obsidian/service-entry-path.mjs +9 -0
  352. package/src/runtime/obsidian/service-main.mjs +93 -0
  353. package/src/runtime/obsidian/service-record.mjs +169 -0
  354. package/src/runtime/obsidian/service-server.mjs +121 -0
  355. package/src/runtime/obsidian/service.mjs +216 -0
  356. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  357. package/src/runtime/obsidian/state-store.mjs +180 -0
  358. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  359. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  360. package/src/runtime/obsidian/watchers.mjs +36 -0
  361. package/src/runtime/repository-observation.mjs +491 -0
  362. package/src/runtime/supervisor.mjs +803 -0
  363. package/src/server/local-sidecar.mjs +125 -59
  364. package/src/server/security.mjs +89 -4
  365. package/src/server/server.mjs +4 -2
  366. package/src/skills/steward.mjs +780 -0
  367. package/src/support/feedback-report.mjs +4 -3
  368. package/src/ui/presentation/browser.mjs +278 -0
  369. package/src/ui/presentation/contract.mjs +116 -0
  370. package/src/ui/presentation/index.mjs +7 -0
  371. package/src/ui/presentation/native.mjs +153 -0
  372. package/src/ui/presentation/proof.mjs +57 -0
  373. package/src/ui/presentation/schema-check.mjs +30 -0
  374. package/src/ui/presentation/schema.generated.mjs +1371 -0
  375. package/src/ui/presentation/state.mjs +43 -0
  376. package/src/ui/presentation/styles.mjs +67 -0
  377. package/src/ui/presentation/tokens.mjs +74 -0
  378. package/src/ui/presentation/web.mjs +85 -0
  379. package/src/ui/review-page.mjs +32 -0
  380. package/src/upgrade/explanation.mjs +33 -0
  381. package/src/upgrade/provenance.mjs +338 -0
  382. package/src/upgrade/transaction-files.mjs +103 -0
  383. package/src/upgrade/transaction.mjs +475 -0
  384. package/src/upgrade/upgrade.mjs +65 -29
  385. package/src/vault/hosts.mjs +21 -0
  386. package/src/vault/identity.mjs +19 -0
  387. package/src/vault/index.mjs +7 -0
  388. package/src/vault/interface.mjs +11 -0
  389. package/src/vault/metadata.mjs +31 -0
  390. package/src/vault/privacy.mjs +148 -0
  391. package/src/vault/probe.mjs +89 -0
  392. package/src/vault/service.mjs +131 -0
  393. package/src/vault/source.mjs +45 -0
  394. package/src/vault/storage.mjs +23 -0
  395. package/templates/distribution-workspace/.claude/launch.json +6 -2
  396. package/templates/distribution-workspace/gitignore +1 -0
  397. package/templates/distribution-workspace/project/README.md +2 -2
  398. package/templates/external-project-workspace/README.md +45 -0
  399. package/templates/external-project-workspace/answers.example.json +4 -0
  400. package/templates/external-project-workspace/atelier.project.json +40 -0
  401. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  402. package/templates/external-project-workspace/gitignore +8 -0
  403. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  404. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  405. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  406. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  407. package/templates/external-project-workspace/source/README.md +11 -0
  408. package/templates/launch.json +6 -2
  409. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  410. package/templates/private-domain-workspace/domain/README.md +2 -2
  411. package/templates/private-domain-workspace/gitignore +1 -0
  412. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  413. package/templates/shared-project-workspace/gitignore +1 -0
  414. package/templates/shared-project-workspace/project/README.md +2 -2
package/src/cli/run.mjs CHANGED
@@ -51,11 +51,18 @@ export const commandMap = new Map([
51
51
  ['extension-pack:list', ['src/commands/extension-pack.mjs', 'list']],
52
52
  ['distribution', ['src/commands/distribution.mjs']],
53
53
  ['distribution:check', ['src/commands/distribution.mjs', 'check']],
54
+ ['disclosure', ['src/commands/disclosure.mjs']],
55
+ ['disclosure:check', ['src/commands/disclosure.mjs', 'check']],
54
56
  ['attestation', ['src/commands/attestation.mjs']],
55
57
  ['feedback', ['src/commands/feedback.mjs']],
56
58
  ['feedback:check', ['src/commands/feedback.mjs', 'check']],
57
59
  ['announcements', ['src/commands/announcements.mjs']],
58
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']],
59
66
  ])
60
67
 
61
68
  function isDefaultBrand(brand) {
@@ -113,18 +120,35 @@ Core commands:
113
120
  support bundle --dry-run Preview a no-send support bundle.
114
121
  feedback --message TEXT Write a local, never-sent feedback report.
115
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.
116
130
  egress check Check extracted Atelier paths for forbidden egress.
117
131
  boundary check Enforce private/shared repo placement rules.
118
132
  boundary audit Report content-rule matches tree-wide without blocking.
119
133
  boundary install-hooks Install staged/private-domain Git guards.
120
134
  promote Record a git.promote disclosure event.
121
- 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.
122
140
  upgrade --apply Apply a branch-based reviewable upgrade.
123
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.
124
147
  config check Validate project config.
125
148
  extension-pack validate Validate declared extension packs.
126
149
  extension-pack list List declared extension packs.
127
150
  distribution check Check a distribution for MNSTRY attribution.
151
+ disclosure check Scan tracked or staged content for disclosure risks.
128
152
 
129
153
  Attestation commands:
130
154
  attestation hash FILE Print the canonical payload hash of a payload.
@@ -132,15 +156,27 @@ Attestation commands:
132
156
  attestation verify FILE Verify an attestation against a public key file.
133
157
  attestation keygen --key-id ID Generate a signing key pair.
134
158
 
135
- Every project-aware command accepts --project-config=PATH or
136
- MNSTRY_ATELIER_PROJECT_CONFIG=PATH. Machine-local repo paths belong in
159
+ Commands whose usage names --project accept --project=PATH or --project PATH.
160
+ The project resolver also accepts --project-config=PATH and
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.
165
+ Machine-local repo paths belong in
137
166
  .atelier-local/, atelier.local.json, or atelier.workspace.local.json.`
138
167
  }
139
168
 
140
169
  export function buildCommandHelpText(command, brand = DEFAULT_BRAND) {
141
170
  const c = brand.command
142
171
  const help = {
143
- 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]
144
180
 
145
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.`,
146
182
  adopt: `Usage: ${c} adopt [--profile single-repo|private-domain|shared-project|multi-repo|monorepo|control-workspace] [--target DIR] [--yes]
@@ -152,9 +188,9 @@ Ensures ignored local Atelier state exists, verifies ignore coverage, and record
152
188
  doctor: `Usage: ${c} doctor [--project ./atelier.project.json] [--fix] [--dry-run]
153
189
 
154
190
  Reports project config, local overlay, and repo boundary readiness. --fix only repairs ignored local state.`,
155
- boundary: `Usage: ${c} boundary check|push-check|audit|install-hooks [--project ./atelier.project.json] [--staged]
191
+ boundary: `Usage: ${c} boundary check|push-check|audit|install-hooks [--project ./atelier.project.json] [--staged] [--source=working-tree|head]
156
192
 
157
- check --staged judges the staged diff. push-check reads pre-push ref updates on stdin and judges only the pushed range. audit scans the whole tree and reports without blocking, so an accepted usage never strands unrelated work.`,
193
+ check --staged judges the staged diff. push-check reads pre-push ref updates on stdin and judges only the pushed range. audit scans the working tree by default and reports without blocking; --source=head selects the committed snapshot.`,
158
194
  graph: `Usage: ${c} graph [--check] [--project ./atelier.project.json]
159
195
 
160
196
  Builds or checks the project knowledge graph from tracked sources plus ignored local path bindings.`,
@@ -171,25 +207,55 @@ Runs the bundled MNSTRY readiness pack claim-first. Protocol state and packet dr
171
207
  ${c} extension-pack validate [--json] [--project ./atelier.project.json]
172
208
  ${c} extension-pack list [--json] [--project ./atelier.project.json]
173
209
 
174
- 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.`,
175
211
  distribution: `Usage: ${c} distribution check [--target DIR] [--pack DIR]
176
212
 
177
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.`,
214
+ disclosure: `Usage: ${c} disclosure check [--root DIR] [--staged] [--denylist FILE | --structural-only] [--fail-on-binary] [--untrusted]
215
+
216
+ Scans Git-tracked files, or staged index blobs with --staged, without following symlinks. A private denylist is required by default and must be supplied through ATELIER_DENYLIST_JSON, --denylist, or ignored .atelier-local/disclosure-denylist.json. --structural-only is the explicit no-denylist lane. --untrusted suppresses finding details.`,
178
217
  attestation: `Usage:
179
218
  ${c} attestation hash <payload.json>
180
219
  ${c} attestation sign <attestation.json> [--key FILE] [--out FILE]
181
220
  ${c} attestation verify <attestation.json> --public-key FILE [--payload FILE] [--json]
182
221
  ${c} attestation keygen --key-id ID [--algorithm ed25519|es256] [--out FILE]
183
222
 
184
- Records and checks admission decisions. hash prints the canonical payload hash (RFC 8785 JCS, SHA-256). sign reads the local signing key file. verify reads a public key file and exits 1 when it judges the attestation invalid. keygen writes the signing key file mode 0600, refuses to overwrite, and prints only the public key document.`,
223
+ Records and checks admission decisions. hash prints the canonical payload hash (RFC 8785 JCS, SHA-256). sign reads the local signing key file. verify reads a public key file and exits 1 when it judges the attestation invalid. keygen writes the signing key file mode 0600 on POSIX, refuses to overwrite, and prints only the public key document.`,
185
224
  feedback: `Usage:
186
225
  ${c} feedback create --message TEXT | --message-file PATH [--context FILE] [--include-gates]
187
226
  ${c} feedback check FILE
188
227
 
189
- Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0600), scanned with the support-bundle banned key and value patterns before writing; any match refuses the write naming pattern label and location only. Files given to --message-file and --context must be valid UTF-8 text of at most 262144 bytes. The scan is a backstop, not clearance: read the whole report before sharing it. The kit has no send path — sharing the file is always the user's own explicit act.`,
228
+ Assembles a local feedback report under ignored .atelier-local/feedback/ (mode 0600 on POSIX), scanned with the support-bundle banned key and value patterns before writing; any match refuses the write naming pattern label and location only. Files given to --message-file and --context must be valid UTF-8 text of at most 262144 bytes. The scan is a backstop, not clearance: read the whole report before sharing it. The kit has no send path — sharing the file is always the user's own explicit act.`,
190
229
  announcements: `Usage: ${c} announcements list [--dir DIR] [--public-key FILE] | verify <file> [--public-key FILE] [--json] | show <file> [--public-key FILE]
191
230
 
192
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.`,
193
259
  }
194
260
  return help[command] || `Usage: ${c} ${command} [args]\n\nRun ${c} --help for the command list.`
195
261
  }
@@ -219,6 +285,7 @@ function normalizeArgs(argv) {
219
285
  if (args[0] === 'extension-pack' && args[1] === 'validate') args.splice(0, 2, 'extension-pack:validate')
220
286
  if (args[0] === 'extension-pack' && args[1] === 'list') args.splice(0, 2, 'extension-pack:list')
221
287
  if (args[0] === 'distribution' && args[1] === 'check') args.splice(0, 2, 'distribution:check')
288
+ if (args[0] === 'disclosure' && args[1] === 'check') args.splice(0, 2, 'disclosure:check')
222
289
  if (args[0] === 'support' && args[1] === 'bundle') args.splice(0, 2, 'support:bundle')
223
290
  if (args[0] === 'support:bundle' && args[1] === '--dry-run') args.splice(1, 1)
224
291
  return { help: false, args }
@@ -261,12 +328,13 @@ export async function runCli({
261
328
 
262
329
  const [script, ...prefixArgs] = target
263
330
  const scriptPath = path.join(packageRoot, script)
331
+ const executorPath = path.join(packageRoot, 'src', 'cli', 'execute-command.mjs')
264
332
  if (!fs.existsSync(scriptPath)) {
265
333
  stderr(`${brand.displayName} command is not available in this package install: ${command}`)
266
334
  return 1
267
335
  }
268
336
 
269
- const result = spawnSync(process.execPath, [scriptPath, ...prefixArgs, ...rest], {
337
+ const result = spawnSync(process.execPath, [executorPath, scriptPath, ...prefixArgs, ...rest], {
270
338
  cwd,
271
339
  stdio: 'inherit',
272
340
  env: {
@@ -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
+ }