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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (383) hide show
  1. package/CHANGELOG.md +191 -0
  2. package/README.md +86 -25
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/docs/assurance-controls.md +12 -2
  31. package/docs/atelier-runtime.md +13 -2
  32. package/docs/atelier-sync.md +172 -0
  33. package/docs/blocks/claims.md +28 -18
  34. package/docs/blocks/will-not-do.md +12 -3
  35. package/docs/coauthor-session.md +57 -0
  36. package/docs/connected-composition.md +75 -0
  37. package/docs/decisions.md +167 -0
  38. package/docs/exact-upgrades.md +159 -0
  39. package/docs/extension-pack-lifecycle.md +33 -0
  40. package/docs/guided-upgrades.md +99 -0
  41. package/docs/install.md +65 -9
  42. package/docs/intake-and-guides.md +65 -0
  43. package/docs/integration-and-review-plan.md +409 -0
  44. package/docs/integration-contract-decisions.md +22 -0
  45. package/docs/local-review.md +84 -0
  46. package/docs/local-services.md +227 -0
  47. package/docs/local-state.md +107 -0
  48. package/docs/obsidian-contract.md +687 -0
  49. package/docs/obsidian.md +387 -0
  50. package/docs/portable-presentation.md +328 -0
  51. package/docs/presentation-consumer-boundaries.md +75 -0
  52. package/docs/presentation-review-disposition.md +47 -0
  53. package/docs/project-command-inventory.md +38 -0
  54. package/docs/project-options.md +53 -0
  55. package/docs/release-engineering.md +99 -2
  56. package/docs/review-portability.md +55 -0
  57. package/docs/skill-steward.md +156 -0
  58. package/docs/upgrade.md +70 -2
  59. package/docs/vault-service.md +292 -0
  60. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  61. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  62. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  63. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  64. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  65. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  66. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  67. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  68. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  69. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  70. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  71. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  72. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  73. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  74. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  75. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  76. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  77. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  78. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  79. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  80. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  81. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  82. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  83. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  84. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  85. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  86. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  87. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  88. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  89. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  90. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  91. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  92. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  93. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  94. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  95. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  96. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  97. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  98. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  99. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  100. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  101. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  102. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  103. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  104. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  105. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  106. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  107. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  108. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  109. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  110. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  111. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  112. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  113. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  114. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  115. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  116. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  117. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  118. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  119. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  120. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  121. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  122. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  123. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  125. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  127. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  128. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  129. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  130. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  131. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  132. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  133. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  134. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  135. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  136. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  137. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  138. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  139. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  140. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  141. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  142. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  143. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  144. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  145. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  146. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  147. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  148. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  149. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  150. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  151. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  152. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  153. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  154. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  159. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  160. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  161. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  162. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  163. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  164. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  165. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  166. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  167. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  168. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  169. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  170. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  171. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  172. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  173. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  174. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  175. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  176. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  177. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  178. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  179. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  181. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  182. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  183. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  186. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  187. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  188. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  189. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  190. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  191. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  192. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  193. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  194. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  195. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  196. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  197. package/fixtures/obsidian/edits/workspace.json +65 -0
  198. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  199. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  200. package/fixtures/obsidian/materialization/workspace.json +54 -0
  201. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  202. package/fixtures/ui/presentation/pane.v1.json +7 -0
  203. package/fixtures/ui/presentation/reference.v1.json +293 -0
  204. package/package.json +61 -6
  205. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  206. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  207. package/skills/claude/atelier-open/SKILL.md +5 -0
  208. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  209. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  210. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  211. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  212. package/skills/codex/atelier-open/SKILL.md +5 -0
  213. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  214. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  215. package/src/access/preview-enforcer.mjs +84 -0
  216. package/src/boundary/content-rules.mjs +9 -4
  217. package/src/boundary/policy.mjs +74 -42
  218. package/src/cli/project-options.mjs +58 -0
  219. package/src/cli/run.mjs +61 -3
  220. package/src/coauthor/session.mjs +161 -0
  221. package/src/coauthor/store.mjs +173 -0
  222. package/src/collaboration/event-ledger.mjs +16 -33
  223. package/src/collaboration/inspection-bundle.mjs +223 -0
  224. package/src/collaboration/review-contracts.mjs +25 -0
  225. package/src/collaboration/review-store.mjs +305 -0
  226. package/src/commands/coauthor.mjs +26 -0
  227. package/src/commands/config.mjs +6 -1
  228. package/src/commands/extension-pack.mjs +15 -8
  229. package/src/commands/init.mjs +44 -5
  230. package/src/commands/obsidian.mjs +368 -0
  231. package/src/commands/review.mjs +147 -0
  232. package/src/commands/setup.mjs +51 -14
  233. package/src/commands/skills.mjs +199 -0
  234. package/src/commands/sync.mjs +107 -0
  235. package/src/composition/registry.mjs +27 -0
  236. package/src/composition/wire.mjs +122 -0
  237. package/src/contracts/corpus.mjs +35 -0
  238. package/src/decisions/contracts.d.mts +96 -0
  239. package/src/decisions/contracts.mjs +321 -0
  240. package/src/disclosure/content-scan.mjs +13 -0
  241. package/src/export/atelier-export-contract.mjs +16 -1
  242. package/src/extension-packs/lifecycle.mjs +154 -0
  243. package/src/graph/graph.mjs +64 -2
  244. package/src/graph/knowledge-graph.mjs +548 -24
  245. package/src/guides/contracts.mjs +54 -0
  246. package/src/index.mjs +29 -0
  247. package/src/intake/store.mjs +119 -0
  248. package/src/preview/channel.mjs +89 -0
  249. package/src/project/config.mjs +50 -49
  250. package/src/project/durable-state.mjs +161 -0
  251. package/src/project/file-class.mjs +166 -0
  252. package/src/project/private-state.mjs +11 -1
  253. package/src/projection/obsidian/contracts.mjs +571 -0
  254. package/src/projection/obsidian/edits/align.mjs +169 -0
  255. package/src/projection/obsidian/edits/apply.mjs +889 -0
  256. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  257. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  258. package/src/projection/obsidian/edits/index.mjs +19 -0
  259. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  260. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  261. package/src/projection/obsidian/edits/observe.mjs +123 -0
  262. package/src/projection/obsidian/edits/policy.mjs +123 -0
  263. package/src/projection/obsidian/edits/regions.mjs +648 -0
  264. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  265. package/src/projection/obsidian/materialize/index.mjs +7 -0
  266. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  267. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  268. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  269. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  270. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  271. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  272. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  273. package/src/projection/obsidian/proposals/index.mjs +23 -0
  274. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  275. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  276. package/src/projection/obsidian/proposals/router.mjs +168 -0
  277. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  278. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  279. package/src/projection/obsidian/publication/index.mjs +5 -0
  280. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  281. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  282. package/src/projection/obsidian/publication/transport.mjs +139 -0
  283. package/src/projection/obsidian/recovery/index.mjs +6 -0
  284. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  285. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  286. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  287. package/src/projection/obsidian/recovery/store.mjs +234 -0
  288. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  289. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  290. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  291. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  292. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  293. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  294. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  295. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  296. package/src/projection/project.mjs +28 -19
  297. package/src/readiness-protocols/evidence.mjs +282 -0
  298. package/src/readiness-protocols/runtime.mjs +4 -3
  299. package/src/readiness-protocols/source-read.mjs +39 -0
  300. package/src/runtime/git-adapter.mjs +189 -0
  301. package/src/runtime/local-state.mjs +439 -0
  302. package/src/runtime/obsidian/app-capability.mjs +110 -0
  303. package/src/runtime/obsidian/app-production-seams.mjs +96 -0
  304. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  305. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  306. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  307. package/src/runtime/obsidian/contributions.mjs +27 -0
  308. package/src/runtime/obsidian/documents.mjs +37 -0
  309. package/src/runtime/obsidian/enablement.mjs +53 -0
  310. package/src/runtime/obsidian/engine.mjs +532 -0
  311. package/src/runtime/obsidian/errors.mjs +14 -0
  312. package/src/runtime/obsidian/extension-points.mjs +103 -0
  313. package/src/runtime/obsidian/index.mjs +34 -0
  314. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  315. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  316. package/src/runtime/obsidian/observation.mjs +158 -0
  317. package/src/runtime/obsidian/opening.mjs +243 -0
  318. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  319. package/src/runtime/obsidian/pipeline.mjs +122 -0
  320. package/src/runtime/obsidian/private-lock.mjs +156 -0
  321. package/src/runtime/obsidian/process-identity.mjs +58 -0
  322. package/src/runtime/obsidian/service-client.mjs +66 -0
  323. package/src/runtime/obsidian/service-main.mjs +89 -0
  324. package/src/runtime/obsidian/service-record.mjs +169 -0
  325. package/src/runtime/obsidian/service-server.mjs +121 -0
  326. package/src/runtime/obsidian/service.mjs +216 -0
  327. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  328. package/src/runtime/obsidian/state-store.mjs +180 -0
  329. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  330. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  331. package/src/runtime/obsidian/watchers.mjs +36 -0
  332. package/src/runtime/repository-observation.mjs +491 -0
  333. package/src/runtime/supervisor.mjs +803 -0
  334. package/src/server/local-sidecar.mjs +44 -0
  335. package/src/server/server.mjs +1 -0
  336. package/src/skills/steward.mjs +780 -0
  337. package/src/ui/presentation/browser.mjs +278 -0
  338. package/src/ui/presentation/contract.mjs +116 -0
  339. package/src/ui/presentation/index.mjs +7 -0
  340. package/src/ui/presentation/native.mjs +153 -0
  341. package/src/ui/presentation/proof.mjs +57 -0
  342. package/src/ui/presentation/schema-check.mjs +30 -0
  343. package/src/ui/presentation/schema.generated.mjs +1371 -0
  344. package/src/ui/presentation/state.mjs +43 -0
  345. package/src/ui/presentation/styles.mjs +67 -0
  346. package/src/ui/presentation/tokens.mjs +74 -0
  347. package/src/ui/presentation/web.mjs +85 -0
  348. package/src/ui/review-page.mjs +32 -0
  349. package/src/upgrade/explanation.mjs +33 -0
  350. package/src/upgrade/provenance.mjs +338 -0
  351. package/src/upgrade/transaction-files.mjs +103 -0
  352. package/src/upgrade/transaction.mjs +475 -0
  353. package/src/upgrade/upgrade.mjs +63 -28
  354. package/src/vault/hosts.mjs +21 -0
  355. package/src/vault/identity.mjs +19 -0
  356. package/src/vault/index.mjs +7 -0
  357. package/src/vault/interface.mjs +11 -0
  358. package/src/vault/metadata.mjs +31 -0
  359. package/src/vault/privacy.mjs +148 -0
  360. package/src/vault/probe.mjs +89 -0
  361. package/src/vault/service.mjs +131 -0
  362. package/src/vault/source.mjs +45 -0
  363. package/src/vault/storage.mjs +23 -0
  364. package/templates/distribution-workspace/.claude/launch.json +6 -2
  365. package/templates/distribution-workspace/gitignore +1 -0
  366. package/templates/distribution-workspace/project/README.md +2 -2
  367. package/templates/external-project-workspace/README.md +45 -0
  368. package/templates/external-project-workspace/answers.example.json +4 -0
  369. package/templates/external-project-workspace/atelier.project.json +40 -0
  370. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  371. package/templates/external-project-workspace/gitignore +8 -0
  372. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  373. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  374. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  375. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  376. package/templates/external-project-workspace/source/README.md +11 -0
  377. package/templates/launch.json +6 -2
  378. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  379. package/templates/private-domain-workspace/domain/README.md +2 -2
  380. package/templates/private-domain-workspace/gitignore +1 -0
  381. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  382. package/templates/shared-project-workspace/gitignore +1 -0
  383. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -0,0 +1,282 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { canonicalize } from '../attestation/jcs.mjs'
6
+ import { buildGraph } from '../graph/graph.mjs'
7
+ import {
8
+ loadExtensionPacks,
9
+ createProtocolRegistry,
10
+ } from '../extension-packs/loader.mjs'
11
+ import { readRegularTextNoFollow } from '../project/private-state.mjs'
12
+ import { createCollaborationEventLedger } from '../collaboration/event-ledger.mjs'
13
+ import { inspectPackLifecycle } from '../extension-packs/lifecycle.mjs'
14
+ import { readLocalOverlay } from '../project/config.mjs'
15
+ import { readBoundedSource } from './source-read.mjs'
16
+ export { readBoundedSource } from './source-read.mjs'
17
+ import { validReviewArtifact } from '../collaboration/review-contracts.mjs'
18
+ import { buildReadinessRun, answerForField } from './runtime.mjs'
19
+
20
+ export const hashEvidence = (value) =>
21
+ `sha256:${crypto
22
+ .createHash('sha256')
23
+ .update(canonicalize(value))
24
+ .digest('hex')}`
25
+ const hashBytes = (value) =>
26
+ `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
27
+ const packageRoot = fileURLToPath(new URL('../../', import.meta.url))
28
+ function evaluatorInventory() {
29
+ const files = ['package.json']
30
+ function visit(relative) {
31
+ for (const name of fs
32
+ .readdirSync(path.join(packageRoot, relative))
33
+ .sort()) {
34
+ const next = path.posix.join(relative, name),
35
+ stat = fs.lstatSync(path.join(packageRoot, next))
36
+ if (stat.isSymbolicLink()) throw new Error('redirected evaluator input')
37
+ if (stat.isDirectory()) visit(next)
38
+ else if (stat.isFile() && /\.(mjs|json)$/.test(name)) files.push(next)
39
+ }
40
+ }
41
+ visit('src')
42
+ visit('contracts')
43
+ return files
44
+ .sort()
45
+ .map((file) => ({
46
+ file,
47
+ digest: hashBytes(readRegularTextNoFollow(path.join(packageRoot, file))),
48
+ }))
49
+ }
50
+ const evaluator = hashEvidence(evaluatorInventory())
51
+
52
+ function currentInputs(project, protocolId, answers) {
53
+ if (
54
+ project.configPath &&
55
+ hashEvidence(JSON.parse(readRegularTextNoFollow(project.configPath))) !==
56
+ hashEvidence(project.config)
57
+ )
58
+ throw new Error('project config changed; resolve the project again')
59
+ if (hashEvidence(evaluatorInventory()) !== evaluator)
60
+ throw new Error('evaluator changed; restart before review')
61
+ if (
62
+ project.localOverlay &&
63
+ hashEvidence(readLocalOverlay({ configDir: project.configDir }).overlay) !==
64
+ hashEvidence(project.localOverlay.overlay)
65
+ )
66
+ throw new Error('local overlay changed; resolve again')
67
+ const loaded = loadExtensionPacks(project)
68
+ const packs = loaded.packs ?? loaded
69
+ const lifecycle = inspectPackLifecycle(project, packs)
70
+ if (!lifecycle.ok) throw new Error(lifecycle.errors.join('; '))
71
+ const registry = createProtocolRegistry({ packs })
72
+ const protocol = registry.protocolById(protocolId)
73
+ if (!protocol) throw new Error('protocol unavailable for evidence binding')
74
+ const graph = buildGraph(project)
75
+ if (graph.errors.length)
76
+ throw new Error('source graph is invalid; evidence capture refused')
77
+ if (graph.nodes.length > 256)
78
+ throw new Error('review evidence exceeds bounded source count')
79
+ const sources = [],
80
+ excerpts = new Map()
81
+ for (const node of graph.nodes) {
82
+ const repo = project.repos.find(
83
+ (entry) => entry.name === node.repo && !entry.external,
84
+ )
85
+ if (!repo?.path) throw new Error('source repository unavailable')
86
+ const source = readBoundedSource(repo.path, node.path)
87
+ const metadata = !/\.md$/i.test(node.path)
88
+ ? readBoundedSource(repo.path, `${node.path}.kg.json`).digest
89
+ : null
90
+ excerpts.set(node.id, source.text.slice(0, 2000))
91
+ sources.push({
92
+ id: node.id,
93
+ repo: node.repo,
94
+ path: node.path,
95
+ audience: node.audience,
96
+ digest: source.digest,
97
+ metadata,
98
+ })
99
+ }
100
+ sources.sort((a, b) =>
101
+ `${a.repo}/${a.path}`.localeCompare(`${b.repo}/${b.path}`, 'en'),
102
+ )
103
+ const claimEvidence = protocol.claimMappings.map((mapping) => {
104
+ const raw = answerForField(answers, mapping.evidence)
105
+ const references = (Array.isArray(raw) ? raw.flat(Infinity) : [raw])
106
+ .filter((value) => typeof value === 'string' && value.trim())
107
+ .map((value) => value.trim())
108
+ const resolved = [],
109
+ unresolved = []
110
+ for (const reference of references) {
111
+ const source = sources.find((item) => item.id === reference)
112
+ if (source)
113
+ resolved.push({
114
+ id: source.id,
115
+ repo: source.repo,
116
+ path: source.path,
117
+ digest: source.digest,
118
+ excerpt: excerpts.get(source.id),
119
+ })
120
+ else unresolved.push(reference)
121
+ }
122
+ return {
123
+ mappingId: mapping.id,
124
+ sourceFields: mapping.sourceFields,
125
+ answers: Object.fromEntries(
126
+ mapping.sourceFields.map((field) => [
127
+ field,
128
+ answerForField(answers, field) ?? null,
129
+ ]),
130
+ ),
131
+ evidenceField: mapping.evidence,
132
+ references,
133
+ resolved,
134
+ unresolved,
135
+ status: !references.length
136
+ ? 'missing'
137
+ : unresolved.length
138
+ ? 'unresolved'
139
+ : 'linked-source',
140
+ }
141
+ })
142
+ const readPolicy = (file) =>
143
+ fs.existsSync(file) ? hashBytes(readRegularTextNoFollow(file)) : null
144
+ return {
145
+ schema: 'atelier-review-evidence@v1',
146
+ protocolId,
147
+ protocol,
148
+ protocolDigest: hashEvidence(protocol),
149
+ answers,
150
+ answersDigest: hashEvidence(answers),
151
+ configDigest: hashEvidence(project.config),
152
+ lifecycleDigest: hashEvidence(lifecycle),
153
+ policyDigest: hashEvidence({
154
+ access: readPolicy(project.repoAccessPath),
155
+ boundary: readPolicy(project.boundaryPolicyPath),
156
+ }),
157
+ packs: packs
158
+ .map(({ id, version, digest }) => ({ id, version, digest }))
159
+ .sort((a, b) => a.id.localeCompare(b.id, 'en')),
160
+ evaluator: { id: 'answer-completeness/v1', digest: evaluator },
161
+ sources,
162
+ claimEvidence,
163
+ limitations: [
164
+ 'Answers are operator assertions; source presence does not substantiate each claim.',
165
+ 'No calibrated evidence confidence or runtime acceptance is established.',
166
+ ],
167
+ }
168
+ }
169
+
170
+ export function captureReviewEvidence(project, protocolId, answers) {
171
+ const first = currentInputs(project, protocolId, answers)
172
+ const second = currentInputs(project, protocolId, answers)
173
+ const digest = hashEvidence(first)
174
+ if (digest !== hashEvidence(second))
175
+ throw new Error('review inputs changed during capture')
176
+ const snapshot = { ...first, digest }
177
+ if (!validReviewArtifact('evidence', snapshot))
178
+ throw new Error('evidence contract invalid or exceeds bounds')
179
+ return snapshot
180
+ }
181
+
182
+ export function evidenceLedger(project) {
183
+ return createCollaborationEventLedger({
184
+ workspaceRoot: project.configDir,
185
+ ledgerPath: path.join(
186
+ project.configDir,
187
+ '.atelier-local/review/evidence.ndjson',
188
+ ),
189
+ })
190
+ }
191
+
192
+ export function recordReviewEvidence(project, run) {
193
+ const snapshot = captureReviewEvidence(project, run.protocolId, run.answers)
194
+ if (
195
+ hashEvidence(
196
+ buildReadinessRun({
197
+ project,
198
+ protocol: snapshot.protocol,
199
+ answers: run.answers,
200
+ createdAt: run.createdAt,
201
+ }),
202
+ ) !== hashEvidence(run)
203
+ )
204
+ throw new Error('run no longer matches its current protocol and evaluator')
205
+ const payload = {
206
+ schema: 'atelier-bound-run@v1',
207
+ run,
208
+ runDigest: hashEvidence(run),
209
+ snapshot,
210
+ }
211
+ if (!validBoundRun(payload)) throw new Error('bound run contract invalid')
212
+ const ledger = evidenceLedger(project)
213
+ const existing = ledger.eventsFor(run.runId)
214
+ if (!existing.ok) throw new Error(existing.error)
215
+ if (existing.events.length) {
216
+ if (hashEvidence(existing.events[0].payload) !== hashEvidence(payload))
217
+ throw new Error('run identity already has different evidence')
218
+ return existing.events[0].payload
219
+ }
220
+ const result = ledger.append({
221
+ aggregateId: run.runId,
222
+ expectedVersion: 0,
223
+ type: 'run-bound',
224
+ actor: 'local-runtime',
225
+ payload,
226
+ })
227
+ if (!result.ok) throw new Error(result.error)
228
+ return payload
229
+ }
230
+
231
+ export function validBoundRun(payload) {
232
+ if (!validReviewArtifact('bound', payload)) return false
233
+ const { digest, ...body } = payload.snapshot
234
+ const { run, snapshot } = payload
235
+ return (
236
+ payload.runDigest === hashEvidence(run) &&
237
+ digest === hashEvidence(body) &&
238
+ snapshot.protocolDigest === hashEvidence(snapshot.protocol) &&
239
+ snapshot.answersDigest === hashEvidence(snapshot.answers) &&
240
+ snapshot.answersDigest === hashEvidence(run.answers) &&
241
+ snapshot.protocol.id === run.protocolId &&
242
+ snapshot.protocolId === run.protocolId &&
243
+ snapshot.claimEvidence.length === run.claims.length &&
244
+ snapshot.claimEvidence.every(
245
+ (entry, index) =>
246
+ entry.mappingId === snapshot.protocol.claimMappings[index]?.id,
247
+ )
248
+ )
249
+ }
250
+
251
+ export function loadBoundRun(project, runId) {
252
+ const result = evidenceLedger(project).eventsFor(runId)
253
+ if (!result.ok) return result
254
+ if (result.events.length !== 1 || result.events[0].type !== 'run-bound')
255
+ return {
256
+ ok: false,
257
+ status: 409,
258
+ error: 'run lacks immutable review evidence',
259
+ }
260
+ const payload = result.events[0].payload
261
+ const { digest, ...body } = payload.snapshot ?? {}
262
+ if (
263
+ !validBoundRun(payload) ||
264
+ payload.schema !== 'atelier-bound-run@v1' ||
265
+ payload.run?.runId !== runId ||
266
+ payload.runDigest !== hashEvidence(payload.run) ||
267
+ digest !== hashEvidence(body)
268
+ )
269
+ return { ok: false, status: 422, error: 'run evidence is invalid' }
270
+ return { ok: true, status: 200, ...payload }
271
+ }
272
+
273
+ export function currentRunEligibility(project, bound) {
274
+ try {
275
+ return (
276
+ captureReviewEvidence(project, bound.run.protocolId, bound.run.answers)
277
+ .digest === bound.snapshot.digest
278
+ )
279
+ } catch {
280
+ return false
281
+ }
282
+ }
@@ -73,7 +73,7 @@ function nestedAnswer(value, parts) {
73
73
  return nestedAnswer(value[head], rest)
74
74
  }
75
75
 
76
- function answerForField(answers, fieldId) {
76
+ export function answerForField(answers, fieldId) {
77
77
  for (const key of pathVariants(fieldId)) {
78
78
  if (Object.hasOwn(answers, key)) return answers[key]
79
79
  }
@@ -119,9 +119,8 @@ export function claimsForProtocolRun({ protocol, answers, projectName, runId, cr
119
119
  provider: 'atelier-readiness',
120
120
  status: 'proposed',
121
121
  promoted: false,
122
- confidence: Object.values(answers).some(answerPresent) ? 0.7 : 0.2,
123
122
  evidence: [`readiness-run:${runId}`],
124
- notes: [`Proposed ${mapping.id} from ${protocol.title}.`],
123
+ notes: [`Proposed ${mapping.id} from ${protocol.title}.`, 'Answer presence is not evidence confidence; human judgment is required.'],
125
124
  createdAt,
126
125
  }
127
126
  })
@@ -147,6 +146,7 @@ export function buildReadinessRun({ project, protocol, answers = {}, createdAt =
147
146
  claims: claimsForProtocolRun({ protocol, answers: normalizedAnswers, projectName, runId, createdAt }),
148
147
  safety: {
149
148
  runtimeMutation: false,
149
+ runtimeImport: false,
150
150
  canonicalWrites: false,
151
151
  claimOnly: true,
152
152
  storage: 'ignored-local',
@@ -367,6 +367,7 @@ export function buildTenantPacket(project, { runs = listProtocolRuns(project), r
367
367
  ext: {
368
368
  'mnstry.atelier': {
369
369
  extensionPacks: packContribution,
370
+ scoreMeaning: 'Legacy readinessScore measures completed input dimensions; it is not evidence confidence or runtime admission.',
370
371
  },
371
372
  },
372
373
  }
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+ import { openRegularFileNoFollow } from '../project/private-state.mjs'
5
+ const hashBytes = (value) =>
6
+ `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`
7
+
8
+ export function readBoundedSource(root, relative) {
9
+ if (
10
+ typeof relative !== 'string' ||
11
+ path.isAbsolute(relative) ||
12
+ relative.split(/[\\/]/).some((part) => part === '..' || part === '.git')
13
+ )
14
+ throw new Error('source reference escapes declared boundary')
15
+ const realRoot = fs.realpathSync(root)
16
+ const file = path.resolve(realRoot, relative)
17
+ let cursor = realRoot
18
+ for (const part of path.relative(realRoot, file).split(path.sep)) {
19
+ cursor = path.join(cursor, part)
20
+ if (fs.lstatSync(cursor).isSymbolicLink())
21
+ throw new Error('redirected source reference refused')
22
+ }
23
+ const descriptor = openRegularFileNoFollow(file)
24
+ try {
25
+ const before = fs.fstatSync(descriptor)
26
+ if (before.size > 1024 * 1024)
27
+ throw new Error('source exceeds review byte bound')
28
+ const bytes = fs.readFileSync(descriptor)
29
+ const after = fs.fstatSync(descriptor)
30
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs)
31
+ throw new Error('source changed during read')
32
+ return {
33
+ digest: hashBytes(bytes),
34
+ text: new TextDecoder('utf-8', { fatal: true }).decode(bytes),
35
+ }
36
+ } finally {
37
+ fs.closeSync(descriptor)
38
+ }
39
+ }
@@ -0,0 +1,189 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ const DEFAULT_TIMEOUT_MS = 30_000
6
+ const DEFAULT_MAX_BUFFER = 16 * 1024 * 1024
7
+
8
+ function executableNames(platform = process.platform, env = process.env) {
9
+ if (platform !== 'win32') return ['git']
10
+ const extensions = String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
11
+ .split(';')
12
+ .map((value) => value.trim().toLowerCase())
13
+ .filter(Boolean)
14
+ return ['git.exe', ...extensions.map((extension) => `git${extension}`).filter((name) => name !== 'git.exe')]
15
+ }
16
+
17
+ function isExecutable(file, platform = process.platform) {
18
+ try {
19
+ fs.accessSync(file, platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK)
20
+ return fs.statSync(file).isFile()
21
+ } catch {
22
+ return false
23
+ }
24
+ }
25
+
26
+ function canonicalExecutable(file) {
27
+ try {
28
+ return fs.realpathSync.native(file)
29
+ } catch {
30
+ return fs.realpathSync(file)
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Resolve one exact Git executable without invoking a shell. The selected path
36
+ * is carried through every later operation so PATH changes cannot swap engines
37
+ * underneath a running supervisor.
38
+ */
39
+ export function resolveGitExecutable({ env = process.env, platform = process.platform } = {}) {
40
+ const configured = String(env.ATELIER_GIT_PATH || '').trim()
41
+ if (configured) {
42
+ if (!path.isAbsolute(configured)) throw new Error('ATELIER_GIT_PATH must be an absolute path')
43
+ if (!isExecutable(configured, platform)) throw new Error(`configured Git executable is unavailable: ${configured}`)
44
+ return canonicalExecutable(configured)
45
+ }
46
+
47
+ const searchPath = String(env.PATH || '')
48
+ const delimiter = platform === 'win32' ? ';' : path.delimiter
49
+ const names = executableNames(platform, env)
50
+ for (const directory of searchPath.split(delimiter).filter(Boolean)) {
51
+ const cleanDir = directory.replace(/^"|"$/g, '')
52
+ for (const name of names) {
53
+ const candidate = path.join(cleanDir, name)
54
+ if (isExecutable(candidate, platform)) return canonicalExecutable(candidate)
55
+ }
56
+ }
57
+ throw new Error('compatible system Git was not found on PATH')
58
+ }
59
+
60
+ export function sanitizedGitEnvironment(env = process.env, allowPrompt = false) {
61
+ const next = { ...env }
62
+ for (const key of Object.keys(next)) {
63
+ if (key.toUpperCase().startsWith('GIT_')) delete next[key]
64
+ }
65
+ if (!allowPrompt) next.GIT_TERMINAL_PROMPT = '0'
66
+ next.GIT_OPTIONAL_LOCKS = next.GIT_OPTIONAL_LOCKS || '1'
67
+ next.GIT_NO_REPLACE_OBJECTS = '1'
68
+ return next
69
+ }
70
+
71
+ export function redactGitDiagnostic(value) {
72
+ return String(value || '')
73
+ .replace(/https?:\/\/[^\s]+/gi, (url) => sanitizeRemoteUrl(url))
74
+ .replace(/\b((?:password|passwd|token|secret|authorization|proxy-authorization|extraheader)\s*[=:]\s*)[^\s]+/gi, '$1[redacted]')
75
+ .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [redacted]')
76
+ }
77
+
78
+ export class GitCommandError extends Error {
79
+ constructor(message, result) {
80
+ super(message)
81
+ this.name = 'GitCommandError'
82
+ this.result = result
83
+ }
84
+ }
85
+
86
+ export function runGit(gitExecutable, repoRoot, args, {
87
+ allowFailure = false,
88
+ allowPrompt = false,
89
+ env = process.env,
90
+ input = undefined,
91
+ timeout = DEFAULT_TIMEOUT_MS,
92
+ } = {}) {
93
+ if (!path.isAbsolute(gitExecutable)) throw new Error('Git executable must be an absolute path')
94
+ if (!Array.isArray(args) || args.some((value) => typeof value !== 'string')) {
95
+ throw new Error('Git arguments must be a string array')
96
+ }
97
+ const argv = repoRoot ? ['-C', repoRoot, ...args] : args
98
+ const child = spawnSync(gitExecutable, argv, {
99
+ encoding: 'utf8',
100
+ env: sanitizedGitEnvironment(env, allowPrompt),
101
+ input,
102
+ maxBuffer: DEFAULT_MAX_BUFFER,
103
+ shell: false,
104
+ timeout,
105
+ windowsHide: true,
106
+ })
107
+ const result = {
108
+ ok: !child.error && child.status === 0,
109
+ status: child.status,
110
+ signal: child.signal ?? null,
111
+ stdout: child.stdout || '',
112
+ stderr: redactGitDiagnostic(child.stderr || ''),
113
+ error: child.error?.message ? redactGitDiagnostic(child.error.message) : null,
114
+ executable: gitExecutable,
115
+ args: argv,
116
+ }
117
+ if (!result.ok && !allowFailure) {
118
+ const detail = result.error || result.stderr.trim() || `exit ${String(result.status)}`
119
+ throw new GitCommandError(`Git ${args[0] || 'command'} failed: ${detail}`, result)
120
+ }
121
+ return result
122
+ }
123
+
124
+ export function gitText(gitExecutable, repoRoot, args, options = {}) {
125
+ return runGit(gitExecutable, repoRoot, args, options).stdout.trim()
126
+ }
127
+
128
+ export function parseGitVersion(text) {
129
+ const match = String(text || '').match(/git version (\d+)\.(\d+)\.(\d+)/i)
130
+ if (!match) return null
131
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), text: match[0] }
132
+ }
133
+
134
+ export function isSupportedGitVersion(version) {
135
+ return Boolean(version && (version.major > 2 || (version.major === 2 && version.minor >= 40)))
136
+ }
137
+
138
+ export function inspectGitEngine(gitExecutable, { env = process.env } = {}) {
139
+ const result = runGit(gitExecutable, null, ['--version'], { env })
140
+ const version = parseGitVersion(result.stdout)
141
+ if (!version) throw new Error(`unrecognized Git version output from ${gitExecutable}`)
142
+ return {
143
+ executable: gitExecutable,
144
+ version: `${version.major}.${version.minor}.${version.patch}`,
145
+ supported: isSupportedGitVersion(version),
146
+ minimum: '2.40.0',
147
+ }
148
+ }
149
+
150
+ export function classifyRemoteAuthentication(url) {
151
+ const value = String(url || '').trim()
152
+ if (!value) return 'none'
153
+ if (/^(?:ssh:\/\/|[^/@\s]+@[^:/\s]+:)/i.test(value)) return 'ssh'
154
+ if (/^https?:\/\//i.test(value)) return 'https'
155
+ if (/^(?:file:\/\/|\.{0,2}[\\/]|[a-z]:[\\/]|[\\/])/i.test(value)) return 'local'
156
+ return 'unknown'
157
+ }
158
+
159
+ /**
160
+ * Remote URLs are evidence, not authentication material. Git may carry HTTP
161
+ * credentials in user-info or query fragments, so local observations retain
162
+ * only the provider/repository address needed for diagnosis.
163
+ */
164
+ export function sanitizeRemoteUrl(url) {
165
+ const value = String(url || '').trim()
166
+ const scpLike = value.match(/^[^/@\s]+@([^:/\s]+:.+)$/)
167
+ if (scpLike) return scpLike[1]
168
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value
169
+ try {
170
+ const parsed = new URL(value)
171
+ parsed.username = ''
172
+ Reflect.set(parsed, 'password', '')
173
+ parsed.search = ''
174
+ parsed.hash = ''
175
+ return parsed.toString()
176
+ } catch {
177
+ return value.replace(/^((?:https?|ssh):\/\/)(?:[^/@:]+:)?[^/@]+@/i, '$1')
178
+ }
179
+ }
180
+
181
+ export function parseNullConfig(text) {
182
+ const entries = []
183
+ for (const record of String(text || '').split('\0').filter(Boolean)) {
184
+ const newline = record.indexOf('\n')
185
+ if (newline === -1) entries.push({ key: record, value: '' })
186
+ else entries.push({ key: record.slice(0, newline), value: record.slice(newline + 1) })
187
+ }
188
+ return entries
189
+ }