@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
@@ -1,6 +1,8 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { spawnSync } from 'node:child_process'
4
+ import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
5
+ import { parseProjectOptions } from '../cli/project-options.mjs'
4
6
 
5
7
  export const PROJECT_CONFIG_ARG_PREFIX = '--project-config='
6
8
  export const PROJECT_CONFIG_ENV = 'MNSTRY_ATELIER_PROJECT_CONFIG'
@@ -17,6 +19,16 @@ export const LOCAL_OVERLAY_FILES = ['atelier.local.json', 'atelier.workspace.loc
17
19
  // a way to silence checks on a repo you do manage.
18
20
  export const EXTERNAL_REPO_KIND = 'external'
19
21
 
22
+ export class AtelierDiagnosticError extends Error {
23
+ constructor(code, message, { hint = null, exitCode = 2, cause = null } = {}) {
24
+ super(message, cause ? { cause } : undefined)
25
+ this.name = 'AtelierDiagnosticError'
26
+ this.code = code
27
+ this.hint = hint
28
+ this.exitCode = exitCode
29
+ }
30
+ }
31
+
20
32
  export const isExternalRepo = (repo) => firstString(repo?.kind) === EXTERNAL_REPO_KIND
21
33
 
22
34
  // Kept here rather than imported from repo-identity.mjs so config validation stays
@@ -59,10 +71,19 @@ export function resolvePathValue(value, baseDir) {
59
71
  }
60
72
 
61
73
  export function readJson(file) {
74
+ if (!fs.existsSync(file)) {
75
+ const graphArtifact = path.basename(file) === 'knowledge.graph.json'
76
+ throw new AtelierDiagnosticError('artifact-missing', `required JSON artifact not found: ${file}`, {
77
+ hint: graphArtifact ? 'Run atelier graph with this project config, then retry.' : 'Create or regenerate the named artifact, then retry.',
78
+ })
79
+ }
62
80
  try {
63
81
  return JSON.parse(fs.readFileSync(file, 'utf8'))
64
82
  } catch (error) {
65
- throw new Error(`invalid JSON at ${file}: ${error.message}`)
83
+ throw new AtelierDiagnosticError('json-invalid', `JSON is malformed at ${file}: ${error.message}`, {
84
+ hint: 'Repair or regenerate the named JSON file, then retry.',
85
+ cause: error,
86
+ })
66
87
  }
67
88
  }
68
89
 
@@ -75,14 +96,7 @@ export function writeJson(file, value) {
75
96
  }
76
97
 
77
98
  export function projectConfigArg(argv = process.argv.slice(2), prefix = PROJECT_CONFIG_ARG_PREFIX) {
78
- for (let index = 0; index < argv.length; index += 1) {
79
- const arg = argv[index]
80
- if (typeof arg !== 'string') continue
81
- if (arg.startsWith(prefix)) return arg.slice(prefix.length).trim() || null
82
- if ((arg === '--project-config' || arg === '--project') && argv[index + 1]) return String(argv[index + 1]).trim() || null
83
- if (arg.startsWith('--project=')) return arg.slice('--project='.length).trim() || null
84
- }
85
- return null
99
+ return parseProjectOptions(argv, prefix).project
86
100
  }
87
101
 
88
102
  export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PREFIX) {
@@ -90,9 +104,30 @@ export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PR
90
104
  }
91
105
 
92
106
  export function readProjectConfig(configPath) {
93
- const doc = readJson(configPath)
107
+ let doc
108
+ try {
109
+ doc = readJson(configPath)
110
+ } catch (error) {
111
+ if (error instanceof AtelierDiagnosticError) {
112
+ throw new AtelierDiagnosticError(
113
+ error.code === 'artifact-missing' ? 'project-config-missing' : 'project-config-json-invalid',
114
+ error.code === 'artifact-missing'
115
+ ? `atelier project config not found: ${configPath}`
116
+ : `atelier project config is malformed: ${configPath}`,
117
+ {
118
+ hint: error.code === 'artifact-missing'
119
+ ? 'Pass --project PATH to a tracked atelier.project.json file.'
120
+ : 'Repair the JSON syntax, then run atelier config check with the same --project path.',
121
+ cause: error,
122
+ },
123
+ )
124
+ }
125
+ throw error
126
+ }
94
127
  if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
95
- throw new Error(`atelier project config must be a JSON object: ${configPath}`)
128
+ throw new AtelierDiagnosticError('project-config-shape-invalid', `Atelier project config must be a JSON object: ${configPath}`, {
129
+ hint: 'Repair the document shape, then run atelier config check with the same --project path.',
130
+ })
96
131
  }
97
132
  return doc
98
133
  }
@@ -130,22 +165,7 @@ export function readLocalOverlay({ configDir, env = process.env, cwd = process.c
130
165
  }
131
166
 
132
167
  export function parseRepoPathOverrides(argv = process.argv.slice(2), cwd = process.cwd()) {
133
- const out = new Map()
134
- for (let index = 0; index < argv.length; index += 1) {
135
- const arg = argv[index]
136
- let value = null
137
- if (arg === '--repo-path' && argv[index + 1]) {
138
- value = argv[index + 1]
139
- index += 1
140
- } else if (typeof arg === 'string' && arg.startsWith('--repo-path=')) {
141
- value = arg.slice('--repo-path='.length)
142
- }
143
- if (!value || !value.includes('=')) continue
144
- const [name, ...rest] = value.split('=')
145
- const repoPath = rest.join('=')
146
- if (firstString(name) && firstString(repoPath)) out.set(name.trim(), resolvePathValue(repoPath, cwd))
147
- }
148
- return out
168
+ return new Map([...parseProjectOptions(argv).repoPaths].map(([name, value]) => [name, resolvePathValue(value, cwd)]))
149
169
  }
150
170
 
151
171
  function overlayRepoPath(overlay, repoName) {
@@ -155,25 +175,25 @@ function overlayRepoPath(overlay, repoName) {
155
175
  return firstString(direct, repo.path, repo.localPath)
156
176
  }
157
177
 
158
- function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths }) {
178
+ function resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay, cliRepoPaths, gitExecutable, env }) {
159
179
  if (repoName && cliRepoPaths.has(repoName)) return cliRepoPaths.get(repoName)
160
180
  const overlayPath = overlayRepoPath(overlay, repoName)
161
181
  const fromOverlay = resolvePathValue(overlayPath, configDir)
162
182
  if (fromOverlay) return fromOverlay
163
183
  const fromConfig = resolvePathValue(repo.path, configDir) || resolvePathValue(repo.path, workspaceRoot)
164
184
  if (fromConfig) return fromConfig
165
- return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })
185
+ return discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })
166
186
  }
167
187
 
168
- function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir }) {
188
+ function repoPathSource({ repo, repoName, overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) {
169
189
  if (repoName && cliRepoPaths.has(repoName)) return 'cli'
170
190
  if (overlayRepoPath(overlay, repoName)) return 'local-overlay'
171
191
  if (firstString(repo.path)) return 'tracked-config'
172
- if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot })) return 'sibling-discovery'
192
+ if (discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable, env })) return 'sibling-discovery'
173
193
  return null
174
194
  }
175
195
 
176
- function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
196
+ function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot, gitExecutable = 'git', env = process.env }) {
177
197
  if (!repoName) return null
178
198
  const candidates = [
179
199
  path.join(workspaceRoot, repoName),
@@ -182,14 +202,14 @@ function discoverSiblingRepoPath({ repo, repoName, configDir, workspaceRoot }) {
182
202
  ]
183
203
  for (const candidate of candidates) {
184
204
  if (!fs.existsSync(candidate)) continue
185
- if (repo.remote && !gitRemoteMatches(candidate, repo.remote)) continue
205
+ if (repo.remote && !gitRemoteMatches(candidate, repo.remote, { gitExecutable, env })) continue
186
206
  return path.resolve(candidate)
187
207
  }
188
208
  return null
189
209
  }
190
210
 
191
- function gitRemoteMatches(repoPath, expected) {
192
- const result = spawnSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
211
+ function gitRemoteMatches(repoPath, expected, { gitExecutable = 'git', env = process.env } = {}) {
212
+ const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
193
213
  if (result.status !== 0) return false
194
214
  const actual = normalizeRemote(result.stdout.trim())
195
215
  return actual === normalizeRemote(expected)
@@ -203,9 +223,9 @@ function normalizeRemote(value) {
203
223
  .toLowerCase()
204
224
  }
205
225
 
206
- export function gitRemoteUrl(repoPath) {
226
+ export function gitRemoteUrl(repoPath, { gitExecutable = 'git', env = process.env } = {}) {
207
227
  if (!repoPath || !fs.existsSync(repoPath)) return null
208
- const result = spawnSync('git', ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
228
+ const result = spawnSync(gitExecutable, ['-C', repoPath, 'remote', 'get-url', 'origin'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
209
229
  return result.status === 0 ? firstString(result.stdout) : null
210
230
  }
211
231
 
@@ -228,7 +248,9 @@ export function resolveProjectConfig({
228
248
  env = process.env,
229
249
  cwd = process.cwd(),
230
250
  configArgPrefix = PROJECT_CONFIG_ARG_PREFIX,
251
+ gitExecutable = 'git',
231
252
  configEnv = PROJECT_CONFIG_ENV,
253
+ writeLocalState = true,
232
254
  defaults = {},
233
255
  } = {}) {
234
256
  const argPath = projectConfigArg(argv, configArgPrefix)
@@ -253,7 +275,9 @@ export function resolveProjectConfig({
253
275
  missingConfigPath = configPath
254
276
  configPath = null
255
277
  } else {
256
- throw new Error(`atelier project config not found: ${configPath || requestedPath}`)
278
+ throw new AtelierDiagnosticError('project-config-missing', `atelier project config not found: ${configPath || requestedPath}`, {
279
+ hint: 'Pass --project PATH to a tracked atelier.project.json file.',
280
+ })
257
281
  }
258
282
  }
259
283
 
@@ -331,7 +355,7 @@ export function resolveProjectConfig({
331
355
  localOverlay,
332
356
  repos: (Array.isArray(config.repos) ? config.repos : []).map((repo) => {
333
357
  const repoName = firstString(repo.name) || (repo.path ? path.basename(repo.path) : null)
334
- const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths })
358
+ const repoPath = resolveRepoPath({ repo, repoName, configDir, workspaceRoot, overlay: localOverlay.overlay, cliRepoPaths, gitExecutable, env })
335
359
  const external = isExternalRepo(repo)
336
360
  return {
337
361
  ...repo,
@@ -339,23 +363,25 @@ export function resolveProjectConfig({
339
363
  path: repoPath,
340
364
  external,
341
365
  readBoundary: external ? null : firstString(repo.readBoundary) || 'team',
342
- pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir }) : null,
366
+ pathSource: repoPath ? repoPathSource({ repo, repoName, overlay: localOverlay.overlay, cliRepoPaths, workspaceRoot, configDir, gitExecutable, env }) : null,
343
367
  }
344
368
  }),
345
369
  }
346
- resolved.localState = ensureLocalState(resolved, { write: true })
370
+ resolved.localState = ensureLocalState(resolved, { write: writeLocalState, gitExecutable, env })
347
371
  return resolved
348
372
  }
349
373
 
350
- export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd() } = {}) {
351
- const project = resolveProjectConfig({ argv, env, cwd })
374
+ export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd(), gitExecutable = 'git', writeLocalState = true } = {}) {
375
+ const project = resolveProjectConfig({ argv, env, cwd, gitExecutable, writeLocalState })
352
376
  // Fail closed at CLI entry, but only when a real config file was loaded; the
353
377
  // defaults/no-file path resolves with an empty config that would spuriously
354
378
  // fail document validation.
355
379
  if (project.configPath != null) {
356
380
  const configErrors = validateProjectConfigDoc(project.config)
357
381
  if (configErrors.length) {
358
- throw new Error(`invalid atelier project config at ${project.configPath}:\n${configErrors.join('\n')}`)
382
+ throw new AtelierDiagnosticError('project-config-contract-invalid', `invalid atelier project config at ${project.configPath}:\n${configErrors.join('\n')}`, {
383
+ hint: 'Fix the listed fields, then run atelier config check with the same --project path.',
384
+ })
359
385
  }
360
386
  }
361
387
  if (project.schema !== PROJECT_CONFIG_SCHEMA) {
@@ -375,21 +401,36 @@ export function localStateRoot(projectOrDir) {
375
401
  return path.join(configDir || process.cwd(), LOCAL_STATE_DIR)
376
402
  }
377
403
 
378
- function gitRootFor(dir) {
379
- const result = spawnSync('git', ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8' })
404
+ function gitRootFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
405
+ const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
380
406
  return result.status === 0 ? result.stdout.trim() : null
381
407
  }
382
408
 
383
- function isIgnoredByGit(gitRoot, rel) {
384
- const result = spawnSync('git', ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8' })
409
+ function gitPrefixFor(dir, { gitExecutable = 'git', env = process.env } = {}) {
410
+ const result = spawnSync(gitExecutable, ['-C', dir, 'rev-parse', '--show-prefix'], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
411
+ return result.status === 0 ? result.stdout.trim() : null
412
+ }
413
+
414
+ function isIgnoredByGit(gitRoot, rel, { gitExecutable = 'git', env = process.env } = {}) {
415
+ const result = spawnSync(gitExecutable, ['-C', gitRoot, 'check-ignore', '-q', rel], { encoding: 'utf8', env: sanitizedGitEnvironment(env) })
385
416
  return result.status === 0
386
417
  }
387
418
 
388
- export function ensureLocalState(project, { write = false } = {}) {
419
+ export function ensureLocalState(project, { write = false, gitExecutable = 'git', env = process.env } = {}) {
389
420
  const root = localStateRoot(project)
390
- const gitRoot = gitRootFor(project.configDir)
391
- const rel = gitRoot ? path.relative(gitRoot, root).split(path.sep).join('/') : LOCAL_STATE_DIR
392
- const ignored = gitRoot ? isIgnoredByGit(gitRoot, `${rel}/`) || isIgnoredByGit(gitRoot, rel) : true
421
+ const gitRoot = gitRootFor(project.configDir, { gitExecutable, env })
422
+ // Git owns the repository-relative spelling. Deriving it by comparing
423
+ // filesystem paths breaks when Windows exposes the cwd through an 8.3 short
424
+ // name (RUNNER~1) but Git reports the same root through its long name.
425
+ const gitPrefix = gitRoot ? gitPrefixFor(project.configDir, { gitExecutable, env }) : null
426
+ const rel = gitRoot && gitPrefix !== null ? `${gitPrefix}${LOCAL_STATE_DIR}` : LOCAL_STATE_DIR
427
+ // A directory-only ignore rule such as `.atelier-local/` is not evaluated
428
+ // consistently by Git for an absent directory on every host. Probe a
429
+ // hypothetical child as well: it proves the directory rule before Atelier
430
+ // creates any local state, including on Git for Windows.
431
+ const ignored = gitRoot && gitPrefix !== null
432
+ ? isIgnoredByGit(gitRoot, `${rel}/.atelier-ignore-probe`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, `${rel}/`, { gitExecutable, env }) || isIgnoredByGit(gitRoot, rel, { gitExecutable, env })
433
+ : gitRoot === null
393
434
  const report = {
394
435
  root,
395
436
  ignored,
@@ -443,6 +484,19 @@ function extErrors(value, label) {
443
484
  .map(([key]) => `${label}.${key} must be an object; ext members are namespaced objects`)
444
485
  }
445
486
 
487
+ // Hands one namespaced ext member to its structured consumer, uninterpreted.
488
+ // The project validator above deliberately does not look inside a member, so
489
+ // the consumer validates it against its own closed contract and fails closed.
490
+ // `present` separates "no such member" (the consumer's feature is simply not
491
+ // configured) from a member that exists with any value at all, including a
492
+ // malformed one, which the consumer must refuse rather than ignore.
493
+ export function projectExtMember(project, key) {
494
+ const ext = project?.config?.ext
495
+ if (ext == null) return { present: false, value: undefined }
496
+ if (typeof ext !== 'object' || Array.isArray(ext)) return { present: true, value: undefined }
497
+ return Object.hasOwn(ext, key) ? { present: true, value: ext[key] } : { present: false, value: undefined }
498
+ }
499
+
446
500
  export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {}) {
447
501
  const errors = []
448
502
  if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return ['project config must be a JSON object']
@@ -504,6 +558,7 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
504
558
  }
505
559
  if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
506
560
 
561
+ const repoNames = new Set()
507
562
  for (const [index, repo] of (Array.isArray(doc.repos) ? doc.repos : []).entries()) {
508
563
  if (!repo || typeof repo !== 'object' || Array.isArray(repo)) {
509
564
  errors.push(`repos[${index}] must be an object`)
@@ -512,6 +567,11 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
512
567
  errors.push(...unknownKeyErrors(repo, `repos[${index}]`, new Set(['name', 'path', 'readBoundary', 'role', 'kind', 'remote', 'identity', 'aliases', 'required', 'ext'])))
513
568
  errors.push(...extErrors(repo.ext, `repos[${index}].ext`))
514
569
  if (!firstString(repo.name)) errors.push(`repos[${index}].name is required`)
570
+ else {
571
+ const key = repo.name.toLowerCase()
572
+ if (repoNames.has(key)) errors.push(`repos[${index}].name duplicates another repository name`)
573
+ repoNames.add(key)
574
+ }
515
575
  if (repo.identity != null) {
516
576
  const identity = asObject(repo.identity)
517
577
  errors.push(...unknownKeyErrors(identity, `repos[${index}].identity`, new Set(['provider', 'id', 'ext'])))
@@ -0,0 +1,161 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { randomUUID, createHash } from 'node:crypto';
4
+ import { hostname } from 'node:os';
5
+ import { ensureContainedPrivateDirectory, openRegularFileNoFollow, readRegularTextNoFollow, syncPrivateDirectory } from './private-state.mjs';
6
+
7
+ const host = createHash('sha256').update(hostname()).digest('hex');
8
+ const pendingPattern = /^\.atelier-write-[a-f0-9-]{36}\.tmp$/;
9
+ export const isPendingPrivateWrite = name => pendingPattern.test(name);
10
+
11
+ export { syncPrivateDirectory };
12
+
13
+ // Publish a complete immutable file with an atomic, non-overwriting hard link.
14
+ // A process interruption can leave a private staging file, never a partial final.
15
+ export function publishPrivateFile(file, bytes) {
16
+ const directory = path.dirname(file);
17
+ const pending = path.join(directory, `.atelier-write-${randomUUID()}.tmp`);
18
+ let fd;
19
+ try {
20
+ fd = openRegularFileNoFollow(pending, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
21
+ fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
22
+ try { fs.linkSync(pending, file); }
23
+ catch (error) {
24
+ if (error.code !== 'EEXIST') throw error;
25
+ const existing = openRegularFileNoFollow(file);
26
+ try { if (!fs.readFileSync(existing).equals(Buffer.from(bytes))) throw error; }
27
+ finally { fs.closeSync(existing); }
28
+ }
29
+ syncPrivateDirectory(directory);
30
+ const actual = openRegularFileNoFollow(file);
31
+ try { if (!fs.readFileSync(actual).equals(Buffer.from(bytes))) throw new Error('published file readback mismatch'); }
32
+ finally { fs.closeSync(actual); }
33
+ } finally {
34
+ if (fd !== undefined) fs.closeSync(fd);
35
+ try { fs.unlinkSync(pending); } catch (error) { if (error.code !== 'ENOENT') throw error; }
36
+ }
37
+ }
38
+
39
+ function unavailable(diagnostic = { reason: 'ownership changed' }) {
40
+ return Object.assign(new Error(`EEXIST: private state is locked (${diagnostic.reason}${diagnostic.file ? `: ${diagnostic.file}` : ''}); inspectPrivateLock and preserve ownership records before offline recovery`), { code: 'EEXIST', diagnostic });
41
+ }
42
+ function ownerState(record, legacy = false) {
43
+ const pid = typeof record === 'number' ? record : record?.pid;
44
+ if (!Number.isSafeInteger(pid) || pid <= 0) return 'invalid owner';
45
+ if (!legacy && record?.host !== host) return 'host identity differs';
46
+ try { process.kill(pid, 0); return 'process exists'; }
47
+ catch (error) { return error.code === 'ESRCH' ? 'absent' : 'process identity unavailable'; }
48
+ }
49
+
50
+ // Read-only diagnosis. Older tickets were superseded by a successful successor;
51
+ // only the newest ticket owns this lock. A release marker prevents a completed
52
+ // successor from disappearing and resurrecting an obsolete owner on PID reuse.
53
+ export function inspectPrivateLock(lockPath) {
54
+ const directory = `${lockPath}.owners`;
55
+ let names;
56
+ try {
57
+ const stat = fs.lstatSync(directory);
58
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return { available: false, reason: 'unsafe ownership directory' };
59
+ names = fs.readdirSync(directory).filter(name => !isPendingPrivateWrite(name)).sort();
60
+ } catch (error) { if (error.code !== 'ENOENT') throw error; names = []; }
61
+ const unexpected = names.find(name => !/^\d{12}\.(json|released)$/.test(name));
62
+ if (unexpected) return { available: false, reason: 'unknown ownership file', file: unexpected };
63
+ const nameSet = new Set(names);
64
+ const tickets = names.filter(name => name.endsWith('.json'));
65
+ const orphan = names.find(name => name.endsWith('.released') && !nameSet.has(name.replace(/\.released$/, '.json')));
66
+ if (orphan) return { available: false, reason: 'release without owner', file: orphan };
67
+ let legacy, legacyDigest = null;
68
+ try {
69
+ const bytes = readRegularTextNoFollow(lockPath);
70
+ legacy = JSON.parse(bytes); legacyDigest = createHash('sha256').update(bytes).digest('hex');
71
+ } catch (error) {
72
+ if (error.code !== 'ENOENT') return { available: false, reason: 'unreadable legacy owner', file: path.basename(lockPath) };
73
+ }
74
+ if (tickets.length) {
75
+ const file = tickets.at(-1), generation = Number(file.slice(0, 12));
76
+ let body, record;
77
+ try { body = readRegularTextNoFollow(path.join(directory, file)); record = JSON.parse(body); }
78
+ catch { return { available: false, reason: 'unreadable owner', file }; }
79
+ if (!Number.isSafeInteger(record?.pid) || record.pid <= 0 || !/^[a-f0-9]{64}$/.test(record?.host) || !/^[a-f0-9-]{36}$/.test(record?.nonce)) return { available: false, reason: 'invalid owner', file };
80
+ if (legacyDigest !== null && record.legacyDigest !== legacyDigest) {
81
+ const reason = ownerState(legacy, true);
82
+ if (reason !== 'absent') return { available: false, reason: `legacy ${reason}`, file: path.basename(lockPath) };
83
+ }
84
+ const marker = file.replace(/\.json$/, '.released');
85
+ if (names.includes(marker)) {
86
+ try {
87
+ if (readRegularTextNoFollow(path.join(directory, marker)) !== body) return { available: false, reason: 'release identity differs', file: marker };
88
+ } catch { return { available: false, reason: 'unreadable release', file: marker }; }
89
+ return { available: true, reason: 'released', generation, file, legacyDigest };
90
+ }
91
+ const reason = ownerState(record);
92
+ return { available: reason === 'absent', reason, generation, file, legacyDigest };
93
+ }
94
+ if (legacyDigest === null) return { available: true, reason: 'unused', generation: 0, legacyDigest };
95
+ const reason = ownerState(legacy, true);
96
+ return { available: reason === 'absent', reason, generation: 0, file: path.basename(lockPath), legacyDigest };
97
+ }
98
+
99
+ export function acquirePrivateLock(lockPath) {
100
+ const parent = path.dirname(lockPath);
101
+ const directory = ensureContainedPrivateDirectory({ workspaceRoot: parent, directory: `${lockPath}.owners`, label: 'private lock' });
102
+ let ticket, body;
103
+ try {
104
+ const status = inspectPrivateLock(lockPath);
105
+ if (!status.available) throw unavailable(status);
106
+ const generation = status.generation;
107
+ if (generation >= 999999999999) throw unavailable({ reason: 'ownership generation exhausted' });
108
+ ticket = path.join(directory, `${String(generation + 1).padStart(12, '0')}.json`);
109
+ body = JSON.stringify({ pid: process.pid, host, nonce: randomUUID(), legacyDigest: status.legacyDigest });
110
+ publishPrivateFile(ticket, body);
111
+ let released = false;
112
+ return () => {
113
+ if (released) return;
114
+ if (readRegularTextNoFollow(ticket) !== body) throw unavailable();
115
+ publishPrivateFile(ticket.replace(/\.json$/, '.released'), body);
116
+ released = true;
117
+ };
118
+ } catch (error) {
119
+ // No operation has started: clean up only this exact attempted owner.
120
+ if (ticket && body) {
121
+ try { if (readRegularTextNoFollow(ticket) === body) fs.unlinkSync(ticket); }
122
+ catch { /* Preserve uncertain ownership for offline diagnosis. */ }
123
+ }
124
+ if (error.code === 'EEXIST' && !error.diagnostic) throw unavailable(); throw error;
125
+ }
126
+ }
127
+
128
+ export function withPrivateLock(lockPath, operation) {
129
+ const release = acquirePrivateLock(lockPath);
130
+ try { return operation(); } finally { release(); }
131
+ }
132
+
133
+ // Revalidate file identities on every read; unchanged prefixes reuse their
134
+ // verified result. A changed existing file forces replay, a missing tail is
135
+ // refused for the lifetime of this reader. Startup always verifies all history.
136
+ export function createVerifiedFileSequence({ directory, initial, apply, ignoreFiles = [] }) {
137
+ let names = [], identities = [], value = initial();
138
+ const identity = file => {
139
+ const s = fs.lstatSync(file, { bigint: true });
140
+ if (!s.isFile() || s.isSymbolicLink()) throw new Error('non-regular history refused');
141
+ return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(':');
142
+ };
143
+ return () => {
144
+ const listed = fs.readdirSync(directory).filter(n => !isPendingPrivateWrite(n)).sort();
145
+ // Explicit adapter metadata exceptions must still be regular files. Never
146
+ // hide unknown filenames or malformed committed records behind a glob.
147
+ for (const name of listed.filter(n => ignoreFiles.includes(n))) identity(path.join(directory, name));
148
+ const next = listed.filter(n => !ignoreFiles.includes(n));
149
+ if (next.length < names.length) throw new Error('history shortened; preserve and inspect');
150
+ const ids = next.map(n => identity(path.join(directory, n)));
151
+ const reuse = names.every((n, i) => n === next[i] && identities[i] === ids[i]);
152
+ let result = reuse ? value : initial();
153
+ for (let i = reuse ? names.length : 0; i < next.length; i++) {
154
+ const file = path.join(directory, next[i]);
155
+ result = apply(readRegularTextNoFollow(file), result, i + 1, next[i]);
156
+ if (identity(file) !== ids[i]) throw new Error('history changed during verification');
157
+ }
158
+ names = next; identities = ids; value = result;
159
+ return structuredClone(result);
160
+ };
161
+ }